Bun, Inferno->React migration (#22529)

Re-creation of https://github.com/Aurorastation/Aurora.3/pull/21046 to
skip merge conflict hell. Brings us modern TGUI.

**ALTERNATE TITLE: TGUI HELLSCAPE PR
ABANDON ALL HOPE YE WHO ENTER HERE**

- [x] Migrate build tools (javascript -> typescript, bun for package
management).
- [x] Upgrade all TGUI dependencies and associated root files to
TG-congruent versions (axios, babel, dompurify, eslint, highlight,
marked, prettier, sass, source-map, stacktrace-parser, typescript).
- [x] InfernoJS -> React migrations
- [x] React cleanup and polish (migrate all remaining .js files to
appropriate .ts or .tsx filetype, all remaining hooks, linting, error
corrections, etc.)
- [ ] Test all remaining TGUI interfaces
This commit is contained in:
Batrachophreno
2026-06-05 15:55:22 +02:00
committed by GitHub
parent a52729c105
commit 0d92359da7
930 changed files with 23130 additions and 50520 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
*
* required user mob The mob interacting with the UI.
*
* return list Statuic Data to be sent to the UI.
* return list Static Data to be sent to the UI.
*/
/datum/proc/ui_static_data(mob/user)
return list()
+98
View File
@@ -0,0 +1,98 @@
GLOBAL_DATUM_INIT(follow_menu, /datum/tgui_module/follow_menu, new)
/datum/tgui_module/follow_menu/ui_interact(var/mob/user, var/datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FollowMenu", "Follow Menu", 800, 400)
ui.open()
/datum/tgui_module/follow_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/abstract/ghost/ghost = usr
if(!istype(ghost))
return
if(action == "follow_target")
if(!ghost.ManualFollow(locate(params["follow_target"]) in GLOB.mob_list))
to_chat(ghost, SPAN_WARNING("Failed to follow selected target. Refreshing mob list..."))
return TRUE
if(action == "refresh")
ui.send_full_update()
return TRUE
/datum/tgui_module/follow_menu/ui_static_data(mob/user)
var/list/data = list()
// Don't worry about this is_mod check being for storytellers as well - all it does is highlight antags in red.
var/is_mod = check_rights(R_MOD|R_ADMIN, 0, user) || isstoryteller(user)
data["is_mod"] = is_mod
var/list/ghosts = list()
var/list/categories = list()
var/list/names = list()
var/list/namecounts = list()
for(var/mob/M in GLOB.mob_list)
var/category
var/name = M.name
if(name in names)
namecounts[name]++
name = "[name] ([namecounts[name]])"
else
names.Add(name)
namecounts[name] = 1
if(M.real_name && M.real_name != M.name)
name += " \[[M.real_name]\]"
if(!M.mind)
if(M.stat == DEAD)
category = "Dead NPCs"
else
category = "NPCs"
else if(M.stat == DEAD)
if(isobserver(M))
name += " \[Ghost\]"
category = "Observer"
else if(isstoryteller(M))
name += "\[Storyteller\]"
category = "Storytellers"
else
name += " \[Dead\]"
category = "Dead"
else
if(player_is_obvious_antag(M.mind))
category = M.mind.special_role
else if(isobserver(user))
var/mob/abstract/ghost/observer/O = user
if(O.antagHUD && player_is_antag(M.mind))
category = M.mind.special_role
if(!category)
category = "Alive"
var/special_char = is_special_character(M)
if(!name)
continue
ghosts += list(list("name" = name, "ref" = REF(M), "category" = category, "special_character" = special_char))
categories |= category
data["categories"] = categories
data["ghosts"] = ghosts
return data
/datum/tgui_module/follow_menu/proc/sort_categories(var/list/menu_info)
var/static/list/ordered_categories = list("Alive", "Dead", "Observer", "NPCs", "Dead NPCs")
var/list/sorted_menu_info = list()
// antags first
for(var/category in menu_info)
if(!(category in ordered_categories))
sorted_menu_info[category] = menu_info[category]
// set categories in the order people probably want to look at them
for(var/ordered_category in ordered_categories)
if(menu_info[ordered_category])
sorted_menu_info[ordered_category] = menu_info[ordered_category]
return sorted_menu_info
@@ -0,0 +1,411 @@
/datum/tgui_module/appearance_changer
var/name = "Appearance Changer"
var/flags = APPEARANCE_ALL_HAIR
var/datum/ui_state/ui_state
var/datum/state_object
var/datum/weakref/target_human
var/datum/weakref/target_id //Mostly for ghostroles and plastic surgery machines. If there is an ID, update it when we close the UI to ensure the correct info is imprinted.
var/change_id = FALSE //Prevents runtimes if target_id is null
var/list/valid_species = list()
var/list/valid_genders = list()
var/list/valid_pronouns = list()
var/list/valid_hairstyles = list()
var/list/valid_gradient_styles = list()
var/list/valid_facial_hairstyles = list()
var/list/valid_cultures = list()
var/list/valid_origins = list()
var/list/valid_citizenships = list()
var/list/valid_accents = list()
var/list/valid_languages = list()
var/list/valid_prosthetics = list()
var/list/valid_limbs = list()
var/list/valid_organs = list()
var/check_whitelist
var/list/whitelist
var/list/blacklist
var/list/culture_map = list()
var/list/origin_map = list()
var/list/culture_restrictions = list()
var/list/origin_restrictions = list()
/datum/tgui_module/appearance_changer/New(var/mob/living/carbon/human/H, var/check_species_whitelist = 1, var/list/species_whitelist = list(), var/list/species_blacklist = list(), var/list/culture_restriction = list(), var/list/origin_restriction = list(), var/datum/ui_state/set_ui_state = GLOB.always_state, var/datum/set_state_object = null, var/update_id)
..()
ui_state = set_ui_state
state_object = set_state_object
target_human = WEAKREF(H)
if(istype(H) && update_id)
var/obj/item/card/id/idcard = H.GetIdCard()
if(idcard)
target_id = WEAKREF(idcard)
change_id = TRUE
src.check_whitelist = check_species_whitelist
src.whitelist = species_whitelist
src.blacklist = species_blacklist
culture_restrictions = culture_restriction
origin_restrictions = culture_restriction
generate_data(check_whitelist, whitelist, blacklist)
/datum/tgui_module/appearance_changer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
switch(action)
if("race")
if(can_change(APPEARANCE_RACE) && (params["race"] in valid_species))
if(owner.change_species(params["race"]))
clear_and_generate_data()
. = TRUE
if("gender")
if(can_change(APPEARANCE_GENDER))
if(owner.change_gender(params["gender"], TRUE))
clear_and_generate_data()
. = TRUE
if("pronoun")
if(can_change(APPEARANCE_GENDER))
owner.pronouns = params["pronouns"]
clear_and_generate_data()
. = TRUE
if("skin_tone")
if(can_change_skin_tone())
var/new_s_tone = input(usr, "Choose your character's skin-tone:\n(Light [owner.species.lower_skin_tone_bound] - [owner.species.upper_skin_tone_bound] Dark)", "Skin Tone", -owner.s_tone + 35) as num|null
if(isnum(new_s_tone))
new_s_tone = 35 - clamp(round(new_s_tone), owner.species.lower_skin_tone_bound, owner.species.upper_skin_tone_bound)
. = owner.change_skin_tone(new_s_tone)
if("skin_color")
if(can_change_skin_color())
var/new_skin = input(usr, "Choose your character's skin colour: ", "Skin Color", rgb(owner.r_skin, owner.g_skin, owner.b_skin)) as color|null
if(new_skin)
var/r_skin = hex2num(copytext(new_skin, 2, 4))
var/g_skin = hex2num(copytext(new_skin, 4, 6))
var/b_skin = hex2num(copytext(new_skin, 6, 8))
if(owner.change_skin_color(r_skin, g_skin, b_skin))
update_dna()
. = TRUE
if("skin_preset")
if(can_change_skin_preset())
var/new_preset = input(usr, "Choose your character's body color preset:", "Character Preference", rgb(owner.r_skin, owner.g_skin, owner.b_skin)) as null|anything in owner.species.character_color_presets
if(new_preset)
new_preset = owner.species.character_color_presets[new_preset]
var/r_skin = GetRedPart(new_preset)
var/g_skin = GetGreenPart(new_preset)
var/b_skin = GetBluePart(new_preset)
if(owner.change_skin_color(r_skin, g_skin, b_skin))
update_dna()
. = TRUE
if("hair")
if(can_change(APPEARANCE_HAIR) && (params["hair"] in valid_hairstyles))
if(owner.change_hair(params["hair"]))
update_dna()
. = TRUE
if("hair_color")
if(can_change(APPEARANCE_HAIR_COLOR))
var/new_hair = input("Please select hair color.", "Hair Color", rgb(owner.r_hair, owner.g_hair, owner.b_hair)) as color|null
if(new_hair)
var/r_hair = hex2num(copytext(new_hair, 2, 4))
var/g_hair = hex2num(copytext(new_hair, 4, 6))
var/b_hair = hex2num(copytext(new_hair, 6, 8))
if(owner.change_hair_color(r_hair, g_hair, b_hair))
update_dna()
. = TRUE
if("gradient")
if(can_change(APPEARANCE_HAIR) && (params["gradient"] in valid_gradient_styles))
if(owner.change_gradient(params["gradient"]))
update_dna()
. = TRUE
if("gradient_color")
if(can_change(APPEARANCE_HAIR_COLOR))
var/new_gradient = input("Please select gradient color.", "Gradient Color", rgb(owner.r_grad, owner.g_grad, owner.b_grad)) as color|null
if(new_gradient)
var/r_grad = hex2num(copytext(new_gradient, 2, 4))
var/g_grad = hex2num(copytext(new_gradient, 4, 6))
var/b_grad = hex2num(copytext(new_gradient, 6, 8))
if(owner.change_gradient_color(r_grad, g_grad, b_grad))
update_dna()
. = TRUE
if("facial_hair")
if(can_change(APPEARANCE_FACIAL_HAIR) && (params["facial_hair"] in valid_facial_hairstyles))
if(owner.change_facial_hair(params["facial_hair"]))
update_dna()
. = TRUE
if("facial_hair_color")
if(can_change(APPEARANCE_FACIAL_HAIR_COLOR))
var/new_facial = input("Please select facial hair color.", "Facial Hair Color", rgb(owner.r_facial, owner.g_facial, owner.b_facial)) as color|null
if(new_facial)
var/r_facial = hex2num(copytext(new_facial, 2, 4))
var/g_facial = hex2num(copytext(new_facial, 4, 6))
var/b_facial = hex2num(copytext(new_facial, 6, 8))
if(owner.change_facial_hair_color(r_facial, g_facial, b_facial))
update_dna()
. = TRUE
if("eye_color")
if(can_change(APPEARANCE_EYE_COLOR))
var/new_eyes = input("Please select eye color.", "Eye Color", rgb(owner.r_eyes, owner.g_eyes, owner.b_eyes)) as color|null
if(new_eyes)
var/r_eyes = hex2num(copytext(new_eyes, 2, 4))
var/g_eyes = hex2num(copytext(new_eyes, 4, 6))
var/b_eyes = hex2num(copytext(new_eyes, 6, 8))
if(owner.change_eye_color(r_eyes, g_eyes, b_eyes))
update_dna()
. = TRUE
if("culture")
if(can_change(APPEARANCE_CULTURE))
var/new_culture_id = params["culture"]
if(new_culture_id in valid_cultures)
var/singleton/origin_item/culture/new_culture = culture_map[new_culture_id]
owner.set_culture(new_culture)
if(!(owner.origin in new_culture.possible_origins))
owner.set_origin(GET_SINGLETON(pick(new_culture.possible_origins)))
clear_and_generate_data()
. = TRUE
if("origin")
if(can_change(APPEARANCE_CULTURE))
var/new_origin_id = params["origin"]
if(new_origin_id in valid_origins)
var/singleton/origin_item/origin/new_origin = origin_map[new_origin_id]
owner.set_origin(new_origin)
if(!(owner.accent in new_origin.possible_accents))
owner.accent = new_origin.possible_accents[1]
if(!(owner.religion in new_origin.possible_religions))
owner.religion = new_origin.possible_religions[1]
if(!(owner.citizenship in new_origin.possible_religions))
owner.citizenship = new_origin.possible_citizenships[1]
clear_and_generate_data()
. = TRUE
if("citizenship")
if(can_change(APPEARANCE_CULTURE))
var/new_citizenship = params["citizenship"]
if(new_citizenship in valid_citizenships)
owner.citizenship = new_citizenship
clear_and_generate_data()
. = TRUE
if("accent")
if(can_change(APPEARANCE_CULTURE))
if(owner.set_accent(params["accent"]))
clear_and_generate_data()
. = TRUE
if("language")
if(can_change(APPEARANCE_LANGUAGE) && (params["language"] in valid_languages))
if(owner.add_or_remove_language(params["language"]))
clear_and_generate_data()
. = TRUE
if("speech_bubble")
if(can_change(APPEARANCE_RACE) && (params["speech_bubble"] in owner.species.possible_speech_bubble_types))
owner.speech_bubble_type = params["speech_bubble"]
. = TRUE
if("set_height")
owner.height = clamp(params["height"], owner.species.height_min, owner.species.height_max)
if("prosthetics")
if(can_change(APPEARANCE_PROSTHETICS))
var/limb = tgui_input_list(owner, "Select a Limb", "Limb Modifications", valid_limbs)
limb = reverse_parse_zone(limb) //turn it back into a proper organ tag
if(limb)
var/company = tgui_input_list(owner, "Select a Limb Type", "Limb Modifications", owner.species.possible_external_organs_modifications) //For amputated or nymph we just pass it straight into change_limb
if(company == "Prosthesis")
company = tgui_input_list(owner, "Select a Manufacturer", "Limb Modifications", valid_prosthetics)
owner.change_limb(limb, company)
. = TRUE
if("organs")
if(can_change(APPEARANCE_PROSTHETICS))
var/organ = tgui_input_list(owner, "Select an Organ", "Organ Modification", valid_organs)
var/obj/item/organ/internal/O = owner.internal_organs_by_name[organ]
var/modification = tgui_input_list(owner, "Select a Modification", "Organ Modification", O.possible_modifications)
owner.change_organ(organ, modification)
if("organ_preset")
if(can_change(APPEARANCE_PROSTHETICS))
var/organ = tgui_input_list(owner, "Select an Organ", "Organ Presets", valid_organs)
var/obj/item/organ/internal/machine/O = owner.internal_organs_by_name[organ]
var/list/possible_presets = list()
for(var/P in O.organ_presets)
var/singleton/synthetic_organ_preset/preset = GET_SINGLETON(P)
possible_presets[preset.name] = preset
var/singleton/synthetic_organ_preset/chosen_preset = tgui_input_list(owner, "Select a Preset", "Organ Presets", possible_presets)
if(chosen_preset)
O.apply_preset_data(chosen_preset)
/datum/tgui_module/appearance_changer/ui_interact(var/mob/user, var/datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AppearanceChanger", "Appearance Changer", 800, 450)
ui.open()
/datum/tgui_module/appearance_changer/ui_data(mob/user)
var/list/data = list()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
to_chat(user, SPAN_WARNING("We couldn't find you, closing."))
SStgui.close_uis(src)
return
data["owner_species"] = owner.species.name
data["change_race"] = can_change(APPEARANCE_RACE)
data["valid_species"] = valid_species
data["valid_speech_bubbles"] = owner.species.possible_speech_bubble_types
data["owner_speech_bubble"] = owner.speech_bubble_type
data["height_max"] = owner.species.height_max
data["height_min"] = owner.species.height_min
data["owner_height"] = owner.height
data["change_prosthetics"] = can_change(APPEARANCE_PROSTHETICS)
data["owner_gender"] = owner.gender
data["owner_pronouns"] = owner.pronouns
data["change_gender"] = can_change(APPEARANCE_GENDER)
data["valid_genders"] = valid_genders
data["valid_pronouns"] = valid_pronouns
data["change_culture"] = can_change(APPEARANCE_CULTURE)
data["owner_culture"] = owner.culture.name
data["valid_cultures"] = valid_cultures
data["owner_origin"] = owner.origin.name
data["valid_origins"] = valid_origins
data["owner_citizenship"] = owner.citizenship
data["valid_citizenships"] = valid_citizenships
data["owner_accent"] = owner.accent
data["valid_accents"] = valid_accents
var/list/owner_languages = list()
for(var/datum/language/L in owner.languages)
owner_languages += L.name
data["owner_languages"] = owner_languages
data["change_language"] = can_change(APPEARANCE_LANGUAGE)
data["valid_languages"] = valid_languages
data["change_skin_tone"] = can_change_skin_tone()
data["change_skin_color"] = can_change_skin_color()
data["change_skin_preset"] = can_change_skin_preset()
data["change_eye_color"] = can_change(APPEARANCE_EYE_COLOR)
data["change_hair"] = can_change(APPEARANCE_HAIR)
data["owner_hair_style"] = owner.h_style
data["valid_hair_styles"] = valid_hairstyles
data["change_gradient"] = can_change(APPEARANCE_HAIR)
data["owner_gradient_style"] = owner.g_style
data["valid_gradient_styles"] = valid_gradient_styles
data["change_facial_hair"] = can_change(APPEARANCE_FACIAL_HAIR)
data["owner_facial_hair_style"] = owner.f_style
data["valid_facial_hair_styles"] = valid_facial_hairstyles
data["change_hair_color"] = can_change(APPEARANCE_HAIR_COLOR)
data["change_gradient_color"] = can_change(APPEARANCE_HAIR_COLOR)
data["change_facial_hair_color"] = can_change(APPEARANCE_FACIAL_HAIR_COLOR)
return data
/datum/tgui_module/appearance_changer/ui_close(mob/user)
. = ..()
if(change_id)
var/mob/living/carbon/human/owner = target_human.resolve()
var/obj/item/card/id/I = target_id.resolve()
if(!istype(I) || !(istype(owner)))
return FALSE
owner.set_id_info(I)
/datum/tgui_module/appearance_changer/proc/update_dna()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
if(owner && (flags & APPEARANCE_UPDATE_DNA))
owner.update_dna()
/datum/tgui_module/appearance_changer/proc/can_change(var/flag)
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
return owner && (flags & flag)
/datum/tgui_module/appearance_changer/proc/can_change_skin_tone()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
return owner && (flags & APPEARANCE_SKIN) && owner.species.appearance_flags & HAS_SKIN_TONE
/datum/tgui_module/appearance_changer/proc/can_change_skin_color()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
return owner && (flags & APPEARANCE_SKIN) && owner.species.appearance_flags & HAS_SKIN_COLOR
/datum/tgui_module/appearance_changer/proc/can_change_skin_preset()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
return owner && (flags & APPEARANCE_SKIN) && owner.species.appearance_flags & HAS_SKIN_PRESET
/datum/tgui_module/appearance_changer/proc/clear_and_generate_data()
// Making the assumption that the available species remain constant
valid_genders = list()
valid_pronouns = list()
valid_hairstyles = list()
valid_gradient_styles = list()
valid_facial_hairstyles = list()
valid_cultures = list()
valid_origins = list()
valid_citizenships = list()
valid_accents = list()
valid_languages = list()
valid_prosthetics = list()
valid_limbs = list()
valid_organs = list()
generate_data()
/datum/tgui_module/appearance_changer/proc/generate_data()
var/mob/living/carbon/human/owner = target_human.resolve()
if(!istype(owner))
return FALSE
if(!length(valid_species))
valid_species = owner.generate_valid_species(check_whitelist, whitelist, blacklist)
if(!length(valid_genders) && length(owner.species.default_genders))
valid_genders = owner.species.default_genders.Copy()
if(!length(valid_pronouns) && length(owner.species.selectable_pronouns))
valid_pronouns = owner.species.selectable_pronouns.Copy()
if(!length(valid_hairstyles) || !length(valid_facial_hairstyles))
valid_hairstyles = owner.generate_valid_hairstyles(check_gender = 1)
if(!length(valid_gradient_styles))
valid_gradient_styles = owner.generate_valid_gradients()
if(!length(valid_facial_hairstyles))
valid_facial_hairstyles = owner.generate_valid_facial_hairstyles()
if(!length(valid_cultures))
for(var/culture in owner.species.possible_cultures)
var/singleton/origin_item/culture/CI = GET_SINGLETON(culture)
if(length(culture_restrictions))
if(!(CI.type in culture_restrictions))
continue
valid_cultures += CI.name
culture_map[CI.name] = CI
if(length(culture_restrictions))
for(var/culture in culture_restrictions)
var/singleton/origin_item/culture/CL = GET_SINGLETON(culture)
for(var/origin in CL.possible_origins)
var/singleton/origin_item/origin/OI = GET_SINGLETON(origin)
valid_origins += OI.name
origin_map[OI.name] = OI
var/singleton/origin_item/culture/OC = owner.culture
for(var/origin in OC.possible_origins)
var/singleton/origin_item/origin/OI = GET_SINGLETON(origin)
if(length(origin_restrictions))
if(!(OI.type in origin_restrictions))
continue
valid_origins += OI.name
origin_map[OI.name] = OI
valid_citizenships = owner.origin.possible_citizenships
valid_accents = owner.origin.possible_accents
if(!length(valid_languages))
valid_languages = owner.generate_valid_languages()
if(!length(valid_prosthetics))
valid_prosthetics = owner.generate_valid_prosthetics()
if(!length(valid_limbs))
valid_limbs = owner.generate_valid_limbs()
if(!length(valid_organs))
valid_organs = owner.species.alterable_internal_organs
+148
View File
@@ -0,0 +1,148 @@
/datum/tgui_module/narrate_panel/ui_interact(var/mob/user, var/datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "NarratePanel", "Narrate Panel", 800, 400)
ui.open()
/datum/tgui_module/narrate_panel/ui_data(mob/user)
var/list/data = list()
data["narrate_styles"] = list("danger", "notice", "warning", "alien", "cult")
data["narrate_locations"] = list("View", "Range", "Z-Level", "Global")
data["narrate_filters"] = list("None", "Skrell-like Psi-sensitives", "Human-like Psi-sensitives", "Silicons", "Silicons + Implants", "Hivenet")
return data
/datum/tgui_module/narrate_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/abstract/ghost/narrate_user = ui.user
var/is_admin = check_rights(R_ADMIN, user = ui.user)
if(!istype(narrate_user) && !is_admin)
return FALSE
if(action == "narrate")
var/narrate_text = sanitizeSafe(params["narrate_text"])
if(!length(narrate_text))
to_chat(narrate_user, SPAN_WARNING("No text was supplied!"))
return FALSE
var/narrate_style = params["narrate_style"]
if(!narrate_style)
to_chat(narrate_user, SPAN_WARNING("No text style was supplied!"))
return FALSE
var/narrate_size = text2num(params["narrate_size"])
if(!isnum(narrate_size))
to_chat(narrate_user, SPAN_WARNING("No text size was supplied!"))
return FALSE
var/narrate_range = text2num(params["narrate_range"])
if(!isnum(narrate_range))
to_chat(narrate_user, SPAN_WARNING("No narrate range was supplied!"))
return FALSE
var/narrate_location = params["narrate_location"]
if(!narrate_location)
to_chat(narrate_user, SPAN_WARNING("No narrate location was supplied!"))
return FALSE
var/list/mobs_to_message = list()
switch(narrate_location)
if("View")
for(var/mob/M in get_hearers_in_view(narrate_range, narrate_user))
mobs_to_message |= M
if("Range")
for(var/mob/M in get_hearers_in_range(narrate_range, narrate_user))
mobs_to_message |= M
if("Z-Level")
for(var/mob/M in GLOB.player_list)
if(GET_Z(M) == narrate_user.z)
mobs_to_message |= M
if("Global")
mobs_to_message = GLOB.player_list.Copy()
// Skip the switch if no filter was selected.
if (params["narrate_filter"] == "None")
for(var/mob/actor in mobs_to_message)
to_chat(actor, "<font size=[narrate_size]><span class='[narrate_style]'>[narrate_text]</span></font>")
return
// Create a copy of the message receivers. We have to iterate the copy of the list in order to safely remove entries from the list of message receivers.
// Otherwise we'll get memory exceptions.
var/list/filtered_list = mobs_to_message.Copy()
var/narrate_filter = params["narrate_filter"]
switch(narrate_filter)
if ("Skrell-like Psi-sensitives")
for(var/mob/filteree in filtered_list)
// Keep observers in the list, which includes ghosts, aghosts, and storytellers.
if (isghost(filteree))
continue
// This will check for anyone who is capable of receiving and interpreting telepathic messages.
// It will include Skrell who don't have a mindshield or the Low Psi-sensitivity trait.
// It will also include characters who have Psi-receivers, the High Psi-sensitivity trait, or are under the effects of Psycho-nootropic drugs.
if (IS_TELEPATHY_BLOCKED(filteree, PSI_RANK_SENSITIVE))
mobs_to_message.Remove(filteree)
continue
if ("Human-like Psi-sensitives")
for(var/mob/filteree in filtered_list)
// Keep observers in the list, which includes ghosts, aghosts, and storytellers.
if (isghost(filteree))
continue
// This will check for anyone who has a Zona Bovina capable of hearing psionics at all.
if (IS_TELEPATHY_BLOCKED(filteree, 0))
mobs_to_message.Remove(filteree)
continue
if ("Silicons")
for(var/mob/filteree in filtered_list)
// Keep observers in the list, which includes ghosts, aghosts, and storytellers.
if (isghost(filteree))
continue
// List will include Borgs, Robots, Shipbounds, pAIs, and IPCs.
if(issilicon(filteree) || isipc(filteree) || ispAI(filteree))
continue
// Remove anyone not made of silicon.
mobs_to_message.Remove(filteree)
if ("Silicons + Implants")
for(var/mob/filteree in filtered_list)
// Keep observers in the list, which includes ghosts, aghosts, and storytellers.
if (isghost(filteree))
continue
// List will include Borgs, Robots, Shipbounds, pAIs, and IPCs.
if(issilicon(filteree) || isipc(filteree) || ispAI(filteree))
continue
if (!ishuman(filteree))
// The mob in question can't have a brain at all, filter it early.
mobs_to_message.Remove(filteree)
continue
var/mob/living/carbon/human/brain_haver = filteree
// Check for silicon brain implants.
var/obj/item/organ/internal/brain = brain_haver.internal_organs_by_name[BP_BRAIN]
if (brain && ((brain.status & ORGAN_ASSISTED) || (brain.status & ORGAN_ROBOT)))
continue
// Remove anyone not made of silicon or doesn't have a cranial implant.
mobs_to_message.Remove(filteree)
if ("Hivenet")
for(var/mob/filteree in filtered_list)
// Keep observers in the list, which includes ghosts, aghosts, and storytellers.
if (isghost(filteree))
continue
// Filter anyone who doesn't have the Vaurca language at all.
if (!(GLOB.all_languages[LANGUAGE_VAURCA] in filteree.languages))
mobs_to_message.Remove(filteree)
continue
for(var/mob/actor in mobs_to_message)
to_chat(actor, "<font size=[narrate_size]><span class='[narrate_style]'>[narrate_text]</span></font>")
+1 -4
View File
@@ -70,7 +70,7 @@
// Disable UIs if unconscious.
else if(stat)
return UI_DISABLED
// Update UIs if incapicitated but concious.
// Update UIs if incapicitated but conscious.
else if(incapacitated())
return UI_UPDATE
return UI_INTERACTIVE
@@ -129,6 +129,3 @@
/datum/ui_state/proc/href_list(mob/user)
return list()
/datum/proc/nano_container()
return src
+5 -2
View File
@@ -13,7 +13,7 @@
GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)
/datum/ui_state/default/can_use_topic(src_object, mob/user)
return user.default_can_use_topic(src_object) // Call the individual mob-overridden procs.
return user?.default_can_use_topic(src_object) // Call the individual mob-overridden procs.
/mob/proc/default_can_use_topic(src_object)
return UI_CLOSE // Don't allow interaction by default.
@@ -41,8 +41,11 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)
if(. < UI_INTERACTIVE)
return
if(src_object == src)
return UI_INTERACTIVE
// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
if(!control_disabled)
if(!control_disabled && can_see(src_object))
return UI_INTERACTIVE
return UI_CLOSE
+11
View File
@@ -83,6 +83,9 @@
return UI_DISABLED // Otherwise they can keep the UI open.
/mob/living/silicon/ai/get_ui_access(atom/source)
if(source == src)
return UI_INTERACTIVE
// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
if(!control_disabled && can_see(source))
return UI_INTERACTIVE
@@ -113,3 +116,11 @@
return UI_CLOSE
return UI_INTERACTIVE
/// Return UI_INTERACTIVE if the user is inside the target atom, whether they can see it or not.
/// Return UI_CLOSE otherwise.
/proc/ui_status_user_inside(mob/user, atom/target)
if(target.contains(user))
return UI_INTERACTIVE
return UI_CLOSE
+35 -51
View File
@@ -37,11 +37,9 @@
var/datum/ui_state/state = null
/// Rate limit client refreshes to prevent DoS.
COOLDOWN_DECLARE(refresh_cooldown)
/// Are byond mouse events beyond the window passed in to the ui
var/mouse_hooked = FALSE
/// Any partial packets that we have received from TGUI, waiting to be sent
var/partial_packets
/// The id of any ByondUi elements that we have opened
var/list/open_byondui_elements
/**
* public
@@ -58,9 +56,6 @@
* return datum/tgui The requested UI.
*/
/datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y)
log_tgui(user,
"new [interface] fancy [user?.client?.prefs.tgui_fancy]",
src_object = src_object)
src.user = user
src.src_object = src_object
src.window_key = "[REF(src_object)]-main"
@@ -85,7 +80,7 @@
* return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists.
*/
/datum/tgui/proc/open()
if(!user?.client)
if(user && !user.client)
return FALSE
if(window)
return FALSE
@@ -100,7 +95,6 @@
if(!window.is_ready())
window.initialize(
strict_mode = TRUE,
fancy = user.client.prefs.tgui_fancy,
assets = list(
get_asset_datum(/datum/asset/simple/tgui),
))
@@ -110,8 +104,6 @@
window.send_message("update", get_payload(
with_data = TRUE,
with_static_data = TRUE))
if(mouse_hooked)
window.set_mouse_macro()
SStgui.on_open(src)
return TRUE
@@ -146,11 +138,27 @@
window.release_lock()
window.close(can_be_suspended)
src_object.ui_close(user)
SEND_SIGNAL(src, COMSIG_TGUI_CLOSE, user)
SStgui.on_close(src)
if(user.client)
terminate_byondui_elements()
state = null
qdel(src)
/**
* public
*
* Closes all ByondUI elements, left dangling by a forceful TGUI exit,
* such as via Alt+F4, or terminating the process
*
*/
/datum/tgui/proc/terminate_byondui_elements()
set waitfor = FALSE
for(var/byondui_element in open_byondui_elements)
winset(user.client, byondui_element, list("parent" = ""))
/**
* public
*
@@ -161,18 +169,6 @@
/datum/tgui/proc/set_autoupdate(autoupdate)
src.autoupdate = autoupdate
/**
* public
*
* Enable/disable passing through byond mouse events to the window
*
* required value bool Enable/disable hooking.
*/
/datum/tgui/proc/set_mouse_hook(value)
src.mouse_hooked = value
//Handle unhooking/hooking on already open windows ?
/**
* public
*
@@ -248,12 +244,13 @@
json_data["config"] = list(
"title" = title,
"status" = status,
"interface" = interface,
"interface" = list(
"name" = interface,
),
"refreshing" = refreshing,
"window" = list(
"key" = window_key,
"size" = window_size,
"fancy" = user.client.prefs.tgui_fancy,
"locked" = user.client.prefs.tgui_lock,
"scale" = user.client.prefs.ui_scale,
),
@@ -329,31 +326,6 @@
// Pass act type messages to ui_act
if(type && copytext(type, 1, 5) == "act/")
var/act_type = copytext(type, 5)
var/id = href_list["packetId"]
if(!isnull(id))
id = text2num(id)
var/total = text2num(href_list["totalPackets"])
if(total > MAX_MESSAGE_CHUNKS)
return
if(id == 1)
partial_packets = new /list(total)
partial_packets[id] = href_list["packet"]
if(id != total)
return
var/assembled_payload = ""
for(var/packet in partial_packets)
assembled_payload += packet
payload = json_decode(assembled_payload)
partial_packets = null
log_tgui(user, "Action: [act_type] [href_list["payload"]]",
window = window,
src_object = src_object)
@@ -381,6 +353,18 @@
LAZYINITLIST(src_object.tgui_shared_states)
src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
SStgui.update_uis(src_object)
if(TGUI_MANAGED_BYONDUI_TYPE_RENDER)
var/byond_ui_id = payload[TGUI_MANAGED_BYONDUI_PAYLOAD_ID]
if(!byond_ui_id || LAZYLEN(open_byondui_elements) > TGUI_MANAGED_BYONDUI_LIMIT)
return
LAZYOR(open_byondui_elements, byond_ui_id)
if(TGUI_MANAGED_BYONDUI_TYPE_UNMOUNT)
var/byond_ui_id = payload[TGUI_MANAGED_BYONDUI_PAYLOAD_ID]
if(!byond_ui_id)
return
LAZYREMOVE(open_byondui_elements, byond_ui_id)
/// Wrapper for behavior to potentially wait until the next tick if the server is overloaded
/datum/tgui/proc/on_act_message(act_type, payload, state)
+52 -49
View File
@@ -11,6 +11,7 @@
var/is_browser = FALSE
var/status = TGUI_WINDOW_CLOSED
var/locked = FALSE
var/visible = FALSE
var/datum/tgui/locked_by
var/datum/subscriber_object
var/subscriber_delegate
@@ -19,12 +20,12 @@
var/sent_assets = list()
// Vars passed to initialize proc (and saved for later)
var/initial_strict_mode
var/initial_fancy
var/initial_assets
var/initial_inline_html
var/initial_inline_js
var/initial_inline_css
var/mouse_event_macro_set = FALSE
var/list/oversized_payloads = list()
/**
* public
@@ -50,7 +51,6 @@
* will be put into the queue until the window finishes loading.
*
* optional strict_mode bool - Enables strict error handling and BSOD.
* optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar.
* optional assets list - List of assets to load during initialization.
* optional inline_html string - Custom HTML to inject.
* optional inline_js string - Custom JS to inject.
@@ -58,7 +58,6 @@
*/
/datum/tgui_window/proc/initialize(
strict_mode = FALSE,
fancy = FALSE,
assets = list(),
inline_html = "",
inline_js = "",
@@ -68,7 +67,6 @@
window = src)
if(!client)
return
src.initial_fancy = fancy
src.initial_assets = assets
src.initial_inline_html = inline_html
src.initial_inline_js = inline_js
@@ -76,16 +74,12 @@
status = TGUI_WINDOW_LOADING
fatally_errored = FALSE
// Build window options
var/options = "file=[id].html;can_minimize=0;auto_format=0;"
// Remove titlebar and resize handles for a fancy window
if(fancy)
options += "titlebar=0;can_resize=0;"
else
options += "titlebar=1;can_resize=1;"
var/options = "file=[id].html;can_minimize=0;auto_format=0;titlebar=0;can_resize=0;"
// Generate page html
var/html = SStgui.basehtml
html = replacetextEx(html, "\[tgui:windowId]", id)
html = replacetextEx(html, "\[tgui:strictMode]", strict_mode)
html = replacetextEx(html, "\[tgui:storagecdn]", GLOB.config.storage_cdn_iframe)
// Inject assets
var/inline_assets_str = ""
for(var/datum/asset/asset in assets)
@@ -128,7 +122,6 @@
/datum/tgui_window/proc/reinitialize()
initialize(
strict_mode = initial_strict_mode,
fancy = initial_fancy,
assets = initial_assets,
inline_html = initial_inline_html,
inline_js = initial_inline_js,
@@ -220,12 +213,11 @@
/datum/tgui_window/proc/close(can_be_suspended = TRUE)
if(!client)
return
if(mouse_event_macro_set)
remove_mouse_macro()
if(can_be_suspended && can_be_suspended())
log_tgui(client,
context = "[id]/close (suspending)",
window = src)
visible = FALSE
status = TGUI_WINDOW_READY
send_message("suspend")
return
@@ -233,6 +225,7 @@
context = "[id]/close",
window = src)
release_lock()
visible = FALSE
status = TGUI_WINDOW_CLOSED
message_queue = null
// Do not close the window to give user some time
@@ -298,9 +291,6 @@
return
sent_assets |= list(asset)
. = asset.send(client)
if(istype(asset, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = asset
send_message("asset/stylesheet", spritesheet.css_filename())
send_raw_message(asset.get_serialized_url_mappings())
/**
@@ -364,6 +354,11 @@
switch(type)
if("ping")
send_message("ping/reply", payload)
if("ping/set")
client?.avgping = payload["ping"]
if("visible")
visible = TRUE
SEND_SIGNAL(src, COMSIG_TGUI_WINDOW_VISIBLE, client)
if("suspend")
close(can_be_suspended = TRUE)
if("close")
@@ -372,40 +367,48 @@
client << link(href_list["url"])
if("cacheReloaded")
reinitialize()
if("chat/resend")
SSchat.handle_resend(client, payload)
if("oversizedPayloadRequest")
var/payload_id = payload["id"]
var/chunk_count = payload["chunkCount"]
var/permit_payload = chunk_count <= 32
if(permit_payload)
create_oversized_payload(payload_id, payload["type"], chunk_count)
send_message("oversizePayloadResponse", list("allow" = permit_payload, "id" = payload_id))
if("payloadChunk")
var/payload_id = payload["id"]
append_payload_chunk(payload_id, payload["chunk"])
send_message("acknowledgePayloadChunk", list("id" = payload_id))
/datum/tgui_window/proc/set_mouse_macro()
if(mouse_event_macro_set)
/datum/tgui_window/vv_edit_var(var_name, var_value)
return var_name != NAMEOF(src, id) && ..()
/datum/tgui_window/proc/create_oversized_payload(payload_id, message_type, chunk_count)
if(oversized_payloads[payload_id])
stack_trace("Attempted to create oversized tgui payload with duplicate ID.")
return
var/list/byondToTguiEventMap = list(
"MouseDown" = "byond/mousedown",
"MouseUp" = "byond/mouseup"
oversized_payloads[payload_id] = list(
"type" = message_type,
"count" = chunk_count,
"chunks" = list(),
"timeout" = addtimer(CALLBACK(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
)
for(var/mouseMacro in byondToTguiEventMap)
var/command_template = ".output CONTROL PAYLOAD"
var/event_message = TGUI_CREATE_MESSAGE(byondToTguiEventMap[mouseMacro], null)
var target_control = is_browser \
? "[id]:update" \
: "[id].browser:update"
var/with_id = replacetext(command_template, "CONTROL", target_control)
var/full_command = replacetext(with_id, "PAYLOAD", event_message)
/datum/tgui_window/proc/append_payload_chunk(payload_id, chunk)
var/list/payload = oversized_payloads[payload_id]
if(!payload)
return
var/list/chunks = payload["chunks"]
chunks += chunk
if(length(chunks) >= payload["count"])
deltimer(payload["timeout"])
var/message_type = payload["type"]
var/final_payload = chunks.Join()
remove_oversized_payload(payload_id)
on_message(message_type, json_decode(final_payload), list("type" = message_type, "payload" = final_payload, "tgui" = TRUE, "window_id" = id))
else
payload["timeout"] = addtimer(CALLBACK(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
var/list/params = list()
params["parent"] = "default" //Technically this is external to tgui but whatever
params["name"] = mouseMacro
params["command"] = full_command
winset(client, "[mouseMacro]Window[id]Macro", params)
mouse_event_macro_set = TRUE
/datum/tgui_window/proc/remove_mouse_macro()
if(!mouse_event_macro_set)
stack_trace("Unsetting mouse macro on tgui window that has none")
var/list/byondToTguiEventMap = list(
"MouseDown" = "byond/mousedown",
"MouseUp" = "byond/mouseup"
)
for(var/mouseMacro in byondToTguiEventMap)
winset(client, null, "[mouseMacro]Window[id]Macro.parent=null")
mouse_event_macro_set = FALSE
/datum/tgui_window/proc/remove_oversized_payload(payload_id)
oversized_payloads -= payload_id