TGUI-fies the Spawn verb (#94094)

## About The Pull Request

This PR converts the native text/list input that pops up when the Spawn
verb is used into a TGUI panel with a dynamic search function. It now
displays atom names, as well if they are abstract (and probably
shouldn't be spawned).
You can still input a typepath after the spawn verb in the tab bar which
will open the menu with said path already in the input box. If only one
typepath matches the input, it will be spawned immediately without
opening the panel.

<img width="400" height="500" alt="Qs0t8xkGj7"
src="https://github.com/user-attachments/assets/d54a57d5-fc22-4f59-af18-45b4fab0b256"
/>

It features RegEx support which activates when the input starts with
``re:``, as well as toggles for searching in atom names and fancy
typepath display. They can be toggled via buttons, or Alt+R/N/F for
regex/names/fancy paths respectively. Invalid regex-es will highlight
the search bar red.
The list itself can be navigated using arrow keys, enter/double click
will spawn the atom, and escape will close the menu.

<img width="400" height="500" alt="xgK2vCd1ck"
src="https://github.com/user-attachments/assets/e621c6ba-ad6b-4a30-922f-eb863797c65a"
/>

<img width="400" height="500" alt="2DeOPBRqoC"
src="https://github.com/user-attachments/assets/18fbba32-17d7-4351-bbc2-bc8e66eb24ae"
/>

Old functionality of spawning multiple objects by leaving a colon +
number after the typepath has been preserved. Using ``*`` and``!`` to
signify final path/end of a path respectively also still works when
regex mode isn't active.

## Why It's Good For The Game

Spawn isn't a critical tool, and using TGUI allows it to have dynamic
search and shifts searching from serverside to clientside, making it
significantly more responsive. Because we don't need to build a fancy
list of atom types, using it the first time won't cause a lag spike on
weaker machines anymore, which should make debugging locally a bit less
annoying.

## Changelog
🆑
admin: Converted the Spawn verb into a TGUI input, featuring for dynamic
search and RegEx support
/🆑
This commit is contained in:
SmArtKar
2025-11-28 15:52:28 -07:00
committed by GitHub
parent 486ef6ebf1
commit fd06311361
8 changed files with 627 additions and 160 deletions
+33 -18
View File
@@ -35,29 +35,44 @@
////////////////////////////////////////////////////////////////////////////////////////////////ADMIN HELPER PROCS
ADMIN_VERB(spawn_atom, R_SPAWN, "Spawn", "Spawn an atom.", ADMIN_CATEGORY_DEBUG, object as text)
if(!object)
return
var/list/preparsed = splittext(object,":")
var/path = preparsed[1]
ADMIN_VERB(spawn_atom, R_SPAWN, "Spawn", "Spawn an atom.", ADMIN_CATEGORY_DEBUG, object as text|null)
var/static/list/atom_types
if (isnull(atom_types))
atom_types = subtypesof(/atom)
var/chosen_path = null
var/list/preparsed = null
if (object)
preparsed = splittext(object, ":")
var/list/matches = filter_fancy_list(atom_types, preparsed[1])
if (length(matches) == 1)
chosen_path = matches[1]
if(!chosen_path)
var/datum/spawn_menu/menu = user.holder.spawn_menu
if (!menu)
menu = new()
user.holder.spawn_menu = menu
menu.init_value = object
menu.ui_interact(user.mob)
BLACKBOX_LOG_ADMIN_VERB("Spawn Atom")
return TRUE
var/amount = 1
if(preparsed.len > 1)
amount = clamp(text2num(preparsed[2]),1,ADMIN_SPAWN_CAP)
if (length(preparsed) > 1)
amount = clamp(text2num(preparsed[2]), 1, ADMIN_SPAWN_CAP)
var/chosen = pick_closest_path(path)
if(!chosen)
return
var/turf/T = get_turf(user.mob)
if(ispath(chosen, /turf))
T.ChangeTurf(chosen)
var/turf/target_turf = get_turf(user.mob)
if (ispath(chosen_path, /turf))
target_turf.ChangeTurf(chosen_path)
else
for(var/i in 1 to amount)
var/atom/A = new chosen(T)
A.flags_1 |= ADMIN_SPAWNED_1
for (var/i in 1 to amount)
var/atom/spawned = new chosen_path(target_turf)
spawned.flags_1 |= ADMIN_SPAWNED_1
log_admin("[key_name(user)] spawned [amount] x [chosen] at [AREACOORD(user.mob)]")
log_admin("[key_name(user.mob)] spawned [amount] x [chosen_path] at [AREACOORD(user.mob)]")
BLACKBOX_LOG_ADMIN_VERB("Spawn Atom")
return TRUE
ADMIN_VERB(spawn_atom_pod, R_SPAWN, "PodSpawn", "Spawn an atom via supply drop.", ADMIN_CATEGORY_DEBUG, object as text)
var/chosen = pick_closest_path(object)
+2
View File
@@ -38,6 +38,8 @@ GLOBAL_PROTECT(href_token)
var/datum/plane_master_debug/plane_debug
var/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/library_manager
var/datum/pathfind_debug/path_debug
var/datum/spawn_menu/spawn_menu
var/datum/spawnpanel/spawn_panel
/// Whether or not the user tried to connect, but was blocked by 2FA
var/blocked_by_2fa = FALSE
+92
View File
@@ -0,0 +1,92 @@
/datum/spawn_menu
/// Does the menu default to a regex prefix?
var/regex_search = FALSE
/// Does the search include atom names?
var/name_search = TRUE
/// Should we display full typepaths or the condensed versions?
var/fancy_types = TRUE
/// Should abstract types be included in the search?
var/include_abstracts = FALSE
/// Initial search value from the latest command
var/init_value = null
/datum/spawn_menu/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if (!ui)
ui = new(user, src, "SpawnSearch")
ui.open()
/datum/spawn_menu/ui_state(mob/user)
return ADMIN_STATE(R_SPAWN)
/datum/spawn_menu/ui_act(action, params, datum/tgui/ui)
if (..() || !check_rights_for(ui.user.client, R_SPAWN))
return FALSE
switch (action)
if ("setRegexSearch")
regex_search = params["regexSearch"]
return TRUE
if ("setNameSearch")
name_search = params["searchNames"]
return TRUE
if ("setFancyTypes")
fancy_types = params["fancyTypes"]
return TRUE
if ("setIncludeAbstracts")
include_abstracts = params["includeAbstracts"]
return TRUE
if ("spawn")
var/path = text2path(params["type"])
if (!path)
return TRUE
var/amount = clamp(text2num(params["amount"]) || 1, 1, ADMIN_SPAWN_CAP)
var/turf/target_turf = get_turf(ui.user)
if(ispath(path, /turf))
target_turf.ChangeTurf(path)
else
for(var/i in 1 to amount)
var/atom/spawned = new path(target_turf)
spawned.flags_1 |= ADMIN_SPAWNED_1
log_admin("[key_name(ui.user)] spawned [amount] x [path] at [AREACOORD(ui.user)]")
SStgui.close_uis(src)
return TRUE
if ("cancel")
SStgui.close_uis(src)
return TRUE
/datum/spawn_menu/ui_data(mob/user)
var/list/data = list()
data["initValue"] = init_value
data["searchNames"] = name_search
data["regexSearch"] = regex_search
data["fancyTypes"] = fancy_types
data["includeAbstracts"] = include_abstracts
return data
/datum/spawn_menu/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/json/spawn_menu),
)
/datum/asset/json/spawn_menu
name = "spawn_menu_atom_data"
/datum/asset/json/spawn_menu/generate()
var/list/data = list()
var/static/list/types_list
if (isnull(types_list))
var/list/local_types = list()
for (var/atom/atom_type as anything in subtypesof(/atom))
local_types[atom_type] = atom_type::name || ""
types_list = local_types
data["types"] = types_list
data["abstractTypes"] = get_abstract_types()
data["fancyTypes"] = GLOB.fancy_type_replacements
return data
+3 -40
View File
@@ -15,14 +15,10 @@
#define OFFSET_ABSOLUTE "Absolute offset"
#define OFFSET_RELATIVE "Relative offset"
GLOBAL_LIST_INIT(spawnpanels_by_ckey, list())
/*
An instance of a /tg/UI™ Spawn Panel. Stores preferences, spawns things, controls the UI. Unique for each user (their ckey).
*/
/datum/spawnpanel
/// Who this instance of spawn panel belongs to. The instances are unique to correctly keep modified values between multiple admins.
var/owner_ckey
/// Where and how the atom should be spawned.
var/where_target_type = WHERE_FLOOR_BELOW_MOB
/// The atom selected from the panel.
@@ -57,45 +53,21 @@ GLOBAL_LIST_INIT(spawnpanels_by_ckey, list())
offset = list("X" = 0, "Y" = 0, "Z" = 0)
/datum/spawnpanel/ui_interact(mob/user, datum/tgui/ui)
if(user.client.ckey != owner_ckey)
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "SpawnPanel")
ui.open()
/// Returns a `spawnpanel` instance belonging to this `user`, or creates and registers a new one
/proc/get_spawnpanel_for_admin(mob/user)
if(!user?.client?.ckey)
return null
var/ckey = user.client.ckey
if(GLOB.spawnpanels_by_ckey[ckey])
return GLOB.spawnpanels_by_ckey[ckey]
var/datum/spawnpanel/new_panel = new()
new_panel.owner_ckey = ckey
GLOB.spawnpanels_by_ckey[ckey] = new_panel
return new_panel
/datum/spawnpanel/ui_close(mob/user)
. = ..()
if (precise_mode && precise_mode != PRECISE_MODE_OFF)
toggle_precise_mode(PRECISE_MODE_OFF)
/datum/spawnpanel/ui_state(mob/user)
if(user.client.ckey != owner_ckey)
return GLOB.never_state
return ADMIN_STATE(R_ADMIN)
return ADMIN_STATE(R_SPAWN)
/datum/spawnpanel/ui_act(action, params)
if(..())
return FALSE
if(usr.client.ckey != owner_ckey)
/datum/spawnpanel/ui_act(action, params, datum/tgui/ui)
if(..() || !check_rights_for(ui.user.client, R_SPAWN))
return FALSE
switch(action)
@@ -106,12 +78,10 @@ GLOBAL_LIST_INIT(spawnpanels_by_ckey, list())
available_icon_states = icon_states(selected_atom_icon)
if(!(selected_atom_icon_state in available_icon_states))
selected_atom_icon_state = available_icon_states[1]
SStgui.update_uis(src)
return TRUE
if("set-apply-icon-override")
apply_icon_override = !!params["value"]
SStgui.update_uis(src)
return TRUE
if("reset-DMI-icon")
@@ -123,35 +93,28 @@ GLOBAL_LIST_INIT(spawnpanels_by_ckey, list())
available_icon_states = icon_states(selected_atom_icon)
else
available_icon_states = list()
SStgui.update_uis(src)
return TRUE
if("select-new-icon-state")
selected_atom_icon_state = params["new_state"]
SStgui.update_uis(src)
return TRUE
if("reset-icon-state")
selected_atom_icon_state = null
if(selected_atom)
selected_atom_icon_state = initial(selected_atom.icon_state)
SStgui.update_uis(src)
return TRUE
if("set-icon-size")
atom_icon_size = params["size"]
SStgui.update_uis(src)
return TRUE
if("reset-icon-size")
atom_icon_size = 100
SStgui.update_uis(src)
return TRUE
if("get-icon-states")
available_icon_states = icon_states(selected_atom_icon)
SStgui.update_uis(src)
return TRUE
if("selected-atom-changed")
+5 -3
View File
@@ -421,7 +421,9 @@ ADMIN_VERB(lag_switch_panel, R_ADMIN, "Show Lag Switches", "Display the controls
user << browse(dat.Join(), "window=lag_switch_panel;size=420x480")
ADMIN_VERB(spawn_panel, R_SPAWN, "Spawn Panel", "Spawn Panel (TGUI).", ADMIN_CATEGORY_GAME)
var/datum/spawnpanel/panel = get_spawnpanel_for_admin(user.mob)
if(panel)
panel.ui_interact(user.mob)
var/datum/spawnpanel/panel = user.holder.spawn_panel
if(!panel)
panel = new()
user.holder.spawn_panel = panel
panel.ui_interact(user.mob)
BLACKBOX_LOG_ADMIN_VERB("Spawn Panel")