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) ("
" + title + "" + content + "
") +/// 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 += "
Add messageAdd watchlist entryAdd note
" output += ruler var/datum/browser/browser = new(usr, "Note panel", "Player notes", 1000, 500) - var/datum/asset/notes_assets = get_asset_datum(/datum/asset/simple/notes) - notes_assets.send(usr.client) browser.set_content(jointext(output, "")) browser.open() diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index abf340de863..7968d0e68c7 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -127,17 +127,21 @@ GLOBAL_LIST_EMPTY(dirty_vars) set name = "Debug verbs - Enable" if(!check_rights(R_DEBUG)) return + remove_verb(src, /client/proc/enable_debug_verbs) add_verb(src, GLOB.admin_verbs_debug_mapping) add_verb(src, /client/proc/disable_debug_verbs) + SSblackbox.record_feedback("tally", "admin_verb", 1, "Enable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/disable_debug_verbs() set category = "Debug.Core" set name = "Debug verbs - Disable" + remove_verb(src, GLOB.admin_verbs_debug_mapping) remove_verb(src, /client/proc/disable_debug_verbs) add_verb(src, /client/proc/enable_debug_verbs) + SSblackbox.record_feedback("tally", "admin_verb", 1, "Disable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/count_objects_on_z_level() diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index dcbbce00a21..27a74db8b25 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -179,11 +179,13 @@ if(web_sound_url || stop_web_sounds) for(var/mob/M as anything in GLOB.player_list) var/client/C = M.client - if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) + if((C.prefs.toggles & SOUND_MIDI)) + SEND_SOUND(C, sound(null, channel = CHANNEL_LOBBYMUSIC)) + SEND_SOUND(C, sound(null, channel = CHANNEL_ADMIN)) if(!stop_web_sounds) - C.chatOutput.sendMusic(web_sound_url, music_extra_data) + C.tgui_panel?.play_music(web_sound_url, music_extra_data) else - C.chatOutput.stopMusic() + C.tgui_panel?.stop_music() SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Internet Sound") @@ -210,6 +212,5 @@ for(var/mob/M in GLOB.player_list) SEND_SOUND(M, sound(null)) var/client/C = M.client - if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) - C.chatOutput.stopMusic() + C.tgui_panel?.stop_music() SSblackbox.record_feedback("tally", "admin_verb", 1, "Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/antagonists/villain/assassin.dm b/code/modules/antagonists/villain/assassin.dm index 2aab127c508..b6debff8321 100644 --- a/code/modules/antagonists/villain/assassin.dm +++ b/code/modules/antagonists/villain/assassin.dm @@ -53,6 +53,7 @@ /datum/antagonist/assassin/on_removal() if(!silent && owner.current) to_chat(owner.current,"The red fog in my mind is fading. I am no longer an [name]!") + remove_verb(owner.current, /mob/living/carbon/human/proc/who_targets) return ..() /datum/antagonist/assassin/roundend_report() diff --git a/code/modules/asset_cache/asset_cache_list.dm b/code/modules/asset_cache/asset_cache_list.dm deleted file mode 100644 index 0cc6b701898..00000000000 --- a/code/modules/asset_cache/asset_cache_list.dm +++ /dev/null @@ -1,207 +0,0 @@ -/datum/asset/simple/vv - assets = list( - "view_variables.css" = 'html/admin/view_variables.css' - ) - -/datum/asset/simple/changelog - assets = list( - "changelog.css" = 'html/changelog.css' - ) - -/datum/asset/simple/namespaced/common - assets = list("padlock.png" = 'html/padlock.png') - parents = list("common.css" = 'html/browser/common.css') - -/datum/asset/simple/stonekeep_class_menu_slop_layout - assets = list( - "try4.png" = 'icons/roguetown/misc/try4.png', - "try4_border.png" = 'icons/roguetown/misc/try4_border.png', - "slop_menustyle2.css" = 'html/browser/slop_menustyle2.css', - "gragstar.gif" = 'icons/roguetown/misc/gragstar.gif' - ) - -/datum/asset/simple/stonekeep_triumph_buy_menu_slop_layout - assets = list( - "try5.png" = 'icons/roguetown/misc/try5.png', - "try5_border.png" = 'icons/roguetown/misc/try5_border.png', - "slop_menustyle3.css" = 'html/browser/slop_menustyle3.css' - ) - -/datum/asset/simple/stonekeep_drifter_queue_menu_slop_layout - assets = list( - "slop_menustyle4.css" = 'html/browser/slop_menustyle4.css', - ) - -/datum/asset/simple/namespaced/roguefonts - legacy = TRUE - assets = list( - "PixelifySans.ttf" = 'interface/fonts/PixelifySans.ttf', - "pterra.ttf" = 'interface/fonts/pterra.ttf', - "blackmoor.ttf" = 'interface/fonts/blackmoor.ttf', - "book1.ttf" = 'interface/fonts/book1.ttf', - "book2.ttf" = 'interface/fonts/book1.ttf', - "book3.ttf" = 'interface/fonts/book1.ttf', - "book4.ttf" = 'interface/fonts/book1.ttf', - "dwarf.ttf" = 'interface/fonts/languages/dwarf.ttf', - "elf.ttf" = 'interface/fonts/languages/elf.ttf', - "oldpsydonic.ttf" = 'interface/fonts/languages/oldpsydonic.ttf', - "zalad.ttf" = 'interface/fonts/languages/zalad.ttf', - "hell.ttf" = 'interface/fonts/languages/hell.ttf', - "orc.ttf" = 'interface/fonts/languages/orc.ttf', - "celestial.ttf" = 'interface/fonts/languages/celestial.ttf', - "undead.ttf" = 'interface/fonts/languages/undead.ttf', - "Vaticanus.ttf" = 'interface/fonts/Vaticanus.ttf', - ) - -//this exists purely to avoid meta by pre-loading all language icons. -/datum/asset/language/register() - for(var/path in typesof(/datum/language)) - set waitfor = FALSE - var/datum/language/L = new path() - L.get_icon() - -/datum/asset/spritesheet_batched/achievements - name = "achievements" - -/datum/asset/spritesheet_batched/achievements/create_spritesheets() - // Needs this proc and can't be empty and not removing entirely - insert_icon("blank", uni_icon('icons/hud/storage.dmi', "blank")) - -/datum/asset/simple/permissions - assets = list( - "search.js" = 'html/admin/search.js', - "panels.css" = 'html/admin/panels.css' - ) - -/datum/asset/group/permissions - children = list( - /datum/asset/simple/permissions, - /datum/asset/simple/namespaced/common - ) - -/datum/asset/simple/notes - -/datum/asset/spritesheet_batched/chat - name = "chat" - -/datum/asset/spritesheet_batched/chat/create_spritesheets() - // pre-loading all lanugage icons also helps to avoid meta - insert_all_icons("language", 'icons/language.dmi') - // catch languages which are pulling icons from another file - for(var/datum/language/L as anything in typesof(/datum/language)) - var/icon = initial(L.icon) - if(icon != 'icons/language.dmi') - var/icon_state = initial(L.icon_state) - insert_icon("language-[icon_state]", uni_icon(icon, icon_state)) - -/datum/asset/group/goonchat - children = list( - /datum/asset/simple/jquery, - /datum/asset/simple/purify, - /datum/asset/simple/namespaced/goonchat, - /datum/asset/spritesheet_batched/chat, - /datum/asset/simple/namespaced/fontawesome, - /datum/asset/simple/namespaced/roguefonts - ) - -/datum/asset/simple/purify - legacy = TRUE - assets = list( - "purify.min.js" = 'code/modules/goonchat/browserassets/js/purify.min.js', - ) - -/datum/asset/simple/jquery - legacy = TRUE - assets = list( - "jquery.min.js" = 'code/modules/goonchat/browserassets/js/jquery.min.js', - ) - -/datum/asset/simple/namespaced/goonchat - legacy = TRUE - assets = list( - "json2.min.js" = 'code/modules/goonchat/browserassets/js/json2.min.js', - "errorHandler.js" = 'code/modules/goonchat/browserassets/js/errorHandler.js', - "browserOutput.js" = 'code/modules/goonchat/browserassets/js/browserOutput.js', - "browserOutput.css" = 'code/modules/goonchat/browserassets/css/browserOutput.css', - "browserOutput_white.css" = 'code/modules/goonchat/browserassets/css/browserOutput.css', - ) - -/datum/asset/simple/namespaced/fontawesome - assets = list( - "fa-regular-400.woff2" = 'html/font-awesome/webfonts/fa-regular-400.woff2', - "fa-solid-900.woff2" = 'html/font-awesome/webfonts/fa-solid-900.woff2', - ) - parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css') - -/// Maps icon names to ref values -/datum/asset/json/icon_ref_map - name = "icon_ref_map" - early = TRUE - -/datum/asset/json/icon_ref_map/generate() - var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]" - - //var/start = "0xc000000" - var/value = 0 - - while(TRUE) - value += 1 - var/ref = "\[0xc[num2text(value,6,16)]\]" - var/mystery_meat = locate(ref) - - if(isicon(mystery_meat)) - if(!isfile(mystery_meat)) // Ignore the runtime icons for now - continue - var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path - if(path) - data[path] = ref - else if(mystery_meat) - continue //Some other non-icon resource, ogg/json/whatever - else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh - break - - return data - -// If you use a file(...) object, instead of caching the asset it will be loaded from disk every time it's requested. -// This is useful for development, but not recommended for production. -// And if TGS is defined, we're being run in a production environment. - -#ifdef TGS -/datum/asset/simple/tgui - keep_local_name = FALSE - assets = list( - "tgui.bundle.js" = "tgui/public/tgui.bundle.js", - "tgui.bundle.css" = "tgui/public/tgui.bundle.css", - ) - -/datum/asset/simple/tgui_panel - keep_local_name = FALSE - assets = list( - "tgui-panel.bundle.js" = "tgui/public/tgui-panel.bundle.js", - "tgui-panel.bundle.css" = "tgui/public/tgui-panel.bundle.css", - ) - -#else -/datum/asset/simple/tgui - keep_local_name = TRUE - assets = list( - "tgui.bundle.js" = file("tgui/public/tgui.bundle.js"), - "tgui.bundle.css" = file("tgui/public/tgui.bundle.css"), - ) - -/datum/asset/simple/tgui_panel - keep_local_name = TRUE - assets = list( - "tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"), - "tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"), - ) -#endif - -/datum/asset/simple/namespaced/tgfont - assets = list( - "tgfont.eot" = file("tgui/packages/tgfont/static/tgfont.eot"), - "tgfont.woff2" = file("tgui/packages/tgfont/static/tgfont.woff2"), - ) - parents = list( - "tgfont.css" = file("tgui/packages/tgfont/static/tgfont.css"), - ) diff --git a/code/modules/asset_cache/asset_client.dm b/code/modules/asset_cache/asset_client.dm index 3fbb5613717..4c021f1bd9b 100644 --- a/code/modules/asset_cache/asset_client.dm +++ b/code/modules/asset_cache/asset_client.dm @@ -1,10 +1,3 @@ -/client - var/list/sent_assets = list() // List of all asset filenames sent to this client by the asset cache, along with their assoicated md5s - var/list/completed_asset_jobs = list() /// List of all completed blocking send jobs awaiting acknowledgement by send_asset - - var/last_asset_job = 0 /// Last asset send job id. - var/last_completed_asset_job = 0 - /// Process asset cache client topic calls for "asset_cache_confirm_arrival=[INT]" /client/proc/asset_cache_confirm_arrival(job_id) var/asset_cache_job = round(text2num(job_id)) @@ -42,10 +35,8 @@ var/job = ++last_asset_job var/t = 0 var/timeout_time = timeout - if(byond_version < 516) - src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") - else - src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") + + src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") while(!completed_asset_jobs["[job]"] && t < timeout_time) // Reception is handled in Topic() stoplag(1) // Lock up the requester until this is received. diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 21f493f1f05..27bc9960002 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -116,6 +116,10 @@ GLOBAL_LIST_EMPTY(asset_datums) for (var/asset_name in assets) SSassets.transport.unregister_asset(asset_name) +// If you use a file(...) object, instead of caching the asset it will be loaded from disk every time it's requested. +// This is useful for development, but not recommended for production. +// And if TGS is defined, we're being run in a production environment. + // For registering or sending multiple others at once /datum/asset/group abstract_type = /datum/asset/group diff --git a/code/modules/asset_cache/assets/_common.dm b/code/modules/asset_cache/assets/_common.dm new file mode 100644 index 00000000000..cfffbceca51 --- /dev/null +++ b/code/modules/asset_cache/assets/_common.dm @@ -0,0 +1,3 @@ +/datum/asset/simple/namespaced/common + assets = list("padlock.png" = 'html/padlock.png') + parents = list("common.css" = 'html/browser/common.css') diff --git a/code/modules/asset_cache/assets/achievements.dm b/code/modules/asset_cache/assets/achievements.dm new file mode 100644 index 00000000000..d9abc2e4702 --- /dev/null +++ b/code/modules/asset_cache/assets/achievements.dm @@ -0,0 +1,6 @@ +/datum/asset/spritesheet_batched/achievements + name = "achievements" + +/datum/asset/spritesheet_batched/achievements/create_spritesheets() + // Needs this proc and can't be empty and not removing entirely + insert_icon("blank", uni_icon('icons/hud/storage.dmi', "blank")) diff --git a/code/modules/asset_cache/assets/changelog.dm b/code/modules/asset_cache/assets/changelog.dm new file mode 100644 index 00000000000..5e9326c36c7 --- /dev/null +++ b/code/modules/asset_cache/assets/changelog.dm @@ -0,0 +1,4 @@ +/datum/asset/simple/changelog + assets = list( + "changelog.css" = 'html/changelog.css' + ) diff --git a/code/modules/asset_cache/assets/chat.dm b/code/modules/asset_cache/assets/chat.dm new file mode 100644 index 00000000000..eed81fab93d --- /dev/null +++ b/code/modules/asset_cache/assets/chat.dm @@ -0,0 +1,12 @@ +/datum/asset/spritesheet_batched/chat + name = "chat" + +/datum/asset/spritesheet_batched/chat/create_spritesheets() + // pre-loading all lanugage icons also helps to avoid meta + insert_all_icons("language", 'icons/language.dmi') + // catch languages which are pulling icons from another file + for(var/datum/language/L as anything in typesof(/datum/language)) + var/icon = initial(L.icon) + if(icon != 'icons/language.dmi') + var/icon_state = initial(L.icon_state) + insert_icon("language-[icon_state]", uni_icon(icon, icon_state)) diff --git a/code/modules/asset_cache/assets/fontawesome.dm b/code/modules/asset_cache/assets/fontawesome.dm new file mode 100644 index 00000000000..9dbc425a157 --- /dev/null +++ b/code/modules/asset_cache/assets/fontawesome.dm @@ -0,0 +1,6 @@ +/datum/asset/simple/namespaced/fontawesome + assets = list( + "fa-regular-400.woff2" = 'html/font-awesome/webfonts/fa-regular-400.woff2', + "fa-solid-900.woff2" = 'html/font-awesome/webfonts/fa-solid-900.woff2', + ) + parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css') diff --git a/code/modules/asset_cache/assets/icon_ref_map.dm b/code/modules/asset_cache/assets/icon_ref_map.dm new file mode 100644 index 00000000000..6eda0a598e6 --- /dev/null +++ b/code/modules/asset_cache/assets/icon_ref_map.dm @@ -0,0 +1,28 @@ + +/// Maps icon names to ref values +/datum/asset/json/icon_ref_map + name = "icon_ref_map" + +/datum/asset/json/icon_ref_map/generate() + var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]" + + //var/start = "0xc000000" + var/value = 0 + + while(TRUE) + value += 1 + var/ref = "\[0xc[num2text(value,6,16)]\]" + var/mystery_meat = locate(ref) + + if(isicon(mystery_meat)) + if(!isfile(mystery_meat)) // Ignore the runtime icons for now + continue + var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path + if(path) + data[path] = ref + else if(mystery_meat) + continue //Some other non-icon resource, ogg/json/whatever + else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh + break + + return data diff --git a/code/modules/asset_cache/assets/jquery.dm b/code/modules/asset_cache/assets/jquery.dm new file mode 100644 index 00000000000..c9e21cff71a --- /dev/null +++ b/code/modules/asset_cache/assets/jquery.dm @@ -0,0 +1,5 @@ +/datum/asset/simple/jquery + keep_local_name = TRUE + assets = list( + "jquery.min.js" = 'html/jquery/jquery.min.js', + ) diff --git a/code/modules/asset_cache/assets/permissions.dm b/code/modules/asset_cache/assets/permissions.dm new file mode 100644 index 00000000000..6cc6c0e754b --- /dev/null +++ b/code/modules/asset_cache/assets/permissions.dm @@ -0,0 +1,11 @@ +/datum/asset/simple/permissions + assets = list( + "search.js" = 'html/admin/search.js', + "panels.css" = 'html/admin/panels.css' + ) + +/datum/asset/group/permissions + children = list( + /datum/asset/simple/permissions, + /datum/asset/simple/namespaced/common + ) diff --git a/code/modules/asset_cache/assets/rougefont.dm b/code/modules/asset_cache/assets/rougefont.dm new file mode 100644 index 00000000000..7771ee67ccf --- /dev/null +++ b/code/modules/asset_cache/assets/rougefont.dm @@ -0,0 +1,21 @@ + +/datum/asset/simple/roguefonts + assets = list( + "PixelifySans.ttf" = 'interface/fonts/PixelifySans.ttf', + "Vaticanus.ttf" = 'interface/fonts/Vaticanus.ttf', + "pterra.ttf" = 'interface/fonts/pterra.ttf', + "BlackmoorLET.ttf" = 'interface/fonts/BlackmoorLET.ttf', + "book1.ttf" = 'interface/fonts/book1.ttf', + "book2.ttf" = 'interface/fonts/book1.ttf', + "book3.ttf" = 'interface/fonts/book1.ttf', + "book4.ttf" = 'interface/fonts/book1.ttf', + "dwarf.ttf" = 'interface/fonts/languages/dwarf.ttf', + "Dauphinn.ttf" = 'interface/fonts/languages/Dauphinn.ttf', + "oldpsydonic.ttf" = 'interface/fonts/languages/oldpsydonic.ttf', + "zalad.ttf" = 'interface/fonts/languages/zalad.ttf', + "hell.ttf" = 'interface/fonts/languages/hell.ttf', + "orc.ttf" = 'interface/fonts/languages/orc.ttf', + "celestial.ttf" = 'interface/fonts/languages/celestial.ttf', + "undead.ttf" = 'interface/fonts/languages/undead.ttf', + "deepspeak.ttf" = 'interface/fonts/languages/deepspeak.ttf', + ) diff --git a/code/modules/asset_cache/assets/stonekeep.dm b/code/modules/asset_cache/assets/stonekeep.dm new file mode 100644 index 00000000000..8a355125002 --- /dev/null +++ b/code/modules/asset_cache/assets/stonekeep.dm @@ -0,0 +1,16 @@ +/datum/asset/simple/stonekeep_class_menu_slop_layout + keep_local_name = TRUE + assets = list( + "try4.png" = 'icons/roguetown/misc/try4.png', + "try4_border.png" = 'icons/roguetown/misc/try4_border.png', + "slop_menustyle2.css" = 'html/browser/slop_menustyle2.css', + "gragstar.gif" = 'icons/roguetown/misc/gragstar.gif' + ) + +/datum/asset/simple/stonekeep_triumph_buy_menu_slop_layout + keep_local_name = TRUE + assets = list( + "try5.png" = 'icons/roguetown/misc/try5.png', + "try5_border.png" = 'icons/roguetown/misc/try5_border.png', + "slop_menustyle3.css" = 'html/browser/slop_menustyle3.css' + ) diff --git a/code/modules/asset_cache/assets/tgfont.dm b/code/modules/asset_cache/assets/tgfont.dm new file mode 100644 index 00000000000..d06fe3d47b2 --- /dev/null +++ b/code/modules/asset_cache/assets/tgfont.dm @@ -0,0 +1,10 @@ +/datum/asset/simple/namespaced/tgfont + assets = list( + "tgfont.eot" = file("tgui/packages/tgfont/static/tgfont.eot"), + "tgfont.woff2" = file("tgui/packages/tgfont/static/tgfont.woff2"), + ) + parents = list( + "tgfont.css" = file("tgui/packages/tgfont/static/tgfont.css"), + ) + + diff --git a/code/modules/asset_cache/assets/tgui.dm b/code/modules/asset_cache/assets/tgui.dm new file mode 100644 index 00000000000..d0f14f0141a --- /dev/null +++ b/code/modules/asset_cache/assets/tgui.dm @@ -0,0 +1,29 @@ +#ifdef TGS +/datum/asset/simple/tgui + keep_local_name = FALSE + assets = list( + "tgui.bundle.js" = "tgui/public/tgui.bundle.js", + "tgui.bundle.css" = "tgui/public/tgui.bundle.css", + ) + +/datum/asset/simple/tgui_panel + keep_local_name = FALSE + assets = list( + "tgui-panel.bundle.js" = "tgui/public/tgui-panel.bundle.js", + "tgui-panel.bundle.css" = "tgui/public/tgui-panel.bundle.css", + ) +#else +/datum/asset/simple/tgui + keep_local_name = TRUE + assets = list( + "tgui.bundle.js" = file("tgui/public/tgui.bundle.js"), + "tgui.bundle.css" = file("tgui/public/tgui.bundle.css"), + ) + +/datum/asset/simple/tgui_panel + keep_local_name = TRUE + assets = list( + "tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"), + "tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"), + ) +#endif diff --git a/code/modules/asset_cache/assets/vv.dm b/code/modules/asset_cache/assets/vv.dm new file mode 100644 index 00000000000..0dd974c7c79 --- /dev/null +++ b/code/modules/asset_cache/assets/vv.dm @@ -0,0 +1,4 @@ +/datum/asset/simple/vv + assets = list( + "view_variables.css" = 'html/admin/view_variables.css' + ) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index a88a0c7e0e7..5c327ce91ae 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -108,9 +108,6 @@ ///Used for limiting the rate of clicks sends by the client to avoid abuse var/list/clicklimiter - ///goonchat chatoutput of the client - var/datum/chatOutput/chatOutput - ///lazy list of all credit object bound to this client var/list/credits = list() @@ -161,5 +158,11 @@ ///A lazy list of atoms we've examined in the last EXAMINE_MORE_TIME (default 1.5) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining var/list/recent_examines + var/list/sent_assets = list() // List of all asset filenames sent to this client by the asset cache, along with their assoicated md5s + var/list/completed_asset_jobs = list() /// List of all completed blocking send jobs awaiting acknowledgement by send_asset + + var/last_asset_job = 0 /// Last asset send job id. + var/last_completed_asset_job = 0 + /// Loot panel for the client var/datum/lootpanel/loot_panel diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index bd637766d1a..993828e60a1 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -54,32 +54,28 @@ GLOBAL_LIST_EMPTY(respawncounts) var/asset_cache_job if(href_list["asset_cache_confirm_arrival"]) asset_cache_job = round(text2num(href_list["asset_cache_confirm_arrival"])) - //because we skip the limiter, we have to make sure this is a valid arrival and not somebody tricking us - // into letting append to a list without limit. - if (asset_cache_job > 0 && asset_cache_job <= last_asset_job && !(asset_cache_job in completed_asset_jobs)) - completed_asset_jobs += asset_cache_job + if(!asset_cache_job) return var/atom/ref = locate(href_list["src"]) - if(!holder && (href_list["window_id"] != "statbrowser") && !istype(ref, /datum/native_say)) - var/mtl = CONFIG_GET(number/minute_topic_limit) - if (mtl) - var/minute = round(world.time, 1 MINUTES) - if (!topiclimiter) - topiclimiter = new(LIMITER_SIZE) - if (minute != topiclimiter[CURRENT_MINUTE]) - topiclimiter[CURRENT_MINUTE] = minute - topiclimiter[MINUTE_COUNT] = 0 - topiclimiter[MINUTE_COUNT] += 1 - if (topiclimiter[MINUTE_COUNT] > mtl) - var/msg = "Your previous action was ignored because you've done too many in a minute." - if (minute != topiclimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them) - topiclimiter[ADMINSWARNED_AT] = minute - msg += " Administrators have been informed." - log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute") - message_admins("[ADMIN_LOOKUPFLW(usr)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute") - to_chat(src, span_danger("[msg]")) - return + var/mtl = CONFIG_GET(number/minute_topic_limit) + if(!holder && mtl && !istype(ref, /datum/native_say)) + var/minute = round(world.time, 1 MINUTES) + if (!topiclimiter) + topiclimiter = new(LIMITER_SIZE) + if (minute != topiclimiter[CURRENT_MINUTE]) + topiclimiter[CURRENT_MINUTE] = minute + topiclimiter[MINUTE_COUNT] = 0 + topiclimiter[MINUTE_COUNT] += 1 + if (topiclimiter[MINUTE_COUNT] > mtl) + var/msg = "Your previous action was ignored because you've done too many in a minute." + if (minute != topiclimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them) + topiclimiter[ADMINSWARNED_AT] = minute + msg += " Administrators have been informed." + log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute") + message_admins("[ADMIN_LOOKUPFLW(usr)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute") + to_chat(src, span_danger("[msg]")) + return var/stl = CONFIG_GET(number/second_topic_limit) if (stl) @@ -98,6 +94,9 @@ GLOBAL_LIST_EMPTY(respawncounts) if(tgui_Topic(href_list)) return + if(href_list["reload_tguipanel"]) + nuke_chat() + if(href_list["reload_statbrowser"]) stat_panel.reinitialize() @@ -110,6 +109,10 @@ GLOBAL_LIST_EMPTY(respawncounts) to_chat(src, "An error has been detected in how my client is receiving resources. Attempting to correct.... (If you keep seeing these messages you might want to close byond and reconnect)") src << browse("...", "window=asset_cache_browser") + if(href_list["asset_cache_preload_data"]) + asset_cache_preload_data(href_list["asset_cache_preload_data"]) + return + // Keypress passthrough if(href_list["__keydown"]) var/keycode = browser_keycode_to_byond(href_list["__keydown"]) @@ -272,8 +275,6 @@ GLOBAL_LIST_EMPTY(respawncounts) return if("vars") return view_var_Topic(href,href_list,hsrc) - if("chat") - return chatOutput.Topic(href, href_list) switch(href_list["action"]) if("openLink") @@ -436,10 +437,6 @@ GLOBAL_LIST_EMPTY(respawncounts) stat_panel = new(src, "statbrowser") stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) - chatOutput = new /datum/chatOutput(src) - spawn(5) // Goonchat does some non-instant checks in start() - chatOutput.start() - GLOB.ahelp_tickets.ClientLogin(src) var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins. //Admin Authorisation @@ -491,6 +488,9 @@ GLOBAL_LIST_EMPTY(respawncounts) prefs.last_id = computer_id //these are gonna be used for banning fps = prefs.clientfps + // Instantiate tgui panel + tgui_panel = new(src, "browseroutput") + if(fexists(roundend_report_file())) add_verb(src, /client/proc/show_previous_roundend_report) @@ -575,12 +575,16 @@ GLOBAL_LIST_EMPTY(respawncounts) ) addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 30 SECONDS) -// chatOutput.start() // Starts the chat + // Initalize tgui panel + tgui_panel.initialize() + INVOKE_ASYNC(src, PROC_REF(acquire_dpi)) - if(alert_mob_dupe_login) - spawn() - alert(mob, "You have logged in already with another key this round, please log out of this one NOW or risk being banned!") + if(alert_mob_dupe_login && !holder) + var/dupe_login_message = "Your ComputerID has already logged in with another key this round, please log out of this one NOW or risk being banned!" + spawn(0.5 SECONDS) //needs to run during world init, do not convert to add timer + alert(mob, dupe_login_message) //players get banned if they don't see this message, do not convert to tgui_alert (or even tg_alert) please. + to_chat_immediate(mob, span_danger(dupe_login_message)) connection_time = world.time connection_realtime = world.realtime @@ -667,10 +671,16 @@ GLOBAL_LIST_EMPTY(respawncounts) send_resources() - generate_clickcatcher() apply_clickcatcher() + if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates. + to_chat(src, span_info("You have unread updates in the changelog.")) + if(CONFIG_GET(flag/aggressive_changelog)) + changelog() + else + stat_panel.send_message("unread_changelog") + if(prefs.toggles & TOGGLE_FULLSCREEN) toggle_fullscreeny(TRUE) else @@ -684,9 +694,11 @@ GLOBAL_LIST_EMPTY(respawncounts) if(CONFIG_GET(flag/autoconvert_notes)) convert_notes_sql(ckey) - to_chat(src, get_message_output("message", ckey)) + if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them. + to_chat(src, "Unable to access asset cache browser, if you are using a custom skin file, please allow DS to download the updated version, if you are not, then make a bug report. This is not a critical issue but can cause issues with resource downloading, as it is impossible to know when extra resources arrived to you.") + update_ambience_pref() //This is down here because of the browse() calls in tooltip/New() @@ -714,9 +726,8 @@ GLOBAL_LIST_EMPTY(respawncounts) if (menuitem) menuitem.Load_checked(src) - - if(byond_version >= 516) // Enable 516 compat browser storage mechanisms - winset(src, null, "browser-options=byondstorage,find,devtools") + if(byond_version >= 516) // byondstorage handled by tgui + winset(src, null, "browser-options=find,devtools") loot_panel = new(src) @@ -762,6 +773,8 @@ GLOBAL_LIST_EMPTY(respawncounts) GLOB.clients -= src GLOB.directory -= ckey + QDEL_NULL(tgui_panel) + log_access("Logout: [key_name(src)]") GLOB.ahelp_tickets.ClientLogout(src) @@ -1454,7 +1467,7 @@ GLOBAL_LIST_EMPTY(respawncounts) continue panel_tabs |= verb_to_init.category verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name) - src.stat_panel.send_message("init_verbs", list(panel_tabs = panel_tabs, verblist = verblist)) + stat_panel.send_message("init_verbs", list(panel_tabs = panel_tabs, verblist = verblist)) /** * Handles incoming messages from the stat-panel TGUI. @@ -1480,6 +1493,7 @@ GLOBAL_LIST_EMPTY(respawncounts) null, ) return address in localhost_addresses + #undef LIMITER_SIZE #undef CURRENT_SECOND #undef SECOND_COUNT diff --git a/code/modules/client/darkmode.dm b/code/modules/client/darkmode.dm deleted file mode 100644 index edd3a4d7faa..00000000000 --- a/code/modules/client/darkmode.dm +++ /dev/null @@ -1,120 +0,0 @@ -//Darkmode preference by Kmc2000// - -/* -This lets you switch chat themes by using winset and CSS loading, you must relog to see this change (or rebuild your browseroutput datum) - -Things to note: -If you change ANYTHING in interface/skin.dmf you need to change it here: -Format: -winset(src, "window as appears in skin.dmf after elem", "var to change = currentvalue;var to change = desired value") - -How this works: -I've added a function to browseroutput.js which registers a cookie for darkmode and swaps the chat accordingly. You can find the button to do this under the "cog" icon next to the ping button (top right of chat) -This then swaps the window theme automatically - -Thanks to spacemaniac and mcdonald for help with the JS side of this. - -*/ - -/client/proc/force_white_theme() //There's no way round it. We're essentially changing the skin by hand. It's painful but it works, and is the way Lummox suggested. - return -/* //Main windows - winset(src, "infowindow", "background-color = [COLOR_DARKMODE_DARKBACKGROUND];background-color = none") - winset(src, "infowindow", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "info", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "info", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "browseroutput", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "browseroutput", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "outputwindow", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "outputwindow", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "mainwindow", "background-color = [COLOR_DARKMODE_DARKBACKGROUND];background-color = none") - winset(src, "split", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - //Buttons - winset(src, "changelog", "background-color = #494949;background-color = none") - winset(src, "changelog", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "rules", "background-color = #494949;background-color = none") - winset(src, "rules", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "wiki", "background-color = #494949;background-color = none") - winset(src, "wiki", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "forum", "background-color = #494949;background-color = none") - winset(src, "forum", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "github", "background-color = #3a3a3a;background-color = none") - winset(src, "github", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "report-issue", "background-color = #492020;background-color = none") - winset(src, "report-issue", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - //Status and verb tabs - winset(src, "output", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "output", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "outputwindow", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "outputwindow", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "statwindow", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "statwindow", "text-color = #eaeaea;text-color = #000000") - winset(src, "stat", "background-color = [COLOR_DARKMODE_DARKBACKGROUND];background-color = #FFFFFF") - winset(src, "stat", "tab-background-color = [COLOR_DARKMODE_BACKGROUND];tab-background-color = none") - winset(src, "stat", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "stat", "tab-text-color = [COLOR_DARKMODE_TEXT];tab-text-color = #000000") - winset(src, "stat", "prefix-color = [COLOR_DARKMODE_TEXT];prefix-color = #000000") - winset(src, "stat", "suffix-color = [COLOR_DARKMODE_TEXT];suffix-color = #000000") - //Say, OOC, me Buttons etc. - winset(src, "saybutton", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "saybutton", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "oocbutton", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "oocbutton", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "mebutton", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "mebutton", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "asset_cache_browser", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "asset_cache_browser", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000") - winset(src, "tooltip", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none") - winset(src, "tooltip", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")*/ - -/client/proc/force_dark_theme() //Inversely, if theyre using white theme and want to swap to the superior dark theme, let's get WINSET() ing - return -/* //Main windows - winset(src, "infowindow", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "infowindow", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "info", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "info", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "browseroutput", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "browseroutput", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "outputwindow", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "outputwindow", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "mainwindow", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "split", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - //Buttons - winset(src, "changelog", "background-color = none;background-color = #494949") - winset(src, "changelog", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "rules", "background-color = none;background-color = #494949") - winset(src, "rules", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "wiki", "background-color = none;background-color = #494949") - winset(src, "wiki", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "forum", "background-color = none;background-color = #494949") - winset(src, "forum", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "github", "background-color = none;background-color = #3a3a3a") - winset(src, "github", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "report-issue", "background-color = none;background-color = #492020") - winset(src, "report-issue", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - //Status and verb tabs - winset(src, "output", "background-color = none;background-color = [COLOR_DARKMODE_DARKBACKGROUND]") - winset(src, "output", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "outputwindow", "background-color = none;background-color = [COLOR_DARKMODE_DARKBACKGROUND]") - winset(src, "outputwindow", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "statwindow", "background-color = none;background-color = [COLOR_DARKMODE_DARKBACKGROUND]") - winset(src, "statwindow", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "stat", "background-color = #FFFFFF;background-color = [COLOR_DARKMODE_DARKBACKGROUND]") - winset(src, "stat", "tab-background-color = none;tab-background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "stat", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "stat", "tab-text-color = #000000;tab-text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "stat", "prefix-color = #000000;prefix-color = [COLOR_DARKMODE_TEXT]") - winset(src, "stat", "suffix-color = #000000;suffix-color = [COLOR_DARKMODE_TEXT]") - //Say, OOC, me Buttons etc. - winset(src, "saybutton", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "saybutton", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "oocbutton", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "oocbutton", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "mebutton", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "mebutton", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "asset_cache_browser", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "asset_cache_browser", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") - winset(src, "tooltip", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]") - winset(src, "tooltip", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]") -*/ diff --git a/code/modules/client/verbs/ghost.dm b/code/modules/client/verbs/ghost.dm index ae7c1069d99..311aa385689 100644 --- a/code/modules/client/verbs/ghost.dm +++ b/code/modules/client/verbs/ghost.dm @@ -60,6 +60,7 @@ to_chat(client, span_biginfo("Necra has guaranteed your passage to the next life. Your toll has been already paid.")) var/area/underworld/underworld = get_area(spawn_loc) + underworld.Entered(live_spirit, null) /mob/proc/can_enter_underworld() diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index 087c689a824..449516cf410 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -295,87 +295,6 @@ GLOBAL_LIST_INIT(oocpronouns_required, list( if(search) return lowertext(copytext(jd, pos+9, search)) -/client/verb/fix_chat() - set name = "Fix Chat" - set category = "OOC.Fix" - if (!chatOutput || !istype(chatOutput)) - var/action = alert(src, "Invalid Chat Output data found!\nRecreate data?", "Wot?", "Recreate Chat Output data", "Cancel") - if (action != "Recreate Chat Output data") - return - chatOutput = new /datum/chatOutput(src) - chatOutput.start() - action = alert(src, "Goon chat reloading, wait a bit and tell me if it's fixed", "", "Fixed", "Nope") - if (action == "Fixed") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by re-creating the chatOutput datum") - else - chatOutput.load() - action = alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No") - if (action == "Yes") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by re-creating the chatOutput datum and forcing a load()") - else - action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat") - if (action == "Switch to old chat") - winset(src, "output", "is-visible=true;is-disabled=false") - winset(src, "browseroutput", "is-visible=false") - log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window after recreating the chatOutput and forcing a load()") - - else if (chatOutput.loaded) - var/action = alert(src, "ChatOutput seems to be loaded\nDo you want me to force a reload, wiping the chat log or just refresh the chat window because it broke/went away?", "Hmmm", "Force Reload", "Refresh", "Cancel") - switch (action) - if ("Force Reload") - chatOutput.loaded = FALSE - chatOutput.start() //this is likely to fail since it asks , but we should try it anyways so we know. - action = alert(src, "Goon chat reloading, wait a bit and tell me if it's fixed", "", "Fixed", "Nope") - if (action == "Fixed") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a start()") - else - chatOutput.load() - action = alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No") - if (action == "Yes") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a load()") - else - action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat") - if (action == "Switch to old chat") - winset(src, "output", "is-visible=true;is-disabled=false") - winset(src, "browseroutput", "is-visible=false") - log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window forcing a start() and forcing a load()") - - if ("Refresh") - chatOutput.showChat() - action = alert(src, "Goon chat refreshing, wait a bit and tell me if it's fixed", "", "Fixed", "Nope, force a reload") - if (action == "Fixed") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a show()") - else - chatOutput.loaded = FALSE - chatOutput.load() - action = alert(src, "How about now? (give it a moment)", "", "Yes", "No") - if (action == "Yes") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a load()") - else - action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat") - if (action == "Switch to old chat") - winset(src, "output", "is-visible=true;is-disabled=false") - winset(src, "browseroutput", "is-visible=false") - log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window forcing a show() and forcing a load()") - return - - else - chatOutput.start() - var/action = alert(src, "Manually loading Chat, wait a bit and tell me if it's fixed", "", "Fixed", "Nope") - if (action == "Fixed") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by manually calling start()") - else - chatOutput.load() - alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No") - if (action == "Yes") - log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by manually calling start() and forcing a load()") - else - action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat") - if (action == "Switch to old chat") - winset(src, "output", list2params(list("on-show" = "", "is-disabled" = "false", "is-visible" = "true"))) - winset(src, "browseroutput", "is-disabled=true;is-visible=false") - log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window after manually calling start() and forcing a load()") - /client/proc/validate_oocpronouns(value) value = lowertext(value) @@ -462,7 +381,7 @@ GLOBAL_LIST_INIT(oocpronouns_required, list( return var/motd = global.config.motd if(motd) - to_chat(src, "
[motd]
", handle_whitespace=FALSE) + to_chat(src, "
[motd]
") else to_chat(src, "The Message of the Day has not been set.") diff --git a/code/modules/goonchat/browserOutput.dm b/code/modules/goonchat/browserOutput.dm deleted file mode 100644 index 0a6a0e960e6..00000000000 --- a/code/modules/goonchat/browserOutput.dm +++ /dev/null @@ -1,302 +0,0 @@ - -#define MAX_COOKIE_LENGTH 5 - -/********************************* -For the main html chat area -*********************************/ - -//Precaching a bunch of shit -GLOBAL_DATUM_INIT(iconCache, /savefile, new("tmp/iconCache.sav")) //Cache of icons for the browser output - -//On client, created on login -/datum/chatOutput - var/client/owner //client ref - var/loaded = FALSE // Has the client loaded the browser output area? - var/list/messageQueue = list()//If they haven't loaded chat, this is where messages will go until they do - var/cookieSent = FALSE // Has the client sent a cookie for analysis - var/broken = FALSE - var/list/connectionHistory //Contains the connection history passed from chat cookie - var/adminMusicVolume = 50 //This is for the Play Global Sound verb - var/total_checks = 0 - var/load_attempts = 0 - - -/datum/chatOutput/New(client/C) - owner = C - messageQueue = list() - connectionHistory = list() - -/datum/chatOutput/proc/start() - //Check for existing chat - if(!owner) - return FALSE - - if(!winexists(owner, "browseroutput")) // Oh goddamnit. - set waitfor = FALSE - broken = TRUE - message_admins("Couldn't start chat for [key_name_admin(owner)]!") - . = FALSE - if(owner) - alert(owner.mob, "Updated chat window does not exist. If you are using a custom skin file please allow the game to update.") - return - - if(!owner) // In case the client vanishes before winexists returns - return 0 - - if(winget(owner, "browseroutput", "is-visible") == "true") //Already setup - doneLoading() - - else //Not setup - load() - - return TRUE - -/datum/chatOutput/proc/load() - set waitfor = FALSE - if(!owner) - return - if(loaded) - return - var/datum/asset/stuff = get_asset_datum(/datum/asset/group/goonchat) - stuff.send(owner) - - //owner << browse(file('code/modules/goonchat/browserassets/html/browserOutput.html'), "window=recipe;size=500x810") - - owner << browse(file('code/modules/goonchat/browserassets/html/browserOutput.html'), "window=browseroutput") - - if (load_attempts < 5) //To a max of 5 load attempts - spawn(20 SECONDS) - if (owner && !loaded) - load_attempts++ - load() - else - return - -/datum/chatOutput/Topic(href, list/href_list) - if(usr.client != owner) - return TRUE - // Build arguments. - // Arguments are in the form "param[paramname]=thing" - var/list/params = list() - for(var/key in href_list) - if(length(key) > 7 && findtext(key, "param")) // 7 is the amount of characters in the basic param key template. - var/param_name = copytext(key, 7, -1) - var/item = href_list[key] - - params[param_name] = item - - var/data // Data to be sent back to the chat. - switch(href_list["proc"]) - if("doneLoading") - data = doneLoading(arglist(params)) - - if("debug") - data = debug(arglist(params)) - - if("ping") - data = ping(arglist(params)) - - if("analyzeClientData") - data = analyzeClientData(arglist(params)) - - if("setMusicVolume") - data = setMusicVolume(arglist(params)) - if("swaptodarkmode") - swaptodarkmode() - if("swaptolightmode") - swaptolightmode() - - if(data) - ehjax_send(data = data) - - -//Called on chat output done-loading by JS. -/datum/chatOutput/proc/doneLoading() - if(loaded || !owner) - return - - loaded = TRUE - showChat() - - for(var/message in messageQueue) - // whitespace has already been handled by the original to_chat - to_chat(owner, message, handle_whitespace=FALSE) - - messageQueue = null - sendClientData() - - syncRegex() - - SEND_TEXT(owner, "Failed to load fancy chat, reverting to old chat. Certain features won't work.") - -/datum/chatOutput/proc/showChat() - winset(owner, "output", "is-visible=false") - winset(owner, "browseroutput", "is-disabled=false;is-visible=true") - -/proc/syncChatRegexes() - for (var/user in GLOB.clients) - var/client/C = user - var/datum/chatOutput/Cchat = C.chatOutput - if (Cchat && !Cchat.broken && Cchat.loaded) - Cchat.syncRegex() - -/datum/chatOutput/proc/syncRegex() - var/list/regexes = list() - - if (config.ic_filter_regex) - regexes["show_filtered_ic_chat"] = list( - config.ic_filter_regex.name, - "ig", - span_boldwarning("$1") - ) - - if (regexes.len) - ehjax_send(data = list("syncRegex" = regexes)) - -/datum/chatOutput/proc/ehjax_send(client/C = owner, window = "browseroutput", data) - if(islist(data)) - data = json_encode(data) - C << output("[data]", "[window]:ehjaxCallback") - -/datum/chatOutput/proc/sendMusic(music, list/extra_data) - if(!findtext(music, GLOB.is_http_protocol)) - return - var/list/music_data = list("adminMusic" = url_encode(url_encode(music))) - - if(extra_data?.len) - music_data["musicRate"] = extra_data["pitch"] - music_data["musicSeek"] = extra_data["start"] - music_data["musicHalt"] = extra_data["end"] - - ehjax_send(data = music_data) - -/datum/chatOutput/proc/stopMusic() - ehjax_send(data = "stopMusic") - -/datum/chatOutput/proc/setMusicVolume(volume = "") - if(volume) - adminMusicVolume = CLAMP(text2num(volume), 0, 100) - -//Sends client connection details to the chat to handle and save -/datum/chatOutput/proc/sendClientData() - //Get dem deets - var/list/deets = list("clientData" = list()) - deets["clientData"]["ckey"] = owner.ckey - deets["clientData"]["ip"] = owner.address - deets["clientData"]["compid"] = owner.computer_id - var/data = json_encode(deets) - ehjax_send(data = data) - - -//Called by client, sent data to investigate (cookie history so far) -/datum/chatOutput/proc/analyzeClientData(cookie = "") - if(!cookie) - return - - if(cookie != "none") - var/regex/simple_crash_regex = new /regex("(\\\[ *){5}") - if(simple_crash_regex.Find(cookie)) - message_admins("[key_name(src.owner)] tried to crash the server using malformed JSON") - log_admin("[key_name(owner)] tried to crash the server using malformed JSON") - return - var/list/connData = json_decode(cookie) - if (connData && islist(connData) && connData.len > 0 && connData["connData"]) - connectionHistory = connData["connData"] //lol fuck - var/list/found = new() - if(connectionHistory.len > MAX_COOKIE_LENGTH) - message_admins("[key_name(src.owner)] was kicked for an invalid ban cookie)") - qdel(owner) - return - for(var/i in connectionHistory.len to 1 step -1) - var/list/row = src.connectionHistory[i] - if (!row || row.len < 3 || (!row["ckey"] || !row["compid"] || !row["ip"])) //Passed malformed history object - return - if (world.IsBanned(row["ckey"], row["ip"], row["compid"], real_bans_only=TRUE)) - found = row - break - - //Uh oh this fucker has a history of playing on a banned account!! - if (found.len > 0) - //TODO: add a new evasion ban for the CURRENT client details, using the matched row details - message_admins("[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])") - log_admin_private("[key_name(owner)] has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])") - - cookieSent = TRUE - -//Called by js client every 60 seconds -/datum/chatOutput/proc/ping() - return "pong" - -//Called by js client on js error -/datum/chatOutput/proc/debug(error) - log_world("\[[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]\] Client: [(src.owner.key ? src.owner.key : src.owner)] triggered JS error: [error]") - -/* -//Global chat procs -/proc/to_chat_immediate(target, message, handle_whitespace = TRUE) - if(!target || !message) - 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) -*/ -/datum/chatOutput/proc/swaptolightmode() //Dark mode light mode stuff. Yell at KMC if this breaks! (See darkmode.dm for documentation) - owner.force_white_theme() - -/datum/chatOutput/proc/swaptodarkmode() - owner.force_dark_theme() - -#undef MAX_COOKIE_LENGTH - diff --git a/code/modules/goonchat/browserassets/css/browserOutput.css b/code/modules/goonchat/browserassets/css/browserOutput.css deleted file mode 100644 index 56e9017c4c1..00000000000 --- a/code/modules/goonchat/browserassets/css/browserOutput.css +++ /dev/null @@ -1,1250 +0,0 @@ -/***************************************** -* -* GLOBAL STYLES -* -******************************************/ -html, body { - padding: 0; - margin: 0; - height: 100%; - color: #c9c1ba; -} -body { - background: #000000; - font-family: Pterra; - font-size: 13px; - color: #c9c1ba; - line-height: 1.2; - overflow-x: hidden; - overflow-y: scroll; - word-wrap: break-word; - scrollbar-face-color:#1A1A1A; - scrollbar-track-color:#171717; - scrollbar-highlight-color:#171717; - text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000; -} - - -/* Fonts */ -@font-face { - font-family: "Pterra"; - src: url('pterra.ttf') format('truetype'); -} -@font-face { - font-family: "Honoka Mincho"; - src: url('HonokaMincho.ttf') format('truetype'); -} - -@font-face { - font-family: "Dauphin"; - src: url('elf.ttf') format('truetype'); -} - -@font-face { - font-family: "MasonAlternate"; - src: url('dwarf.ttf') format('truetype'); -} - -@font-face { - font-family: "Arabolical"; - src: url('sand.ttf') format('truetype'); -} - -@font-face { - font-family: "Xaphan"; - src: url('hell.ttf') format('truetype'); -} - -@font-face { - font-family: "FriskyVampire"; - src: url('undead.ttf') format('truetype'); -} - -@font-face { - font-family: "Thief by The Riddler"; - src: url('orc.ttf') format('truetype'); -} - -@font-face { - font-family: "Notredame"; - src: url('otavan.ttf'); -} - -@font-face { - font-family: "Underwater Love"; - src: url('deepspeak.ttf') format('truetype'); -} - -@font-face { - font-family: "Kingthings Petrock"; - src: url('draconic.ttf') format('truetype'); -} - -@font-face { - font-family: "Emperialisme"; - src: url('lupian.ttf') format('truetype'); -} - -/* */ - -em { - font-style: normal; - font-weight: bold; -} - -img { - margin: 0; - padding: 0; - line-height: 1; - -ms-interpolation-mode: nearest-neighbor; - image-rendering: pixelated; -} -img.icon { - height: 1em; - min-height: 16px; - width: auto; - vertical-align: bottom; -} - -.r:before { /* "repeated" badge class for combined messages */ - content: 'x'; -} -.r { - display: inline-block; - min-width: 0.5em; - font-size: 0.7em; - padding: 0.2em 0.3em; - line-height: 1.2; - color: white; - text-align: center; - white-space: nowrap; - vertical-align: middle; - background-color: crimson; - border-radius: 10px; -} - -a {color: #397ea5;} -a.visited {color: #7c00e6;} -a:visited {color: #7c00e6;} -a.popt {text-decoration: none;} - -.chat_box { - display: inline-block; - width: calc(100% - 1em); - margin: 0.5em; - padding: 0.5em 0.75em; - box-sizing: border-box; -} - -.examine_block { - background: #0f0f0f; - border: 1px solid #1d1d1f; -} - -.mentor_block { - background: #3e263a; - border: 1px dotted #7f4274; -} - -.announcement_block { - background: #2c032f; - border: 6px inset #990b3f; -} - -/***************************************** -* -* OUTPUT NOT RELATED TO ACTUAL MESSAGES -* -******************************************/ -#loading { - position: fixed; - width: 300px; - height: 150px; - text-align: center; - left: 50%; - top: 50%; - margin: -75px 0 0 -150px; -} -#loading i {display: block; padding-bottom: 3px;} - -#filterTabs { - position: fixed; - top: 0; - left: 0; - right: 0; - background: #2d2d2d; - border-bottom: 2px solid #4CAF50; - z-index: 100; - display: flex; - flex-wrap: wrap; - padding: 5px; - box-shadow: 0 2px 5px rgba(0,0,0,0.3); -} - -.filter-tab { - background: #3d3d3d; - color: #ffffff; - border: 1px solid #000000; - padding: 8px 15px; - margin: 2px; - cursor: pointer; - border-radius: 4px; - transition: all 0.3s ease; - font-size: 12px; - position: relative; - user-select: none; -} - -.filter-tab:hover { - background: #4d4d4d; - transform: translateY(-1px); -} - -.filter-tab.active { - background: #1b1b1b; - font-weight: bold; -} - -.filter-tab.custom { - background: #413e3c; -} - -.filter-tab.custom.active { - background: #555251; -} - -.filter-tab .remove-tab { - margin-left: 8px; - color: #ff4444; - font-weight: bold; - cursor: pointer; -} - -.filter-tab .remove-tab:hover { - color: #ff0000; -} - -#addTabBtn { - background: #6c757d; - color: white; - border: none; - padding: 8px 12px; - margin: 2px; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - transition: background 0.3s ease; -} - -#addTabBtn:hover { - background: #5a6268; -} - -#addTabForm { - display: none; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: #2d2d2d; - border: 1px solid #555; - border-radius: 8px; - padding: 20px; - z-index: 1000; - min-width: 300px; -} - -#addTabForm input { - width: 100%; - padding: 8px; - margin: 5px 0; - background: #3d3d3d; - border: 1px solid #555; - border-radius: 4px; - color: #ffffff; -} - -#addTabForm button { - padding: 8px 15px; - margin: 5px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -#addTabForm .save { - background: #4CAF50; - color: white; -} - -#addTabForm .cancel { - background: #6c757d; - color: white; -} - -.hidden { - display: none !important; -} - -/* Responsive design */ -@media (max-width: 768px) { - #filterTabs { - font-size: 11px; - } - - .filter-tab { - padding: 6px 10px; - font-size: 11px; - } - - #userBar { - flex-direction: column; - gap: 5px; - } -} -#messages { - font-size: 13px; - padding: 3px; - margin: 0; - word-wrap: break-word; - padding-top: 60px; -} -#newMessages { - position: fixed; - display: block; - bottom: 0; - right: 0; - padding: 8px; - background: #202020; - text-decoration: none; - font-variant: small-caps; - font-size: 1.1em; - font-weight: bold; - color: #c9c1ba; -} -#newMessages:hover {background: #000000;} -#newMessages i {vertical-align: middle; padding-left: 3px;} -#ping { - padding: 8px 0 2px 0; -} -#ping i {display: block; text-align: center;} -#ping .ms { - display: block; - text-align: center; - font-size: 8pt; - padding-top: 2px; -} -#userBar { - position: fixed; - top: 0; - right: 0; - padding-top: 60px; -} -#userBar .subCell { - background: #202020; - height: 30px; - padding: 5px 0; - display: block; - color: #EEEEEE; - text-decoration: none; - line-height: 28px; - border-top: 1px solid #171717; -} -#userBar .subCell:hover {background: #202020;} -#userBar .toggle { - width: 45px; - background: #202020; - border-top: 0; - float: right; - text-align: center; -} -#userBar .sub {clear: both; display: none; width: 200px;} -#userBar .sub.scroll {overflow-y: scroll;} -#userBar .sub.subCell {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;} -#userBar .sub span { - display: block; - line-height: 30px; - float: left; -} -#userBar .sub i { - display: block; - padding: 0 5px; - font-size: 1.1em; - width: 22px; - text-align: center; - line-height: 30px; - float: right; -} -#userBar .sub input { - position: absolute; - padding: 7px 5px; - width: 121px; - line-height: 30px; - float: left; -} -#userBar .topCell {border-top: 0;} - -/* WebSocket Standalone Popup - Centers on screen like filters */ -#subWebsocket { - position: fixed !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - z-index: 9999 !important; - width: 320px !important; - background: #2a2a2a !important; - border: 1px solid #555 !important; - border-radius: 8px !important; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6) !important; -} - -/* Backdrop overlay */ -#subWebsocket::before { - content: '' !important; - position: fixed !important; - top: 0 !important; - left: 0 !important; - width: 100vw !important; - height: 100vh !important; - background: rgba(0, 0, 0, 0.5) !important; - z-index: -1 !important; -} - -#subWebsocket .websocket-settings { - position: relative !important; - padding: 20px !important; - background: #2a2a2a !important; - border-radius: 8px !important; - width: 100% !important; - height: 220px !important; - box-sizing: border-box !important; -} - -/* Status */ -#websocketStatus { - position: absolute !important; - top: 20px !important; - left: 20px !important; - right: 20px !important; - height: 30px !important; - padding: 6px 12px !important; - border-radius: 4px !important; - font-weight: bold !important; - text-align: center !important; - font-size: 13px !important; - line-height: 18px !important; -} - -#websocketStatus.connected { background: #4CAF50 !important; color: white !important; } -#websocketStatus.disconnected { background: #f44336 !important; color: white !important; } -#websocketStatus.connecting { background: #ff9800 !important; color: white !important; } - -/* Checkbox Row */ -.websocket-checkbox-row { - position: absolute !important; - top: 70px !important; - left: 20px !important; - right: 20px !important; - height: 25px !important; - display: flex !important; - align-items: center !important; - color: #fff !important; - font-size: 14px !important; - cursor: pointer !important; -} - -.websocket-checkbox-row input[type="checkbox"] { - margin: 0 10px 0 0 !important; - transform: scale(1.2) !important; -} - -/* Server Input Group */ -.websocket-server-group { - position: absolute !important; - top: 110px !important; - left: 20px !important; - right: 20px !important; - height: 60px !important; -} - -.websocket-server-group label { - position: absolute !important; - top: 0 !important; - left: 0 !important; - color: #fff !important; - font-size: 14px !important; - line-height: 20px !important; -} - -#websocketServer { - position: static !important; - display: block !important; - width: 100% !important; - height: 36px !important; - padding: 10px 12px !important; - margin: 8px 0 16px 0 !important; - border: 1px solid #555 !important; - background: #333 !important; - color: #fff !important; - border-radius: 4px !important; - box-sizing: border-box !important; - font-size: 14px !important; - float: none !important; - clear: both !important; -} - -.websocket-server-group input:focus { - outline: none !important; - border-color: #007cba !important; - box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.3) !important; -} - -/* Button Controls */ -.websocket-controls { - position: absolute !important; - bottom: 20px !important; - left: 20px !important; - right: 20px !important; - height: 36px !important; - display: flex !important; - gap: 8px !important; -} - -.websocket-controls button { - flex: 1 !important; - height: 36px !important; - padding: 0 !important; - border: none !important; - border-radius: 4px !important; - cursor: pointer !important; - background: #555 !important; - color: white !important; - font-size: 12px !important; - font-weight: 500 !important; - transition: background-color 0.2s ease !important; - text-align: center !important; -} - -.websocket-controls button:hover { - background: #666 !important; -} - -#connectWebsocket { - background: #007cba !important; -} - -#connectWebsocket:hover { - background: #0056b3 !important; -} - -.websocket-controls button:disabled { - opacity: 0.5 !important; - cursor: not-allowed !important; -} - -/* Close button (add this to your HTML if needed) */ -.websocket-close { - position: absolute !important; - top: 8px !important; - right: 8px !important; - width: 24px !important; - height: 24px !important; - background: none !important; - border: none !important; - color: #aaa !important; - cursor: pointer !important; - font-size: 18px !important; - line-height: 1 !important; -} - -.websocket-close:hover { - color: #fff !important; -} -/* POPUPS */ -.popup { - position: fixed; - top: 50%; - left: 50%; - background: #ddd; -} -.popup .close { - position: absolute; - background: #aaa; - top: 0; - right: 0; - color: #333; - text-decoration: none; - z-index: 2; - padding: 0 10px; - height: 30px; - line-height: 30px; -} -.popup .close:hover {background: #999;} -.popup .head { - background: #999; - color: #ddd; - padding: 0 10px; - height: 30px; - line-height: 30px; - text-transform: uppercase; - font-size: 0.9em; - font-weight: bold; - border-bottom: 2px solid green; -} -.popup input {border: 1px solid #999; background: #fff; margin: 0; padding: 5px; outline: none; color: #333;} -.popup input[type=text]:hover, .popup input[type=text]:active, .popup input[type=text]:focus {border-color: green;} -.popup input[type=submit] {padding: 5px 10px; background: #999; color: #ddd; text-transform: uppercase; font-size: 0.9em; font-weight: bold;} -.popup input[type=submit]:hover, .popup input[type=submit]:focus, .popup input[type=submit]:active {background: #aaa; cursor: pointer;} - -.changeFont {padding: 10px;} -.changeFont a {display: block; text-decoration: none; padding: 3px; color: #333;} -.changeFont a:hover {background: #ccc;} - -.highlightPopup {padding: 10px; text-align: center;} -.highlightPopup input[type=text] {display: block; width: 215px; text-align: left; margin-top: 5px;} -.highlightPopup input.highlightColor {background-color: #FFFF00;} -.highlightPopup input.highlightTermSubmit {margin-top: 5px;} - -/* ADMIN FILTER MESSAGES MENU */ -.filterMessages {padding: 5px;} -.filterMessages div {padding: 2px 0;} -.filterMessages input {} -.filterMessages label {} - -.icon-stack {height: 1em; line-height: 1em; width: 1em; vertical-align: middle; margin-top: -2px;} - - -/***************************************** -* -* OUTPUT ACTUALLY RELATED TO MESSAGES -* -******************************************/ - -body {word-wrap: break-word; overflow-x: hidden; overflow-y: scroll; color: #c9c1ba; font-size: 16px; font-family: "Pterra";} - -h1, h2, h3, h4, h5, h6 {color: #c9c1ba; font-family: Pterra;} - -em {font-style: normal; font-weight: bold; font-family: Pterra;} - -a:link {color: #ae83cb; font-weight: bold;} - -.motd {color: #638500; font-family: Pterra;} -.motd h1, .motd h2, .motd h3, .motd h4, .motd h5, .motd h6 - {color: #638500; text-decoration: underline;} -.motd a, .motd a:link, .motd a:visited, .motd a:active, .motd a:hover - {color: #638500;} -h1.alert, h2.alert {color: #c9c1ba;font-family: Pterra, TrueType;} -.italics {font-style: italic;} - -.bold {font-weight: bold;} - -.prefix {font-weight: bold;} - -.ooc {color: #c5c5c5; font-weight: bold; font-family: Pterra;} -.adminobserverooc {color: #cca300; font-weight: bold; font-family: Pterra;} -.adminooc {color: #4972bc; font-weight: bold;} - -.adminsay {color: #FF4500; font-weight: bold;} -.admin {color: #386aff; font-weight: bold;} - -.name { font-weight: bold;} - -.say {font-family: Pterra;} -.deadsay {color: #e2c1ff;} -.binarysay {color: #20c20e; background-color: #000000; display: block;} -.binarysay a {color: #00ff00;} -.binarysay a:active, .binarysay a:visited {color: #88ff88;} -.radio {color: #1ecc43;} - - -.yell {font-weight: bold;} - -.alert {color: #d82020;} - -.alert_holder{ - border: 0.1rem solid #FFF; - border-radius: 0.6rem; - padding: 1rem; -} - -.emote {color: #b1bb9f; font-size: 75%;} - -.crit {color: #c71d76;} -.userdanger {color: #c71d76; font-weight: bold; font-size: 120%;} -.danger {color: #b9322b; font-weight: bold;} -.warning {color: #bb4e28; font-size: 75%;} -.warningbig {color: #bb4e28;} -.boldwarning {color: #bb4e28; font-weight: bold} -.announce {color: #c51e1e; font-weight: bold;} -.boldannounce {color: #c51e1e; font-weight: bold;} -.greenannounce {color: #059223; font-weight: bold;} -.rose {color: #e7bed8;} -.love {color: #e7bed8; font-size: 75%;} -.info {color: #a9a5b6; font-size: 75%; line-height:1} -.biginfo {color: #a9a5b6;} -.notice {color: #f1d669;} -.boldnotice {color: #f1d669; font-weight: bold;} -.smallnotice {color: #f1d669; font-size: 75%;} -.hear {color: #6685f5; font-style: italic;} -.adminnotice {color: #6685f5;} -.adminannouncebig {color: #6685f5; font-style: bold; font-size: 200%} -.adminannounce {color: #6685f5; font-style: bold; font-size: 120%} -.adminhelp {color: #ff0000; font-weight: bold; font-size: 120%} -.unconscious {color: #c9c1ba; font-weight: bold;} -.suicide {color: #ff5050; font-style: italic;} -.green {color: #80b077;} -.good {color: #00ff00;} -.smallgreen {color: #80b077; font-size: 75%;} -.boldgreen {color: #80b077; font-weight: bold;} -.red {color: #b84d47;} -.smallred {color: #b84d47; font-size: 75%;} -.boldred {color: #b84d47; font-weight: bold;} -.blue {color: #6a8cb7;} -.lightpurple {color: #967aaf;} -.nicegreen {color: #9bccd0;} -.cult {color: #960000;} -.cultsmall {color: #960000; font-size: 75%;} -.narsie {color: #960000; font-weight: bold; font-size: 12;} -.narsiesmall {color: #960000; font-weight: bold; font-size: 6;} -.colossus {color: #7F282A; font-size: 5;} -.hierophant {color: #660099; font-weight: bold; font-style: italic;} -.hierophant_warning {color: #660099; font-style: italic;} -.purple {color: #5e2d79;} -.beautifulmasc {color: #083eab;} -.beautifulfem {color: #d70a74;} -.beautifulnb {color: #9656c9;} - -.ghostalert {color: #5c00e6; font-style: italic; font-weight: bold;} - -.alien {color: #543354;} -.noticealien {color: #00c000;} -.alertalien {color: #00c000; font-weight: bold;} - -.interface {color: #DA00DA;} - -.sans {font-family: "Comic Sans MS", cursive, sans-serif;} -.papyrus {font-family: "Papyrus", cursive, sans-serif;} - -.human {font-family: "Honoka Mincho", "Pterra";} -.elf {font-family: "Dauphin", cursive, "Pterra";} -.dwarf {font-family: "MasonAlternate", "Pterra";} -.sandspeak {font-family: "Arabolical", "Pterra";} -.delf {font-family: "Dauphin", "Pterra";} -.hellspeak {font-family: "Xaphan", "Pterra"; font-size: 110%} -.undead {font-family: "FriskyVampire", "Pterra";} -.orc {font-family: "Thief by The Riddler", Pterra;} -.otavan {font-family: "Notredame", Pterra; font-size: 200%;} -.beast {font-family: "Thief by The Riddler", Pterra;} -.reptile {font-family: "Kingthings Petrock", Pterra; font-size: 120%;} -.deepspeak {font-family: "Underwater Love", Pterra;} -.lupian {font-family: "Emperialisme", Pterra; font-size: 120%;} - -.torture {color: #42ff20} - -.command_headset {font-weight: bold; font-size: 3;} -.small {font-size: 50%;} -.smallyell {font-size: 70%;font-family: Pterra;} -.big {font-size: 120%;} -.reallybig {font-size: 180%;} -.extremelybig {font-size: 220%;} -.greentext {color: #00FF00;} -.redtext {color: #FF0000;} -.clown {color: #FF69Bf; font-size: 3; font-family: "Comic Sans MS", cursive, sans-serif; font-weight: bold;} -.his_grace {color: #15D512; font-family: "Courier New", cursive, sans-serif; font-style: italic;} -.hypnophrase {color: #3bb5d3; font-weight: bold; animation: hypnocolor 1500ms infinite;} - -.phobia {color: #dd0000; font-weight: bold;} - -.icon {height: 1em; width: auto;} - -.memo {color: #638500; text-align: center;} -.memoedit {text-align: center; font-size: 2;} -.abductor {color: #800080; font-style: italic;} -.mind_control {color: #A00D6F; font-size: 3; font-weight: bold; font-style: italic;} -.drone {color: #848482;} - -.dead {color: #b280df;} -.bloody {color: #cc0f0f;} -.artery {color: #9B5455;} -.infection {color: #77c72b;} -.necrosis {color: #558d20;} -.bone {color: #e3dac9;} -.love_low {color: #eac8de; font-size: 75%;} -.love_mid {color: #e9a8d1; font-size: 75%;} -.love_high {color: #f05ee1; font-size: 75%;} -.love_extreme {color: #d146f5; font-size: 75%;} - -.silver {color: #c0c0c0;} - -.connectionClosed, .fatalError {background: red; color: white; padding: 5px;} -.connectionClosed.restored {background: green;} -.internal.boldnshit {color: #3d5bc3; font-weight: bold;} - -/* HELPER CLASSES */ -.text-normal {font-weight: normal; font-style: normal;} -.hidden {display: none; visibility: hidden;} - -/* Tooltips */ -.tooltip-trigger { - position: relative; - cursor: help; - text-decoration: underline dotted; -} -/* Alt Tooltip (No Italics) */ -.tooltip-alt-trigger { - font-style: normal; -} - -.tooltip-trigger::after, -.tooltip-trigger::before { - content: ''; - position: absolute; - visibility: hidden; - opacity: 0; - transition: opacity 0.3s, visibility 0.3s; - pointer-events: none; -} - -/* Tooltip content */ -.tooltip-trigger::after { - content: attr(data-tooltip); - background-color: rgba(0, 0, 0, 0.85); - color: #fff; - text-align: center; - border-radius: 5px; - padding: 8px 12px; - font-size: 0.9em; - white-space: nowrap; - z-index: 1000; - - bottom: 120%; - left: 50%; - transform: translateX(-50%); -} - -/* Show tooltip on hover */ -.tooltip-trigger:hover::after, -.tooltip-trigger:hover::before { - visibility: visible; - opacity: 1; -} - - -/* ############################### PRAYERS ############################### */ - -.god_generic { -color: #f57777; -font-weight: bold; - -animation: prayer_flicker 1s infinite; -} -@keyframes prayer_flicker { - 25% { - color: #f57777; - } - - 50% { - color: #e46262; - } - - 75% { - color: #e68888; - } - - 100% { - color: #f35e5e; - } -} - -/* ##### ASTRATA ##### */ -.god_astrata { -color: #e5d569; -font-weight: bold; - -animation: astrata_flicker 1s infinite; -} -@keyframes astrata_flicker { - 25% { - color: #e5d569; - } - - 50% { - color: #b1a34b; - } - - 75% { - color: #e6d048; - } - - 100% { - color: #ad9f44; - } -} -/* ##### END ##### */ - - -/* ##### NOC ##### */ -.god_noc { -color: #82a1d9; -font-weight: bold; - -animation: noc_flicker 1s infinite; -} -@keyframes noc_flicker { - 25% { - color: #82a1d9; - } - - 50% { - color: #6180b9; - } - - 75% { - color: #687997; - } - - 100% { - color: #739eec; - } -} -/* ##### END ##### */ - - -/* ##### ABYSSOR ##### */ -.god_abyssor { -color: #8b2d39; -font-weight: bold; - -animation: abyssor_flicker 1s infinite; -} -@keyframes abyssor_flicker { - 25% { - color: #8b2d39; - } - - 50% { - color: #64262f; - } - - 75% { - color: #c04152; - } - - 100% { - color: #b62b3d; - } -} -/* ##### END ##### */ - - -/* ##### DENDOR ##### */ -.god_dendor { -color: #74875a; -font-weight: bold; - -animation: dendor_flicker 1s infinite; -} -@keyframes dendor_flicker { - 25% { - color: #74875a; - } - - 50% { - color: #86a35c; - } - - 75% { - color: #5c6d43; - } - - 100% { - color: #7e8b6b; - } -} -/* ##### END ##### */ - -/* ##### RAVOX ##### */ -.god_ravox { -color: #774b44; -font-weight: bold; - -animation: ravox_flicker 1s infinite; -} -@keyframes ravox_flicker { - 25% { - color: #774b44; - } - - 50% { - color: #8a4c42; - } - - 75% { - color: #ad6c62; - } - - 100% { - color: #5f443f; - } -} -/* ##### END ##### */ - -/* ##### MALUM ##### */ -.god_malum { -color: #d39459; -font-weight: bold; - -animation: malum_flicker 1s infinite; -} -@keyframes malum_flicker { - 25% { - color: #d39459; - } - - 50% { - color: #b87b42; - } - - 75% { - color: #e7ac74; - } - - 100% { - color: #a7794f; - } -} -/* ##### END ##### */ - -/* ##### EORA ##### */ -.god_eora { -color: #e4bae6; -font-weight: bold; - -animation: eora_flicker 1s infinite; -} -@keyframes eora_flicker { - 25% { - color: #e4bae6; - } - - 50% { - color: #edb2f0; - } - - 75% { - color: #bf8ec2; - } - - 100% { - color: #c0a6c2; - } -} -/* ##### END ##### */ - -/* ##### XYLIX ##### */ -.god_xylix { -color: #dfe6ba; -font-weight: bold; - -animation: xylix_flicker 1s infinite; -} -@keyframes xylix_flicker { - 25% { - color: #dfe6ba; - } - - 50% { - color: #c7d192; - } - - 75% { - color: #a4ac7f; - } - - 100% { - color: #bdc2a6; - } -} -/* ##### END ##### */ - -/* ##### PESTRA ##### */ -.god_pestra { -color: #8aa01c; -font-weight: bold; - -animation: pestra_flicker 1s infinite; -} -@keyframes pestra_flicker { - 25% { - color: #8aa01c; - } - - 50% { - color: #a0bb19; - } - - 75% { - color: #768a14; - } - - 100% { - color: #8da126; - } -} -/* ##### END ##### */ - -/* ##### NECRA ##### */ -.god_necra { -color: #5a53d5; -font-weight: bold; - -animation: necra_flicker 1s infinite; -} -@keyframes necra_flicker { - 25% { - color: #5a53d5; - } - - 50% { - color: #5e59be; - } - - 75% { - color: #433eaf; - } - - 100% { - color: #524ae4; - } -} -/* ##### END ##### */ - -/* ##### MATTHIOS ##### */ -.god_matthios { -color: #17181a; -font-weight: bold; - -animation: matthios_flicker 1s infinite; -} -@keyframes matthios_flicker { - 25% { - color: #313338; - } - - 50% { - color: #2f3033; - } - - 75% { - color: #434750; - } - - 100% { - color: #52545a; - } -} -/* ##### END ##### */ - -/* ##### BAOTHA ##### */ -.god_baotha { -color: #88528d; -font-weight: bold; - -animation: baotha_flicker 1s infinite; -} -@keyframes baotha_flicker { - 25% { - color: #88528d; - } - - 50% { - color: #9d59a3; - } - - 75% { - color: #76427a; - } - - 100% { - color: #936197; - } -} -/* ##### END ##### */ - -/* ##### GRAGGAR ##### */ -.god_graggar { -color: #2a7b32; -font-weight: bold; - -animation: graggar_flicker 1s infinite; -} -@keyframes graggar_flicker { - 25% { - color: #2a7b32; - } - - 50% { - color: #298532; - } - - 75% { - color: #226e2a; - } - - 100% { - color: #2a6930; - } -} -/* ##### END ##### */ - -/* ##### ZIZO ##### */ -.god_zizo { -color: #791f3f; -font-weight: bold; - -animation: zizo_flicker 1s infinite; -} -@keyframes zizo_flicker { - 25% { - color: #791f3f; - } - - 50% { - color: #8a1d43; - } - - 75% { - color: #631832; - } - - 100% { - color: #7c2745; - } -} -/* ##### END ##### */ - -/* ##### PSYDON ##### */ -.god_psydon { -color: #c1dce2; -font-weight: bold; - -animation: psydon_flicker 1s infinite; -} -@keyframes psydon_flicker { - 25% { - color: #c1dce2; - } - - 50% { - color: #b4dfe9; - } - - 75% { - color: #a2c2c9; - } - - 100% { - color: #beced1; - } -} -/* ##### END ##### */ diff --git a/code/modules/goonchat/browserassets/html/adminOutput.html b/code/modules/goonchat/browserassets/html/adminOutput.html deleted file mode 100644 index 7796be092dc..00000000000 --- a/code/modules/goonchat/browserassets/html/adminOutput.html +++ /dev/null @@ -1,66 +0,0 @@ - diff --git a/code/modules/goonchat/browserassets/html/browserOutput.html b/code/modules/goonchat/browserassets/html/browserOutput.html deleted file mode 100644 index 08904b73471..00000000000 --- a/code/modules/goonchat/browserassets/html/browserOutput.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - Chat - - - - - - - -
- -
- Loading...

