This commit is contained in:
vm-silicons
2025-12-19 23:46:31 -08:00
parent 963ea21468
commit ecd7645db9
47 changed files with 4742 additions and 5055 deletions
+15
View File
@@ -0,0 +1,15 @@
/**
* Render a HTML string with the given header and body.
*/
#define HTML_SKELETON_HEADER_BODY(head, body) \
"<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><meta http-equiv='X-UA-Compatible' content='IE=edge'>[head]</head><body>[body]</body></html>"
/**
* Render a HTML string with the given title and body, without additional headers.
*/
#define HTML_SKELETON_TITLE(title, body) HTML_SKELETON_HEADER_BODY("<title>[title]</title>", body)
/**
* Render a HTML string with the given body, without a title or additional headers.
*/
#define HTML_SKELETON(body) HTML_SKELETON_HEADER_BODY("", body)
+2 -2
View File
@@ -137,7 +137,7 @@ SUBSYSTEM_DEF(pai)
dat += "<a href='byond://?src=[REF(src)];option=save;new=1;candidate=[REF(candidate)]'>Save Personality</a><br>"
dat += "<a href='byond://?src=[REF(src)];option=load;new=1;candidate=[REF(candidate)]'>Load Personality</a><br>"
M << browse(dat, "window=paiRecruit")
M << HTML_SKELETON(dat), "window=paiRecruit")
/datum/controller/subsystem/pai/proc/spam_again()
ghost_spam = FALSE
@@ -198,7 +198,7 @@ SUBSYSTEM_DEF(pai)
dat += "</table>"
user << browse(dat, "window=findPai")
user << HTML_SKELETON(dat), "window=findPai")
/datum/paiCandidate
var/name
+1 -1
View File
@@ -39,7 +39,7 @@ SUBSYSTEM_DEF(tgui)
ntos_error = "<style type='text/css'>\n[ntos_error]\n</style>"
basehtml = replacetextEx(basehtml, "<!-- tgui:ntos-error -->", ntos_error)
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2525-[GLOB.startup_year + 544]")
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2525-[text2num(time2text(world.realtime, "YYYY")) + 544]")
/datum/controller/subsystem/tgui/Shutdown()
close_all_uis()
+1 -1
View File
@@ -27,10 +27,10 @@
/// Deactivates the functionality defines by the element on the given datum
/datum/element/proc/Detach(datum/source, force)
SHOULD_CALL_PARENT(TRUE)
SIGNAL_HANDLER
SEND_SIGNAL(source, COMSIG_ELEMENT_DETACH, src)
SHOULD_CALL_PARENT(TRUE)
UnregisterSignal(source, COMSIG_PARENT_QDELETING)
/datum/element/Destroy(force)
+1 -1
View File
@@ -155,7 +155,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
var/shown_threat
/datum/game_mode/dynamic/admin_panel()
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Game Mode Panel</title></head><body><h1><B>Game Mode Panel</B></h1>")
var/list/dat = list("<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Game Mode Panel</title></head><body><h1><B>Game Mode Panel</B></h1>")
dat += "Dynamic Mode <a href='?_src_=vars;[HrefToken()];Vars=[REF(src)]'>\[VV\]</a> <a href='?src=\ref[src];[HrefToken()]'>\[Refresh\]</a><BR>"
dat += "Threat Level: <b>[threat_level]</b><br/>"
dat += "Budgets (Roundstart/Midrounds): <b>[initial_round_start_budget]/[threat_level - initial_round_start_budget]</b><br/>"
+1 -1
View File
@@ -68,7 +68,7 @@
dat += "</td></tr>"
dat += "</table><hr><a href='?src=[REF(src)];done'>Finalize Airlock Construction</a> | <a href='?src=[REF(src)];cancel'>Cancel and Destroy Airlock</a>"
usr << browse(dat,"window=airlockmaker")
usr << HTML_SKELETON(dat),"window=airlockmaker")
/datum/airlock_maker/Topic(var/href,var/list/href_list)
if(!usr)
+1 -1
View File
@@ -259,7 +259,7 @@
dat += "Moving: <a href='?src=[REF(src)];operation=togglemoving'>[moving ? "Enabled":"Disabled"]</a>"
user << browse(dat, "window=magnet;size=400x500")
user << HTML_SKELETON(dat), "window=magnet;size=400x500")
onclose(user, "magnet")
/obj/machinery/magnetic_controller/Topic(href, href_list)
+1 -1
View File
@@ -61,7 +61,7 @@
dat += "No personality installed.<br>"
dat += "Searching for a personality... Press view available personalities to notify potential candidates."
dat += "<A href='byond://?src=[REF(src)];request=1'>\[View available personalities\]</a><br>"
user << browse(dat, "window=paicard")
user << HTML_SKELETON(dat), "window=paicard")
onclose(user, "paicard")
return
@@ -52,7 +52,7 @@
dat += "The implant casing is empty."
else
dat += "Please insert an implant casing!"
user << browse(dat, "window=implantpad")
user << HTML_SKELETON(dat), "window=implantpad")
onclose(user, "implantpad")
+1 -1
View File
@@ -24,7 +24,7 @@
dat += "<B>Four uses, use them wisely:</B><BR>"
dat += "<A href='byond://?src=[REF(src)];spell_teleport=1'>Teleport</A><BR>"
dat += "Kind regards,<br>Wizards Federation<br><br>P.S. Don't forget to bring your gear, you'll need it to cast most spells.<HR>"
user << browse(dat, "window=scroll")
user << HTML_SKELETON(dat), "window=scroll")
onclose(user, "scroll")
return
+1 -1
View File
@@ -76,7 +76,7 @@
if (!locked)
message = "*****"
dat += text("<HR>\n>[]<BR>\n<A href='?src=[REF(src)];type=1'>1</A>-<A href='?src=[REF(src)];type=2'>2</A>-<A href='?src=[REF(src)];type=3'>3</A><BR>\n<A href='?src=[REF(src)];type=4'>4</A>-<A href='?src=[REF(src)];type=5'>5</A>-<A href='?src=[REF(src)];type=6'>6</A><BR>\n<A href='?src=[REF(src)];type=7'>7</A>-<A href='?src=[REF(src)];type=8'>8</A>-<A href='?src=[REF(src)];type=9'>9</A><BR>\n<A href='?src=[REF(src)];type=R'>R</A>-<A href='?src=[REF(src)];type=0'>0</A>-<A href='?src=[REF(src)];type=E'>E</A><BR>\n</TT>", message)
user << browse(dat, "window=caselock;size=300x280")
user << HTML_SKELETON(dat), "window=caselock;size=300x280")
/obj/item/storage/secure/Topic(href, href_list)
..()
+2 -2
View File
@@ -63,7 +63,7 @@ GLOBAL_PROTECT(Banlist)
/proc/LoadBans()
if(!CONFIG_GET(flag/ban_legacy_system))
return
GLOB.Banlist = new("data/banlist.bdb")
log_admin("Loading Banlist")
@@ -199,7 +199,7 @@ GLOBAL_PROTECT(Banlist)
dat += "</table>"
dat = "<HR><B>Bans:</B> <FONT COLOR=blue>(U) = Unban , (E) = Edit Ban</FONT> - <FONT COLOR=green>([count] Bans)</FONT><HR><table border=1 rules=all frame=void cellspacing=0 cellpadding=3 >[dat]"
usr << browse(dat, "window=unbanp;size=875x400")
usr << HTML_SKELETON(dat), "window=unbanp;size=875x400")
//////////////////////////////////// DEBUG ////////////////////////////////////
+4 -3
View File
@@ -231,7 +231,7 @@
to_chat(usr, "Error: you are not an admin!", confidential = TRUE)
return
var/dat
dat = text("<HEAD><TITLE>Admin Newscaster</TITLE></HEAD><H3>Admin Newscaster Unit</H3>")
dat = text("<!DOCTYPE html><html><HEAD><TITLE>Admin Newscaster</TITLE></HEAD><body><H3>Admin Newscaster Unit</H3>")
switch(admincaster_screen)
if(0)
@@ -424,6 +424,7 @@
else
dat+="I'm sorry to break your immersion. This shit's bugged. Report this bug to Agouri, polyxenitopalidou@gmail.com"
dat += "</body></html>"
usr << browse(dat, "window=admincaster_main;size=400x600")
onclose(usr, "admincaster_main")
@@ -460,7 +461,7 @@
if(marked_datum && istype(marked_datum, /atom))
dat += "<A href='?src=[REF(src)];[HrefToken()];dupe_marked_datum=1'>Duplicate Marked Datum</A><br>"
usr << browse(dat, "window=admin2;size=240x280")
usr << HTML_SKELETON(dat), "window=admin2;size=240x280")
return
/////////////////////////////////////////////////////////////////////////////////////////////////admins2.dm merge
@@ -934,7 +935,7 @@
<br/>The threshold at which "round-ender" rulesets will stack. A value higher than 100 ensure this never happens. <br/>
"}
user << browse(dat, "window=dyn_mode_options;size=900x650")
user << HTML_SKELETON(dat), "window=dyn_mode_options;size=900x650")
/datum/admins/proc/create_or_modify_area()
set category = "Debug"
+1 -1
View File
@@ -136,7 +136,7 @@
if(!SSticker.HasRoundStarted())
alert("The game hasn't started yet!")
return
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Round Status</title></head><body><h1><B>Round Status</B></h1>")
var/list/dat = list("<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Round Status</title></head><body><h1><B>Round Status</B></h1>")
if(SSticker.mode.replacementmode)
dat += "Former Game Mode: <B>[SSticker.mode.name]</B><BR>"
dat += "Replacement Game Mode: <B>[SSticker.mode.replacementmode.name]</B><BR>"
+1 -1
View File
@@ -2,7 +2,7 @@
if(!check_rights())
return
log_admin("[key_name(usr)] checked the player panel.")
var/dat = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Player Panel</title></head>"
var/dat = "<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Player Panel</title></head>"
//javascript, the part that does most of the work~
dat += {"
+1 -1
View File
@@ -107,7 +107,7 @@
dat += "No datums tagged :("
dat = dat.Join("<br>")
usr << browse(dat, "window=tag;size=800x480")
usr << HTML_SKELETON(dat), "window=tag;size=800x480")
/**
* Clears tagged datums
+5 -5
View File
@@ -612,7 +612,7 @@
to_chat(usr, "This mob has no ckey.")
return
var/dat = "<head><title>Job-Ban Panel: [key_name(M)]</title></head>"
var/dat = "<!DOCTYPE html><html><head><title>Job-Ban Panel: [key_name(M)]</title></head><body>"
/***********************************WARNING!************************************
The jobban stuff looks mangled and disgusting
@@ -917,7 +917,7 @@
else
dat += "<td width='20%'><a href='?src=[REF(src)];[HrefToken()];jobban3=[ROLE_RESPAWN];jobban4=[REF(M)]'>Respawns</a></td>"
dat += "</tr></table>"
dat += "</tr></table></body></html>"
usr << browse(dat, "window=jobban2;size=800x450")
return
@@ -2616,7 +2616,7 @@
var/list/dat = list("Related accounts by [uppertext(href_list["showrelatedacc"])]:")
dat += thing_to_check
usr << browse(dat.Join("<br>"), "window=related_[C];size=420x300")
usr << HTML_SKELETON(dat).Join("<br>"), "window=related_[C];size=420x300")
else if(href_list["centcomlookup"])
if(!check_rights(R_ADMIN))
@@ -2742,7 +2742,7 @@
dat += {"<A href='?src=[REF(src)];[HrefToken()];c_mode2=secret'>Secret</A><br>"}
dat += {"<A href='?src=[REF(src)];[HrefToken()];c_mode2=random'>Random</A><br>"}
dat += {"Now: [GLOB.master_mode]"}
usr << browse(dat, "window=c_mode")
usr << HTML_SKELETON(dat), "window=c_mode")
/datum/admins/proc/HandleFSecret()
if(!check_rights(R_ADMIN))
@@ -2757,7 +2757,7 @@
dat += {"<A href='?src=[REF(src)];[HrefToken()];f_secret2=[mode]'>[config.mode_names[mode]]</A><br>"}
dat += {"<A href='?src=[REF(src)];[HrefToken()];f_secret2=secret'>Random (default)</A><br>"}
dat += {"Now: [GLOB.secret_force_mode]"}
usr << browse(dat, "window=f_secret")
usr << HTML_SKELETON(dat), "window=f_secret")
/datum/admins/proc/makeMentor(ckey)
if(!usr.client)
+2 -2
View File
@@ -79,7 +79,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
title = "Resolved Tickets"
if(!l2b)
return
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>[title]</title></head>")
var/list/dat = list("<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>[title]</title></head>")
dat += "<A href='?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
for(var/I in l2b)
var/datum/admin_help/AH = I
@@ -413,7 +413,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
//Show the ticket panel
/datum/admin_help/proc/TicketPanel()
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Ticket #[id]</title></head>")
var/list/dat = list("<!DOCTYPE html><html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Ticket #[id]</title></head>")
var/ref_src = "[REF(src)]"
dat += "<h4>Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>State: "
+1 -1
View File
@@ -176,7 +176,7 @@ GLOBAL_LIST_EMPTY(dirty_vars)
dat += "[ADMIN_VERBOSEJMP(T)]\n"
dat += "<br>"
usr << browse(dat, "window=at_list")
usr << HTML_SKELETON(dat), "window=at_list")
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Roundstart Active Turfs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+2 -2
View File
@@ -1072,7 +1072,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
<input type="submit" value="Save">
</form></body></html>
"}
usr << browse(dat, "window=dressup;size=550x600")
usr << HTML_SKELETON(dat), "window=dressup;size=550x600")
/client/proc/toggle_combo_hud()
set category = "Admin.Game"
@@ -1245,7 +1245,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
for(var/datum/station_goal/S in SSticker.mode.station_goals)
dat += "[S.name] - <a href='?src=[REF(S)];[HrefToken()];announce=1'>Announce</a> | <a href='?src=[REF(S)];[HrefToken()];remove=1'>Remove</a><br>"
dat += "<br><a href='?src=[REF(src)];[HrefToken()];add_station_goal=1'>Add New Goal</a>"
usr << browse(dat, "window=goals;size=400x400")
usr << HTML_SKELETON(dat), "window=goals;size=400x400")
/client/proc/toggle_hub()
+9 -9
View File
@@ -58,14 +58,14 @@
dat += "<li>[l]</li>"
if(!GLOB.admin_log.len)
dat += "No-one has done anything this round!"
holder << browse(dat, "window=admin_log")
holder << HTML_SKELETON(dat), "window=admin_log")
if("show_admins")
var/dat = "<B>Current admins:</B><HR>"
if(GLOB.admin_datums)
for(var/ckey in GLOB.admin_datums)
var/datum/admins/D = GLOB.admin_datums[ckey]
dat += "[ckey] - [D.rank.name]<br>"
holder << browse(dat, "window=showadmins;size=600x500")
holder << HTML_SKELETON(dat), "window=showadmins;size=600x500")
if("mentor_log")
var/dat = "<B>Mentor Log<HR></B>"
for(var/l in GLOB.mentorlog)
@@ -73,7 +73,7 @@
if(!GLOB.mentorlog.len)
dat += "No mentors have done anything this round!"
usr << browse(dat, "window=mentor_log")
usr << HTML_SKELETON(dat), "window=mentor_log")
//Buttons for debug.
if("maint_access_engiebrig")
@@ -114,18 +114,18 @@
var/dat = "<B>Bombing List</B><HR>"
for(var/l in GLOB.bombers)
dat += text("[l]<BR>")
holder << browse(dat, "window=bombers")
holder << HTML_SKELETON(dat), "window=bombers")
if("list_signalers")
var/dat = "<B>Showing last [length(GLOB.lastsignalers)] signalers.</B><HR>"
for(var/sig in GLOB.lastsignalers)
dat += "[sig]<BR>"
holder << browse(dat, "window=lastsignalers;size=800x500")
holder << HTML_SKELETON(dat), "window=lastsignalers;size=800x500")
if("list_lawchanges")
var/dat = "<B>Showing last [length(GLOB.lawchanges)] law changes.</B><HR>"
for(var/sig in GLOB.lawchanges)
dat += "[sig]<BR>"
holder << browse(dat, "window=lawchanges;size=800x500")
holder << HTML_SKELETON(dat), "window=lawchanges;size=800x500")
if("showailaws")
holder.holder.output_ai_laws()//huh, inconvenient var naming, huh?
if("showgm")
@@ -141,7 +141,7 @@
for(var/datum/data/record/t in GLOB.data_core.general)
dat += "<tr><td>[t.fields["name"]]</td><td>[t.fields["rank"]]</td></tr>"
dat += "</table>"
holder << browse(dat, "window=manifest;size=440x410")
holder << HTML_SKELETON(dat), "window=manifest;size=440x410")
if("dna")
var/dat = "<B>Showing DNA from blood.</B><HR>"
dat += "<table cellspacing=5><tr><th>Name</th><th>DNA</th><th>Blood Type</th></tr>"
@@ -150,7 +150,7 @@
if(H.ckey)
dat += "<tr><td>[H]</td><td>[H.dna.unique_enzymes]</td><td>[H.dna.blood_type]</td></tr>"
dat += "</table>"
holder << browse(dat, "window=DNA;size=440x410")
holder << HTML_SKELETON(dat), "window=DNA;size=440x410")
if("fingerprints")
var/dat = "<B>Showing Fingerprints.</B><HR>"
dat += "<table cellspacing=5><tr><th>Name</th><th>Fingerprints</th></tr>"
@@ -159,7 +159,7 @@
if(H.ckey)
dat += "<tr><td>[H]</td><td>[md5(H.dna.uni_identity)]</td></tr>"
dat += "</table>"
holder << browse(dat, "window=fingerprints;size=440x410")
holder << HTML_SKELETON(dat), "window=fingerprints;size=440x410")
if("ctfbutton")
toggle_all_ctf(holder)
if("tdomereset")
@@ -39,7 +39,7 @@
dat += "<I>Your apprentice is training to cast spells without their robes. They know Knock and Mindswap.</I><BR>"
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_MARTIAL]'>Martial Artist</a><BR>"
dat += "<I>Your apprentice is training in ancient martial arts. They know an Inner Mantra and the Nuclear Fist technique.</I><BR>"
user << browse(dat, "window=radio")
user << HTML_SKELETON(dat), "window=radio")
onclose(user, "radio")
return
@@ -31,7 +31,7 @@
dat += "<B>Charge progress: [reload]/[reload_cooldown]:</B><BR>"
dat += "<A href='byond://?src=[REF(src)];fire=1'>Open Fire</A><BR>"
dat += "Deployment of weapon authorized by <br>Nanotrasen Naval Command<br><br>Remember, friendly fire is grounds for termination of your contract and life.<HR>"
user << browse(dat, "window=scroll")
user << HTML_SKELETON(dat), "window=scroll")
onclose(user, "scroll")
/obj/machinery/artillerycontrol/Topic(href, href_list)
+1 -1
View File
@@ -106,7 +106,7 @@
dat+="<BR><I>There is a small scribble near the end of this page... It reads: \"[scribble]\"</I>"
dat+= "<HR><DIV STYLE='float:left;'><A href='?src=[REF(src)];prev_page=1'>Previous Page</A></DIV>"
dat+="<BR><HR><div align='center'>[curr_page+1]</div>"
human_user << browse(dat, "window=newspaper_main;size=300x400")
human_user << HTML_SKELETON(dat), "window=newspaper_main;size=300x400")
onclose(human_user, "newspaper_main")
else
to_chat(user, "The paper is full of unintelligible symbols!")
+1 -1
View File
@@ -69,7 +69,7 @@
for(var/obj/item/I in src)
dat += "<A href='?src=[REF(src)];remove=[REF(I)]'>Remove</A> - <A href='?src=[REF(src)];read=[REF(I)]'>[I.name]</A><BR>"
user << browse(dat, "window=folder")
user << HTML_SKELETON(dat), "window=folder")
onclose(user, "folder")
add_fingerprint(usr)
+1 -1
View File
@@ -297,7 +297,7 @@
dat += "- <A href='?src=[REF(src)];strengthdown=1'>--</A>|<A href='?src=[REF(src)];strengthup=1'>++</A><BR><BR>"
user << browse(dat, "window=AMcontrol;size=420x500")
user << HTML_SKELETON(dat), "window=AMcontrol;size=420x500")
onclose(user, "AMcontrol")
return
+1 -1
View File
@@ -317,7 +317,7 @@
dat += "</tr></table><br>Current Selection: [currTag ? GLOB.TAGGERLOCATIONS[currTag] : "None"]</tt>"
user << browse(dat, "window=destTagScreen;size=450x350")
user << HTML_SKELETON(dat), "window=destTagScreen;size=450x350")
onclose(user, "destTagScreen")
/obj/item/dest_tagger/attack_self(mob/user)
+1 -1
View File
@@ -91,7 +91,7 @@
if(!window.is_ready())
window.initialize(
fancy = user.client.prefs.tgui_fancy,
inline_assets = list(
assets = list(
get_asset_datum(/datum/asset/simple/tgui),
))
else
+8 -9
View File
@@ -21,7 +21,7 @@
/**
* sent asset packs (citrp asset backend)
*/
var/list/datum/asset_pack/sent_assets = list()
var/list/datum/asset/sent_assets = list()
// Vars passed to initialize proc (and saved for later)
var/initial_strict_mode
var/initial_fancy
@@ -106,8 +106,8 @@
html = replacetextEx(html, "\[tgui:strictMode]", strict_mode)
// Inject assets
var/inline_assets_str = ""
for(var/datum/asset_pack/asset as anything in assets)
asset = SSassets.ready_asset_pack(asset)
for(var/datum/asset/asset as anything in assets)
asset.send(client)
var/mappings = asset.get_url_mappings()
for(var/name in mappings)
var/url = mappings[name]
@@ -116,11 +116,10 @@
inline_assets_str += "Byond.loadCss('[url]', true);\n"
else if(copytext(name, -3) == ".js")
inline_assets_str += "Byond.loadJs('[url]', true);\n"
SSassets.send_asset_pack(client, asset)
// incase they logged out
if(!client)
return
client.asset_cache_flush_browse_queue() // flush their assets
client.browse_queue_flush()
// incase they logged out
if(!client)
return
@@ -321,7 +320,6 @@
/datum/tgui_window/proc/send_asset(datum/asset/asset)
asset.send(client)
sent_assets |= asset
. = SSassets.send_asset_pack(client, asset)
if(istype(asset, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = asset
send_message("asset/stylesheet", spritesheet.css_filename())
@@ -399,12 +397,13 @@
client << link(href_list["url"])
if("cacheReloaded")
reinitialize()
if("chat/resend")
SSchat.handle_resend(client, payload)
// 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 <= CONFIG_GET(number/tgui_max_chunk_count)
// var/permit_payload = chunk_count <= CONFIG_GET(number/tgui_max_chunk_count)
var/permit_payload = chunk_count <= 10
if(permit_payload)
create_oversized_payload(payload_id, payload["type"], chunk_count)
send_message("oversizePayloadResponse", list("allow" = permit_payload, "id" = payload_id))
+1 -1
View File
@@ -42,7 +42,7 @@
sleep(1)
initialized_at = world.time
// Perform a clean initialization
window.initialize(inline_assets = list(
window.initialize(assets = list(
get_asset_datum(/datum/asset/simple/tgui_panel),
))
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
@@ -5,4 +5,4 @@
if(!GLOB.mentorlog.len)
dat += "No mentors have done anything this round!"
usr << browse(dat, "window=mentor_log")
usr << browse(HTML_SKELETON(dat), "window=mentor_log")
+197 -142
View File
@@ -4,147 +4,202 @@
* @license MIT
*/
declare global {
// Webpack asset modules.
// Should match extensions used in webpack config.
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
type ByondType = {
/**
* True if javascript is running in BYOND.
*/
IS_BYOND: boolean;
/**
* Makes a BYOND call.
*
* If path is empty, this will trigger a Topic call.
* You can reference a specific object by setting the "src" parameter.
*
* See: https://secure.byond.com/docs/ref/skinparams.html
*/
call(path: string, params: object): void;
/**
* Makes an asynchronous BYOND call. Returns a promise.
*/
callAsync(path: string, params: object): Promise<any>;
/**
* Makes a Topic call.
*
* You can reference a specific object by setting the "src" parameter.
*/
topic(params: object): void;
/**
* Runs a command or a verb.
*/
command(command: string): void;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string): Promise<object>;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string, propName: '*'): Promise<object>;
/**
* Retrieves an exactly one property of the BYOND skin element,
* as defined in `propName`.
*
* Returns a promise with the value of that property.
*/
winget(id: string, propName: string): Promise<any>;
/**
* Retrieves multiple properties of the BYOND skin element,
* as defined in the `propNames` array.
*
* Returns a promise with a key-value object containing listed properties.
*/
winget(id: string, propNames: string[]): Promise<object>;
/**
* Assigns properties to BYOND skin elements.
*/
winset(props: object): void;
/**
* Assigns properties to the BYOND skin element.
*/
winset(id: string, props: object): void;
/**
* Sets a property on the BYOND skin element to a certain value.
*/
winset(id: string, propName: string, propValue: any): void;
/**
* Parses BYOND JSON.
*
* Uses a special encoding to preverse Infinity and NaN.
*/
parseJson(text: string): any;
/**
* Loads a stylesheet into the document.
*/
loadCss(url: string): void;
/**
* Loads a script into the document.
*/
loadJs(url: string): void;
};
/**
* Object that provides access to Byond Skin API and is available in
* any tgui application.
*/
const Byond: ByondType;
interface Window {
/**
* ID of the Byond window this script is running on.
* Should be used as a parameter to winget/winset.
*/
__windowId__: string;
Byond: ByondType;
__store__: Store<unknown, AnyAction>;
__augmentStack__: (store: Store) => StackAugmentor;
// IE IndexedDB stuff.
msIndexedDB: IDBFactory;
msIDBTransaction: IDBTransaction;
// 516 byondstorage API.
hubStorage: Storage;
domainStorage: Storage;
serverStorage: Storage;
}
// Webpack asset modules.
// Should match extensions used in webpack config.
declare module '*.png' {
const content: string;
export default content;
}
export {};
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
namespace JSX {
interface IntrinsicElements {
marquee: any;
blink: any;
}
}
type TguiMessage = {
type: string;
payload?: any;
[key: string]: any;
};
type ByondType = {
/**
* ID of the Byond window this script is running on.
* Can be used as a parameter to winget/winset.
*/
windowId: string;
/**
* True if javascript is running in BYOND.
*/
IS_BYOND: boolean;
/**
* Version of Blink engine of WebView2. Null if N/A.
*/
BLINK: number | null;
/**
* If `true`, unhandled errors and common mistakes result in a blue screen
* of death, which stops this window from handling incoming messages and
* closes the active instance of tgui datum if there was one.
*
* It can be defined in window.initialize() in DM, or changed in runtime
* here via this property to `true` or `false`.
*
* It is recommended that you keep this ON to detect hard to find bugs.
*/
strictMode: boolean;
/**
* Makes a BYOND call.
*
* If path is empty, this will trigger a Topic call.
* You can reference a specific object by setting the "src" parameter.
*
* See: https://secure.byond.com/docs/ref/skinparams.html
*/
call(path: string, params: object): void;
/**
* Makes an asynchronous BYOND call. Returns a promise.
*/
callAsync(path: string, params: object): Promise<any>;
/**
* Makes a Topic call.
*
* You can reference a specific object by setting the "src" parameter.
*/
topic(params: object): void;
/**
* Runs a command or a verb.
*/
command(command: string): void;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string | null): Promise<Record<string, any>>;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string | null, propName: '*'): Promise<Record<string, any>>;
/**
* Retrieves an exactly one property of the BYOND skin element,
* as defined in `propName`.
*
* Returns a promise with the value of that property.
*/
winget(id: string | null, propName: string): Promise<any>;
/**
* Retrieves multiple properties of the BYOND skin element,
* as defined in the `propNames` array.
*
* Returns a promise with a key-value object containing listed properties.
*/
winget(id: string | null, propNames: string[]): Promise<Record<string, any>>;
/**
* Assigns properties to BYOND skin elements in bulk.
*/
winset(props: object): void;
/**
* Assigns properties to the BYOND skin element.
*/
winset(id: string | null, props: object): void;
/**
* Sets a property on the BYOND skin element to a certain value.
*/
winset(id: string | null, propName: string, propValue: any): void;
/**
* Parses BYOND JSON.
*
* Uses a special encoding to preserve `Infinity` and `NaN`.
*/
parseJson(text: string): any;
/**
* Sends a message to `/datum/tgui_window` which hosts this window instance.
*/
sendMessage(type: string, payload?: any): void;
sendMessage(message: TguiMessage): void;
/**
* Subscribe to incoming messages that were sent from `/datum/tgui_window`.
*/
subscribe(listener: (type: string, payload: any) => void): void;
/**
* Subscribe to incoming messages *of some specific type*
* that were sent from `/datum/tgui_window`.
*/
subscribeTo(type: string, listener: (payload: any) => void): void;
/**
* Loads a stylesheet into the document.
*/
loadCss(url: string): void;
/**
* Loads a script into the document.
*/
loadJs(url: string): void;
/**
* Maps icons to their ref
*/
iconRefMap: Record<string, string>;
/**
* Downloads a blob, platform-agnostic
*/
saveBlob(blob: Blob, filename: string, ext: string): void;
};
/**
* Object that provides access to Byond Skin API and is available in
* any tgui application.
*/
const Byond: ByondType;
interface Window {
Byond: ByondType;
__store__: Store<unknown, AnyAction>;
__augmentStack__: (store: Store) => StackAugmentor;
// IE IndexedDB stuff.
msIndexedDB: IDBFactory;
msIDBTransaction: IDBTransaction;
// 516 byondstorage API.
hubStorage: Storage;
domainStorage: Storage;
serverStorage: Storage;
// TGUI stuff
__chatRenderer__: ChatRenderer;
}
+31 -26
View File
@@ -5,42 +5,47 @@
"workspaces": [
"packages/*"
],
"scripts": {
"tgui:analyze": "rspack --analyze",
"tgui:bench": "rspack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js",
"tgui:build": "rspack build",
"tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js",
"tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx",
"tgui:prettier": "prettier --check .",
"tgui:sonar": "eslint packages -c .eslintrc-sonar.yml",
"tgui:test": "vitest",
"tgui:test-simple": "CI=true vitest",
"tgui:test-ci": "CI=true vitest",
"tgui:tsc": "tsc",
"tgui:prettier-fix": "prettier --write .",
"tgui:eslint-fix": "eslint --fix packages --ext .js,.cjs,.ts,.jsx,.tsx"
},
"dependencies": {
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-jscript": "^7.27.1",
"@babel/preset-env": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@types/jest": "^27.5.2",
"@types/jsdom": "^16.2.15",
"@types/node": "^14.18.63",
"@typescript-eslint/parser": "^4.33.0",
"babel-jest": "^27.5.1",
"babel-loader": "^8.4.1",
"babel-plugin-inferno": "^6.8.4",
"babel-plugin-transform-remove-console": "^6.9.4",
"@rspack/cli": "^1.6.8",
"@rspack/core": "^1.6.8",
"@types/jest": "^30.0.0",
"@types/jsdom": "^27.0.0",
"@types/node": "^25.0.3",
"@typescript-eslint/parser": "^8.50.0",
"common": "workspace:*",
"css-loader": "^5.2.7",
"eslint": "^7.32.0",
"css-loader": "^7.1.2",
"eslint": "^9.39.2",
"eslint-plugin-radar": "^0.2.1",
"eslint-plugin-react": "^7.37.5",
"file-loader": "^6.2.0",
"inferno": "^7.4.11",
"jest": "^27.5.1",
"jest-circus": "^27.5.1",
"inferno": "^9.0.8",
"jest": "^30.2.0",
"jest-circus": "^30.2.0",
"jsdom": "^27.3.0",
"katex": "^0.15.6",
"mini-css-extract-plugin": "^1.6.2",
"katex": "^0.16.27",
"mini-css-extract-plugin": "^2.9.4",
"sass": "^1.97.1",
"sass-loader": "^16.0.6",
"style-loader": "^2.0.0",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.16",
"tgui-core": "^5.6.0",
"typescript": "^5.9.3",
"url-loader": "^4.1.1",
"webpack": "^5.104.1",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^4.10.0"
"url-loader": "^4.1.1"
},
"packageManager": "yarn@3.4.1"
}
-153
View File
@@ -1,153 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { compose } from './fp';
/**
* Creates a Redux store.
*/
export const createStore = (reducer, enhancer) => {
// Apply a store enhancer (applyMiddleware is one of them).
if (enhancer) {
return enhancer(createStore)(reducer);
}
let currentState;
let listeners = [];
const getState = () => currentState;
const subscribe = listener => {
listeners.push(listener);
};
const dispatch = action => {
currentState = reducer(currentState, action);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
};
// This creates the initial store by causing each reducer to be called
// with an undefined state
dispatch({
type: '@@INIT',
});
return {
dispatch,
subscribe,
getState,
};
};
/**
* Creates a store enhancer which applies middleware to all dispatched
* actions.
*/
export const applyMiddleware = (...middlewares) => {
return createStore => (reducer, ...args) => {
const store = createStore(reducer, ...args);
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed.');
};
const storeApi = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
const chain = middlewares.map(middleware => middleware(storeApi));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
};
/**
* Combines reducers by running them in their own object namespaces as
* defined in reducersObj paramter.
*
* Main difference from redux/combineReducers is that it preserves keys
* in the state that are not present in the reducers object. This function
* is also more flexible than the redux counterpart.
*/
export const combineReducers = reducersObj => {
const keys = Object.keys(reducersObj);
let hasChanged = false;
return (prevState = {}, action) => {
const nextState = { ...prevState };
for (let key of keys) {
const reducer = reducersObj[key];
const prevDomainState = prevState[key];
const nextDomainState = reducer(prevDomainState, action);
if (prevDomainState !== nextDomainState) {
hasChanged = true;
nextState[key] = nextDomainState;
}
}
return hasChanged
? nextState
: prevState;
};
};
/**
* A utility function to create an action creator for the given action
* type string. The action creator accepts a single argument, which will
* be included in the action object as a field called payload. The action
* creator function will also have its toString() overriden so that it
* returns the action type, allowing it to be used in reducer logic that
* is looking for that action type.
*
* @param {string} type The action type to use for created actions.
* @param {any} prepare (optional) a method that takes any number of arguments
* and returns { payload } or { payload, meta }. If this is given, the
* resulting action creator will pass it's arguments to this method to
* calculate payload & meta.
*
* @public
*/
export const createAction = (type, prepare = null) => {
const actionCreator = (...args) => {
if (!prepare) {
return { type, payload: args[0] };
}
const prepared = prepare(...args);
if (!prepared) {
throw new Error('prepare function did not return an object');
}
const action = { type };
if ('payload' in prepared) {
action.payload = prepared.payload;
}
if ('meta' in prepared) {
action.meta = prepared.meta;
}
return action;
};
actionCreator.toString = () => '' + type;
actionCreator.type = type;
actionCreator.match = action => action.type === type;
return actionCreator;
};
// Implementation specific
// --------------------------------------------------------
export const useDispatch = context => {
return context.store.dispatch;
};
export const useSelector = (context, selector) => {
return selector(context.store.getState());
};
+196
View File
@@ -0,0 +1,196 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export type Reducer<State = any, ActionType extends Action = AnyAction> = (
state: State | undefined,
action: ActionType,
) => State;
export type Store<State = any, ActionType extends Action = AnyAction> = {
dispatch: Dispatch<ActionType>;
subscribe: (listener: () => void) => void;
getState: () => State;
};
type MiddlewareAPI<State = any, ActionType extends Action = AnyAction> = {
getState: () => State;
dispatch: Dispatch<ActionType>;
};
export type Middleware = <State = any, ActionType extends Action = AnyAction>(
storeApi: MiddlewareAPI<State, ActionType>,
) => (next: Dispatch<ActionType>) => Dispatch<ActionType>;
export type Action<TType = any> = {
type: TType;
};
export type AnyAction = Action & {
[extraProps: string]: any;
};
export type Dispatch<ActionType extends Action = AnyAction> = (
action: ActionType,
) => void;
type StoreEnhancer = (createStoreFunction: Function) => Function;
type PreparedAction = {
payload?: any;
meta?: any;
};
/**
* Creates a Redux store.
*/
export const createStore = <State, ActionType extends Action = AnyAction>(
reducer: Reducer<State, ActionType>,
enhancer?: StoreEnhancer,
): Store<State, ActionType> => {
// Apply a store enhancer (applyMiddleware is one of them).
if (enhancer) {
return enhancer(createStore)(reducer);
}
let currentState: State;
let listeners: Array<() => void> = [];
const getState = (): State => currentState;
const subscribe = (listener: () => void): void => {
listeners.push(listener);
};
const dispatch = (action: ActionType): void => {
currentState = reducer(currentState, action);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
};
// This creates the initial store by causing each reducer to be called
// with an undefined state
dispatch({ type: '@@INIT' } as ActionType);
return {
dispatch,
subscribe,
getState,
};
};
/**
* Creates a store enhancer which applies middleware to all dispatched
* actions.
*/
export const applyMiddleware = (
...middlewares: Middleware[]
): StoreEnhancer => {
return (
createStoreFunction: (reducer: Reducer, enhancer?: StoreEnhancer) => Store,
) => {
return (reducer, ...args): Store => {
const store = createStoreFunction(reducer, ...args);
let dispatch: Dispatch = (action, ...args) => {
throw new Error(
'Dispatching while constructing your middleware is not allowed.',
);
};
const storeApi: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
const chain = middlewares.map((middleware) => middleware(storeApi));
dispatch = chain.reduceRight(
(next, middleware) => middleware(next),
store.dispatch,
);
return {
...store,
dispatch,
};
};
};
};
/**
* Combines reducers by running them in their own object namespaces as
* defined in reducersObj paramter.
*
* Main difference from redux/combineReducers is that it preserves keys
* in the state that are not present in the reducers object. This function
* is also more flexible than the redux counterpart.
*/
export const combineReducers = (
reducersObj: Record<string, Reducer>,
): Reducer => {
const keys = Object.keys(reducersObj);
return (prevState = {}, action) => {
const nextState = { ...prevState };
let hasChanged = false;
for (const key of keys) {
const reducer = reducersObj[key];
const prevDomainState = prevState[key];
const nextDomainState = reducer(prevDomainState, action);
if (prevDomainState !== nextDomainState) {
hasChanged = true;
nextState[key] = nextDomainState;
}
}
return hasChanged ? nextState : prevState;
};
};
/**
* A utility function to create an action creator for the given action
* type string. The action creator accepts a single argument, which will
* be included in the action object as a field called payload. The action
* creator function will also have its toString() overriden so that it
* returns the action type, allowing it to be used in reducer logic that
* is looking for that action type.
*
* @param {string} type The action type to use for created actions.
* @param {any} prepare (optional) a method that takes any number of arguments
* and returns { payload } or { payload, meta }. If this is given, the
* resulting action creator will pass its arguments to this method to
* calculate payload & meta.
*
* @public
*/
export const createAction = <TAction extends string>(
type: TAction,
prepare?: (...args: any[]) => PreparedAction,
) => {
const actionCreator = (...args: any[]) => {
let action: Action<TAction> & PreparedAction = { type };
if (prepare) {
const prepared = prepare(...args);
if (!prepared) {
throw new Error('prepare function did not return an object');
}
action = { ...action, ...prepared };
} else {
action.payload = args[0];
}
return action;
};
actionCreator.toString = () => type;
actionCreator.type = type;
actionCreator.match = (action) => action.type === type;
return actionCreator;
};
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "tgfont",
"version": "1.0.0",
"dependencies": {
"fantasticon": "^1.2.3"
"fantasticon": "^3.0.0"
},
"scripts": {
"build": "node mkdist.cjs && fantasticon --config config.cjs"
+2 -2
View File
@@ -5,9 +5,9 @@
"type": "module",
"dependencies": {
"axios": "^1.13.2",
"glob": "^7.2.3",
"glob": "^13.0.0",
"source-map": "^0.7.6",
"stacktrace-parser": "^0.1.11",
"ws": "^7.5.10"
"ws": "^8.18.3"
}
}
+2 -2
View File
@@ -4,8 +4,8 @@
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.5.8",
"inferno": "^7.4.11",
"dompurify": "^3.3.1",
"inferno": "^9.0.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*"
}
+1 -1
View File
@@ -9,6 +9,6 @@
},
"dependencies": {
"clean-css-cli": "^5.6.3",
"terser": "^5.39.0"
"terser": "^5.44.1"
}
}
+6 -6
View File
@@ -5,13 +5,13 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"common": "workspace:*",
"dateformat": "^4.6.3",
"dompurify": "^2.5.8",
"inferno": "^7.4.11",
"inferno-vnode-flags": "^7.4.11",
"dateformat": "^5.0.3",
"dompurify": "^3.3.1",
"inferno": "^9.0.8",
"inferno-vnode-flags": "^9.0.8",
"js-yaml": "^4.1.1",
"katex": "^0.15.6",
"marked": "^4.3.0",
"katex": "^0.16.27",
"marked": "^17.0.1",
"tgui-dev-server": "workspace:*"
}
}
-93
View File
@@ -1,93 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { flow } from 'common/fp';
import { applyMiddleware, combineReducers, createStore } from 'common/redux';
import { Component } from 'inferno';
import { assetMiddleware } from './assets';
import { backendMiddleware, backendReducer } from './backend';
import { debugMiddleware, debugReducer, relayMiddleware } from './debug';
import { createLogger } from './logging';
const logger = createLogger('store');
export const configureStore = (options = {}) => {
const reducer = flow([
combineReducers({
debug: debugReducer,
backend: backendReducer,
}),
options.reducer,
]);
const middleware = [
...(options.middleware?.pre || []),
assetMiddleware,
backendMiddleware,
...(options.middleware?.post || []),
];
if (process.env.NODE_ENV !== 'production') {
middleware.unshift(
loggingMiddleware,
debugMiddleware,
relayMiddleware);
}
const enhancer = applyMiddleware(...middleware);
const store = createStore(reducer, enhancer);
// Globals
window.__store__ = store;
window.__augmentStack__ = createStackAugmentor(store);
return store;
};
const loggingMiddleware = store => next => action => {
const { type, payload } = action;
if (type === 'update' || type === 'backend/update') {
logger.debug('action', { type });
}
else {
logger.debug('action', action);
}
return next(action);
};
/**
* Creates a function, which can be assigned to window.__augmentStack__
* to augment reported stack traces with useful data for debugging.
*/
const createStackAugmentor = store => (stack, error) => {
if (!error) {
error = new Error(stack.split('\n')[0]);
error.stack = stack;
}
else if (typeof error === 'object' && !error.stack) {
error.stack = stack;
}
logger.log('FatalError:', error);
const state = store.getState();
const config = state?.backend?.config;
let augmentedStack = stack;
augmentedStack += '\nUser Agent: ' + navigator.userAgent;
augmentedStack += '\nState: ' + JSON.stringify({
ckey: config?.client?.ckey,
interface: config?.interface,
window: config?.window,
});
return augmentedStack;
};
/**
* Store provider for Inferno apps.
*/
export class StoreProvider extends Component {
getChildContext() {
const { store } = this.props;
return { store };
}
render() {
return this.props.children;
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import {
applyMiddleware,
combineReducers,
createStore,
Middleware,
Reducer,
Store,
} from 'common/redux';
import { flow } from 'tgui-core/fp';
import { assetMiddleware } from './assets';
import { backendMiddleware, backendReducer } from './backend';
import { debugMiddleware, debugReducer, relayMiddleware } from './debug';
import { createLogger } from './logging';
type ConfigureStoreOptions = {
sideEffects?: boolean;
reducer?: Reducer;
middleware?: {
pre?: Middleware[];
post?: Middleware[];
};
};
type StackAugmentor = (stack: string, error?: Error) => string;
const logger = createLogger('store');
export const configureStore = (options: ConfigureStoreOptions = {}): Store => {
const { sideEffects = true, reducer, middleware } = options;
const rootReducer: Reducer = flow([
combineReducers({
debug: debugReducer,
backend: backendReducer,
}),
reducer as any,
]);
const middlewares: Middleware[] = !sideEffects
? []
: [
...(middleware?.pre || []),
assetMiddleware,
backendMiddleware,
...(middleware?.post || []),
];
if (process.env.NODE_ENV !== 'production') {
// We are using two if statements because Webpack is capable of
// removing this specific block as dead code.
if (sideEffects) {
middlewares.unshift(loggingMiddleware, debugMiddleware, relayMiddleware);
}
}
const enhancer = applyMiddleware(...middlewares);
const store = createStore(rootReducer, enhancer);
// Globals
window.__store__ = store;
window.__augmentStack__ = createStackAugmentor(store);
return store;
};
const loggingMiddleware: Middleware = (store) => (next) => (action) => {
const { type } = action;
logger.debug(
'action',
type === 'update' || type === 'backend/update' ? { type } : action,
);
return next(action);
};
/**
* Creates a function, which can be assigned to window.__augmentStack__
* to augment reported stack traces with useful data for debugging.
*/
const createStackAugmentor
= (store: Store): StackAugmentor =>
(stack, error) => {
error = error || new Error(stack.split('\n')[0]);
error.stack = error.stack || stack;
logger.log('FatalError:', error);
const state = store.getState();
const config = state?.backend?.config;
return (
stack
+ '\nUser Agent: '
+ navigator.userAgent
+ '\nState: '
+ JSON.stringify({
ckey: config?.client?.ckey,
interface: config?.interface,
window: config?.window,
})
);
};
+119
View File
@@ -0,0 +1,119 @@
const { defineConfig } = require('@rspack/cli');
const { rspack } = require('@rspack/core');
const createStats = (verbose) => ({
assets: verbose,
builtAt: verbose,
cached: false,
children: false,
chunks: false,
colors: true,
entrypoints: true,
hash: false,
modules: false,
performance: false,
timings: verbose,
version: verbose,
});
/**
* 04/25/2025
* There is a bug in rspack, possibly only ours, with the experimental css
* feature that throws an error in tgui-dev. This prevents hot reloading from
* working properly and there doesn't seem to be any way to fix it.
*
* This config exists to switch to the old css loader during development.
*
* `TypeError: Cannot read properties of null (reading 'removeChild')`
*/
module.exports = (env = {}, argv) => {
/** @type {import('@rspack/core').Configuration} */
const config = defineConfig({
cache: false,
experiments: undefined,
mode: 'development',
module: {
rules: [
{
test: /\.([tj]s(x)?|cjs)$/,
use: [
{
loader: 'builtin:swc-loader',
options: {
isModule: 'unknown',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
},
],
type: 'javascript/auto',
},
{
test: /\.(s)?css$/,
use: [
{
loader: rspack.CssExtractRspackPlugin.loader,
},
{
loader: require.resolve('css-loader'),
},
{
loader: require.resolve('sass-loader'),
options: {
api: 'modern-compiler',
implementation: 'sass-embedded',
},
},
],
type: 'javascript/auto',
},
{
test: /\.(png|jpg)$/,
type: 'asset/resource',
},
{
test: /\.svg$/,
oneOf: [
{
issuer: /\.(s)?css$/,
type: 'asset/inline',
},
{
type: 'asset/resource',
},
],
},
],
},
plugins: [
new rspack.EnvironmentPlugin({
NODE_ENV: 'development',
WEBPACK_HMR_ENABLED: env.WEBPACK_HMR_ENABLED || argv.hot || false,
DEV_SERVER_IP: env.DEV_SERVER_IP || null,
}),
new rspack.CssExtractRspackPlugin({
filename: '[name].bundle.css',
chunkFilename: '[name].bundle.css',
}),
],
});
config.devtool = 'cheap-module-source-map';
config.devServer = {
progress: false,
quiet: false,
noInfo: false,
clientLogLevel: 'silent',
stats: createStats(false),
};
return config;
};
+168
View File
@@ -0,0 +1,168 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const path = require('path');
const { defineConfig } = require('@rspack/cli');
const { rspack } = require('@rspack/core');
const createStats = (verbose) => ({
assets: verbose,
builtAt: verbose,
cached: false,
children: false,
chunks: false,
colors: true,
entrypoints: true,
hash: false,
modules: false,
performance: false,
timings: verbose,
version: verbose,
});
module.exports = (env = {}, argv) => {
const mode = argv.mode || 'production';
const bench = env.TGUI_BENCH;
/** @type {import('@rspack/core').Configuration} */
const config = defineConfig({
cache: true,
experiments: {
cache: {
type: 'persistent',
storage: {
type: 'filesystem',
directory: path.resolve(__dirname, '.yarn/rspack'),
},
},
css: true,
},
mode: mode === 'production' ? 'production' : 'development',
context: path.resolve(__dirname),
target: ['web', 'browserslist:edge >= 123'],
entry: {
tgui: ['./packages/tgui'],
'tgui-panel': ['./packages/tgui-panel'],
},
output: {
path: argv.useTmpFolder
? path.resolve(__dirname, './public/.tmp')
: path.resolve(__dirname, './public'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
chunkLoadTimeout: 15000,
publicPath: '/',
assetModuleFilename: '[name][ext]',
},
resolve: {
pnp: true,
extensions: ['.tsx', '.ts', '.js', '.jsx'],
alias: {
tgui: path.resolve(__dirname, './packages/tgui'),
'tgui-panel': path.resolve(__dirname, './packages/tgui-panel'),
'tgui-dev-server': path.resolve(
__dirname,
'./packages/tgui-dev-server',
),
},
},
module: {
rules: [
{
test: /\.([tj]s(x)?|cjs)$/,
use: [
{
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
},
],
type: 'javascript/auto',
},
{
test: /\.(s)?css$/,
use: [
{
loader: require.resolve('sass-loader'),
options: {
api: 'modern-compiler',
implementation: 'sass-embedded',
},
},
],
type: 'css',
},
{
test: /\.(png|jpg)$/,
type: 'asset/resource',
},
{
test: /\.svg$/,
oneOf: [
{
issuer: /\.(s)?css$/,
type: 'asset/inline',
},
{
type: 'asset/resource',
},
],
},
],
},
optimization: {
emitOnErrors: false,
},
performance: {
hints: false,
},
devtool: false,
stats: createStats(true),
plugins: [
new rspack.EnvironmentPlugin({
NODE_ENV: env.NODE_ENV || mode,
WEBPACK_HMR_ENABLED: env.WEBPACK_HMR_ENABLED || argv.hot || false,
DEV_SERVER_IP: env.DEV_SERVER_IP || null,
}),
],
});
if (bench) {
config.entry = {
'tgui-bench': ['./packages/tgui-bench/entrypoint'],
};
}
// Development build specific options
if (mode !== 'production') {
config.devtool = 'cheap-module-source-map';
}
// Development server specific options
if (argv.devServer) {
config.devServer = {
progress: false,
quiet: false,
noInfo: false,
clientLogLevel: 'silent',
stats: createStats(false),
};
}
return config;
};
+19 -26
View File
@@ -1,28 +1,21 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"checkJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": false,
"jsx": "preserve",
"lib": [
"DOM",
"DOM.Iterable",
"ESNext",
"ScriptHost"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": false,
"target": "ES5"
},
"include": [
"./*.d.ts",
"./packages"
]
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"checkJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "preserve",
"lib": ["DOM", "DOM.Iterable", "ESNext", "ScriptHost"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"target": "ESNext"
},
"include": ["./*.d.ts", "./packages"]
}
-168
View File
@@ -1,168 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const webpack = require('webpack');
const path = require('path');
const ExtractCssPlugin = require('mini-css-extract-plugin');
const { createBabelConfig } = require('./babel.config.js');
const createStats = verbose => ({
assets: verbose,
builtAt: verbose,
cached: false,
children: false,
chunks: false,
colors: true,
entrypoints: true,
hash: false,
modules: false,
performance: false,
timings: verbose,
version: verbose,
});
module.exports = (env = {}, argv) => {
const mode = argv.mode === 'production' ? 'production' : 'development';
const config = {
mode,
context: path.resolve(__dirname),
target: ['web', 'es5', 'browserslist:ie 11'],
entry: {
'tgui': [
'./packages/tgui',
],
'tgui-panel': [
'./packages/tgui-panel',
],
},
output: {
path: argv.useTmpFolder
? path.resolve(__dirname, './public/.tmp')
: path.resolve(__dirname, './public'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
chunkLoadTimeout: 15000,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {},
},
module: {
rules: [
{
test: /\.(js|cjs|ts|tsx)$/,
use: [
{
loader: require.resolve('babel-loader'),
options: createBabelConfig({ mode }),
},
],
},
{
test: /\.scss$/,
use: [
{
loader: ExtractCssPlugin.loader,
options: {
esModule: false,
},
},
{
loader: require.resolve('css-loader'),
options: {
esModule: false,
},
},
{
loader: require.resolve('sass-loader'),
},
],
},
{
test: /\.(png|jpg|svg)$/,
use: [
{
loader: require.resolve('url-loader'),
options: {
esModule: false,
},
},
],
},
],
},
optimization: {
emitOnErrors: false,
},
performance: {
hints: false,
},
devtool: false,
cache: {
type: 'filesystem',
cacheLocation: path.resolve(__dirname, `.yarn/webpack/${mode}`),
buildDependencies: {
config: [__filename],
},
},
stats: createStats(true),
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: env.NODE_ENV || argv.mode || 'development',
WEBPACK_HMR_ENABLED: env.WEBPACK_HMR_ENABLED || argv.hot || false,
DEV_SERVER_IP: env.DEV_SERVER_IP || null,
}),
new ExtractCssPlugin({
filename: '[name].bundle.css',
chunkFilename: '[name].bundle.css',
}),
],
};
// Add a bundle analyzer to the plugins array
if (argv.analyze) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
config.plugins = [
...config.plugins,
new BundleAnalyzerPlugin(),
];
}
// Production build specific options
if (argv.mode === 'production') {
const TerserPlugin = require('terser-webpack-plugin');
config.optimization.minimizer = [
new TerserPlugin({
extractComments: false,
terserOptions: {
ie8: true,
output: {
ascii_only: true,
comments: false,
},
},
}),
];
}
// Development build specific options
if (argv.mode !== 'production') {
config.devtool = 'cheap-module-source-map';
}
// Development server specific options
if (argv.devServer) {
config.devServer = {
progress: false,
quiet: false,
noInfo: false,
clientLogLevel: 'silent',
stats: createStats(false),
};
}
return config;
};
+3823 -4379
View File
File diff suppressed because it is too large Load Diff