diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm
index c430cb82f3d..c4539322e69 100644
--- a/code/__DEFINES/chat.dm
+++ b/code/__DEFINES/chat.dm
@@ -1,31 +1,56 @@
-#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
-#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
-#define CHAT_MESSAGE_EOL_FADE 0.8 SECONDS
-#define CHAT_SPELLING_DELAY 0.02 SECONDS
-#define CHAT_MESSAGE_EXP_DECAY 0.7 // Messages decay at pow(factor, idx in stack)
-#define CHAT_MESSAGE_HEIGHT_DECAY 0.9 // Increase message decay based on the height of the message
-#define CHAT_MESSAGE_APPROX_LHEIGHT 11 // Approximate height in pixels of an 'average' line, used for height decay
-#define CHAT_MESSAGE_WIDTH 96 // pixels
-#define CHAT_MESSAGE_MAX_LENGTH 110 // characters
-#define CHAT_GLORF_LIST list(\
- "-ah!!",\
- "-GLORF!!",\
- "-OW!!"\
- )
+/*!
+ * Copyright (c) 2020 Aleksej Komarov
+ * SPDX-License-Identifier: MIT
+ */
-#define CHAT_SPELLING_PUNCTUATION list(\
- "," = 0.25 SECONDS,\
- "." = 0.4 SECONDS,\
- " " = 0.03 SECONDS,\
- "-" = 0.2 SECONDS,\
- "!" = 0.2 SECONDS,\
- "?" = 0.15 SECONDS,\
- )
+/// How many chat payloads to keep in history
+#define CHAT_RELIABILITY_HISTORY_SIZE 5
+/// How many resends to allow before giving up
+#define CHAT_RELIABILITY_MAX_RESENDS 3
+#define MESSAGE_TYPE_SYSTEM "system"
+#define MESSAGE_TYPE_LOCALCHAT "localchat"
+#define MESSAGE_TYPE_RADIO "radio"
+#define MESSAGE_TYPE_ENTERTAINMENT "entertainment"
+#define MESSAGE_TYPE_INFO "info"
+#define MESSAGE_TYPE_WARNING "warning"
+#define MESSAGE_TYPE_DEADCHAT "deadchat"
+#define MESSAGE_TYPE_OOC "ooc"
+#define MESSAGE_TYPE_ADMINPM "adminpm"
+#define MESSAGE_TYPE_COMBAT "combat"
+#define MESSAGE_TYPE_ADMINCHAT "adminchat"
+#define MESSAGE_TYPE_PRAYER "prayer"
+#define MESSAGE_TYPE_MODCHAT "modchat"
+#define MESSAGE_TYPE_EVENTCHAT "eventchat"
+#define MESSAGE_TYPE_ADMINLOG "adminlog"
+#define MESSAGE_TYPE_ATTACKLOG "attacklog"
+#define MESSAGE_TYPE_DEBUG "debug"
-#define CHAT_SPELLING_EXCEPTIONS list(\
- "'",\
- )
+//debug printing macros (for development and testing)
+/// Used for debug messages to the world
+#define debug_world(msg) if (GLOB.Debug2) to_chat(world, \
+ type = MESSAGE_TYPE_DEBUG, \
+ text = "DEBUG: [msg]")
+/// Used for debug messages to the player
+#define debug_usr(msg) if (GLOB.Debug2 && usr) to_chat(usr, \
+ type = MESSAGE_TYPE_DEBUG, \
+ text = "DEBUG: [msg]")
+/// Used for debug messages to the admins
+#define debug_admins(msg) if (GLOB.Debug2) to_chat(GLOB.admins, \
+ type = MESSAGE_TYPE_DEBUG, \
+ text = "DEBUG: [msg]")
+/// Used for debug messages to the server
+#define debug_world_log(msg) if (GLOB.Debug2) log_world("DEBUG: [msg]")
/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out.
#define boxed_message(str) ("
" + str + "
")
+/// Adds a box around whatever message you're sending in chat. Can apply color and/or additional classes. Available colors: red, green, blue, purple. Use it like red_box
+
+#define custom_boxed_message(classes, str) ("
" + str + "
")
+/// Makes a fieldset with a neaty styled name. Can apply additional classes.
+#define fieldset_block(title, content, classes) ("")
+/// Makes a horizontal line with text in the middle
+#define separator_hr(str) ("
" + str + "
")
+
+/// Emboldens runechat messages
+#define RUNECHAT_BOLD(str) "+[str]+"
diff --git a/code/__DEFINES/chat_message.dm b/code/__DEFINES/chat_message.dm
new file mode 100644
index 00000000000..421991d48c3
--- /dev/null
+++ b/code/__DEFINES/chat_message.dm
@@ -0,0 +1,29 @@
+#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
+#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
+#define CHAT_MESSAGE_EOL_FADE 0.8 SECONDS
+#define CHAT_SPELLING_DELAY 0.02 SECONDS
+#define CHAT_MESSAGE_EXP_DECAY 0.7 // Messages decay at pow(factor, idx in stack)
+#define CHAT_MESSAGE_HEIGHT_DECAY 0.9 // Increase message decay based on the height of the message
+#define CHAT_MESSAGE_APPROX_LHEIGHT 11 // Approximate height in pixels of an 'average' line, used for height decay
+#define CHAT_MESSAGE_WIDTH 96 // pixels
+#define CHAT_MESSAGE_MAX_LENGTH 110 // characters
+
+#define CHAT_GLORF_LIST list(\
+ "-ah!!",\
+ "-GLORF!!",\
+ "-OW!!"\
+)
+
+#define CHAT_SPELLING_PUNCTUATION list(\
+ "," = 0.25 SECONDS,\
+ "." = 0.4 SECONDS,\
+ " " = 0.03 SECONDS,\
+ "-" = 0.2 SECONDS,\
+ "!" = 0.2 SECONDS,\
+ "?" = 0.15 SECONDS,\
+)
+
+
+#define CHAT_SPELLING_EXCEPTIONS list(\
+ "'",\
+)
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 7d3861d08c1..5ddd905ef9c 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -286,12 +286,6 @@ GLOBAL_LIST_INIT(pda_styles, sortList(list(MONO, VT, ORBITRON, SHARE)))
#define SHELTER_DEPLOY_BAD_AREA "bad area"
#define SHELTER_DEPLOY_ANCHORED_OBJECTS "anchored objects"
-//debug printing macros
-#define debug_world(msg) if (GLOB.Debug2) to_chat(world, "DEBUG: [msg]")
-#define debug_usr(msg) if (GLOB.Debug2&&usr) to_chat(usr, "DEBUG: [msg]")
-#define debug_admins(msg) if (GLOB.Debug2) to_chat(GLOB.admins, "DEBUG: [msg]")
-#define debug_world_log(msg) if (GLOB.Debug2) log_world("DEBUG: [msg]")
-
#define INCREMENT_TALLY(L, stat) if(L[stat]){L[stat]++}else{L[stat] = 1}
//TODO Move to a pref
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 7153d6dfbe2..4589b89624f 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -53,7 +53,6 @@
loadmaplist(CONFIG_MAPS_FILE)
LoadMOTD()
LoadPolicy()
- LoadChatFilter()
LoadRelays()
if(Master)
@@ -307,25 +306,6 @@ Example config:
else
log_config("Unknown command in map vote config: '[command]'")
-/datum/controller/configuration/proc/LoadChatFilter()
- var/list/in_character_filter = list()
-
- if(!fexists("[directory]/in_character_filter.txt"))
- return
-
- log_config("Loading config file in_character_filter.txt...")
-
- for(var/line in file2list("[directory]/in_character_filter.txt"))
- if(!line)
- continue
- if(findtextEx(line,"#",1,2))
- continue
- in_character_filter += REGEX_QUOTE(line)
-
- ic_filter_regex = in_character_filter.len ? regex("\\b([jointext(in_character_filter, "|")])\\b", "i") : null
-
- syncChatRegexes()
-
//Message admins when you can.
/datum/controller/configuration/proc/DelayedMessageAdmins(text)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(message_admins), text), 0)
diff --git a/code/controllers/subsystem/chat.dm b/code/controllers/subsystem/chat.dm
index bda4fb57d9b..be39f7559cb 100644
--- a/code/controllers/subsystem/chat.dm
+++ b/code/controllers/subsystem/chat.dm
@@ -1,139 +1,97 @@
+/*!
+ * Copyright (c) 2020 Aleksej Komarov
+ * SPDX-License-Identifier: MIT
+ */
+
SUBSYSTEM_DEF(chat)
name = "Chat"
- flags = SS_TICKER
+ flags = SS_TICKER|SS_NO_INIT
wait = 1
priority = FIRE_PRIORITY_CHAT
- init_order = INIT_ORDER_CHAT
-
- var/list/payload = list()
-
-/datum/controller/subsystem/chat/fire()
- for(var/client/C as anything in payload)
- C << output(payload[C], "browseroutput:output")
- payload -= C
-
- if(MC_TICK_CHECK)
- return
-/datum/controller/subsystem/chat/proc/queue(target, message, handle_whitespace = TRUE)
- if(!target || !message)
- return
-
- if(!istext(message))
- stack_trace("to_chat called with invalid input type")
- return
+ /// Assosciates a ckey with a list of messages to send to them.
+ var/list/list/datum/chat_payload/client_to_payloads = list()
- if(target == world)
- target = GLOB.clients
+ /// Associates a ckey with an assosciative list of their last CHAT_RELIABILITY_HISTORY_SIZE messages.
+ var/list/list/datum/chat_payload/client_to_reliability_history = list()
- //Some macros remain in the string even after parsing and fuck up the eventual output
- var/original_message = message
- message = replacetext(message, "\improper", "")
- message = replacetext(message, "\proper", "")
- if(handle_whitespace)
- message = replacetext(message, "\n", " ")
- message = replacetext(message, "\t", "[FOURSPACES][FOURSPACES]")
- message += " "
+ /// Assosciates a ckey with their next sequence number.
+ var/list/client_to_sequence_number = list()
+/datum/controller/subsystem/chat/proc/generate_payload(client/target, message_data)
+ var/sequence = client_to_sequence_number[target.ckey]
+ client_to_sequence_number[target.ckey] += 1
- //url_encode it TWICE, this way any UTF-8 characters are able to be decoded by the Javascript.
- //Do the double-encoding here to save nanoseconds
- var/twiceEncoded = url_encode(url_encode(message))
+ var/datum/chat_payload/payload = new
+ payload.sequence = sequence
+ payload.content = message_data
- if(islist(target))
- for(var/I in target)
- var/client/C = CLIENT_FROM_VAR(I) //Grab us a client if possible
+ if(!(target.ckey in client_to_reliability_history))
+ client_to_reliability_history[target.ckey] = list()
+ var/list/client_history = client_to_reliability_history[target.ckey]
+ client_history["[sequence]"] = payload
- if(!C)
- return
+ if(length(client_history) > CHAT_RELIABILITY_HISTORY_SIZE)
+ var/oldest = text2num(client_history[1])
+ for(var/index in 2 to length(client_history))
+ var/test = text2num(client_history[index])
+ if(test < oldest)
+ oldest = test
+ client_history -= "[oldest]"
+ return payload
- //Send it to the old style output window.
- SEND_TEXT(C, original_message)
+/datum/controller/subsystem/chat/proc/send_payload_to_client(client/target, datum/chat_payload/payload)
+ target.tgui_panel.window.send_message("chat/message", payload.into_message())
+ SEND_TEXT(target, payload.get_content_as_html())
- if(!C?.chatOutput || C.chatOutput.broken) //A player who hasn't updated his skin file.
- continue
-
- if(!C.chatOutput.loaded) //Client still loading, put their messages in a queue
- C.chatOutput.messageQueue += message
- continue
-
- payload[C] += twiceEncoded
-
- else
- var/client/C = CLIENT_FROM_VAR(target) //Grab us a client if possible
-
- if(!C)
- return
-
- //Send it to the old style output window.
- SEND_TEXT(C, original_message)
+/datum/controller/subsystem/chat/fire()
+ for(var/ckey in client_to_payloads)
+ var/client/target = GLOB.directory[ckey]
+ if(isnull(target)) // verify client still exists
+ LAZYREMOVE(client_to_payloads, ckey)
+ continue
- if(!C?.chatOutput || C.chatOutput.broken) //A player who hasn't updated his skin file.
- return
+ for(var/datum/chat_payload/payload as anything in client_to_payloads[ckey])
+ send_payload_to_client(target, payload)
+ LAZYREMOVE(client_to_payloads, ckey)
- if(!C.chatOutput.loaded) //Client still loading, put their messages in a queue
- C.chatOutput.messageQueue += message
+ if(MC_TICK_CHECK)
return
- payload[C] += twiceEncoded
-
-
-//Global chat procs
-/proc/to_chat_immediate(target, message, handle_whitespace = TRUE)
- if(!target || !message)
+/datum/controller/subsystem/chat/proc/queue(queue_target, list/message_data)
+ var/list/targets = islist(queue_target) ? queue_target : list(queue_target)
+ for(var/target in targets)
+ var/client/client = CLIENT_FROM_VAR(target)
+ if(isnull(client))
+ continue
+ LAZYADDASSOCLIST(client_to_payloads, client.ckey, generate_payload(client, message_data))
+
+/datum/controller/subsystem/chat/proc/send_immediate(send_target, list/message_data)
+ var/list/targets = islist(send_target) ? send_target : list(send_target)
+ for(var/target in targets)
+ var/client/client = CLIENT_FROM_VAR(target)
+ if(isnull(client))
+ continue
+ send_payload_to_client(client, generate_payload(client, message_data))
+
+/datum/controller/subsystem/chat/proc/handle_resend(client/client, sequence)
+ var/list/client_history = client_to_reliability_history[client.ckey]
+ sequence = "[sequence]"
+ if(isnull(client_history) || !(sequence in client_history))
return
- if(target == world)
- target = GLOB.clients
-
- var/original_message = message
- if(handle_whitespace)
- message = replacetext(message, "\n", " ")
- message = replacetext(message, "\t", "[FOURSPACES][FOURSPACES]") //EIGHT SPACES IN TOTAL!!
-
- if(islist(target))
- // Do the double-encoding outside the loop to save nanoseconds
- var/twiceEncoded = url_encode(url_encode(message))
- for(var/I in target)
- var/client/C = CLIENT_FROM_VAR(I) //Grab us a client if possible
-
- if (!C)
- continue
-
- //Send it to the old style output window.
- SEND_TEXT(C, original_message)
-
- if(!C.chatOutput || C.chatOutput.broken) // A player who hasn't updated his skin file.
- continue
-
- if(!C.chatOutput.loaded)
- //Client still loading, put their messages in a queue
- C.chatOutput.messageQueue += message
- continue
-
- C << output(twiceEncoded, "browseroutput:output")
- else
- var/client/C = CLIENT_FROM_VAR(target) //Grab us a client if possible
-
- if (!C)
- return
-
- //Send it to the old style output window.
- SEND_TEXT(C, original_message)
-
- if(!C.chatOutput || C.chatOutput.broken) // A player who hasn't updated his skin file.
- return
-
- if(!C.chatOutput.loaded)
- //Client still loading, put their messages in a queue
- C.chatOutput.messageQueue += message
- return
-
- // url_encode it TWICE, this way any UTF-8 characters are able to be decoded by the Javascript.
- C << output(url_encode(url_encode(message)), "browseroutput:output")
-
-/proc/to_chat(target, message, handle_whitespace = TRUE)
- if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized)
- to_chat_immediate(target, message, handle_whitespace)
- return
- SSchat.queue(target, message, handle_whitespace)
+ var/datum/chat_payload/payload = client_history[sequence]
+ if(payload.resends > CHAT_RELIABILITY_MAX_RESENDS)
+ return // we tried but byond said no
+
+ payload.resends += 1
+ send_payload_to_client(client, client_history[sequence])
+ SSblackbox.record_feedback(
+ "nested tally",
+ "chat_resend_byond_version",
+ 1,
+ list(
+ "[client.byond_version]",
+ "[client.byond_build]",
+ ),
+ )
diff --git a/code/controllers/subsystem/events.dm b/code/controllers/subsystem/events.dm
index a823e416046..0abe09221fc 100644
--- a/code/controllers/subsystem/events.dm
+++ b/code/controllers/subsystem/events.dm
@@ -99,6 +99,7 @@ SUBSYSTEM_DEF(events)
/client/proc/forceGamemode()
set name = "Open Gamemode Panel"
set category = "GameMaster.Fun"
+
if(!holder ||!check_rights(R_FUN))
return
holder.forceGamemode(usr)
diff --git a/code/controllers/subsystem/ping.dm b/code/controllers/subsystem/ping.dm
index c5c9bb39335..44930810feb 100644
--- a/code/controllers/subsystem/ping.dm
+++ b/code/controllers/subsystem/ping.dm
@@ -8,7 +8,7 @@ SUBSYSTEM_DEF(ping)
var/list/currentrun = list()
/datum/controller/subsystem/ping/stat_entry()
- ..("P:[GLOB.clients.len]")
+ ..("P:[length(GLOB.clients)]")
/datum/controller/subsystem/ping/fire(resumed = 0)
@@ -18,16 +18,14 @@ SUBSYSTEM_DEF(ping)
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
- while (currentrun.len)
- var/client/C = currentrun[currentrun.len]
+ while(length(currentrun))
+ var/client/client = currentrun[length(currentrun)]
currentrun.len--
- if (!C || !C.chatOutput || !C.chatOutput.loaded)
- if (MC_TICK_CHECK)
- return
- continue
+ if(client?.tgui_panel?.is_ready())
+ client.tgui_panel.window.send_message("ping/soft", list(
+ "afk" = client.is_afk(3.5 SECONDS)
+ ))
- // softPang isn't handled anywhere but it'll always reset the opts.lastPang.
- C.chatOutput.ehjax_send(data = C.is_afk(29) ? "softPang" : "pang")
- if (MC_TICK_CHECK)
+ if(MC_TICK_CHECK)
return
diff --git a/code/controllers/subsystem/plexora.dm b/code/controllers/subsystem/plexora.dm
index 57bd85bfccb..7f160f2ca9b 100644
--- a/code/controllers/subsystem/plexora.dm
+++ b/code/controllers/subsystem/plexora.dm
@@ -724,18 +724,14 @@ SUBSYSTEM_DEF(plexora)
message_admins("External message from [sender] to [recipient_name_linked] : [message]")
log_admin_private("External PM: [sender] -> [recipient_name] : [message]")
- to_chat(recipient,
- message = "-- Administrator private message --",
- )
+ to_chat(recipient, html = "-- Administrator private message --")
recipient.receive_ahelp(
"[adminname]",
message,
)
- to_chat(recipient,
- message = span_adminsay("Click on the administrator's name to reply."),)
-
+ to_chat(recipient, html = span_adminsay("Click on the administrator's name to reply."))
admin_ticket_log(recipient, "PM From [adminname]: [message]", player_message = "PM From [adminname]: [message]")
@@ -827,10 +823,7 @@ SUBSYSTEM_DEF(plexora)
/client/proc/receive_ahelp(reply_to, message, span_class = "adminsay")
- to_chat(
- src,
- message = "Admin PM from-[reply_to]: [message]",
- )
+ to_chat(src, html = "Admin PM from-[reply_to]: [message]")
/// This should match the interface of /client wherever necessary.
/datum/client_interface
diff --git a/code/controllers/subsystem/server_maint.dm b/code/controllers/subsystem/server_maint.dm
index 120a398fdd6..09a419378b7 100644
--- a/code/controllers/subsystem/server_maint.dm
+++ b/code/controllers/subsystem/server_maint.dm
@@ -92,9 +92,7 @@ SUBSYSTEM_DEF(server_maint)
if(!thing)
continue
var/client/C = thing
- var/datum/chatOutput/co = C.chatOutput
- if(co)
- co.ehjax_send(data = "roundrestart")
+ C?.tgui_panel?.send_roundrestart()
if(server) //if you set a server location in config.txt, it sends you there instead of trying to reconnect to the same world address. -- NeoFite
C << link("byond://[server]")
var/datum/tgs_version/tgsversion = world.TgsVersion()
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index e25171e18e0..296e001be87 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -72,8 +72,6 @@ SUBSYSTEM_DEF(statpanels)
if(!target.holder)
target.stat_panel.send_message("remove_admin_tabs")
else
- //target.stat_panel.send_message("update_split_admin_tabs", !!(target.prefs.toggles & SPLIT_ADMIN_TABS))
-
if(!("MC" in target.panel_tabs) || !("Tickets" in target.panel_tabs))
target.stat_panel.send_message("add_admin_tabs", target.holder.href_token)
@@ -210,4 +208,8 @@ SUBSYSTEM_DEF(statpanels)
set_SDQL2_tab(target)
/// Stat panel window declaration
-/client/var/datum/tgui_window/stat_panel
+/client/var/datum/tgui_window/stat/stat_panel
+
+/datum/tgui_window/stat/initialize(strict_mode, fancy, assets, inline_html, inline_js, inline_css)
+ . = ..()
+ send_message("build_topbar") // This is the best way of doing it... don't @ me
diff --git a/code/datums/chat_payload.dm b/code/datums/chat_payload.dm
new file mode 100644
index 00000000000..fd35bbc4eec
--- /dev/null
+++ b/code/datums/chat_payload.dm
@@ -0,0 +1,16 @@
+/// Stores information about a chat payload
+/datum/chat_payload
+ /// Sequence number of this payload
+ var/sequence = 0
+ /// Message we are sending
+ var/list/content
+ /// Resend count
+ var/resends = 0
+
+/// Converts the chat payload into a JSON string
+/datum/chat_payload/proc/into_message()
+ return "{\"sequence\":[sequence],\"content\":[json_encode(content)]}"
+
+/// Returns an HTML-encoded message from our contents.
+/datum/chat_payload/proc/get_content_as_html()
+ return message_to_html(content)
diff --git a/code/modules/admin/sql_message_system.dm b/code/modules/admin/sql_message_system.dm
index dd34ca087be..13afc89766f 100644
--- a/code/modules/admin/sql_message_system.dm
+++ b/code/modules/admin/sql_message_system.dm
@@ -632,8 +632,6 @@
output += "
- If this takes longer than 30 seconds, it will automatically reload a maximum of 5 times.
- If it still doesn't work, use the bug report button at the top right of the window.
-
`);
- $('#addTabBtn').before(tabElement);
- });
- } catch (e) {
- console.log('Error loading custom tabs:', e);
- }
- }
-
- // Set initial filter
- if (opts.currentFilter && opts.currentFilter !== 'all') {
- switchFilter(opts.currentFilter);
- }
- if (savedConfig.fontsize) {
- $messages.css('font-size', savedConfig.fontsize);
- internalOutput('Loaded font size setting of: '+savedConfig.fontsize+'', 'internal');
- }
- if(savedConfig.sdarkmode == 'true'){
- swap();
- }
- if (savedConfig.spingDisabled) {
- if (savedConfig.spingDisabled == 'true') {
- opts.pingDisabled = true;
- $('#ping').hide();
- }
- internalOutput('Loaded ping display of: '+(opts.pingDisabled ? 'hidden' : 'visible')+'', 'internal');
- }
- if (savedConfig.smusicVolume) {
- var newVolume = clamp(savedConfig.smusicVolume, 0, 100);
- $('#adminMusic').prop('volume', newVolume / 100);
- $('#musicVolume').val(newVolume);
- opts.updatedVolume = newVolume;
- sendVolumeUpdate();
- internalOutput('Loaded music volume of: '+savedConfig.smusicVolume+'', 'internal');
- }
- else{
- $('#adminMusic').prop('volume', opts.defaultMusicVolume / 100);
- }
-
- if (savedConfig.smessagecombining) {
- if (savedConfig.smessagecombining == 'false') {
- opts.messageCombining = false;
- } else {
- opts.messageCombining = true;
- }
- }
- (function() {
- var dataCookie = getCookie('connData');
- if (dataCookie) {
- var dataJ;
- try {
- dataJ = $.parseJSON(dataCookie);
- } catch (e) {
- window.onerror('JSON '+e+'. '+dataCookie, 'browserOutput.html', 434);
- return;
- }
- opts.clientData = dataJ;
- }
- })();
-
-
- /*****************************************
- *
- * BASE CHAT OUTPUT EVENTS
- *
- ******************************************/
-
- $('body').on('click', 'a', function(e) {
- e.preventDefault();
- });
-
- $('body').on('mousedown', function(e) {
- var $target = $(e.target);
-
- if ($contextMenu) {
- $contextMenu.hide();
- return false;
- }
-
- if ($target.is('a') || $target.parent('a').length || $target.is('input') || $target.is('textarea')) {
- opts.preventFocus = true;
- } else {
- opts.preventFocus = false;
- opts.mouseDownX = e.pageX;
- opts.mouseDownY = e.pageY;
- }
- });
-
- $messages.on('mousedown', function(e) {
- if ($selectedSub && $selectedSub.is(':visible')) {
- $selectedSub.slideUp('fast', subSlideUp);
- clearInterval(opts.selectedSubLoop);
- }
- });
-
- $('body').on('mouseup', function(e) {
- if (!opts.preventFocus &&
- (e.pageX >= opts.mouseDownX - opts.clickTolerance && e.pageX <= opts.mouseDownX + opts.clickTolerance) &&
- (e.pageY >= opts.mouseDownY - opts.clickTolerance && e.pageY <= opts.mouseDownY + opts.clickTolerance)
- ) {
- opts.mouseDownX = null;
- opts.mouseDownY = null;
- runByond('byond://winset?mapwindow.map.focus=true');
- }
- });
-
- $messages.on('click', 'a', function(e) {
- var href = $(this).attr('href');
- $(this).addClass('visited');
- if (href[0] == '?' || (href.length >= 8 && href.substring(0,8) == 'byond://')) {
- runByond(href);
- } else {
- href = escaper(href);
- runByond('?action=openLink&link='+href);
- }
- });
-
- //Fuck everything about this event. Will look into alternatives.
- $('body').on('keydown', function(e) {
- if (e.target.nodeName == 'INPUT' || e.target.nodeName == 'TEXTAREA') {
- return;
- }
-
- if (e.ctrlKey || e.altKey || e.shiftKey) { //Band-aid "fix" for allowing ctrl+c copy paste etc. Needs a proper fix.
- return;
- }
-
- e.preventDefault()
-
- var k = e.which;
- // Hardcoded because else there would be no feedback message.
- if (k == 113) { // F2
- runByond('byond://winset?screenshot=auto');
- internalOutput('Screenshot taken', 'internal');
- }
-
- var c = "";
- switch (k) {
- case 8:
- c = 'BACK';
- case 9:
- c = 'TAB';
- case 13:
- c = 'ENTER';
- case 19:
- c = 'PAUSE';
- case 27:
- c = 'ESCAPE';
- case 33: // Page up
- c = 'NORTHEAST';
- case 34: // Page down
- c = 'SOUTHEAST';
- case 35: // End
- c = 'SOUTHWEST';
- case 36: // Home
- c = 'NORTHWEST';
- case 37:
- c = 'WEST';
- case 38:
- c = 'NORTH';
- case 39:
- c = 'EAST';
- case 40:
- c = 'SOUTH';
- case 45:
- c = 'INSERT';
- case 46:
- c = 'DELETE';
- case 93: // That weird thing to the right of alt gr.
- c = 'APPS';
-
- default:
- c = String.fromCharCode(k);
- }
-
- if (c.length == 0) {
- if (!e.shiftKey) {
- c = c.toLowerCase();
- }
- runByond('byond://winset?mapwindow.map.focus=true;mainwindow.input.text='+c);
- return false;
- } else {
- runByond('byond://winset?mapwindow.map.focus=true');
- return false;
- }
- });
-
- //Mildly hacky fix for scroll issues on mob change (interface gets resized sometimes, messing up snap-scroll)
- $(window).on('resize', function(e) {
- if ($(this).height() !== opts.priorChatHeight) {
- $('body,html').scrollTop($messages.outerHeight());
- opts.priorChatHeight = $(this).height();
- }
- });
-
-
- /*****************************************
- *
- * OPTIONS INTERFACE EVENTS
- *
- ******************************************/
-
- $('body').on('click', '#newMessages', function(e) {
- var messagesHeight = $messages.outerHeight();
- $('body,html').scrollTop(messagesHeight);
- $('#newMessages').remove();
- runByond('byond://winset?mapwindow.map.focus=true');
- });
-
- // Filter tab click handlers
- $(document).on('click', '.filter-tab', function(e) {
- e.preventDefault();
- var filterName = $(this).data('filter');
- switchFilter(filterName);
- });
-
- $(document).on('click', '#addTabBtn', function(e) {
- e.preventDefault();
- showAddTabForm();
- });
-
- $(document).on('click', '.remove-tab', function(e) {
- e.preventDefault();
- e.stopPropagation();
- var tabName = $(this).parent().data('filter');
- removeCustomTab(tabName);
- });
-
- $('#toggleOptions').click(function(e) {
- handleToggleClick($subOptions, $(this));
- });
- $('#darkmodetoggle').click(function(e) {
- swap();
- });
- $('#toggleAudio').click(function(e) {
- handleToggleClick($subAudio, $(this));
- });
-
- $('.sub, .toggle').mouseenter(function() {
- opts.suppressSubClose = true;
- });
-
- $('.sub, .toggle').mouseleave(function() {
- opts.suppressSubClose = false;
- });
-
- $('#decreaseFont').click(function(e) {
- savedConfig.fontsize = Math.max(parseInt(savedConfig.fontsize || 13) - 1, 1) + 'px';
- $messages.css({'font-size': savedConfig.fontsize});
- setCookie('fontsize', savedConfig.fontsize, 365);
- internalOutput('Font size set to '+savedConfig.fontsize+'', 'internal');
- });
-
- $('#increaseFont').click(function(e) {
- savedConfig.fontsize = (parseInt(savedConfig.fontsize || 13) + 1) + 'px';
- $messages.css({'font-size': savedConfig.fontsize});
- setCookie('fontsize', savedConfig.fontsize, 365);
- internalOutput('Font size set to '+savedConfig.fontsize+'', 'internal');
- });
-
- $('#togglePing').click(function(e) {
- if (opts.pingDisabled) {
- $('#ping').slideDown('fast');
- opts.pingDisabled = false;
- } else {
- $('#ping').slideUp('fast');
- opts.pingDisabled = true;
- }
- setCookie('pingdisabled', (opts.pingDisabled ? 'true' : 'false'), 365);
- });
-
- $('#saveLog').click(function(e) {
- var date = new Date();
- var fname = ' Vanderlin Chat Log ' +
- date.getFullYear() + '-' +
- (date.getMonth() + 1 < 10 ? '0' : '') + (date.getMonth() + 1) + '-' +
- (date.getDate() < 10 ? '0' : '') + date.getDate() + ' ' +
- (date.getHours() < 10 ? '0' : '') + date.getHours() +
- (date.getMinutes() < 10 ? '0' : '') + date.getMinutes() +
- (date.getSeconds() < 10 ? '0' : '') + date.getSeconds() +
- '.html';
-
- $.ajax({
- type: 'GET',
- url: 'browserOutput_white.css',
- success: function(styleData) {
- var blob = new Blob([
- 'Vanderlin Chat Log',
- $messages.html(),
- ''
- ], { type: 'text/html;charset=utf-8' });
-
- if (window.navigator.msSaveBlob) {
- window.navigator.msSaveBlob(blob, fname);
- } else {
- var link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.download = fname;
- link.click();
- URL.revokeObjectURL(link.href);
- }
- },
- });
- });
-
- highlightSystem.init();
- $('#highlightTerm').off('click').on('click', function(e) {
- e.preventDefault();
-
- if (window.highlightSystem) {
- highlightSystem.showManager();
- } else {
- // Fallback to old popup if new system isn't available
- showLegacyHighlightPopup();
- }
- });
-
- // Initialize the new highlight system
- if (window.highlightSystem) {
- highlightSystem.init();
-
- // Migrate old highlight terms to new system
- if (opts.highlightTerms && opts.highlightTerms.length > 0) {
- opts.highlightTerms.forEach(function(term) {
- if (term && term.trim()) {
- highlightSystem.addFilter(term, opts.highlightColor || '#FFFF00', 'none');
- }
- });
-
- // Clear old terms to avoid duplication
- opts.highlightTerms = [];
- setCookie('highlightterms', JSON.stringify([]), 365);
- }
- };
-
- $('#clearMessages').click(function() {
- $messages.empty();
- opts.messageCount = 0;
- });
-
- $('#musicVolumeSpan').hover(function() {
- $('#musicVolumeText').addClass('hidden');
- $('#musicVolume').removeClass('hidden');
- }, function() {
- $('#musicVolume').addClass('hidden');
- $('#musicVolumeText').removeClass('hidden');
- });
-
- $('#musicVolume').change(function() {
- var newVolume = $('#musicVolume').val();
- newVolume = clamp(newVolume, 0, 100);
- $('#adminMusic').prop('volume', newVolume / 100);
- setCookie('musicVolume', newVolume, 365);
- opts.updatedVolume = newVolume;
- if(!opts.volumeUpdating) {
- setTimeout(sendVolumeUpdate, opts.volumeUpdateDelay);
- opts.volumeUpdating = true;
- }
- });
-
- $('#toggleCombine').click(function(e) {
- opts.messageCombining = !opts.messageCombining;
- setCookie('messagecombining', (opts.messageCombining ? 'true' : 'false'), 365);
- });
-
- $('img.icon').error(iconError);
-
-
-
-
- /*****************************************
- *
- * KICK EVERYTHING OFF
- *
- ******************************************/
-
- runByond('?_src_=chat&proc=doneLoading');
- if ($('#loading').is(':visible')) {
- $('#loading').remove();
- }
- $('#userBar').show();
- opts.priorChatHeight = $(window).height();
-});
diff --git a/code/modules/goonchat/browserassets/js/errorHandler.js b/code/modules/goonchat/browserassets/js/errorHandler.js
deleted file mode 100644
index bc9511bd577..00000000000
--- a/code/modules/goonchat/browserassets/js/errorHandler.js
+++ /dev/null
@@ -1,51 +0,0 @@
-(function(window, navigator) {
-
- var escaper = encodeURIComponent || escape;
-
- var triggerError = function(msg, url, line, col, error) {
- window.onerror(msg, url, line, col, error);
- };
-
- /**
- * Directs JS errors to a byond proc for logging
- *
- * @param string file Name of the logfile to dump errors in, do not prepend with data/
- * @param boolean overrideDefault True to prevent default JS errors (an big honking error prompt thing)
- * @param function customSuppress Pass a function that returns true to prevent logging of a specific error
- * @return boolean
- */
- var attach = function(file, overrideDefault, customSuppress) {
- overrideDefault = typeof overrideDefault === 'undefined' ? false : overrideDefault;
- file = escaper(file);
-
- //Prevent debug logging for those using anything lower than IE 10
- var trident = navigator.userAgent.match(/Trident\/(\d)\.\d(?:;|$)/gi);
- var msie = document.documentMode;
- var suppressLogging = (msie && msie < 10) || (trident && parseInt(trident) < 6);
-
- //Ok enough is enough, this prevents A CERTAIN PLAYER (Studenterhue) from spamming the error logs with bullshit
- if (!window.JSON) {
- suppressLogging = true;
- }
-
- window.onerror = function(msg, url, line, col, error) {
- if (typeof customSuppress === 'function' && customSuppress(msg, url, line, col, error)) {
- suppressLogging = true;
- }
-
- if (!suppressLogging) {
- var extra = !col ? '' : ' | column: ' + col;
- extra += !error ? '' : ' | error: ' + error;
- extra += !navigator.userAgent ? '' : ' | user agent: ' + navigator.userAgent;
- var debugLine = 'Error: ' + msg + ' | url: ' + url + ' | line: ' + line + extra;
- window.location = '?action=debugFileOutput&file=' + file + '&message=' + escaper(debugLine);
- }
- return overrideDefault;
- };
-
- return triggerError;
- };
-
- window.attachErrorHandler = attach;
-
-}(window, window.navigator));
diff --git a/code/modules/goonchat/browserassets/js/json2.min.js b/code/modules/goonchat/browserassets/js/json2.min.js
deleted file mode 100644
index d867407f265..00000000000
--- a/code/modules/goonchat/browserassets/js/json2.min.js
+++ /dev/null
@@ -1 +0,0 @@
-"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(t){return 10>t?"0"+t:t}function this_value(){return this.valueOf()}function quote(t){return rx_escapable.lastIndex=0,rx_escapable.test(t)?'"'+t.replace(rx_escapable,function(t){var e=meta[t];return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}function str(t,e){var r,n,o,u,f,a=gap,i=e[t];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(t)),"function"==typeof rep&&(i=rep.call(e,t,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,f=[],"[object Array]"===Object.prototype.toString.apply(i)){for(u=i.length,r=0;u>r;r+=1)f[r]=str(r,i)||"null";return o=0===f.length?"[]":gap?"[\n"+gap+f.join(",\n"+gap)+"\n"+a+"]":"["+f.join(",")+"]",gap=a,o}if(rep&&"object"==typeof rep)for(u=rep.length,r=0;u>r;r+=1)"string"==typeof rep[r]&&(n=rep[r],o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));else for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));return o=0===f.length?"{}":gap?"{\n"+gap+f.join(",\n"+gap)+"\n"+a+"}":"{"+f.join(",")+"}",gap=a,o}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(t,e,r){var n;if(gap="",indent="","number"==typeof r)for(n=0;r>n;n+=1)indent+=" ";else"string"==typeof r&&(indent=r);if(rep=e,e&&"function"!=typeof e&&("object"!=typeof e||"number"!=typeof e.length))throw new Error("JSON.stringify");return str("",{"":t})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(t,e){var r,n,o=t[e];if(o&&"object"==typeof o)for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(n=walk(o,r),void 0!==n?o[r]=n:delete o[r]);return reviver.call(t,e,o)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(t){return"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();
\ No newline at end of file
diff --git a/code/modules/goonchat/browserassets/js/purify.min.js b/code/modules/goonchat/browserassets/js/purify.min.js
deleted file mode 100644
index 0360b41fcb1..00000000000
--- a/code/modules/goonchat/browserassets/js/purify.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/*! @license DOMPurify 2.5.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.5.8/LICENSE */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(e,r,o){return n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}()?Reflect.construct:function(e,n,r){var o=[null];o.push.apply(o,n);var a=new(Function.bind.apply(e,o));return r&&t(a,r.prototype),a},n.apply(null,arguments)}function r(e){return function(e){if(Array.isArray(e))return o(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return o(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return o(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),o=1;o/gm),q=m(/\${[\w\W]*}/gm),$=m(/^data-[\-\w.\u00B7-\uFFFF]+$/),Y=m(/^aria-[\-\w]+$/),K=m(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),V=m(/^(?:\w+script|data):/i),X=m(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Z=m(/^html$/i),J=m(/^[a-z][.\w]*(-[.\w]+)+$/i),Q=function(){return"undefined"==typeof window?null:window};var ee=function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Q(),o=function(e){return t(e)};if(o.version="2.5.8",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;var a=n.document,i=n.document,l=n.DocumentFragment,c=n.HTMLTemplateElement,u=n.Node,m=n.Element,f=n.NodeFilter,p=n.NamedNodeMap,d=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,h=n.HTMLFormElement,g=n.DOMParser,O=n.trustedTypes,ee=m.prototype,te=C(ee,"cloneNode"),ne=C(ee,"nextSibling"),re=C(ee,"childNodes"),oe=C(ee,"parentNode");if("function"==typeof c){var ae=i.createElement("template");ae.content&&ae.content.ownerDocument&&(i=ae.content.ownerDocument)}var ie=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,o="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(o)&&(r=n.currentScript.getAttribute(o));var a="dompurify"+(r?"#"+r:"");try{return t.createPolicy(a,{createHTML:function(e){return e},createScriptURL:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+a+" could not be created."),null}}(O,a),le=ie?ie.createHTML(""):"",ce=i,ue=ce.implementation,se=ce.createNodeIterator,me=ce.createDocumentFragment,fe=ce.getElementsByTagName,pe=a.importNode,de={};try{de=L(i).documentMode?i.documentMode:{}}catch(e){}var he={};o.isSupported="function"==typeof oe&&ue&&void 0!==ue.createHTMLDocument&&9!==de;var ge,ye,be=G,Te=W,ve=q,Ne=$,Ee=Y,Ae=V,Se=X,_e=J,we=K,xe=null,Oe=k({},[].concat(r(D),r(R),r(M),r(F),r(H))),ke=null,Le=k({},[].concat(r(z),r(P),r(B),r(j))),Ce=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),De=null,Re=null,Me=!0,Ie=!0,Fe=!1,Ue=!0,He=!1,ze=!0,Pe=!1,Be=!1,je=!1,Ge=!1,We=!1,qe=!1,$e=!0,Ye=!1,Ke=!0,Ve=!1,Xe={},Ze=null,Je=k({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Qe=null,et=k({},["audio","video","img","source","image","track"]),tt=null,nt=k({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),rt="http://www.w3.org/1998/Math/MathML",ot="http://www.w3.org/2000/svg",at="http://www.w3.org/1999/xhtml",it=at,lt=!1,ct=null,ut=k({},[rt,ot,at],N),st=["application/xhtml+xml","text/html"],mt=null,ft=i.createElement("form"),pt=function(e){return e instanceof RegExp||e instanceof Function},dt=function(t){mt&&mt===t||(t&&"object"===e(t)||(t={}),t=L(t),ge=ge=-1===st.indexOf(t.PARSER_MEDIA_TYPE)?"text/html":t.PARSER_MEDIA_TYPE,ye="application/xhtml+xml"===ge?N:v,xe="ALLOWED_TAGS"in t?k({},t.ALLOWED_TAGS,ye):Oe,ke="ALLOWED_ATTR"in t?k({},t.ALLOWED_ATTR,ye):Le,ct="ALLOWED_NAMESPACES"in t?k({},t.ALLOWED_NAMESPACES,N):ut,tt="ADD_URI_SAFE_ATTR"in t?k(L(nt),t.ADD_URI_SAFE_ATTR,ye):nt,Qe="ADD_DATA_URI_TAGS"in t?k(L(et),t.ADD_DATA_URI_TAGS,ye):et,Ze="FORBID_CONTENTS"in t?k({},t.FORBID_CONTENTS,ye):Je,De="FORBID_TAGS"in t?k({},t.FORBID_TAGS,ye):{},Re="FORBID_ATTR"in t?k({},t.FORBID_ATTR,ye):{},Xe="USE_PROFILES"in t&&t.USE_PROFILES,Me=!1!==t.ALLOW_ARIA_ATTR,Ie=!1!==t.ALLOW_DATA_ATTR,Fe=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Ue=!1!==t.ALLOW_SELF_CLOSE_IN_ATTR,He=t.SAFE_FOR_TEMPLATES||!1,ze=!1!==t.SAFE_FOR_XML,Pe=t.WHOLE_DOCUMENT||!1,Ge=t.RETURN_DOM||!1,We=t.RETURN_DOM_FRAGMENT||!1,qe=t.RETURN_TRUSTED_TYPE||!1,je=t.FORCE_BODY||!1,$e=!1!==t.SANITIZE_DOM,Ye=t.SANITIZE_NAMED_PROPS||!1,Ke=!1!==t.KEEP_CONTENT,Ve=t.IN_PLACE||!1,we=t.ALLOWED_URI_REGEXP||we,it=t.NAMESPACE||at,Ce=t.CUSTOM_ELEMENT_HANDLING||{},t.CUSTOM_ELEMENT_HANDLING&&pt(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&pt(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),He&&(Ie=!1),We&&(Ge=!0),Xe&&(xe=k({},r(H)),ke=[],!0===Xe.html&&(k(xe,D),k(ke,z)),!0===Xe.svg&&(k(xe,R),k(ke,P),k(ke,j)),!0===Xe.svgFilters&&(k(xe,M),k(ke,P),k(ke,j)),!0===Xe.mathMl&&(k(xe,F),k(ke,B),k(ke,j))),t.ADD_TAGS&&(xe===Oe&&(xe=L(xe)),k(xe,t.ADD_TAGS,ye)),t.ADD_ATTR&&(ke===Le&&(ke=L(ke)),k(ke,t.ADD_ATTR,ye)),t.ADD_URI_SAFE_ATTR&&k(tt,t.ADD_URI_SAFE_ATTR,ye),t.FORBID_CONTENTS&&(Ze===Je&&(Ze=L(Ze)),k(Ze,t.FORBID_CONTENTS,ye)),Ke&&(xe["#text"]=!0),Pe&&k(xe,["html","head","body"]),xe.table&&(k(xe,["tbody"]),delete De.tbody),s&&s(t),mt=t)},ht=k({},["mi","mo","mn","ms","mtext"]),gt=k({},["annotation-xml"]),yt=k({},["title","style","font","a","script"]),bt=k({},R);k(bt,M),k(bt,I);var Tt=k({},F);k(Tt,U);var vt=function(e){T(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=le}catch(t){e.remove()}}},Nt=function(e,t){try{T(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){T(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!ke[e])if(Ge||We)try{vt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){var t,n;if(je)e=""+e;else{var r=E(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===ge&&it===at&&(e=''+e+"");var o=ie?ie.createHTML(e):e;if(it===at)try{t=(new g).parseFromString(o,ge)}catch(e){}if(!t||!t.documentElement){t=ue.createDocument(it,"template",null);try{t.documentElement.innerHTML=lt?le:o}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(i.createTextNode(n),a.childNodes[0]||null),it===at?fe.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:a},At=function(e){return se.call(e.ownerDocument||e,e,f.SHOW_ELEMENT|f.SHOW_COMMENT|f.SHOW_TEXT|f.SHOW_PROCESSING_INSTRUCTION|f.SHOW_CDATA_SECTION,null,!1)},St=function(e){return e instanceof h&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof d)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},_t=function(t){return"object"===e(u)?t instanceof u:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},wt=function(e,t,n){he[e]&&y(he[e],(function(e){e.call(o,t,n,mt)}))},xt=function(e){var t;if(wt("beforeSanitizeElements",e,null),St(e))return vt(e),!0;if(w(/[\u0080-\uFFFF]/,e.nodeName))return vt(e),!0;var n=ye(e.nodeName);if(wt("uponSanitizeElement",e,{tagName:n,allowedTags:xe}),e.hasChildNodes()&&!_t(e.firstElementChild)&&(!_t(e.content)||!_t(e.content.firstElementChild))&&w(/<[/\w]/g,e.innerHTML)&&w(/<[/\w]/g,e.textContent))return vt(e),!0;if("select"===n&&w(/=0;--i){var l=te(a[i],!0);l.__removalCount=(e.__removalCount||0)+1,r.insertBefore(l,ne(e))}}return vt(e),!0}return e instanceof m&&!function(e){var t=oe(e);t&&t.tagName||(t={namespaceURI:it,tagName:"template"});var n=v(e.tagName),r=v(t.tagName);return!!ct[e.namespaceURI]&&(e.namespaceURI===ot?t.namespaceURI===at?"svg"===n:t.namespaceURI===rt?"svg"===n&&("annotation-xml"===r||ht[r]):Boolean(bt[n]):e.namespaceURI===rt?t.namespaceURI===at?"math"===n:t.namespaceURI===ot?"math"===n&>[r]:Boolean(Tt[n]):e.namespaceURI===at?!(t.namespaceURI===ot&&!gt[r])&&!(t.namespaceURI===rt&&!ht[r])&&!Tt[n]&&(yt[n]||!bt[n]):!("application/xhtml+xml"!==ge||!ct[e.namespaceURI]))}(e)?(vt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!w(/<\/no(script|embed|frames)/i,e.innerHTML)?(He&&3===e.nodeType&&(t=e.textContent,t=A(t,be," "),t=A(t,Te," "),t=A(t,ve," "),e.textContent!==t&&(T(o.removed,{element:e.cloneNode()}),e.textContent=t)),wt("afterSanitizeElements",e,null),!1):(vt(e),!0)},Ot=function(e,t,n){if($e&&("id"===t||"name"===t)&&(n in i||n in ft))return!1;if(Ie&&!Re[t]&&w(Ne,t));else if(Me&&w(Ee,t));else if(!ke[t]||Re[t]){if(!(kt(e)&&(Ce.tagNameCheck instanceof RegExp&&w(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&w(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&w(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(tt[t]);else if(w(we,A(n,Se,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==S(n,"data:")||!Qe[e]){if(Fe&&!w(Ae,A(n,Se,"")));else if(n)return!1}else;return!0},kt=function(e){return"annotation-xml"!==e&&E(e,_e)},Lt=function(t){var n,r,a,i;wt("beforeSanitizeAttributes",t,null);var l=t.attributes;if(l&&!St(t)){var c={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:ke};for(i=l.length;i--;){var u=n=l[i],s=u.name,m=u.namespaceURI;if(r="value"===s?n.value:_(n.value),a=ye(s),c.attrName=a,c.attrValue=r,c.keepAttr=!0,c.forceKeepAttr=void 0,wt("uponSanitizeAttribute",t,c),r=c.attrValue,!c.forceKeepAttr&&(Nt(s,t),c.keepAttr))if(Ue||!w(/\/>/i,r)){He&&(r=A(r,be," "),r=A(r,Te," "),r=A(r,ve," "));var f=ye(t.nodeName);if(Ot(f,a,r))if(!Ye||"id"!==a&&"name"!==a||(Nt(s,t),r="user-content-"+r),ze&&w(/((--!?|])>)|<\/(style|title)/i,r))Nt(s,t);else{if(ie&&"object"===e(O)&&"function"==typeof O.getAttributeType)if(m);else switch(O.getAttributeType(f,a)){case"TrustedHTML":r=ie.createHTML(r);break;case"TrustedScriptURL":r=ie.createScriptURL(r)}try{m?t.setAttributeNS(m,s,r):t.setAttribute(s,r),St(t)?vt(t):b(o.removed)}catch(e){}}}else Nt(s,t)}wt("afterSanitizeAttributes",t,null)}},Ct=function e(t){var n,r=At(t);for(wt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)wt("uponSanitizeShadowNode",n,null),xt(n),Lt(n),n.content instanceof l&&e(n.content);wt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(t){var r,i,c,s,m,f=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if((lt=!t)&&(t="\x3c!--\x3e"),"string"!=typeof t&&!_t(t)){if("function"!=typeof t.toString)throw x("toString is not a function");if("string"!=typeof(t=t.toString()))throw x("dirty is not a string, aborting")}if(!o.isSupported){if("object"===e(n.toStaticHTML)||"function"==typeof n.toStaticHTML){if("string"==typeof t)return n.toStaticHTML(t);if(_t(t))return n.toStaticHTML(t.outerHTML)}return t}if(Be||dt(f),o.removed=[],"string"==typeof t&&(Ve=!1),Ve){if(t.nodeName){var p=ye(t.nodeName);if(!xe[p]||De[p])throw x("root node is forbidden and cannot be sanitized in-place")}}else if(t instanceof u)1===(i=(r=Et("\x3c!----\x3e")).ownerDocument.importNode(t,!0)).nodeType&&"BODY"===i.nodeName||"HTML"===i.nodeName?r=i:r.appendChild(i);else{if(!Ge&&!He&&!Pe&&-1===t.indexOf("<"))return ie&&qe?ie.createHTML(t):t;if(!(r=Et(t)))return Ge?null:qe?le:""}r&&je&&vt(r.firstChild);for(var d=At(Ve?t:r);c=d.nextNode();)3===c.nodeType&&c===s||(xt(c),Lt(c),c.content instanceof l&&Ct(c.content),s=c);if(s=null,Ve)return t;if(Ge){if(We)for(m=me.call(r.ownerDocument);r.firstChild;)m.appendChild(r.firstChild);else m=r;return(ke.shadowroot||ke.shadowrootmod)&&(m=pe.call(a,m,!0)),m}var h=Pe?r.outerHTML:r.innerHTML;return Pe&&xe["!doctype"]&&r.ownerDocument&&r.ownerDocument.doctype&&r.ownerDocument.doctype.name&&w(Z,r.ownerDocument.doctype.name)&&(h="\n"+h),He&&(h=A(h,be," "),h=A(h,Te," "),h=A(h,ve," ")),ie&&qe?ie.createHTML(h):h},o.setConfig=function(e){dt(e),Be=!0},o.clearConfig=function(){mt=null,Be=!1},o.isValidAttribute=function(e,t,n){mt||dt({});var r=ye(e),o=ye(t);return Ot(r,o,n)},o.addHook=function(e,t){"function"==typeof t&&(he[e]=he[e]||[],T(he[e],t))},o.removeHook=function(e){if(he[e])return b(he[e])},o.removeHooks=function(e){he[e]&&(he[e]=[])},o.removeAllHooks=function(){he={}},o}();return ee}));
-//# sourceMappingURL=purify.min.js.map
diff --git a/code/modules/language/language_menu.dm b/code/modules/language/language_menu.dm
index 4251be74dde..7db972b5edf 100644
--- a/code/modules/language/language_menu.dm
+++ b/code/modules/language/language_menu.dm
@@ -65,6 +65,7 @@
if(language_datum && AM.can_speak_in_language(language_datum))
language_holder.selected_default_language = language_datum
. = TRUE
+
if("grant_language")
if((is_admin || isobserver(AM)) && language_datum)
language_holder.grant_language(language_datum)
@@ -72,6 +73,7 @@
message_admins("[key_name_admin(user)] granted the [language_name] language to [key_name_admin(AM)].")
log_admin("[key_name(user)] granted the language [language_name] to [key_name(AM)].")
. = TRUE
+
if("remove_language")
if((is_admin || isobserver(AM)) && language_datum)
language_holder.remove_language(language_datum)
@@ -79,6 +81,7 @@
message_admins("[key_name_admin(user)] removed the [language_name] language from [key_name_admin(AM)].")
log_admin("[key_name(user)] removed the language [language_name] from [key_name(AM)].")
. = TRUE
+
if("toggle_omnitongue")
if(is_admin || isobserver(AM))
language_holder.omnitongue = !language_holder.omnitongue
diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm
index 3d392eac2ed..3ec21089f10 100644
--- a/code/modules/mob/dead/new_player/login.dm
+++ b/code/modules/mob/dead/new_player/login.dm
@@ -14,14 +14,11 @@
var/motd = global.config.motd
if(motd)
- to_chat(src, "
[motd]
", handle_whitespace=FALSE)
+ to_chat(src, "
[motd]
")
if(GLOB.rogue_round_id)
to_chat(src, "ROUND ID: [GLOB.rogue_round_id]")
-// if(motd)
-// to_chat(src, "If this is your first time here,read this lore primer.", handle_whitespace=FALSE)
-
if(GLOB.admin_notice)
to_chat(src, "Admin Notice:\n \t [GLOB.admin_notice]")
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 0f15c991f96..b6b85d005a0 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -460,8 +460,6 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
M.key = key
// M.Login() //wat
- return
-
/mob/dead/observer/verb/stay_dead()
set category = "Spirit"
diff --git a/code/modules/mob/dead/observer/observer_say.dm b/code/modules/mob/dead/observer/observer_say.dm
index 9511989f5bb..cce7351e5e0 100644
--- a/code/modules/mob/dead/observer/observer_say.dm
+++ b/code/modules/mob/dead/observer/observer_say.dm
@@ -63,7 +63,6 @@
. = ..()
var/atom/movable/to_follow = speaker
var/link = FOLLOW_LINK(src, to_follow)
- // Create map text prior to modifying message for goonchat
if(client?.prefs)
if(!(client?.prefs.toggles_maptext & DISABLE_RUNECHAT) && (client.prefs.see_chat_non_mob || ismob(speaker)))
create_chat_message(speaker, message_language, raw_message, spans)
diff --git a/code/modules/mob/living/carbon/spirit/spirit.dm b/code/modules/mob/living/carbon/spirit/spirit.dm
index c18b11c0a49..408558b1068 100644
--- a/code/modules/mob/living/carbon/spirit/spirit.dm
+++ b/code/modules/mob/living/carbon/spirit/spirit.dm
@@ -104,7 +104,6 @@
slow += (health_deficiency / 25)
add_movespeed_modifier(MOVESPEED_ID_MONKEY_HEALTH_SPEEDMOD, TRUE, 100, override = TRUE, multiplicative_slowdown = slow)
-
/mob/living/carbon/spirit/returntolobby()
set name = "{RETURN TO LOBBY}"
set category = "Preferences.Options"
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 8e5addb84ad..3676fd14663 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -967,9 +967,11 @@
. = TRUE
if(mind)
mind.remove_antag_datum(/datum/antagonist/zombie)
+
if(ishuman(src))
var/mob/living/carbon/human/human = src
human.funeral = FALSE
+
if(excess_healing)
INVOKE_ASYNC(src, PROC_REF(emote), "breathgasp")
log_combat(src, src, "revived")
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index c30a4f5ab2a..5052f31c1ea 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -37,6 +37,7 @@
/mob/living/proc/funeral_login()
if(QDELETED(src) || QDELETED(mind))
return FALSE
+
if(!client)
return FALSE
diff --git a/code/modules/tgchat/README.md b/code/modules/tgchat/README.md
new file mode 100644
index 00000000000..d2a99e5ffd2
--- /dev/null
+++ b/code/modules/tgchat/README.md
@@ -0,0 +1,31 @@
+## /TG/ Chat
+
+/TG/ Chat, which will be referred to as TgChat from this point onwards, is a system in which we can send messages to clients in a controlled and semi-reliable manner. The standard way of sending messages to BYOND clients simply dumps whatever you output to them directly into their chat window, however BYOND allows us to load our own code on the client to change this behaviour in a way that allows us to do some pretty neat things.
+
+### Message Format
+
+TgChat handles sending messages from the server to the client through the use of JSON payloads, of which the format will change depending on the type of message and the intended client endpoint. An example of the payload for chat messages is as follows:
+
+```json
+{
+ "sequence": 0,
+ "content": {
+ "type": ". . .", // ?optional
+ "text": ". . .", // ?optional !atleast-one
+ "html": ". . .", // ?optional !atleast-one
+ "avoidHighlighting": 0 // ?optional
+ }
+}
+```
+
+### Reliability
+
+In the past there have been issues where BYOND will silently and without reason lose a message we sent to the client, to detect this and recover from it seamlessly TgChat also has a baked in reliability layer. This reliability layer is very primitive, and simply keeps track of received sequence numbers. Should the client receive an unexpected sequence number TgChat asks the server to resend any missing packets.
+
+### Ping System
+
+TgChat supports a round trip time ping measurement, which is displayed to the client so they can know how long it takes for their commands and inputs to reach the server. This is done by sending the client a ping request, `ping/soft`, which tells the client to send a ping to the server. When the server receives said ping it sends a reply, `ping/reply`, to the client with a payload containing the current DateTime which the client can reference against the initial ping request.
+
+### Chat Tabs, Local Storage, and Highlighting
+
+To make organizing and managing chat easier and more functional for both players and admins, TgChat has the ability to filter out messages based on their primary tag, such as individual departmental radios, to a dedicated chat tab for easier reading and comprehension. These tabs can also be configured to highlist messages based on a simple keyword search. You can set a multitude of different keywords to search for and they will be highlighting for instant alerting of the client. Said tabs, highlighting rules, and your chat history will persist thanks to use of local storage on the client. Using local storage TgChat can ensure that your preferences are saved and maintained between client restarts and switching between other /TG/ servers. Local Storage is also used to keep your chat history aswell, should you need to scroll through your chat logs.
diff --git a/code/modules/tgchat/message.dm b/code/modules/tgchat/message.dm
new file mode 100644
index 00000000000..2e9bfe784ec
--- /dev/null
+++ b/code/modules/tgchat/message.dm
@@ -0,0 +1,17 @@
+/**
+ * Message-related procs
+ *
+ * Message format (/list):
+ * - type - Message type, must be one of defines in `code/__DEFINES/chat.dm`
+ * - text - Plain message text
+ * - html - HTML message text
+ * - Optional metadata, can be any key/value pair.
+ *
+ * Copyright (c) 2020 Aleksej Komarov
+ * SPDX-License-Identifier: MIT
+ */
+
+/proc/message_to_html(message)
+ // Here it is possible to add a switch statement
+ // to custom-handle various message types.
+ return message["html"] || message["text"]
diff --git a/code/modules/tgchat/to_chat.dm b/code/modules/tgchat/to_chat.dm
new file mode 100644
index 00000000000..795f7fa940d
--- /dev/null
+++ b/code/modules/tgchat/to_chat.dm
@@ -0,0 +1,74 @@
+/*!
+ * Copyright (c) 2020 Aleksej Komarov
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * Circumvents the message queue and sends the message
+ * to the recipient (target) as soon as possible.
+ */
+/proc/to_chat_immediate(target, html, type = null, text = null, avoid_highlighting = FALSE)
+ // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "")
+ html = "[html]"
+ text = "[text]"
+
+ if(!target)
+ return
+ if(!html && !text)
+ // CRASH("Empty or null string in to_chat proc call.")
+ return
+ if(target == world)
+ target = GLOB.clients
+
+ // Build a message
+ var/message = list()
+ if(type)
+ message["type"] = type
+ if(text)
+ message["text"] = text
+ if(html)
+ message["html"] = html
+ if(avoid_highlighting)
+ message["avoidHighlighting"] = avoid_highlighting
+
+ // send it immediately
+ SSchat.send_immediate(target, message)
+
+/**
+ * Sends the message to the recipient (target).
+ *
+ * Recommended way to write to_chat calls:
+ * ```
+ * to_chat(client,
+ * type = MESSAGE_TYPE_INFO,
+ * html = "You have found [object]")
+ * ```
+ */
+/proc/to_chat(target, html, type = null, text = null, avoid_highlighting = FALSE)
+ if(isnull(Master) || !SSchat?.initialized)
+ to_chat_immediate(target, html, type, text, avoid_highlighting)
+ return
+
+ // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "")
+ html = "[html]"
+ text = "[text]"
+
+ if(!target)
+ return
+ if(!html && !text)
+ CRASH("Empty or null string in to_chat proc call.")
+ if(target == world)
+ target = GLOB.clients
+
+ // Build a message
+ var/message = list()
+ if(type)
+ message["type"] = type
+ if(text)
+ message["text"] = text
+ if(html)
+ message["html"] = html
+ if(avoid_highlighting)
+ message["avoidHighlighting"] = avoid_highlighting
+
+ SSchat.queue(target, message)
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 45cfd093c3c..2535f7abf32 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -115,6 +115,7 @@
/datum/tgui/proc/send_assets()
var/flush_queue = window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
flush_queue |= window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont))
+ flush_queue |= window.send_asset(get_asset_datum(/datum/asset/simple/roguefonts))
flush_queue |= window.send_asset(get_asset_datum(/datum/asset/json/icon_ref_map))
for(var/datum/asset/asset in src_object.ui_assets(user))
diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm
index c60ffcc0dbb..66347e1cae3 100644
--- a/code/modules/tgui_panel/external.dm
+++ b/code/modules/tgui_panel/external.dm
@@ -5,6 +5,34 @@
/client/var/datum/tgui_panel/tgui_panel
+/**
+ * tgui panel / chat troubleshooting verb
+ */
+/client/verb/fix_tgui_panel()
+ set name = "Fix chat"
+ set category = "OOC.Fix"
+ var/action
+ log_tgui(src, "Started fixing.", context = "verb/fix_tgui_panel")
+
+ nuke_chat()
+
+ // Failed to fix, using tg_alert as fallback
+ action = alert(src, "Did that work?", "", "Yes", "No, switch to old ui")
+ if (action == "No, switch to old ui")
+ winset(src, "output_selector.legacy_output_selector", "left=output_legacy")
+ log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel")
+
+/client/proc/nuke_chat()
+ // Catch all solution (kick the whole thing in the pants)
+ winset(src, "output_selector.legacy_output_selector", "left=output_legacy")
+ if(!tgui_panel || !istype(tgui_panel))
+ log_tgui(src, "tgui_panel datum is missing",
+ context = "verb/fix_tgui_panel")
+ tgui_panel = new(src)
+ tgui_panel.initialize(force = TRUE)
+ // Force show the panel to see if there are any errors
+ winset(src, "output_selector.legacy_output_selector", "left=output_browser")
+
/client/verb/refresh_tgui()
set name = "Refresh TGUI"
set category = "OOC.Fix"
diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm
index 1b03aeb3184..cc2b18331d6 100644
--- a/code/modules/tgui_panel/tgui_panel.dm
+++ b/code/modules/tgui_panel/tgui_panel.dm
@@ -49,8 +49,12 @@
assets = list(
get_asset_datum(/datum/asset/simple/tgui_panel),
))
+
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
+ window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont))
+ window.send_asset(get_asset_datum(/datum/asset/simple/roguefonts))
window.send_asset(get_asset_datum(/datum/asset/spritesheet_batched/chat))
+
// Other setup
request_telemetry()
addtimer(CALLBACK(src, PROC_REF(on_initialize_timed_out)), 5 SECONDS)
diff --git a/html/font-awesome/webfonts/fa-regular-400.ttf b/html/font-awesome/webfonts/fa-regular-400.ttf
new file mode 100644
index 00000000000..9ee1919dc23
Binary files /dev/null and b/html/font-awesome/webfonts/fa-regular-400.ttf differ
diff --git a/html/font-awesome/webfonts/fa-solid-900.ttf b/html/font-awesome/webfonts/fa-solid-900.ttf
new file mode 100644
index 00000000000..1c10972ecea
Binary files /dev/null and b/html/font-awesome/webfonts/fa-solid-900.ttf differ
diff --git a/html/font-awesome/webfonts/fa-v4compatibility.ttf b/html/font-awesome/webfonts/fa-v4compatibility.ttf
new file mode 100644
index 00000000000..3bcb67ffcc1
Binary files /dev/null and b/html/font-awesome/webfonts/fa-v4compatibility.ttf differ
diff --git a/code/modules/goonchat/browserassets/js/jquery.min.js b/html/jquery/jquery.min.js
similarity index 100%
rename from code/modules/goonchat/browserassets/js/jquery.min.js
rename to html/jquery/jquery.min.js
diff --git a/html/statbrowser.css b/html/statbrowser.css
index 857dc2e6c93..c2f12ea2f11 100644
--- a/html/statbrowser.css
+++ b/html/statbrowser.css
@@ -1,3 +1,8 @@
+@font-face {
+ font-family: 'Pterra';
+ src: url('pterra.ttf') format('truetype');
+}
+
.light:root {
--color-base: #ffffff;
--scrollbar-base: #f2f2f2;
@@ -11,11 +16,10 @@ body {
body {
font-family: Pterra;
- line-height: 100%;
- font-size: 16px;
- margin: 0 !important;
- padding: 0 !important;
- overflow: hidden;
+ font-size: 12px;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden;
}
a {
@@ -44,10 +48,18 @@ img {
height: 100vh;
}
+#topbar {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: stretch;
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding: 0.25em 0.25em 0;
+ background-color: #ffffff;
+}
+
#menu {
display: flex;
- flex-direction: row;
- flex-wrap: wrap;
overflow-x: auto;
overflow-y: hidden;
padding: 0.25em 0.25em 0;
@@ -76,15 +88,14 @@ img {
}
.button {
- font-family: Blackmoor LET;
- font-size: 18px;
display: inline-table;
cursor: pointer;
user-select: none;
-ms-user-select: none; /* Remove after Byond 516 */
text-align: center;
+ font-size: 1.1em;
min-width: 2.9em;
- padding: 0.5em 0.5em 0.3em;
+ padding: 0.5em 0.5em 0.4em;
background-color: transparent;
color: rgba(0, 0, 0, 0.5);
border: 0;
@@ -103,6 +114,29 @@ img {
border-bottom-color: #000000;
}
+.top-item {
+ cursor: pointer;
+ display: inline-table;
+ user-select: none;
+ -ms-user-select: none; /* Remove after Byond 516 */
+ text-align: center;
+ font-size: 0.9em;
+ width: 100%;
+ padding: 0.5em 0.5em 0.4em;
+ background-color: transparent;
+ border: 0;
+ border-radius: 0.25em;
+}
+
+.top-item:hover {
+ background-color: #ececec;
+}
+
+.top-item.unread {
+ background-color: hsl(0, 53%, 45%);
+ color: white;
+}
+
.statcontent {
flex: 1;
padding: 0.75em 0.5em;
@@ -126,6 +160,11 @@ img {
font-family: "Consolas", "Lucida Console", "Courier New", monospace;
}
+#under-topbar {
+ height: 0.5em;
+ background-color: #eeeeee;
+}
+
#under-menu {
height: 0.5em;
background-color: #eeeeee;
@@ -216,62 +255,65 @@ img {
/**
* MARK: Dark theme colors
*/
-:root {
- --color-base: #151515;
- --scrollbar-base: #151515;
- --scrollbar-thumb: #363636;
+.dark:root {
+ --color-base: hsl(0, 0%, 2%);
+ --scrollbar-base: hsl(0, 0%, 8%);
+ --scrollbar-thumb: hsl(0, 0%, 21%);
}
-body{
- background-color: #151515;
- color: #b2c4dd;
+body.dark {
+ background-color: hsl(0, 0%, 2%);
+ color: hsl(6, 14%, 55%);
}
-a {
- color: #6699ff;
+.dark a {
+ color: hsl(0, 63%, 46%);
}
-a:hover,
-.grid-item:hover,
-.grid-item:active {
- color: #80bfff;
+.dark a:hover,
+.dark .grid-item:hover,
+.dark .grid-item:active {
+ color: hsl(0, 82%, 38%);
}
-#menu {
- background-color: #151515;
+.dark #menu,
+.dark #topbar {
+ background-color: hsl(0, 0%, 2%);
}
-#menu.tabs-classic .button.active {
- background-color: #20b142;
+.dark #menu.tabs-classic .button.active {
+ background-color: hsl(0, 53%, 45%);
}
-.button {
- color: rgba(255, 255, 255, 0.5);
+.dark .button {
+ color: hsl(0, 12%, 54%);
}
-.button:hover {
- background-color: #252525;
+.dark .button:hover,
+.dark .top-item:hover {
+ background-color: hsl(0, 2%, 26%);
}
-.button.active {
- background-color: #313131;
- color: #d4dfec;
- border-bottom-color: #d4dfec;
+.dark .button.active {
+ background-color: hsl(0, 2%, 22%);
+ color: hsl(5, 21%, 69%);
+ border-bottom-color: hsl(5, 21%, 69%);;
}
-#under-menu,
-#under-content {
- background-color: #202020;
+.dark #under-menu,
+.dark #under-content,
+.dark #under-topbar {
+ background-color: hsl(0, 0%, 8%);
}
-.grid-item {
- color: #b2c4dd;
+.dark .grid-item {
+ color: hsl(6, 24%, 59%);
}
-.grid-item:hover .grid-item-text {
- background-color: #252525;
+.dark .grid-item:hover .grid-item-text {
+ background-color: hsl(0, 2%, 26%);
}
-.grid-item:active .grid-item-text {
- background-color: #313131;
+.dark .grid-item:active .grid-item-text {
+ background-color: hsl(0, 2%, 26%);
}
diff --git a/html/statbrowser.html b/html/statbrowser.html
index bbbd8fa53ab..45b1023f8b0 100644
--- a/html/statbrowser.html
+++ b/html/statbrowser.html
@@ -1,5 +1,7 @@