- 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. -
-
- -
-
- -
-
All
- -
- -
-

Add Custom Filter Tab

- - -
- - -
-
- - - -
- -
- - - - - - - - - diff --git a/code/modules/goonchat/browserassets/js/browserOutput.js b/code/modules/goonchat/browserassets/js/browserOutput.js deleted file mode 100644 index 92b675c6790..00000000000 --- a/code/modules/goonchat/browserassets/js/browserOutput.js +++ /dev/null @@ -1,2782 +0,0 @@ - -/***************************************** -* -* FUNCTION AND VAR DECLARATIONS -* -******************************************/ - -//DEBUG STUFF -var escaper = encodeURIComponent || escape; -var decoder = decodeURIComponent || unescape; -window.onerror = function(msg, url, line, col, error) { - if (document.location.href.indexOf("proc=debug") <= 0) { - 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 = '?_src_=chat&proc=debug¶m[error]='+escaper(debugLine); - } - return true; -}; - -//Globals -var messageQueue = { - messages: [], - processing: false, - batchSize: 10, // Process this many at once - batchDelay: 16, // ~60fps - - add: function(message, flag) { - this.messages.push({message: message, flag: flag}); - if (!this.processing) { - this.processing = true; - requestAnimationFrame(() => this.processBatch()); - } - }, - - processBatch: function() { - if (this.messages.length === 0) { - this.processing = false; - return; - } - - // Process up to batchSize messages - var batch = this.messages.splice(0, this.batchSize); - var fragment = document.createDocumentFragment(); - var atBottom = this.checkIfAtBottom(); - - // Process all messages in batch - batch.forEach(item => { - var entry = this.createMessageElement(item.message, item.flag); - if (entry) { - fragment.appendChild(entry); - } - }); - - // Apply highlights BEFORE appending to DOM (more efficient) - if (window.highlightSystem && highlightSystem.filters && highlightSystem.filters.length > 0) { - this.batchHighlight(fragment); - } - - // Apply linkify and other processing - var entries = Array.prototype.slice.call(fragment.children); - entries.forEach(function(entry) { - // Linkify - var to_linkify = entry.querySelectorAll ? entry.querySelectorAll('.linkify') : []; - if (typeof Node === 'undefined') { - for(var i = 0; i < to_linkify.length; ++i) { - to_linkify[i].innerHTML = linkify_fallback(to_linkify[i].innerHTML); - } - } else { - for(var i = 0; i < to_linkify.length; ++i) { - linkify_node(to_linkify[i]); - } - } - - // Icon error handlers - var icons = entry.querySelectorAll ? entry.querySelectorAll('img.icon') : []; - for(var i = 0; i < icons.length; i++) { - icons[i].addEventListener('error', iconError); - } - - // Replace regex - var replaceElements = entry.querySelectorAll ? entry.querySelectorAll('[replaceRegex]') : []; - for(var i = 0; i < replaceElements.length; i++) { - var selectedRegex = replaceRegexes[replaceElements[i].getAttribute('replaceRegex')]; - if (selectedRegex) { - var replacedText = replaceElements[i].innerHTML.replace(selectedRegex[0], selectedRegex[1]); - replaceElements[i].innerHTML = replacedText; - } - replaceElements[i].removeAttribute('replaceRegex'); - } - }); - - // Single DOM append for entire batch - $messages[0].appendChild(fragment); - - // Cleanup old messages in one go - this.cleanupOldMessages(); - - // Handle scroll once - if (atBottom) { - this.scrollToBottom(); - } - - // Continue processing if more messages - if (this.messages.length > 0) { - setTimeout(() => requestAnimationFrame(() => this.processBatch()), this.batchDelay); - } else { - this.processing = false; - } - }, - - checkIfAtBottom: function() { - var bodyHeight = window.innerHeight; - var messagesHeight = $messages[0].scrollHeight; - var scrollPos = window.pageYOffset || document.documentElement.scrollTop; - return bodyHeight + scrollPos >= messagesHeight - opts.scrollSnapTolerance; - }, - - scrollToBottom: function() { - // Use RAF for smooth scroll - requestAnimationFrame(() => { - $('body,html').scrollTop($messages.outerHeight()); - }); - }, - - createMessageElement: function(message, flag) { - if (typeof message === 'undefined') return null; - - message = byondDecode(message).trim(); - - var entry = document.createElement('div'); - entry.innerHTML = message; - entry.className = 'entry'; - - opts.messageCount++; - - // Apply filter efficiently - this.quickFilterCheck(entry); - - return entry; - }, - - quickFilterCheck: function(entry) { - // Fast path for 'all' filter - if (opts.currentFilter === 'all') return; - - // Cache class list as string for faster searching - var classStr = ' ' + entry.className + ' '; - var innerHTML = entry.innerHTML; - - var shouldShow = classStr.indexOf(' ' + opts.currentFilter + ' ') !== -1 || - innerHTML.indexOf('class="' + opts.currentFilter) !== -1; - - // Check custom tabs (optimized) - if (!shouldShow && opts.customTabs && opts.customTabs.length > 0) { - for (var i = 0; i < opts.customTabs.length; i++) { - var tab = opts.customTabs[i]; - if (tab.name.toLowerCase() === opts.currentFilter) { - for (var j = 0; j < tab.classes.length; j++) { - if (classStr.indexOf(' ' + tab.classes[j] + ' ') !== -1 || - innerHTML.indexOf('class="' + tab.classes[j]) !== -1) { - shouldShow = true; - break; - } - } - break; - } - } - } - - if (!shouldShow) { - entry.classList.add('filtered-hidden'); - entry.style.display = 'none'; - } - }, - - cleanupOldMessages: function() { - while (opts.messageCount > opts.messageLimit) { - var first = $messages[0].firstElementChild; - if (first) { - first.remove(); - opts.messageCount--; - } else { - break; - } - } - }, - - batchHighlight: function(fragment) { - // Only highlight if filters are enabled - if (!highlightSystem || !highlightSystem.filters) return; - - var enabledFilters = highlightSystem.filters.filter(f => f.enabled && f.term.trim()); - if (enabledFilters.length === 0) return; - - // Get entries - fragment is not yet in DOM so we need to iterate its children - var entries; - if (fragment.children) { - entries = Array.prototype.slice.call(fragment.children); - } else if (fragment.childNodes) { - entries = Array.prototype.filter.call(fragment.childNodes, function(node) { - return node.nodeType === 1; // Element nodes only - }); - } else { - return; - } - - // Apply highlights to each entry - entries.forEach(function(entry) { - if (entry && entry.nodeType === 1) { - highlightSystem.highlightElement(entry); - } - }); - } -}; - -var highlightSystem = { - filters: [], // Array of {term: string, color: string, animation: string, enabled: boolean, id: string, soundEnabled: boolean} - animations: { - 'none': 'No animation', - 'glow': 'Glow effect', - 'pulse': 'Pulse animation', - 'flash': 'Flash animation', - 'rainbow': 'Rainbow effect', - }, - - // Initialize the system - init: function() { - this.loadFilters(); - this.injectStyles(); - }, - - // Inject required CSS styles - injectStyles: function() { - if (document.getElementById('highlightSystemStyles')) return; - - var style = document.createElement('style'); - style.id = 'highlightSystemStyles'; - style.textContent = ` - /* Fixed animations */ - @keyframes glow { - 0%, 100% { - box-shadow: 0 0 5px currentColor; - filter: brightness(1); - } - 50% { - box-shadow: 0 0 20px currentColor, 0 0 30px currentColor; - filter: brightness(1.3); - } - } - .highlight-glow { - animation: glow 2s infinite; - border-radius: 3px; - } - - @keyframes pulse { - 0%, 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.1); - opacity: 0.7; - } - } - .highlight-pulse { - animation: pulse 1.5s infinite; - display: inline-block; - border-radius: 3px; - } - - @keyframes flash { - 0%, 50%, 100% { opacity: 1; } - 25%, 75% { opacity: 0.3; } - } - .highlight-flash { - animation: flash 1s infinite; - border-radius: 3px; - } - - @keyframes bounce { - 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } - 40% { transform: translateY(-3px); } - 60% { transform: translateY(-2px); } - } - .highlight-bounce { - animation: bounce 2s infinite; - display: inline-block; - border-radius: 3px; - } - - @keyframes slide { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } - } - .highlight-slide { - background: linear-gradient(90deg, transparent 30%, var(--highlight-color, #FFFF00) 50%, transparent 70%); - background-size: 200% 100%; - animation: slide 2s infinite; - border-radius: 3px; - } - - @keyframes rainbow { - 0% { background-color: #ff0000; color: white; } - 14% { background-color: #ff8000; color: white; } - 28% { background-color: #ffff00; color: black; } - 42% { background-color: #80ff00; color: black; } - 57% { background-color: #00ff80; color: black; } - 71% { background-color: #0080ff; color: white; } - 85% { background-color: #8000ff; color: white; } - 100% { background-color: #ff0000; color: white; } - } - .highlight-rainbow { - animation: rainbow 3s infinite; - font-weight: bold; - border-radius: 3px; - padding: 1px 2px; - } - - /* Darkened popup styles */ - .popup { - position: fixed; - background: linear-gradient(135deg, #0f1419 0%, #1a1d23 100%); - border: 2px solid #000000; - border-radius: 12px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8); - z-index: 10000; - width: 95vw; - max-width: 600px; - max-height: 85vh; - color: #c9d1d9; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - display: flex; - flex-direction: column; - /* Smart positioning - will be set by JavaScript */ - } - - .popup .head { - background: linear-gradient(135deg, #161b22, #21262d); - padding: 15px 20px; - border-radius: 10px 10px 0 0; - font-size: 18px; - font-weight: bold; - text-align: center; - position: relative; - flex-shrink: 0; - } - - .popup .close { - position: absolute; - top: 10px; - right: 15px; - font-size: 24px; - text-decoration: none; - color: #c9d1d9; - opacity: 0.7; - transition: opacity 0.3s ease; - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: rgba(255,255,255,0.05); - } - - .popup .close:hover { - opacity: 1; - background: rgba(255,255,255,0.1); - } - - .highlight-manager { - padding: 15px; - flex: 1; - overflow: hidden; - display: flex; - flex-direction: column; - } - - #highlightFilters { - flex: 1; - overflow-y: auto; - margin-bottom: 15px; - padding-right: 8px; - max-height: 400px; - } - - /* Custom scrollbar - darker */ - #highlightFilters::-webkit-scrollbar { - width: 6px; - } - - #highlightFilters::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.3); - border-radius: 3px; - } - - #highlightFilters::-webkit-scrollbar-thumb { - background: rgba(100, 100, 100, 0.4); - border-radius: 3px; - } - - #highlightFilters::-webkit-scrollbar-thumb:hover { - background: rgba(120, 120, 120, 0.6); - } - - .highlight-filter-item { - background: rgba(0, 0, 0, 0.3); - border: 1px solid rgba(100, 100, 100, 0.2); - border-radius: 8px; - padding: 12px; - margin-bottom: 12px; - transition: all 0.3s ease; - } - - .highlight-filter-item:hover { - background: rgba(0, 0, 0, 0.4); - border-color: rgba(120, 120, 120, 0.3); - } - - /* Main row for term input and color */ - .filter-main-row { - display: flex; - gap: 8px; - margin-bottom: 8px; - align-items: center; - } - - .filter-term-input { - flex: 1; - min-width: 0; - } - - .filter-color-input { - flex-shrink: 0; - } - - /* Controls row for animation, toggles, and remove */ - .filter-controls-row { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: center; - } - - .filter-animation-select { - flex: 1; - min-width: 100px; - } - - .filter-buttons { - display: flex; - gap: 4px; - flex-shrink: 0; - } - - .highlight-filter-item input[type="text"] { - background: rgba(0, 0, 0, 0.6); - border: 1px solid rgba(80, 80, 80, 0.4); - border-radius: 4px; - padding: 6px 8px; - font-size: 13px; - color: #c9d1d9; - transition: border-color 0.3s ease; - width: 100%; - box-sizing: border-box; - } - - .highlight-filter-item input[type="text"]:focus { - outline: none; - border-color: rgba(120, 120, 120, 0.6); - box-shadow: 0 0 3px rgba(80, 80, 80, 0.5); - } - - .highlight-filter-item input[type="color"] { - width: 40px; - height: 32px; - border: 2px solid rgba(80, 80, 80, 0.4); - border-radius: 4px; - cursor: pointer; - background: rgba(0, 0, 0, 0.5); - transition: border-color 0.3s ease; - } - - .highlight-filter-item input[type="color"]:hover { - border-color: rgba(120, 120, 120, 0.6); - } - - .highlight-filter-item select { - background: rgba(0, 0, 0, 0.6); - border: 1px solid rgba(80, 80, 80, 0.4); - border-radius: 4px; - padding: 6px; - font-size: 12px; - color: #c9d1d9; - cursor: pointer; - transition: border-color 0.3s ease; - } - - .highlight-filter-item select:focus { - outline: none; - border-color: rgba(120, 120, 120, 0.6); - } - - .highlight-filter-item button { - background: rgba(30, 90, 130, 0.3); - border: 1px solid rgba(30, 90, 130, 0.5); - border-radius: 4px; - padding: 4px 8px; - color: #c9d1d9; - cursor: pointer; - font-size: 10px; - font-weight: bold; - transition: all 0.3s ease; - white-space: nowrap; - min-width: 35px; - } - - .highlight-filter-item button:hover { - background: rgba(30, 90, 130, 0.4); - transform: translateY(-1px); - } - - .toggle-btn.enabled { - background: rgba(25, 130, 70, 0.4) !important; - border-color: rgba(25, 130, 70, 0.7) !important; - color: #40d47e !important; - } - - .remove-btn:hover { - background: rgba(150, 40, 30, 0.4) !important; - border-color: rgba(150, 40, 30, 0.7) !important; - color: #ff6b6b !important; - } - - .sound-btn.enabled { - background: rgba(160, 120, 10, 0.4) !important; - border-color: rgba(160, 120, 10, 0.7) !important; - color: #ffd93d !important; - } - - .highlight-controls { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: center; - padding-top: 12px; - border-top: 1px solid rgba(80, 80, 80, 0.3); - flex-shrink: 0; - } - - .add-filter-btn { - background: rgba(25, 130, 70, 0.3); - border: 1px solid rgba(25, 130, 70, 0.5); - border-radius: 6px; - padding: 8px 12px; - color: #c9d1d9; - cursor: pointer; - font-size: 12px; - font-weight: bold; - transition: all 0.3s ease; - white-space: nowrap; - } - - .add-filter-btn:hover { - background: rgba(25, 130, 70, 0.4); - transform: translateY(-1px); - box-shadow: 0 3px 8px rgba(25, 130, 70, 0.4); - } - - .no-filters-message { - text-align: center; - padding: 30px 15px; - color: rgba(201, 209, 217, 0.4); - font-style: italic; - } - - /* Mobile-specific styles */ - @media (max-width: 600px) { - .popup { - width: 98vw; - max-height: 95vh; - margin: 0; - } - - .popup .head { - font-size: 16px; - padding: 12px 15px; - } - - .highlight-manager { - padding: 10px; - } - - .filter-controls-row { - flex-direction: column; - align-items: stretch; - } - - .filter-buttons { - justify-content: space-between; - margin-top: 6px; - } - - .highlight-filter-item button { - flex: 1; - padding: 8px 4px; - font-size: 9px; - } - - .highlight-controls { - flex-direction: column; - gap: 6px; - } - - .add-filter-btn { - font-size: 11px; - padding: 10px; - } - } - - /* Very small screens */ - @media (max-width: 400px) { - .filter-main-row { - flex-direction: column; - align-items: stretch; - } - - .filter-color-input { - align-self: center; - } - - .highlight-filter-item input[type="color"] { - width: 60px; - height: 35px; - } - } - `; - document.head.appendChild(style); - }, - - // Generate unique ID for filters - generateId: function() { - return 'highlight_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - }, - - // Add a new highlight filter - addFilter: function(term, color = '#FFFF00', animation = 'none') { - var filter = { - id: this.generateId(), - term: term.toLowerCase(), - color: color, - animation: animation, - enabled: true, - soundEnabled: false, - soundType: 'beep', - customSoundUrl: '' // Changed from customSound to customSoundUrl - }; - this.filters.push(filter); - this.saveFilters(); - return filter; - }, - - - // Remove a filter by ID - removeFilter: function(id) { - this.filters = this.filters.filter(f => f.id !== id); - this.saveFilters(); - }, - - // Update a filter - updateFilter: function(id, updates) { - var filter = this.filters.find(f => f.id === id); - if (filter) { - Object.assign(filter, updates); - if (updates.term) { - filter.term = updates.term.toLowerCase(); - } - this.saveFilters(); - } - }, - - // Toggle filter enabled state - toggleFilter: function(id) { - var filter = this.filters.find(f => f.id === id); - if (filter) { - filter.enabled = !filter.enabled; - this.saveFilters(); - } - }, - - // Toggle sound for filter - toggleSound: function(id) { - var filter = this.filters.find(f => f.id === id); - if (filter) { - filter.soundEnabled = !filter.soundEnabled; - this.saveFilters(); - } - }, - - // Play sound notification - playSound: function(filter) { - try { - if (filter && filter.customSoundUrl && filter.soundType === 'custom') { - // Play custom audio from URL - var audio = new Audio(filter.customSoundUrl); - audio.volume = 0.5; - audio.play().catch(e => console.warn('Custom sound failed:', e)); - } else { - // Use built-in sound based on soundType - var audioContext = new (window.AudioContext || window.webkitAudioContext)(); - var oscillator = audioContext.createOscillator(); - var gainNode = audioContext.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); - - var soundType = (filter && filter.soundType) || 'beep'; - - switch(soundType) { - case 'beep': - oscillator.frequency.value = 800; - oscillator.type = 'sine'; - gainNode.gain.setValueAtTime(0, audioContext.currentTime); - gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.3); - break; - case 'chime': - oscillator.frequency.value = 1200; - oscillator.type = 'sine'; - gainNode.gain.setValueAtTime(0, audioContext.currentTime); - gainNode.gain.linearRampToValueAtTime(0.2, audioContext.currentTime + 0.01); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.8); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.8); - break; - case 'pop': - oscillator.frequency.value = 400; - oscillator.type = 'square'; - gainNode.gain.setValueAtTime(0, audioContext.currentTime); - gainNode.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.01); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.1); - break; - case 'ding': - oscillator.frequency.value = 1800; - oscillator.type = 'triangle'; - gainNode.gain.setValueAtTime(0, audioContext.currentTime); - gainNode.gain.linearRampToValueAtTime(0.25, audioContext.currentTime + 0.01); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.5); - break; - } - } - } catch (e) { - console.warn('Sound notification failed:', e); - } - }, - - // Apply highlights to an element - highlightElement: function(element) { - if (!this.filters.length) return; - - var enabledFilters = this.filters.filter(f => f.enabled && f.term.trim()); - if (!enabledFilters.length) return; - - this.highlightTextNodes(element, enabledFilters); - }, - - // Recursively highlight text nodes - highlightTextNodes: function(element, filters) { - // Build combined regex for all filters at once - var terms = filters.map(f => this.escapeRegex(f.term)).join('|'); - if (!terms) return; - - var regex = new RegExp('(' + terms + ')', 'gi'); - - // Use faster tree walker - var walker = document.createTreeWalker( - element, - NodeFilter.SHOW_TEXT, - { - acceptNode: function(node) { - var parent = node.parentNode; - if (parent.classList && parent.classList.contains('highlight-filter')) { - return NodeFilter.FILTER_REJECT; - } - if (parent.tagName === 'SCRIPT' || parent.tagName === 'STYLE') { - return NodeFilter.FILTER_REJECT; - } - // Quick check if text contains any terms - var text = node.textContent.toLowerCase(); - for (var i = 0; i < filters.length; i++) { - if (text.indexOf(filters[i].term) !== -1) { - return NodeFilter.FILTER_ACCEPT; - } - } - return NodeFilter.FILTER_REJECT; - } - }, - false - ); - - var textNodes = []; - var node; - var maxNodes = 100; // Safety limit - var count = 0; - - while (node = walker.nextNode()) { - textNodes.push(node); - if (++count > maxNodes) break; - } - - // Process in chunks to avoid blocking - if (textNodes.length > 10) { - this.highlightInChunks(textNodes, filters, 0); - } else { - textNodes.forEach(textNode => { - this.highlightTextNode(textNode, filters); - }); - } - }, - - highlightInChunks: function(textNodes, filters, index) { - var chunkSize = 5; - var end = Math.min(index + chunkSize, textNodes.length); - - for (var i = index; i < end; i++) { - this.highlightTextNode(textNodes[i], filters); - } - - if (end < textNodes.length) { - requestAnimationFrame(() => { - this.highlightInChunks(textNodes, filters, end); - }); - } - }, - - // Highlight matches in a single text node - highlightTextNode: function(textNode, filters) { - var text = textNode.textContent; - var matches = []; - - // Find all matches - filters.forEach(filter => { - var regex = new RegExp(this.escapeRegex(filter.term), 'gi'); - var match; - while ((match = regex.exec(text)) !== null) { - matches.push({ - start: match.index, - end: match.index + match[0].length, - filter: filter, - text: match[0] - }); - - // Play sound if enabled for this filter - if (filter.soundEnabled) { - this.playSound(filter); - } - } - }); - - if (!matches.length) return; - - // Sort matches by position and remove overlaps - matches.sort((a, b) => a.start - b.start); - var cleanMatches = this.removeOverlaps(matches); - - if (!cleanMatches.length) return; - - // Create highlighted content - var result = ''; - var lastEnd = 0; - - cleanMatches.forEach(match => { - // Add text before match - result += this.escapeHtml(text.substring(lastEnd, match.start)); - - // Add highlighted match - var animationClass = match.filter.animation !== 'none' ? `highlight-${match.filter.animation}` : ''; - var style = match.filter.animation === 'slide' - ? `background-color: ${match.filter.color}; --highlight-color: ${match.filter.color};` - : match.filter.animation === 'rainbow' - ? '' // Rainbow uses its own colors - : `background-color: ${match.filter.color};`; - - result += ``; - result += this.escapeHtml(match.text); - result += ''; - - lastEnd = match.end; - }); - - // Add remaining text - result += this.escapeHtml(text.substring(lastEnd)); - - // Replace the text node with highlighted content - var wrapper = document.createElement('span'); - wrapper.innerHTML = result; - textNode.parentNode.replaceChild(wrapper, textNode); - }, - - // Remove overlapping matches (prioritize first match) - removeOverlaps: function(matches) { - var result = []; - var lastEnd = 0; - - matches.forEach(match => { - if (match.start >= lastEnd) { - result.push(match); - lastEnd = match.end; - } - }); - - return result; - }, - - // Escape regex special characters - escapeRegex: function(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }, - - // Escape HTML - escapeHtml: function(text) { - var div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - }, - - // Show the highlight manager popup - showManager: function() { - var content = this.createManagerHTML(); - if (typeof createPopup === 'function') { - // Try to use existing createPopup function - var popup = createPopup(content, 600); - // If createPopup returns the popup element, apply smart positioning - if (popup && popup.nodeType) { - this.positionPopup(popup); - } - } else { - // Fallback popup creation with smart positioning - this.createFallbackPopup(content); - } - this.bindManagerEvents(); - }, - - // Create manager HTML - createManagerHTML: function() { - var html = ` -
- Highlight Filter Manager - × -
-
-
- ${this.createFiltersHTML()} -
-
- - - - -
-
- `; - return html; - }, - - // Create HTML for existing filters - createFiltersHTML: function() { - if (!this.filters.length) { - return '
No highlight filters configured.
Click "Add Filter" to get started!
'; - } - - return this.filters.map(filter => ` -
-
- - -
-
- -
- - - -
-
- ${filter.soundEnabled ? ` -
- - - ${filter.soundType === 'custom' ? ` - - ` : ''} -
- ` : ''} -
- `).join(''); - }, - - updateCustomSoundUrl: function(id, url) { - this.updateFilter(id, { customSoundUrl: url }); - }, - - updateSoundType: function(id, soundType) { - this.updateFilter(id, { soundType: soundType }); - if (soundType !== 'custom') { - this.updateFilter(id, { customSoundUrl: '' }); - } - this.refreshManager(); - }, - - testSound: function(id) { - var filter = this.filters.find(f => f.id === id); - if (filter) { - this.playSound(filter); - } - }, - - triggerFileUpload: function(id) { - var input = document.getElementById('customSound_' + id); - if (input) { - input.click(); - } - }, - - uploadCustomSound: function(id, input) { - var file = input.files[0]; - if (!file) return; - - if (!file.type.startsWith('audio/')) { - alert('Please select an audio file'); - return; - } - - var reader = new FileReader(); - reader.onload = (e) => { - this.updateFilter(id, { - customSound: e.target.result, - soundType: 'custom' - }); - input.value = ''; // Reset input - // Show confirmation - alert('Custom sound uploaded successfully!'); - }; - reader.readAsDataURL(file); - }, - - // Fallback popup creation - createFallbackPopup: function(content) { - var popup = document.createElement('div'); - popup.className = 'popup'; - popup.innerHTML = content; - - document.body.appendChild(popup); - - // Smart positioning to keep popup within viewport - this.positionPopup(popup); - - // Handle window resize - var resizeHandler = () => this.positionPopup(popup); - window.addEventListener('resize', resizeHandler); - - popup.querySelector('.close').onclick = function(e) { - e.preventDefault(); - window.removeEventListener('resize', resizeHandler); - document.body.removeChild(popup); - }; - }, - - // Smart popup positioning - positionPopup: function(popup) { - // Get viewport dimensions - var viewportWidth = window.innerWidth; - var viewportHeight = window.innerHeight; - - // Get popup dimensions (after it's been added to DOM) - var popupRect = popup.getBoundingClientRect(); - var popupWidth = popupRect.width || popup.offsetWidth; - var popupHeight = popupRect.height || popup.offsetHeight; - - // Calculate ideal center position - var idealLeft = (viewportWidth - popupWidth) / 2; - var idealTop = (viewportHeight - popupHeight) / 2; - - // Ensure minimum margins from edges - var margin = 10; - var left = Math.max(margin, Math.min(idealLeft, viewportWidth - popupWidth - margin)); - var top = Math.max(margin, Math.min(idealTop, viewportHeight - popupHeight - margin)); - - // Apply positioning - popup.style.left = left + 'px'; - popup.style.top = top + 'px'; - popup.style.transform = 'none'; // Remove any existing transform - - // If popup is still too tall, make it scrollable - if (popupHeight > viewportHeight - (margin * 2)) { - popup.style.maxHeight = (viewportHeight - (margin * 2)) + 'px'; - popup.style.top = margin + 'px'; - } - }, - - // Bind events for manager - bindManagerEvents: function() { - // Events are handled by inline handlers in the HTML - }, - - // Manager event handlers - addNewFilter: function() { - this.addFilter('', '#FFFF00', 'none'); - this.refreshManager(); - }, - - updateFilterTerm: function(id, term) { - this.updateFilter(id, { term: term }); - }, - - updateFilterColor: function(id, color) { - this.updateFilter(id, { color: color }); - }, - - updateFilterAnimation: function(id, animation) { - this.updateFilter(id, { animation: animation }); - }, - - toggleFilterInManager: function(id) { - this.toggleFilter(id); - this.refreshManager(); - }, - - toggleSoundInManager: function(id) { - this.toggleSound(id); - this.refreshManager(); - }, - - removeFilterFromManager: function(id) { - if (confirm('Are you sure you want to remove this highlight filter?')) { - this.removeFilter(id); - this.refreshManager(); - } - }, - - // Refresh the manager display - refreshManager: function() { - var container = document.getElementById('highlightFilters'); - if (container) { - container.innerHTML = this.createFiltersHTML(); - } - }, - - // Export filters to JSON - exportFilters: function() { - var data = JSON.stringify(this.filters, null, 2); - var blob = new Blob([data], { type: 'application/json' }); - var url = URL.createObjectURL(blob); - var a = document.createElement('a'); - a.href = url; - a.download = 'highlight_filters.json'; - a.click(); - URL.revokeObjectURL(url); - }, - - // Import filters from JSON - importFilters: function(input) { - var file = input.files[0]; - if (!file) return; - - var reader = new FileReader(); - reader.onload = (e) => { - try { - var imported = JSON.parse(e.target.result); - if (Array.isArray(imported)) { - // Validate and add filters - imported.forEach(filter => { - if (filter.term && filter.color) { - var newFilter = this.addFilter(filter.term, filter.color, filter.animation || 'none'); - if (filter.soundEnabled) { - this.updateFilter(newFilter.id, { soundEnabled: true }); - } - } - }); - this.refreshManager(); - alert('Filters imported successfully!'); - } - } catch (error) { - alert('Error importing filters: Invalid file format'); - } - }; - reader.readAsText(file); - input.value = ''; // Reset input - }, - - // Save filters to cookie - saveFilters: function() { - var data = JSON.stringify(this.filters); - try { - if (typeof setCookie === 'function') { - setCookie('highlightFilters', data, 365); - } - } catch (e) { - console.warn('Failed to save highlight filters:', e); - } - }, - - // Load filters from cookie - loadFilters: function() { - var saved = null; - - try { - if (typeof getCookie === 'function') { - saved = getCookie('highlightFilters'); - } - - if (saved) { - var parsed = JSON.parse(saved); - if (Array.isArray(parsed)) { - this.filters = parsed; - // Ensure all filters have required properties - this.filters.forEach(filter => { - if (filter.soundEnabled === undefined) { - filter.soundEnabled = false; - } - if (filter.soundType === undefined) { - filter.soundType = 'beep'; - } - // Migrate old customSound to customSoundUrl - if (filter.customSound !== undefined) { - filter.customSoundUrl = ''; - delete filter.customSound; - } - if (filter.customSoundUrl === undefined) { - filter.customSoundUrl = ''; - } - }); - } - } - } catch (e) { - console.warn('Failed to load highlight filters:', e); - } - } -}; - -// Initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - highlightSystem.init(); - }); -} else { - highlightSystem.init(); -} - -window.status = 'Output'; -var $messages, $subOptions, $subAudio, $selectedSub, $contextMenu, $filterMessages, $last_message; -var opts = { - //General - 'messageCount': 0, //A count...of messages... - 'messageLimit': 200, //A limit...for the messages... - 'scrollSnapTolerance': 10, //If within x pixels of bottom - 'clickTolerance': 10, //Keep focus if outside x pixels of mousedown position on mouseup - 'imageRetryDelay': 50, //how long between attempts to reload images (in ms) - 'imageRetryLimit': 50, //how many attempts should we make? - 'popups': 0, //Amount of popups opened ever - 'wasd': false, //Is the user in wasd mode? - 'priorChatHeight': 0, //Thing for height-resizing detection - 'restarting': false, //Is the round restarting? - 'darkmode':false, //Are we using darkmode? If not WHY ARE YOU LIVING IN 2009??? - - //Options menu - 'selectedSubLoop': null, //Contains the interval loop for closing the selected sub menu - 'suppressSubClose': false, //Whether or not we should be hiding the selected sub menu - 'highlightTerms': [], - 'highlightLimit': 10, - 'highlightColor': '#FFFF00', //The color of the highlighted message - 'pingDisabled': true, //Has the user disabled the ping counter - - //Ping display - 'lastPang': 0, //Timestamp of the last response from the server. - 'pangLimit': 35000, - 'pingTime': 0, //Timestamp of when ping sent - 'pongTime': 0, //Timestamp of when ping received - 'noResponse': false, //Tracks the state of the previous ping request - 'noResponseCount': 0, //How many failed pings? - - //Clicks - 'mouseDownX': null, - 'mouseDownY': null, - 'preventFocus': false, //Prevents switching focus to the game window - - //Client Connection Data - 'clientDataLimit': 5, - 'clientData': [], - - //Admin music volume update - 'volumeUpdateDelay': 5000, //Time from when the volume updates to data being sent to the server - 'volumeUpdating': false, //True if volume update function set to fire - 'updatedVolume': 0, //The volume level that is sent to the server - 'musicStartAt': 0, //The position the music starts playing - 'musicEndAt': 0, //The position the music... stops playing... if null, doesn't apply (so the music runs through) - - 'defaultMusicVolume': 25, - - 'messageCombining': false, - - 'currentFilter': 'all', - 'customTabs': [] - -}; -var replaceRegexes = {}; - -function clamp(val, min, max) { - return Math.max(min, Math.min(val, max)) -} - -function outerHTML(el) { - var wrap = document.createElement('div'); - wrap.appendChild(el.cloneNode(true)); - return wrap.innerHTML; -} - -//Polyfill for fucking date now because of course IE8 and below don't support it -if (!Date.now) { - Date.now = function now() { - return new Date().getTime(); - }; -} -//Polyfill for trim() (IE8 and below) -if (typeof String.prototype.trim !== 'function') { - String.prototype.trim = function () { - return this.replace(/^\s+|\s+$/g, ''); - }; -} - -// Linkify the contents of a node, within its parent. -function linkify(parent, insertBefore, text) { - var start = 0; - var match; - var regex = /(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#\/%?=~_|$!:,.;()]+/ig; - while ((match = regex.exec(text)) !== null) { - // add the unmatched text - parent.insertBefore(document.createTextNode(text.substring(start, match.index)), insertBefore); - - var href = match[0]; - if (!/^https?:\/\//i.test(match[0])) { - href = "http://" + match[0]; - } - - // add the link - var link = document.createElement("a"); - link.href = href; - link.textContent = match[0]; - parent.insertBefore(link, insertBefore); - - start = regex.lastIndex; - } - if (start !== 0) { - // add the remaining text and remove the original text node - parent.insertBefore(document.createTextNode(text.substring(start)), insertBefore); - parent.removeChild(insertBefore); - } -} - -// Recursively linkify the children of a given node. -function linkify_node(node) { - var children = node.childNodes; - // work backwards to avoid the risk of looping forever on our own output - for (var i = children.length - 1; i >= 0; --i) { - var child = children[i]; - if (child.nodeType == Node.TEXT_NODE) { - // text is to be linkified - linkify(node, child, child.textContent); - } else if (child.nodeName != "A" && child.nodeName != "a") { - // do not linkify existing links - linkify_node(child); - } - } -} - -//Shit fucking piece of crap that doesn't work god fuckin damn it -function linkify_fallback(text) { - var rex = /((?:'+$0+''; - } - else { - return $1 ? $0: ''+$0+''; - } - }); -} - -function byondDecode(message) { - // Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8. - // The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b. - // Marvelous. - message = message.replace(/\+/g, "%20"); - try { - // This is a workaround for the above not always working when BYOND's shitty url encoding breaks. (byond bug id:2399401) - if (decodeURIComponent) { - message = decodeURIComponent(message); - } else { - throw new Error("Easiest way to trigger the fallback") - } - } catch (err) { - message = unescape(message); - } - return message; -} - -function replaceRegex() { - var selectedRegex = replaceRegexes[$(this).attr('replaceRegex')]; - if (selectedRegex) { - var replacedText = $(this).html().replace(selectedRegex[0], selectedRegex[1]); - $(this).html(replacedText); - } - $(this).removeAttr('replaceRegex'); -} - -//Actually turns the highlight term match into appropriate html -function addHighlightMarkup(match) { - var extra = ''; - if (opts.highlightColor) { - extra += ' style="background-color: '+opts.highlightColor+'"'; - } - return ''+match+''; -} - -//Highlights words based on user settings -function highlightTerms(el) { - if (window.highlightSystem) { - highlightSystem.highlightElement(el); - } else { - // Fallback to old system if new one isn't loaded - legacyHighlightTerms(el); - } -} - -function legacyHighlightTerms(el) { - if (el.children.length > 0) { - for(var h = 0; h < el.children.length; h++){ - legacyHighlightTerms(el.children[h]); - } - } - - var hasTextNode = false; - for (var node = 0; node < el.childNodes.length; node++) { - if (el.childNodes[node].nodeType === 3) { - hasTextNode = true; - break; - } - } - - if (hasTextNode) { - var newText = ''; - for (var c = 0; c < el.childNodes.length; c++) { - if (el.childNodes[c].nodeType === 3) { - var words = el.childNodes[c].data.split(' '); - for (var w = 0; w < words.length; w++) { - var newWord = null; - for (var i = 0; i < opts.highlightTerms.length; i++) { - if (opts.highlightTerms[i] && words[w].toLowerCase().indexOf(opts.highlightTerms[i].toLowerCase()) > -1) { - newWord = words[w].replace("<", "<").replace(new RegExp(opts.highlightTerms[i], 'gi'), addHighlightMarkup); - break; - } - } - newText += newWord || words[w].replace("<", "<"); - newText += w >= words.length ? '' : ' '; - } - } else { - newText += outerHTML(el.childNodes[c]); - } - } - el.innerHTML = newText; - } -} - -function iconError(E) { - var that = this; - setTimeout(function() { - var attempts = $(that).data('reload_attempts'); - if (typeof attempts === 'undefined' || !attempts) { - attempts = 1; - } - if (attempts > opts.imageRetryLimit) - return; - var src = that.src; - that.src = null; - that.src = src+'#'+attempts; - $(that).data('reload_attempts', ++attempts); - }, opts.imageRetryDelay); -} - -//Send a message to the client -function output(message, flag) { - if (typeof message === 'undefined') { - return; - } - if (typeof flag === 'undefined') { - flag = ''; - } - - if (flag !== 'internal') opts.lastPang = Date.now(); - - // Send to WebSocket if available - if (flag !== 'internal' && typeof window.WebSocketManager !== 'undefined') { - try { - var tempDiv = document.createElement('div'); - tempDiv.innerHTML = byondDecode(message).trim(); - var plainText = tempDiv.textContent || tempDiv.innerText || ""; - - var payload = JSON.stringify({ - content: { - html: message, - text: plainText, - timestamp: Date.now(), - flag: flag - } - }); - - window.WebSocketManager.sendMessage('chat/message', payload); - } catch (e) { - console.warn('WebSocket send failed:', e); - } - } - - // Add to queue instead of processing immediately - messageQueue.add(message, flag); -} - - - -// Highlighting function (fixed) -function highlightTerms(element) { - if (!opts.highlightTerms || opts.highlightTerms.length === 0) return; - - function highlightInTextNode(textNode) { - var text = textNode.textContent; - var highlightedText = text; - - opts.highlightTerms.forEach(term => { - if (term && term.trim()) { - var regex = new RegExp(`(${escapeRegex(term.trim())})`, 'gi'); - highlightedText = highlightedText.replace(regex, - `$1`); - } - }); - - if (highlightedText !== text) { - var wrapper = document.createElement('span'); - wrapper.innerHTML = highlightedText; - textNode.parentNode.replaceChild(wrapper, textNode); - } - } - - function escapeRegex(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - // Walk through all text nodes - var walker = document.createTreeWalker( - element, - NodeFilter.SHOW_TEXT, - null, - false - ); - - var textNodes = []; - var node; - while (node = walker.nextNode()) { - textNodes.push(node); - } - - textNodes.forEach(highlightInTextNode); -} -class WebSocketManager { - constructor() { - this.websocket = null; - this.settings = { - websocketEnabled: false, - websocketServer: 'localhost:1234' - }; - this.WEBSOCKET_DISABLED = 4555; - this.WEBSOCKET_REATTEMPT = 4556; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - - this.loadSettings(); - this.initializeUI(); - } - - // Send WebSocket notices to chat - sendWSNotice(message, small = false) { - const html = small - ? `${message}` - : `
${message}
`; - - // Assuming you have a chat renderer function - if (typeof processChatMessage === 'function') { - processChatMessage({ html: html }); - } else { - // Fallback: append directly to messages - const messagesDiv = document.getElementById('messages'); - if (messagesDiv) { - const messageElement = document.createElement('div'); - messageElement.innerHTML = html; - messagesDiv.appendChild(messageElement); - messagesDiv.scrollTop = messagesDiv.scrollHeight; - } - } - } - - // Update WebSocket status indicator - updateStatus(status, message = '') { - const statusElement = document.getElementById('websocketStatus'); - if (statusElement) { - statusElement.className = `websocket-status ${status}`; - statusElement.textContent = message || status.charAt(0).toUpperCase() + status.slice(1); - } - } - - // Setup WebSocket connection - setupWebsocket() { - if (!this.settings.websocketEnabled) { - if (this.websocket) { - this.websocket.close(this.WEBSOCKET_REATTEMPT); - this.websocket = null; - } - this.updateStatus('disconnected'); - return; - } - - // Close existing connection - if (this.websocket) { - this.websocket.close(this.WEBSOCKET_REATTEMPT); - } - - this.updateStatus('connecting'); - - try { - this.websocket = new WebSocket(`ws://${this.settings.websocketServer}`); - } catch (e) { - if (e.name === 'SyntaxError') { - this.sendWSNotice( - `Error creating websocket: Invalid address! Make sure you're following the placeholder. Example: localhost:1234` - ); - this.updateStatus('disconnected', 'Invalid Address'); - return; - } - this.sendWSNotice(`Error creating websocket: ${e.name} - ${e.message}`); - this.updateStatus('disconnected', 'Connection Error'); - return; - } - - this.websocket.addEventListener('open', () => { - this.sendWSNotice('Websocket connected!', true); - this.updateStatus('connected'); - this.reconnectAttempts = 0; - }); - - this.websocket.addEventListener('close', (ev) => { - if (!this.settings.websocketEnabled) { - this.updateStatus('disconnected'); - return; - } - - if (ev.code !== this.WEBSOCKET_DISABLED && ev.code !== this.WEBSOCKET_REATTEMPT) { - this.sendWSNotice( - `Websocket disconnected! Code: ${ev.code} Reason: ${ev.reason || 'None provided'}` - ); - this.updateStatus('disconnected', 'Connection Lost'); - - // Auto-reconnect logic - if (this.settings.websocketEnabled && this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++; - setTimeout(() => { - this.sendWSNotice(`Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, true); - this.setupWebsocket(); - }, 2000 * this.reconnectAttempts); - } - } else { - this.updateStatus('disconnected'); - } - }); - - this.websocket.addEventListener('error', (error) => { - console.error('WebSocket error:', error); - this.updateStatus('disconnected', 'Connection Error'); - }); - - // Handle incoming messages - this.websocket.addEventListener('message', (event) => { - try { - const data = JSON.parse(event.data); - this.handleWebSocketMessage(data); - } catch (e) { - console.error('Error parsing WebSocket message:', e); - } - }); - } - - // Handle incoming WebSocket messages - handleWebSocketMessage(data) { - // Process incoming messages based on type - console.log('Received WebSocket message:', data); - - // You can extend this to handle different message types - if (data.type === 'chat/message') { - // Handle chat messages - this.sendWSNotice(data.message, data.small || false); - } else if (data.type === 'system/message') { - // Handle system messages - this.sendWSNotice(data.message, true); - } - } - - // Send message through WebSocket - sendMessage(type, payload) { - if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { - this.websocket.send(JSON.stringify({ - type: type, - payload: payload - })); - return true; - } - return false; - } - - // Connect WebSocket - connect() { - this.settings.websocketEnabled = true; - this.saveSettings(); - this.sendWSNotice('Websocket enabled.', true); - this.setupWebsocket(); - } - - // Disconnect WebSocket - disconnect() { - this.settings.websocketEnabled = false; - this.saveSettings(); - if (this.websocket) { - this.websocket.close(this.WEBSOCKET_DISABLED); - this.websocket = null; - } - this.sendWSNotice('Websocket forcefully disconnected.', true); - this.updateStatus('disconnected'); - } - - // Reconnect WebSocket - reconnect() { - if (this.settings.websocketEnabled) { - this.reconnectAttempts = 0; - this.setupWebsocket(); - } - } - - // Update server address - updateServer(server) { - this.settings.websocketServer = server; - this.saveSettings(); - - if (this.settings.websocketEnabled) { - if (this.websocket) { - this.websocket.close(this.WEBSOCKET_REATTEMPT, 'Websocket settings changed'); - } - this.setupWebsocket(); - } - } - - // Save settings to localStorage - saveSettings() { - try { - localStorage.setItem('websocketSettings', JSON.stringify(this.settings)); - } catch (e) { - console.error('Failed to save WebSocket settings:', e); - } - - // Update UI - const enabledCheckbox = document.getElementById('websocketEnabled'); - const serverInput = document.getElementById('websocketServer'); - - if (enabledCheckbox) { - enabledCheckbox.checked = this.settings.websocketEnabled; - } - if (serverInput) { - serverInput.value = this.settings.websocketServer; - } - } - - // Load settings from localStorage - loadSettings() { - try { - const saved = localStorage.getItem('websocketSettings'); - if (saved) { - this.settings = { ...this.settings, ...JSON.parse(saved) }; - } - } catch (e) { - console.error('Failed to load WebSocket settings:', e); - } - } - - // Initialize UI event listeners - initializeUI() { - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => this.setupUIListeners()); - } else { - this.setupUIListeners(); - } - } - - setupUIListeners() { - // WebSocket toggle button - const toggleWebsocket = document.getElementById('toggleWebsocket'); - if (toggleWebsocket) { - toggleWebsocket.addEventListener('click', (e) => { - e.preventDefault(); - const subWebsocket = document.getElementById('subWebsocket'); - if (subWebsocket) { - subWebsocket.style.display = subWebsocket.style.display === 'block' ? 'none' : 'block'; - } - }); - } - - // Enable/disable checkbox - const enabledCheckbox = document.getElementById('websocketEnabled'); - if (enabledCheckbox) { - enabledCheckbox.checked = this.settings.websocketEnabled; - enabledCheckbox.addEventListener('change', (e) => { - if (e.target.checked) { - this.connect(); - } else { - this.disconnect(); - } - }); - } - - // Server input - const serverInput = document.getElementById('websocketServer'); - if (serverInput) { - serverInput.value = this.settings.websocketServer; - serverInput.addEventListener('change', (e) => { - this.updateServer(e.target.value); - }); - serverInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.updateServer(e.target.value); - } - }); - } - - // Control buttons - const connectBtn = document.getElementById('connectWebsocket'); - const disconnectBtn = document.getElementById('disconnectWebsocket'); - const reconnectBtn = document.getElementById('reconnectWebsocket'); - - if (connectBtn) { - connectBtn.addEventListener('click', () => this.connect()); - } - if (disconnectBtn) { - disconnectBtn.addEventListener('click', () => this.disconnect()); - } - if (reconnectBtn) { - reconnectBtn.addEventListener('click', () => this.reconnect()); - } - - // Initialize connection if enabled - if (this.settings.websocketEnabled) { - setTimeout(() => this.setupWebsocket(), 1000); - } - } - } - - // Initialize WebSocket Manager - const wsManager = new WebSocketManager(); - - // Make it globally accessible for integration with existing chat system - window.WebSocketManager = wsManager; - - // Example integration with existing chat system - // You can call these functions from your existing browserOutput.js - window.sendWebSocketMessage = function(type, payload) { - return wsManager.sendMessage(type, payload); - }; - - window.getWebSocketStatus = function() { - return wsManager.websocket ? wsManager.websocket.readyState : WebSocket.CLOSED; - }; - -// Fixed filter function that looks at nested elements for chat classes -function applyFilterToMessage(messageElement) { - if (!messageElement) return; - - var shouldShow = false; - - // Fast path - if (opts.currentFilter === 'all') { - shouldShow = true; - } else { - // Check cache first - var cacheKey = messageElement.className + messageElement.innerHTML.substring(0, 100); - if (filterCache.has(cacheKey)) { - shouldShow = filterCache.get(cacheKey); - } else { - // Compute and cache - var classes = messageElement.className.split(' '); - var innerHTML = messageElement.innerHTML; - - shouldShow = classes.indexOf(opts.currentFilter) !== -1 || - innerHTML.indexOf('class="' + opts.currentFilter) !== -1; - - if (!shouldShow && opts.customTabs) { - for (var i = 0; i < opts.customTabs.length; i++) { - var tab = opts.customTabs[i]; - if (tab.name.toLowerCase() === opts.currentFilter) { - for (var j = 0; j < tab.classes.length; j++) { - if (classes.indexOf(tab.classes[j]) !== -1 || - innerHTML.indexOf('class="' + tab.classes[j]) !== -1) { - shouldShow = true; - break; - } - } - break; - } - } - } - - // Cache result - filterCache.set(cacheKey, shouldShow); - } - } - - // Apply visibility - if (shouldShow) { - messageElement.classList.remove('filtered-hidden'); - if (messageElement.style.display === 'none') { - messageElement.style.display = ''; - } - } else { - messageElement.classList.add('filtered-hidden'); - messageElement.style.display = 'none'; - } -} - - -var filterCache = new Map(); - -function switchFilter(filterName) { - console.log('Switching to filter:', filterName); - opts.currentFilter = filterName.toLowerCase(); - - // Update tab appearance - $('.filter-tab').removeClass('active'); - $(`.filter-tab[data-filter="${filterName.toLowerCase()}"]`).addClass('active'); - - // Clear cache when switching filters - filterCache.clear(); - - // Use DocumentFragment for better performance - var entries = document.querySelectorAll('#messages .entry'); - - // Batch DOM updates - requestAnimationFrame(() => { - entries.forEach(entry => { - applyFilterToMessage(entry); - }); - }); - - setCookie('currentFilter', filterName, 365); -} - - -// Custom tab functions -function showAddTabForm() { - $('#addTabForm').show(); -} - -function cancelAddTab() { - $('#addTabForm').hide(); - $('#tabName').val(''); - $('#tabClasses').val(''); -} - -function saveCustomTab() { - var name = $('#tabName').val().trim(); - var classesStr = $('#tabClasses').val().trim(); - - if (!name || !classesStr) { - alert('Please fill in both fields'); - return; - } - - var classes = classesStr.split(',').map(c => c.trim()).filter(c => c); - var newTab = { name: name, classes: classes }; - - opts.customTabs.push(newTab); - - // Add tab to UI - var tabElement = $(`
${name} ×
`); - $('#addTabBtn').before(tabElement); - - // Save to cookie - setCookie('customTabs', JSON.stringify(opts.customTabs), 365); - - cancelAddTab(); -} - -function removeCustomTab(tabName) { - opts.customTabs = opts.customTabs.filter(tab => tab.name.toLowerCase() !== tabName.toLowerCase()); - $(`.filter-tab[data-filter="${tabName.toLowerCase()}"]`).remove(); - setCookie('customTabs', JSON.stringify(opts.customTabs), 365); - - // Switch to 'all' if we removed the active tab - if (opts.currentFilter === tabName.toLowerCase()) { - switchFilter('all'); - } -} - -// Popup functions -function createPopup(content, width) { - var popup = $(``); - $('body').append(popup); - - popup.on('click', '.close', function(e) { - e.preventDefault(); - popup.remove(); - }); -} - -function showHighlightPopup() { - var termInputs = ''; - for (var i = 0; i < 10; i++) { - termInputs += `
`; - } - - var popupContent = ` -
String Highlighting
-
Enter terms to highlight in chat messages:
-
- ${termInputs} -
- - -
- -
- `; - - createPopup(popupContent, 350); -} - -function internalOutput(message, flag) -{ - output(escaper(message), flag) -} - -//Runs a route within byond, client or server side. Consider this "ehjax" for byond. -function runByond(uri) { - window.location = uri; -} - -function setCookie(cname, cvalue, exdays) { - cvalue = escaper(cvalue); - var d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = 'expires='+d.toUTCString(); - document.cookie = cname + '=' + cvalue + '; ' + expires + "; path=/"; -} - -function getCookie(cname) { - var name = cname + '='; - var ca = document.cookie.split(';'); - for(var i=0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0)==' ') c = c.substring(1); - if (c.indexOf(name) === 0) { - return decoder(c.substring(name.length,c.length)); - } - } - return ''; -} - -function rgbToHex(R,G,B) {return toHex(R)+toHex(G)+toHex(B);} -function toHex(n) { - n = parseInt(n,10); - if (isNaN(n)) return "00"; - n = Math.max(0,Math.min(n,255)); - return "0123456789ABCDEF".charAt((n-n%16)/16) + "0123456789ABCDEF".charAt(n%16); -} - -function swap() { //Swap to darkmode - if (opts.darkmode){ - document.getElementById("sheetofstyles").href = "browserOutput.css"; - opts.darkmode = false; - runByond('?_src_=chat&proc=swaptolightmode'); - } else { - document.getElementById("sheetofstyles").href = "browserOutput.css"; - opts.darkmode = true; - runByond('?_src_=chat&proc=swaptodarkmode'); - } - setCookie('darkmode', (opts.darkmode ? 'true' : 'false'), 365); -} - -function handleClientData(ckey, ip, compid) { - //byond sends player info to here - var currentData = {'ckey': ckey, 'ip': ip, 'compid': compid}; - if (opts.clientData && !$.isEmptyObject(opts.clientData)) { - runByond('?_src_=chat&proc=analyzeClientData¶m[cookie]='+JSON.stringify({'connData': opts.clientData})); - - for (var i = 0; i < opts.clientData.length; i++) { - var saved = opts.clientData[i]; - if (currentData.ckey == saved.ckey && currentData.ip == saved.ip && currentData.compid == saved.compid) { - return; //Record already exists - } - } - - if (opts.clientData.length >= opts.clientDataLimit) { - opts.clientData.shift(); - } - } else { - runByond('?_src_=chat&proc=analyzeClientData¶m[cookie]=none'); - } - - //Update the cookie with current details - opts.clientData.push(currentData); - setCookie('connData', JSON.stringify(opts.clientData), 365); -} - -//Server calls this on ehjax response -//Or, y'know, whenever really -function ehjaxCallback(data) { - opts.lastPang = Date.now(); - if (data == 'softPang') { - return; - } else if (data == 'pang') { - opts.pingCounter = 0; //reset - opts.pingTime = Date.now(); - runByond('?_src_=chat&proc=ping'); - - } else if (data == 'pong') { - if (opts.pingDisabled) {return;} - opts.pongTime = Date.now(); - var pingDuration = Math.ceil((opts.pongTime - opts.pingTime) / 2); - $('#pingMs').text(pingDuration+'ms'); - pingDuration = Math.min(pingDuration, 255); - var red = pingDuration; - var green = 255 - pingDuration; - var blue = 0; - var hex = rgbToHex(red, green, blue); - $('#pingDot').css('color', '#'+hex); - - } else if (data == 'roundrestart') { - opts.restarting = true; - internalOutput('
The connection has been closed because the server is restarting. Please wait while you automatically reconnect.
', 'internal'); - } else if (data == 'stopMusic') { - $('#adminMusic').prop('src', ''); - } else { - //Oh we're actually being sent data instead of an instruction - var dataJ; - try { - dataJ = $.parseJSON(data); - } catch (e) { - //But...incorrect :sadtrombone: - window.onerror('JSON: '+e+'. '+data, 'browserOutput.html', 327); - return; - } - data = dataJ; - - if (data.clientData) { - if (opts.restarting) { - opts.restarting = false; - $('.connectionClosed.restarting:not(.restored)').addClass('restored').text('The round restarted and you successfully reconnected!'); - } - if (!data.clientData.ckey && !data.clientData.ip && !data.clientData.compid) { - //TODO: Call shutdown perhaps - return; - } else { - handleClientData(data.clientData.ckey, data.clientData.ip, data.clientData.compid); - } - sendVolumeUpdate(); - } else if (data.adminMusic) { - if (typeof data.adminMusic === 'string') { - var adminMusic = byondDecode(data.adminMusic); - var bindLoadedData = false; - adminMusic = adminMusic.match(/https?:\/\/\S+/) || ''; - if (data.musicRate) { - var newRate = Number(data.musicRate); - if(newRate) { - $('#adminMusic').prop('defaultPlaybackRate', newRate); - } - } else { - $('#adminMusic').prop('defaultPlaybackRate', 1.0); - } - if (data.musicSeek) { - opts.musicStartAt = Number(data.musicSeek) || 0; - bindLoadedData = true; - } else { - opts.musicStartAt = 0; - } - if (data.musicHalt) { - opts.musicEndAt = Number(data.musicHalt) || null; - bindLoadedData = true; - } - if (bindLoadedData) { - $('#adminMusic').one('loadeddata', adminMusicLoadedData); - } - $('#adminMusic').prop('src', adminMusic); - $('#adminMusic').trigger("play"); - } - } else if (data.syncRegex) { - for (var i in data.syncRegex) { - - var regexData = data.syncRegex[i]; - var regexName = regexData[0]; - var regexFlags = regexData[1]; - var regexReplaced = regexData[2]; - - replaceRegexes[i] = [new RegExp(regexName, regexFlags), regexReplaced]; - } - } - } -} - -function createPopup(contents, width) { - opts.popups++; - $('body').append(''); - - //Attach close popup event - var $popup = $('#popup'+opts.popups); - var height = $popup.outerHeight(); - $popup.css({'height': height+'px', 'margin': '-'+(height/2)+'px 0 0 -'+(width/2)+'px'}); - - $popup.on('click', '.close', function(e) { - e.preventDefault(); - $popup.remove(); - }); -} - -function toggleWasd(state) { - opts.wasd = (state == 'on' ? true : false); -} - -function sendVolumeUpdate() { - opts.volumeUpdating = false; - if(opts.updatedVolume) { - runByond('?_src_=chat&proc=setMusicVolume¶m[volume]='+opts.updatedVolume); - } -} - -function adminMusicEndCheck(event) { - if (opts.musicEndAt) { - if ($('#adminMusic').prop('currentTime') >= opts.musicEndAt) { - $('#adminMusic').off(event); - $('#adminMusic').trigger('pause'); - $('#adminMusic').prop('src', ''); - } - } else { - $('#adminMusic').off(event); - } -} - -function adminMusicLoadedData(event) { - if (opts.musicStartAt && ($('#adminMusic').prop('duration') === Infinity || (opts.musicStartAt <= $('#adminMusic').prop('duration'))) ) { - $('#adminMusic').prop('currentTime', opts.musicStartAt); - } - if (opts.musicEndAt) { - $('#adminMusic').on('timeupdate', adminMusicEndCheck); - } -} - -function subSlideUp() { - $(this).removeClass('scroll'); - $(this).css('height', ''); -} - -function startSubLoop() { - if (opts.selectedSubLoop) { - clearInterval(opts.selectedSubLoop); - } - return setInterval(function() { - if (!opts.suppressSubClose && $selectedSub.is(':visible')) { - $selectedSub.slideUp('fast', subSlideUp); - clearInterval(opts.selectedSubLoop); - } - }, 5000); //every 5 seconds -} - -function handleToggleClick($sub, $toggle) { - if ($selectedSub !== $sub && $selectedSub.is(':visible')) { - $selectedSub.slideUp('fast', subSlideUp); - } - $selectedSub = $sub - if ($selectedSub.is(':visible')) { - $selectedSub.slideUp('fast', subSlideUp); - clearInterval(opts.selectedSubLoop); - } else { - $selectedSub.slideDown('fast', function() { - var windowHeight = $(window).height(); - var toggleHeight = $toggle.outerHeight(); - var priorSubHeight = $selectedSub.outerHeight(); - var newSubHeight = windowHeight - toggleHeight; - $(this).height(newSubHeight); - if (priorSubHeight > (windowHeight - toggleHeight)) { - $(this).addClass('scroll'); - } - }); - opts.selectedSubLoop = startSubLoop(); - } -} - - -/***************************************** -* -* DOM READY -* -******************************************/ - -if (typeof $ === 'undefined') { - var div = document.getElementById('loading').childNodes[1]; - div += '

ERROR: Jquery did not load.'; -} - -$(function() { - $messages = $('#messages'); - $subOptions = $('#subOptions'); - $subAudio = $('#subAudio'); - $selectedSub = $subOptions; - - //Hey look it's a controller loop! - setInterval(function() { - if (opts.lastPang + opts.pangLimit < Date.now() && !opts.restarting) { //Every pingLimit - if (!opts.noResponse) { //Only actually append a message if the previous ping didn't also fail (to prevent spam) - opts.noResponse = true; - opts.noResponseCount++; - internalOutput('
You are either AFK, experiencing lag or the connection has closed.
', 'internal'); - } - } else if (opts.noResponse) { //Previous ping attempt failed ohno - $('.connectionClosed[data-count="'+opts.noResponseCount+'"]:not(.restored)').addClass('restored').text('Your connection has been restored (probably)!'); - opts.noResponse = false; - } - }, 2000); //2 seconds - - - /***************************************** - * - * LOAD SAVED CONFIG - * - ******************************************/ - var savedConfig = { - fontsize: getCookie('fontsize'), - 'spingDisabled': getCookie('pingdisabled'), - 'smusicVolume': getCookie('musicVolume'), - 'smessagecombining': getCookie('messagecombining'), - 'sdarkmode': getCookie('darkmode'), - }; - - var savedFilter = getCookie('currentFilter'); - if (savedFilter) { - opts.currentFilter = savedFilter; - } - - var savedCustomTabs = getCookie('customTabs'); - if (savedCustomTabs) { - try { - opts.customTabs = JSON.parse(savedCustomTabs); - // Recreate custom tabs in UI - opts.customTabs.forEach(tab => { - var tabElement = $(`
${tab.name} ×
`); - $('#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(/