diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 2a98e34a458..63319aee335 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -292,9 +292,9 @@ GLOBAL_LIST_INIT(pda_styles, sortList(list(MONO, VT, ORBITRON, SHARE))) #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(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} diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 4b1b9fcf3d1..0644af0b748 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -142,7 +142,6 @@ // Subsystem fire priority, from lowest to highest priority // If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) -#define FIRE_PRIORITY_PING 10 #define FIRE_PRIORITY_IDLE_NPC 10 #define FIRE_PRIORITY_SERVER_MAINT 10 #define FIRE_PRIORITY_RESEARCH 10 diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm index f5adeadadeb..f594b735b6b 100644 --- a/code/__DEFINES/tgui.dm +++ b/code/__DEFINES/tgui.dm @@ -26,3 +26,10 @@ #define TGUI_WINDOW_ID(index) "tgui-window-[index]" /// Get a pool index of the provided window id #define TGUI_WINDOW_INDEX(window_id) text2num(copytext(window_id, 13)) + +/// Creates a message packet for sending via output() +#define TGUI_CREATE_MESSAGE(type, payload) ( \ + url_encode(json_encode(list( \ + "type" = type, \ + "payload" = payload, \ + )))) diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 81092440d45..3af048f9b18 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1483,47 +1483,3 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) return call(source, proctype)(arglist(arguments)) #define TURF_FROM_COORDS_LIST(List) (locate(List[1], List[2], List[3])) - -// Converts browser keycodes to BYOND keycodes. -/proc/browser_keycode_to_byond(keycode) - keycode = text2num(keycode) - switch(keycode) - // letters and numbers - if(65 to 90, 48 to 57) - return ascii2text(keycode) - if(17) - return "Ctrl" - if(18) - return "Alt" - if(16) - return "Shift" - if(37) - return "West" - if(38) - return "North" - if(39) - return "East" - if(40) - return "South" - if(45) - return "Insert" - if(46) - return "Delete" - if(36) - return "Northwest" - if(35) - return "Southwest" - if(33) - return "Northeast" - if(34) - return "Southeast" - if(112 to 123) - return "F[keycode-111]" - if(96 to 105) - return "Numpad[keycode-96]" - if(188) - return "," - if(190) - return "." - if(189) - return "-" diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm index be69d77a9cb..eb4c75312bc 100644 --- a/code/controllers/configuration/configuration.dm +++ b/code/controllers/configuration/configuration.dm @@ -405,23 +405,17 @@ Example config: /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 world.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, /proc/message_admins, text), 0) diff --git a/code/controllers/subsystem/chat.dm b/code/controllers/subsystem/chat.dm index bbeb0683f0f..a4f8dfdc5d3 100644 --- a/code/controllers/subsystem/chat.dm +++ b/code/controllers/subsystem/chat.dm @@ -5,77 +5,35 @@ SUBSYSTEM_DEF(chat) priority = FIRE_PRIORITY_CHAT init_order = INIT_ORDER_CHAT - var/list/payload = list() - + var/list/payload_by_client = list() /datum/controller/subsystem/chat/fire() - for(var/i in payload) - var/client/C = i - C << output(payload[C], "browseroutput:output") - payload -= C - + for(var/key in payload_by_client) + var/client/client = key + var/payload = payload_by_client[key] + payload_by_client -= key + if(client) + // Send to tgchat + client.tgui_panel?.window.send_message("chat/message", payload) + // Send to old chat + for(var/msg in payload) + SEND_TEXT(client, msg["text"]) if(MC_TICK_CHECK) return - -/datum/controller/subsystem/chat/proc/queue(target, message, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = TRUE) - if(!target || !message) - return - - if(!istext(message)) - stack_trace("to_chat called with invalid input type") - return - - if(target == world) - target = GLOB.clients - - //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]") - if (trailing_newline) - message += "
" - - //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)) - +/datum/controller/subsystem/chat/proc/queue(target, text, flags) if(islist(target)) - 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 - - 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) - - 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 - - payload[C] += twiceEncoded + for(var/_target in target) + var/client/client = CLIENT_FROM_VAR(_target) + if(client) + LAZYADD(payload_by_client[client], list(list( + "text" = text, + "flags" = flags, + ))) + return + var/client/client = CLIENT_FROM_VAR(target) + if(client) + LAZYADD(payload_by_client[client], list(list( + "text" = text, + "flags" = flags, + ))) diff --git a/code/controllers/subsystem/ping.dm b/code/controllers/subsystem/ping.dm deleted file mode 100644 index c5c9bb39335..00000000000 --- a/code/controllers/subsystem/ping.dm +++ /dev/null @@ -1,33 +0,0 @@ -SUBSYSTEM_DEF(ping) - name = "Ping" - priority = FIRE_PRIORITY_PING - wait = 3 SECONDS - flags = SS_NO_INIT - runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME - - var/list/currentrun = list() - -/datum/controller/subsystem/ping/stat_entry() - ..("P:[GLOB.clients.len]") - - -/datum/controller/subsystem/ping/fire(resumed = 0) - if (!resumed) - src.currentrun = GLOB.clients.Copy() - - //cache for sanic speed (lists are references anyways) - var/list/currentrun = src.currentrun - - while (currentrun.len) - var/client/C = currentrun[currentrun.len] - currentrun.len-- - - if (!C || !C.chatOutput || !C.chatOutput.loaded) - if (MC_TICK_CHECK) - return - continue - - // 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) - return diff --git a/code/controllers/subsystem/server_maint.dm b/code/controllers/subsystem/server_maint.dm index d9ce72242ac..b29c71563e8 100644 --- a/code/controllers/subsystem/server_maint.dm +++ b/code/controllers/subsystem/server_maint.dm @@ -81,9 +81,7 @@ SUBSYSTEM_DEF(server_maint) if(!thing) continue var/client/C = thing - var/datum/chat_output/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/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm index 3b21757bc52..5318f1d233e 100644 --- a/code/game/objects/items/devices/PDA/PDA.dm +++ b/code/game/objects/items/devices/PDA/PDA.dm @@ -206,7 +206,7 @@ GLOBAL_LIST_EMPTY(PDAs) var/datum/asset/spritesheet/assets = get_asset_datum(/datum/asset/spritesheet/simple/pda) assets.send(user) - var/datum/asset/spritesheet/emoji_s = get_asset_datum(/datum/asset/spritesheet/goonchat) + var/datum/asset/spritesheet/emoji_s = get_asset_datum(/datum/asset/spritesheet/chat) emoji_s.send(user) //Already sent by chat but no harm doing this user.set_machine(src) diff --git a/code/game/objects/items/devices/PDA/cart.dm b/code/game/objects/items/devices/PDA/cart.dm index 566a2f96ab1..41ec0c8d258 100644 --- a/code/game/objects/items/devices/PDA/cart.dm +++ b/code/game/objects/items/devices/PDA/cart.dm @@ -546,7 +546,7 @@ Code: var/static/list/emoji_icon_states var/static/emoji_table if(!emoji_table) - var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/goonchat) + var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat) var/list/collate = list("
") for(var/emoji in sortList(icon_states(icon('icons/emoji.dmi')))) var/tag = sheet.icon_tag("emoji-[emoji]") diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index e11a695ada2..e834d9a609c 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -1,11 +1,11 @@ //////////////////////////////// /proc/message_admins(msg) - msg = "ADMIN LOG: [msg]" + msg = "ADMIN LOG: [msg]" to_chat(GLOB.admins, msg, confidential = TRUE) /proc/relay_msg_admins(msg) - msg = "RELAY: [msg]" + msg = "RELAY: [msg]" to_chat(GLOB.admins, msg, confidential = TRUE) diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 91fb01ccdf8..701351b238d 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -32,9 +32,7 @@ for(var/mob/M in GLOB.player_list) if(M.client.prefs.toggles & SOUND_MIDI) - var/user_vol = M.client.chatOutput.adminMusicVolume - if(user_vol) - admin_sound.volume = vol * (user_vol / 100) + admin_sound.volume = vol * M.client.admin_music_volume SEND_SOUND(M, admin_sound) admin_sound.volume = vol @@ -112,6 +110,8 @@ webpage_url = "[title]" music_extra_data["start"] = data["start_time"] music_extra_data["end"] = data["end_time"] + music_extra_data["link"] = data["webpage_url"] + music_extra_data["title"] = data["title"] var/res = alert(usr, "Show the title of and link to this song to the players?\n[title]",, "No", "Yes", "Cancel") switch(res) @@ -141,11 +141,11 @@ for(var/m in GLOB.player_list) var/mob/M = m 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) 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") @@ -172,6 +172,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/asset_cache/asset_cache_item.dm b/code/modules/asset_cache/asset_cache_item.dm index 4b14144aa41..41cee06db1e 100644 --- a/code/modules/asset_cache/asset_cache_item.dm +++ b/code/modules/asset_cache/asset_cache_item.dm @@ -1,16 +1,25 @@ /** - * # asset_cache_item - * - * An internal datum containing info on items in the asset cache. Mainly used to cache md5 info for speed. -**/ + * # asset_cache_item + * + * An internal datum containing info on items in the asset cache. Mainly used to cache md5 info for speed. + */ /datum/asset_cache_item var/name var/hash var/resource var/ext = "" - var/legacy = FALSE //! Should this file also be sent via the legacy browse_rsc system when cdn transports are enabled? - var/namespace = null //! used by the cdn system to keep legacy css assets with their parent css file. (css files resolve urls relative to the css file, so the legacy system can't be used if the css file itself could go out over the cdn) - var/namespace_parent = FALSE //! True if this is the parent css or html file for an asset's namespace + /// Should this file also be sent via the legacy browse_rsc system + /// when cdn transports are enabled? + var/legacy = FALSE + /// Used by the cdn system to keep legacy css assets with their parent + /// css file. (css files resolve urls relative to the css file, so the + /// legacy system can't be used if the css file itself could go out over + /// the cdn) + var/namespace = null + /// True if this is the parent css or html file for an asset's namespace + var/namespace_parent = FALSE + /// TRUE for keeping local asset names when browse_rsc backend is used + var/keep_local_name = FALSE /datum/asset_cache_item/New(name, file) if (!isfile(file)) @@ -27,7 +36,6 @@ ext = ".[copytext(name, extstart+1)]" resource = file - /datum/asset_cache_item/vv_edit_var(var_name, var_value) return FALSE diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 8aff5334fd2..221febbe14d 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -26,11 +26,18 @@ GLOBAL_LIST_EMPTY(asset_datums) return -//If you don't need anything complicated. +/// If you don't need anything complicated. /datum/asset/simple _abstract = /datum/asset/simple - var/assets = list() //! list of assets for this datum in the form of asset_filename = asset_file. At runtime the asset_file will be converted into a asset_cache datum. - var/legacy = FALSE //! set to true to have this asset also be sent via browse_rsc when cdn asset transports are enabled. + /// list of assets for this datum in the form of: + /// asset_filename = asset_file. At runtime the asset_file will be + /// converted into a asset_cache datum. + var/assets = list() + /// Set to true to have this asset also be sent via the legacy browse_rsc + /// system when cdn transports are enabled? + var/legacy = FALSE + /// TRUE for keeping local asset names when browse_rsc backend is used + var/keep_local_name = FALSE /datum/asset/simple/register() for(var/asset_name in assets) @@ -39,7 +46,9 @@ GLOBAL_LIST_EMPTY(asset_datums) log_asset("ERROR: Invalid asset: [type]:[asset_name]:[ACI]") continue if (legacy) - ACI.legacy = TRUE + ACI.legacy = legacy + if (keep_local_name) + ACI.keep_local_name = keep_local_name assets[asset_name] = ACI /datum/asset/simple/send(client) @@ -273,7 +282,7 @@ GLOBAL_LIST_EMPTY(asset_datums) assets |= parents var/list/hashlist = list() var/list/sorted_assets = sortList(assets) - + for (var/asset_name in sorted_assets) var/datum/asset_cache_item/ACI = new(asset_name, sorted_assets[asset_name]) if (!ACI?.hash) @@ -302,7 +311,7 @@ GLOBAL_LIST_EMPTY(asset_datums) ..() /// Get a html string that will load a html asset. -/// Needed because byond doesn't allow you to browse() to a url. +/// Needed because byond doesn't allow you to browse() to a url. /datum/asset/simple/namespaced/proc/get_htmlloader(filename) return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename])) diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 1ffaba22ffa..8a11340a7d4 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -1,81 +1,95 @@ //DEFINITIONS FOR ASSET DATUMS START HERE. +/datum/asset/simple/tgui_common + keep_local_name = TRUE + assets = list( + "tgui-common.chunk.js" = 'tgui/packages/tgui/public/tgui-common.chunk.js', + ) + /datum/asset/simple/tgui + keep_local_name = TRUE assets = list( "tgui.bundle.js" = 'tgui/packages/tgui/public/tgui.bundle.js', "tgui.bundle.css" = 'tgui/packages/tgui/public/tgui.bundle.css', ) +/datum/asset/simple/tgui_panel + keep_local_name = TRUE + assets = list( + "tgui-panel.bundle.js" = 'tgui/packages/tgui/public/tgui-panel.bundle.js', + "tgui-panel.bundle.css" = 'tgui/packages/tgui/public/tgui-panel.bundle.css', + ) + /datum/asset/simple/headers assets = list( - "alarm_green.gif" = 'icons/program_icons/alarm_green.gif', - "alarm_red.gif" = 'icons/program_icons/alarm_red.gif', - "batt_5.gif" = 'icons/program_icons/batt_5.gif', - "batt_20.gif" = 'icons/program_icons/batt_20.gif', - "batt_40.gif" = 'icons/program_icons/batt_40.gif', - "batt_60.gif" = 'icons/program_icons/batt_60.gif', - "batt_80.gif" = 'icons/program_icons/batt_80.gif', - "batt_100.gif" = 'icons/program_icons/batt_100.gif', - "charging.gif" = 'icons/program_icons/charging.gif', - "downloader_finished.gif" = 'icons/program_icons/downloader_finished.gif', - "downloader_running.gif" = 'icons/program_icons/downloader_running.gif', - "ntnrc_idle.gif" = 'icons/program_icons/ntnrc_idle.gif', - "ntnrc_new.gif" = 'icons/program_icons/ntnrc_new.gif', - "power_norm.gif" = 'icons/program_icons/power_norm.gif', - "power_warn.gif" = 'icons/program_icons/power_warn.gif', - "sig_high.gif" = 'icons/program_icons/sig_high.gif', - "sig_low.gif" = 'icons/program_icons/sig_low.gif', - "sig_lan.gif" = 'icons/program_icons/sig_lan.gif', - "sig_none.gif" = 'icons/program_icons/sig_none.gif', - "smmon_0.gif" = 'icons/program_icons/smmon_0.gif', - "smmon_1.gif" = 'icons/program_icons/smmon_1.gif', - "smmon_2.gif" = 'icons/program_icons/smmon_2.gif', - "smmon_3.gif" = 'icons/program_icons/smmon_3.gif', - "smmon_4.gif" = 'icons/program_icons/smmon_4.gif', - "smmon_5.gif" = 'icons/program_icons/smmon_5.gif', - "smmon_6.gif" = 'icons/program_icons/smmon_6.gif', - "borg_mon.gif" = 'icons/program_icons/borg_mon.gif' + "alarm_green.gif" = 'icons/program_icons/alarm_green.gif', + "alarm_red.gif" = 'icons/program_icons/alarm_red.gif', + "batt_5.gif" = 'icons/program_icons/batt_5.gif', + "batt_20.gif" = 'icons/program_icons/batt_20.gif', + "batt_40.gif" = 'icons/program_icons/batt_40.gif', + "batt_60.gif" = 'icons/program_icons/batt_60.gif', + "batt_80.gif" = 'icons/program_icons/batt_80.gif', + "batt_100.gif" = 'icons/program_icons/batt_100.gif', + "charging.gif" = 'icons/program_icons/charging.gif', + "downloader_finished.gif" = 'icons/program_icons/downloader_finished.gif', + "downloader_running.gif" = 'icons/program_icons/downloader_running.gif', + "ntnrc_idle.gif" = 'icons/program_icons/ntnrc_idle.gif', + "ntnrc_new.gif" = 'icons/program_icons/ntnrc_new.gif', + "power_norm.gif" = 'icons/program_icons/power_norm.gif', + "power_warn.gif" = 'icons/program_icons/power_warn.gif', + "sig_high.gif" = 'icons/program_icons/sig_high.gif', + "sig_low.gif" = 'icons/program_icons/sig_low.gif', + "sig_lan.gif" = 'icons/program_icons/sig_lan.gif', + "sig_none.gif" = 'icons/program_icons/sig_none.gif', + "smmon_0.gif" = 'icons/program_icons/smmon_0.gif', + "smmon_1.gif" = 'icons/program_icons/smmon_1.gif', + "smmon_2.gif" = 'icons/program_icons/smmon_2.gif', + "smmon_3.gif" = 'icons/program_icons/smmon_3.gif', + "smmon_4.gif" = 'icons/program_icons/smmon_4.gif', + "smmon_5.gif" = 'icons/program_icons/smmon_5.gif', + "smmon_6.gif" = 'icons/program_icons/smmon_6.gif', + "borg_mon.gif" = 'icons/program_icons/borg_mon.gif' ) /datum/asset/simple/radar_assets assets = list( - "ntosradarbackground.png" = 'icons/UI_Icons/tgui/ntosradar_background.png', - "ntosradarpointer.png" = 'icons/UI_Icons/tgui/ntosradar_pointer.png', - "ntosradarpointerS.png" = 'icons/UI_Icons/tgui/ntosradar_pointer_S.png' + "ntosradarbackground.png" = 'icons/UI_Icons/tgui/ntosradar_background.png', + "ntosradarpointer.png" = 'icons/UI_Icons/tgui/ntosradar_pointer.png', + "ntosradarpointerS.png" = 'icons/UI_Icons/tgui/ntosradar_pointer_S.png' ) /datum/asset/spritesheet/simple/pda name = "pda" assets = list( - "atmos" = 'icons/pda_icons/pda_atmos.png', - "back" = 'icons/pda_icons/pda_back.png', - "bell" = 'icons/pda_icons/pda_bell.png', - "blank" = 'icons/pda_icons/pda_blank.png', - "boom" = 'icons/pda_icons/pda_boom.png', - "bucket" = 'icons/pda_icons/pda_bucket.png', - "medbot" = 'icons/pda_icons/pda_medbot.png', - "floorbot" = 'icons/pda_icons/pda_floorbot.png', - "cleanbot" = 'icons/pda_icons/pda_cleanbot.png', - "crate" = 'icons/pda_icons/pda_crate.png', - "cuffs" = 'icons/pda_icons/pda_cuffs.png', - "eject" = 'icons/pda_icons/pda_eject.png', - "flashlight" = 'icons/pda_icons/pda_flashlight.png', - "honk" = 'icons/pda_icons/pda_honk.png', - "mail" = 'icons/pda_icons/pda_mail.png', - "medical" = 'icons/pda_icons/pda_medical.png', - "menu" = 'icons/pda_icons/pda_menu.png', - "mule" = 'icons/pda_icons/pda_mule.png', - "notes" = 'icons/pda_icons/pda_notes.png', - "power" = 'icons/pda_icons/pda_power.png', - "rdoor" = 'icons/pda_icons/pda_rdoor.png', - "reagent" = 'icons/pda_icons/pda_reagent.png', - "refresh" = 'icons/pda_icons/pda_refresh.png', - "scanner" = 'icons/pda_icons/pda_scanner.png', - "signaler" = 'icons/pda_icons/pda_signaler.png', - "skills" = 'icons/pda_icons/pda_skills.png', - "status" = 'icons/pda_icons/pda_status.png', - "dronephone" = 'icons/pda_icons/pda_dronephone.png', - "emoji" = 'icons/pda_icons/pda_emoji.png' + "atmos" = 'icons/pda_icons/pda_atmos.png', + "back" = 'icons/pda_icons/pda_back.png', + "bell" = 'icons/pda_icons/pda_bell.png', + "blank" = 'icons/pda_icons/pda_blank.png', + "boom" = 'icons/pda_icons/pda_boom.png', + "bucket" = 'icons/pda_icons/pda_bucket.png', + "medbot" = 'icons/pda_icons/pda_medbot.png', + "floorbot" = 'icons/pda_icons/pda_floorbot.png', + "cleanbot" = 'icons/pda_icons/pda_cleanbot.png', + "crate" = 'icons/pda_icons/pda_crate.png', + "cuffs" = 'icons/pda_icons/pda_cuffs.png', + "eject" = 'icons/pda_icons/pda_eject.png', + "flashlight" = 'icons/pda_icons/pda_flashlight.png', + "honk" = 'icons/pda_icons/pda_honk.png', + "mail" = 'icons/pda_icons/pda_mail.png', + "medical" = 'icons/pda_icons/pda_medical.png', + "menu" = 'icons/pda_icons/pda_menu.png', + "mule" = 'icons/pda_icons/pda_mule.png', + "notes" = 'icons/pda_icons/pda_notes.png', + "power" = 'icons/pda_icons/pda_power.png', + "rdoor" = 'icons/pda_icons/pda_rdoor.png', + "reagent" = 'icons/pda_icons/pda_reagent.png', + "refresh" = 'icons/pda_icons/pda_refresh.png', + "scanner" = 'icons/pda_icons/pda_scanner.png', + "signaler" = 'icons/pda_icons/pda_signaler.png', + "skills" = 'icons/pda_icons/pda_skills.png', + "status" = 'icons/pda_icons/pda_status.png', + "dronephone" = 'icons/pda_icons/pda_dronephone.png', + "emoji" = 'icons/pda_icons/pda_emoji.png' ) /datum/asset/spritesheet/simple/paper @@ -133,32 +147,12 @@ "changelog.css" = 'html/changelog.css' ) parents = list("changelog.html" = 'html/changelog.html') - -/datum/asset/group/goonchat - children = list( - /datum/asset/simple/jquery, - /datum/asset/simple/namespaced/goonchat, - /datum/asset/spritesheet/goonchat, - /datum/asset/simple/namespaced/fontawesome - ) /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', - "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_white.css', - ) - parents = list( - //this list intentionally left empty (parent namespaced assets can't be referred to by name, only by generated url, and goonchat isn't smart enough for that. yet) + "jquery.min.js" = 'html/jquery.min.js', ) /datum/asset/simple/namespaced/fontawesome @@ -170,14 +164,13 @@ "fa-solid-900.woff" = 'html/font-awesome/webfonts/fa-solid-900.woff', "v4shim.css" = 'html/font-awesome/css/v4-shims.min.css' ) - parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css') + parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css') -/datum/asset/spritesheet/goonchat +/datum/asset/spritesheet/chat name = "chat" -/datum/asset/spritesheet/goonchat/register() +/datum/asset/spritesheet/chat/register() InsertAll("emoji", 'icons/emoji.dmi') - // pre-loading all lanugage icons also helps to avoid meta InsertAll("language", 'icons/misc/language.dmi') // catch languages which are pulling icons from another file @@ -187,7 +180,6 @@ if (icon != 'icons/misc/language.dmi') var/icon_state = initial(L.icon_state) Insert("language-[icon_state]", icon, icon_state=icon_state) - ..() /datum/asset/simple/lobby @@ -227,7 +219,7 @@ "boss4.gif" = 'icons/UI_Icons/Arcade/boss4.gif', "boss5.gif" = 'icons/UI_Icons/Arcade/boss5.gif', "boss6.gif" = 'icons/UI_Icons/Arcade/boss6.gif', - ) + ) /datum/asset/spritesheet/simple/achievements name ="achievements" @@ -409,9 +401,9 @@ /datum/asset/simple/genetics assets = list( - "dna_discovered.gif" = 'html/dna_discovered.gif', - "dna_undiscovered.gif" = 'html/dna_undiscovered.gif', - "dna_extra.gif" = 'html/dna_extra.gif' + "dna_discovered.gif" = 'html/dna_discovered.gif', + "dna_undiscovered.gif" = 'html/dna_undiscovered.gif', + "dna_extra.gif" = 'html/dna_extra.gif' ) /datum/asset/simple/orbit diff --git a/code/modules/asset_cache/transports/asset_transport.dm b/code/modules/asset_cache/transports/asset_transport.dm index 6655fcbd79e..f5a1af4f057 100644 --- a/code/modules/asset_cache/transports/asset_transport.dm +++ b/code/modules/asset_cache/transports/asset_transport.dm @@ -5,7 +5,7 @@ /datum/asset_transport var/name = "Simple browse_rsc asset transport" var/static/list/preload - /// Don't mutate the filename of assets when sending via browse_rsc. + /// Don't mutate the filename of assets when sending via browse_rsc. /// This is to make it easier to debug issues with assets, and allow server operators to bypass issues that make it to production. /// If turning this on fixes asset issues, something isn't using get_asset_url and the asset isn't marked legacy, fix one of those. var/dont_mutate_filenames = FALSE @@ -16,7 +16,7 @@ for(var/client/C in GLOB.clients) addtimer(CALLBACK(src, .proc/send_assets_slow, C, preload), 1 SECONDS) -/// Initialize - Called when SSassets initializes. +/// Initialize - Called when SSassets initializes. /datum/asset_transport/proc/Initialize(list/assets) preload = assets.Copy() if (!CONFIG_GET(flag/asset_simple_preload)) @@ -60,7 +60,13 @@ /datum/asset_transport/proc/get_asset_url(asset_name, datum/asset_cache_item/asset_cache_item) if (!istype(asset_cache_item)) asset_cache_item = SSassets.cache[asset_name] - if (dont_mutate_filenames || asset_cache_item.legacy || (asset_cache_item.namespace && !asset_cache_item.namespace_parent)) // to ensure code that breaks on cdns breaks in local testing, we only use the normal filename on legacy assets and name space assets. + // To ensure code that breaks on cdns breaks in local testing, we only + // use the normal filename on legacy assets and name space assets. + var/keep_local_name = dont_mutate_filenames \ + || asset_cache_item.legacy \ + || asset_cache_item.keep_local_name \ + || (asset_cache_item.namespace && !asset_cache_item.namespace_parent) + if (keep_local_name) return url_encode(asset_cache_item.name) return url_encode("asset.[asset_cache_item.hash][asset_cache_item.ext]") @@ -84,7 +90,7 @@ var/list/unreceived = list() for (var/asset_name in asset_list) - var/datum/asset_cache_item/ACI = asset_list[asset_name] + var/datum/asset_cache_item/ACI = asset_list[asset_name] if (!istype(ACI) && !(ACI = SSassets.cache[asset_name])) log_asset("ERROR: can't send asset `[asset_name]`: unregistered or invalid state: `[ACI]`") continue @@ -92,10 +98,14 @@ if (!asset_file) log_asset("ERROR: can't send asset `[asset_name]`: invalid registered resource: `[ACI.resource]`") continue - + var/asset_hash = ACI.hash var/new_asset_name = asset_name - if (!dont_mutate_filenames && !ACI.legacy && (!ACI.namespace || ACI.namespace_parent)) + var/keep_local_name = dont_mutate_filenames \ + || ACI.legacy \ + || ACI.keep_local_name \ + || (ACI.namespace && !ACI.namespace_parent) + if (!keep_local_name) new_asset_name = "asset.[ACI.hash][ACI.ext]" if (client.sent_assets[new_asset_name] == asset_hash) if (GLOB.Debug2) @@ -110,11 +120,15 @@ for (var/asset_name in unreceived) var/new_asset_name = asset_name var/datum/asset_cache_item/ACI = unreceived[asset_name] - if (!dont_mutate_filenames && !ACI.legacy && (!ACI.namespace || ACI.namespace_parent)) + var/keep_local_name = dont_mutate_filenames \ + || ACI.legacy \ + || ACI.keep_local_name \ + || (ACI.namespace && !ACI.namespace_parent) + if (!keep_local_name) new_asset_name = "asset.[ACI.hash][ACI.ext]" log_asset("Sending asset `[asset_name]` to client `[client]` as `[new_asset_name]`") client << browse_rsc(ACI.resource, new_asset_name) - + client.sent_assets[new_asset_name] = ACI.hash addtimer(CALLBACK(client, /client/proc/asset_cache_update_json), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 5c4ead8e189..dad02f6c93b 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -101,9 +101,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/chat_output/chatOutput - ///lazy list of all credit object bound to this client var/list/credits diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 8b95620d9d8..f9dfd83cd55 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -89,20 +89,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( 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"]) - if(keycode) - keyDown(keycode) - return - if(href_list["__keyup"]) - var/keycode = browser_keycode_to_byond(href_list["__keyup"]) - if(keycode) - keyUp(keycode) - return - // Tgui Topic middleware - if(!tgui_Topic(href_list)) + if(tgui_Topic(href_list)) return // Admin PM @@ -124,8 +112,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( 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") @@ -207,7 +193,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( /client/New(TopicData) var/tdata = TopicData //save this for later use - chatOutput = new /datum/chat_output(src) TopicData = null //Prevent calls to client.Topic from connect if(connection != "seeker" && connection != "web")//Invalid connection type. @@ -216,6 +201,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.clients += src GLOB.directory[ckey] = src + // Instantiate tgui panel + tgui_panel = new(src) + GLOB.ahelp_tickets.ClientLogin(src) var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins. //Admin Authorisation @@ -320,7 +308,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( set_macros() update_movement_keys() - chatOutput.start() // Starts the chat + // Initialize tgui panel + tgui_panel.initialize() if(alert_mob_dupe_login) spawn() diff --git a/code/modules/client/darkmode.dm b/code/modules/client/darkmode.dm deleted file mode 100644 index a58850ed3c7..00000000000 --- a/code/modules/client/darkmode.dm +++ /dev/null @@ -1,117 +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. - //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 - //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/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm index d766adb394e..a6ea9cd00e9 100644 --- a/code/modules/client/preferences_toggles.dm +++ b/code/modules/client/preferences_toggles.dm @@ -171,8 +171,7 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/settings/sound, togglemidis)() to_chat(usr, "You will no longer hear sounds uploaded by admins") usr.stop_sound_channel(CHANNEL_ADMIN) var/client/C = usr.client - if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) - C.chatOutput.stopMusic() + C?.tgui_panel?.stop_music() SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Hearing Midis", "[usr.client.prefs.toggles & SOUND_MIDI ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /datum/verbs/menu/settings/sound/togglemidis/Get_checked(client/C) return C.prefs.toggles & SOUND_MIDI @@ -245,8 +244,7 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/settings/sound, toggle_announcement_sound)() set desc = "Stop Current Sounds" SEND_SOUND(usr, sound(null)) var/client/C = usr.client - if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) - C.chatOutput.stopMusic() + C?.tgui_panel?.stop_music() SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Stop Self Sounds")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index beeded6fe8c..291befebe0f 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -57,7 +57,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") var/keyname = key if(prefs.hearted) - var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/goonchat) + var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat) keyname = "[sheet.icon_tag("emoji-heart")][keyname]" if(prefs.unlock_content) if(prefs.toggles & MEMBER_PUBLIC) @@ -154,89 +154,6 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") else to_chat(src, "There are no admin notices at the moment.") -/client/verb/fix_chat() - set name = "Fix chat" - set category = "OOC" - 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/chat_output(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/verb/motd() set name = "MOTD" set category = "OOC" diff --git a/code/modules/emoji/emoji_parse.dm b/code/modules/emoji/emoji_parse.dm index 65e8063ce0c..185341d294c 100644 --- a/code/modules/emoji/emoji_parse.dm +++ b/code/modules/emoji/emoji_parse.dm @@ -15,7 +15,7 @@ search = findtext(text, ":", pos + length(text[pos])) if(search) emoji = lowertext(copytext(text, pos + length(text[pos]), search)) - var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/goonchat) + var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat) var/tag = sheet.icon_tag("emoji-[emoji]") if(tag) parsed += tag diff --git a/code/modules/goonchat/browserOutput.dm b/code/modules/goonchat/browserOutput.dm deleted file mode 100644 index c8e7a68946f..00000000000 --- a/code/modules/goonchat/browserOutput.dm +++ /dev/null @@ -1,331 +0,0 @@ -/********************************* -For the main html chat area -*********************************/ - -/// Should match the value set in the browser js -#define MAX_COOKIE_LENGTH 5 - -/** - * The chatOutput datum exists to handle the goonchat browser. - * On client, created on Client/New() - */ -/datum/chat_output - /// The client that owns us. - var/client/owner - /// How many times client data has been checked - var/total_checks = 0 - /// When to next clear the client data checks counter - var/next_time_to_clear = 0 - /// Has the client loaded the browser output area? - var/loaded = FALSE - /// If they haven't loaded chat, this is where messages will go until they do - var/list/messageQueue - 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 = 25 //This is for the Play Global Sound verb - -/datum/chat_output/New(client/C) - owner = C - messageQueue = list() - connectionHistory = list() - -/** - * start: Tries to load the chat browser - * Aborts if a problem is encountered. - * Async because this is called from Client/New. - */ -/datum/chat_output/proc/start() - set waitfor = FALSE - //Check for existing chat - if(!owner) - return FALSE - - if(!winexists(owner, "browseroutput")) // Oh goddamnit. - broken = TRUE - message_admins("Couldn't start chat for [key_name_admin(owner)]!") - . = FALSE - 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(winget(owner, "browseroutput", "is-visible") == "true") //Already setup - doneLoading() - - else //Not setup - load() - - return TRUE - -/// Loads goonchat and sends assets. -/datum/chat_output/proc/load() - set waitfor = FALSE - if(!owner) - 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=browseroutput") - -/// Interprets input from the client. Will send data back if required. -/datum/chat_output/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_char(key) > 7 && findtext(key, "param")) // 7 is the amount of characters in the basic param key template. - var/param_name = copytext_char(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/chat_output/proc/doneLoading() - if(loaded) - return - - testing("Chat loaded for [owner.ckey]") - 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() - - //do not convert to to_chat() - SEND_TEXT(owner, "Failed to load fancy chat, reverting to old chat. Certain features won't work.") - -/// Hides the standard output and makes the browser visible. -/datum/chat_output/proc/showChat() - winset(owner, "output", "is-visible=false") - winset(owner, "browseroutput", "is-disabled=false;is-visible=true") - -/// Calls syncRegex on all currently owned chatOutput datums -/proc/syncChatRegexes() - for (var/user in GLOB.clients) - var/client/C = user - var/datum/chat_output/Cchat = C.chatOutput - if (Cchat && !Cchat.broken && Cchat.loaded) - Cchat.syncRegex() - -/// Used to dynamically add regexes to the browser output. Currently only used by the IC filter. -/datum/chat_output/proc/syncRegex() - var/list/regexes = list() - - if (config.ic_filter_regex) - regexes["show_filtered_ic_chat"] = list( - config.ic_filter_regex.name, - "ig", - "$1" - ) - - if (regexes.len) - ehjax_send(data = list("syncRegex" = regexes)) - -/// Sends json encoded data to the browser. -/datum/chat_output/proc/ehjax_send(client/C = owner, window = "browseroutput", data) - if(islist(data)) - data = json_encode(data) - C << output("[data]", "[window]:ehjaxCallback") - -/** - * Sends music data to the browser. If enabled by the browser, it will start playing. - * Arguments: - * music must be a https adress. - * extra_data is a list. The keys "pitch", "start" and "end" are used. - ** "pitch" determines the playback rate - ** "start" determines the start time of the sound - ** "end" determines when the musics stops playing - */ -/datum/chat_output/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) - -/// Stops music playing throw the browser. -/datum/chat_output/proc/stopMusic() - ehjax_send(data = "stopMusic") - -/// Setter for adminMusicVolume. Sanitizes the value to between 0 and 100. -/datum/chat_output/proc/setMusicVolume(volume = "") - if(volume) - adminMusicVolume = clamp(text2num(volume), 0, 100) - -/// Sends client connection details to the chat to handle and save -/datum/chat_output/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/chat_output/proc/analyzeClientData(cookie = "") - //Spam check - if(world.time > next_time_to_clear) - next_time_to_clear = world.time + (3 SECONDS) - total_checks = 0 - - total_checks += 1 - - if(total_checks > SPAM_TRIGGER_AUTOMUTE) - message_admins("[key_name(owner)] kicked for goonchat topic spam") - qdel(owner) - return - - if(!cookie) - return - - if(cookie != "none") - 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) - if(QDELETED(owner)) - //he got cleaned up before we were done - return - 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 - CHECK_TICK - - //Uh oh this fucker has a history of playing on a banned account!! - if (found.len > 0) - 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/chat_output/proc/ping() - return "pong" - -/// Called by js client on js error -/datum/chat_output/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 proc. to_chat_immediate will circumvent SSchat and send data as soon as possible. -/proc/to_chat_immediate(target, message, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE) - 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(trailing_newline) - message += "
" - - 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") - -/// Sends a text message to the target. -/proc/to_chat(target, message, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE) - if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) - to_chat_immediate(target, message, handle_whitespace, trailing_newline, confidential) - return - SSchat.queue(target, message, handle_whitespace, trailing_newline, confidential) - -/// Dark mode light mode stuff. Yell at KMC if this breaks! (See darkmode.dm for documentation) -/datum/chat_output/proc/swaptolightmode() - owner.force_white_theme() - -/// Light mode stuff. (See darkmode.dm for documentation) -/datum/chat_output/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 135ef6ea1e5..00000000000 --- a/code/modules/goonchat/browserassets/css/browserOutput.css +++ /dev/null @@ -1,417 +0,0 @@ -/***************************************** -* -* GLOBAL STYLES -* -******************************************/ -html, body { - padding: 0; - margin: 0; - height: 100%; - color: #a4bad6; -} -body { - background: #171717; - font-family: Verdana, sans-serif; - font-size: 13px; - font-color: #a4bad6; - 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; -} - -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; - 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;} - -/***************************************** -* -* 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;} - -#messages { - font-size: 13px; - padding: 3px; - margin: 0; - word-wrap: break-word; -} -#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: #a4bad6; -} -#newMessages:hover {background: #171717;} -#newMessages i {vertical-align: middle; padding-left: 3px;} -#ping { - position: fixed; - top: 0; - right: 135px; - width: 45px; - background: #202020; - height: 30px; - 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; -} -#userBar .subCell { - background: #202020; - height: 30px; - padding: 5px 0; - display: block; - color: #a4bad6; - 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: 180px;} -#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;} - -/* 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 CONTEXT MENU */ -.contextMenu { - background-color: #ddd; - position: fixed; - margin: 2px; - width: 150px; -} -.contextMenu a { - display: block; - padding: 2px 5px; - text-decoration: none; - color: #333; -} - -.contextMenu a:hover { - background-color: #ccc; -} - -/* 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 -* -******************************************/ - -/* MOTD */ -.motd {color: #a4bad6; font-family: Verdana, sans-serif;} -.motd h1, .motd h2, .motd h3, .motd h4, .motd h5, .motd h6 {color: #a4bad6; text-decoration: underline;} -.motd a, .motd a:link, .motd a:visited, .motd a:active, .motd a:hover {color: #a4bad6;} - -/* ADD HERE FOR BOLD */ -.bold, .name, .prefix, .ooc, .looc, .adminooc, .admin, .medal, .yell {font-weight: bold;} - -/* ADD HERE FOR ITALIC */ -.italic, .italics, .emote {font-style: italic;} - -/* OUTPUT COLORS */ -.highlight {background: yellow;} - -h1, h2, h3, h4, h5, h6 {color: #a4bad6;font-family: Georgia, Verdana, sans-serif;} -h1.alert, h2.alert {color: #a4bad6;} - -em {font-style: normal; font-weight: bold;} - -.ooc {color: #cca300; font-weight: bold;} -.adminobserverooc {color: #0099cc; font-weight: bold;} -.adminooc {color: #3d5bc3; font-weight: bold;} - -.adminsay {color: #ff4500; font-weight: bold;} -.admin {color: #5975da; font-weight: bold;} - -.name { font-weight: bold;} - -.say {} -.deadsay {color: #e2c1ff} -.binarysay {color: #1e90ff;} -.binarysay a {color: #00ff00;} -.binarysay a:active, .binarysay a:visited {color: #88ff88;} -.radio {color: #1ecc43;} -.sciradio {color: #c68cfa;} -.comradio {color: #fcdf03;} -.secradio {color: #dd3535;} -.medradio {color: #57b8f0;} -.engradio {color: #f37746;} -.suppradio {color: #b88646;} -.servradio {color: #6ca729;} -.syndradio {color: #8f4a4b;} -.centcomradio {color: #2681a5;} -.aiprivradio {color: #d65d95;} -.redteamradio {color: #ff4444;} -.blueteamradio {color: #3434fd;} - -.yell { font-weight: bold;} - -.alert {color: #d82020;} - -.emote { font-style: italic;} - -.userdanger {color: #c51e1e; font-weight: bold; font-size: 185%;} -.bolddanger {color: #c51e1e;font-weight: bold;} -.danger {color: #c51e1e;} -.tinydanger {color: #c51e1e; font-size: 85%;} -.smalldanger {color: #c51e1e; font-size: 90%;} -.warning {color: #c51e1e; font-style: italic;} -.alertwarning {color: #FF0000; font-weight: bold} -.boldwarning {color: #c51e1e; font-style: italic; font-weight: bold} -.announce {color: #c51e1e; font-weight: bold;} -.boldannounce {color: #c51e1e; font-weight: bold;} -.greenannounce {color: #059223; font-weight: bold;} -.rose {color: #ff5050;} -.info {color: #9ab0ff;} -.notice {color: #6685f5;} -.tinynotice {color: #6685f5; font-style: italic; font-size: 85%;} -.smallnotice {color: #6685f5; font-size: 90%;} -.smallnoticeital {color: #6685f5; font-style: italic; font-size: 90%;} -.boldnotice {color: #6685f5; font-weight: bold;} -.hear {color: #6685f5; font-style: italic;} -.adminnotice {color: #6685f5;} -.adminhelp {color: #ff0000; font-weight: bold;} -.unconscious {color: #a4bad6; font-weight: bold;} -.suicide {color: #ff5050; font-style: italic;} -.green {color: #059223;} -.red {color: #FF0000} -.blue {color: #215cff} -.nicegreen {color: #059223;} - -.cult {color: #aa1c1c;} -.cultitalic {color: #aa1c1c; font-style: italic;} -.cultbold {color: #aa1c1c; font-style: italic; font-weight: bold;} -.cultboldtalic {color: #aa1c1c; font-weight: bold; font-size: 185%;} - -.cultlarge {color: #aa1c1c; font-weight: bold; font-size: 185%;} -.narsie {color: #aa1c1c; font-weight: bold; font-size: 925%;} -.narsiesmall {color: #aa1c1c; font-weight: bold; font-size: 370%;} -.colossus {color: #7F282A; font-size: 310%;} -.hierophant {color: #b441ee; font-weight: bold; font-style: italic;} -.hierophant_warning {color: #c56bf1; font-style: italic;} -.purple {color: #9956d3;} -.holoparasite {color: #88809c;} - -.revennotice {color: #c099e2;} -.revenboldnotice {color: #c099e2; font-weight: bold;} -.revenbignotice {color: #c099e2; font-weight: bold; font-size: 185%;} -.revenminor {color: #823abb} -.revenwarning {color: #760fbb; font-style: italic;} -.revendanger {color: #760fbb; font-weight: bold; font-size: 185%;} - -.deconversion_message {color: #a947ff; font-size: 185%; font-style: italic;} - -.ghostalert {color: #6600ff; font-style: italic; font-weight: bold;} - -.alien {color: #855d85;} -.noticealien {color: #059223;} -.alertalien {color: #059223; font-weight: bold;} -.changeling {color: #059223; font-style: italic;} -.alertsyndie {color: #FF0000; font-size: 185%; font-weight: bold;} - -.spider {color: #8800ff; font-weight: bold; font-size: 185%;} - -.interface {color: #750e75;} - -.sans {font-family: "Comic Sans MS", cursive, sans-serif;} -.papyrus {font-family: "Papyrus", cursive, sans-serif;} -.robot {font-family: "Courier New", cursive, sans-serif;} - -.command_headset {font-weight: bold; font-size: 160%;} -.small {font-size: 60%;} -.big {font-size: 185%;} -.reallybig {font-size: 245%;} -.extremelybig {font-size: 310%;} -.greentext {color: #059223; font-size: 185%;} -.redtext {color: #c51e1e; font-size: 185%;} -.clown {color: #ff70c1; font-size: 160%; font-family: "Comic Sans MS", cursive, sans-serif; font-weight: bold;} -.singing {font-family: "Trebuchet MS", cursive, sans-serif; font-style: italic;} -.his_grace {color: #15D512; font-family: "Courier New", cursive, sans-serif; font-style: italic;} -.hypnophrase {color: #202020; font-weight: bold; animation: hypnocolor 1500ms infinite; animation-direction: alternate;} -@keyframes hypnocolor { - 0% {color: #202020;} - 25% {color: #4b02ac;} - 50% {color: #9f41f1;} - 75% {color: #541c9c;} - 100% {color: #7adbf3;} -} - -.phobia {color: #dd0000; font-weight: bold; animation: phobia 750ms infinite;} -@keyframes phobia { - 0% {color: #f75a5a;} - 50% {color: #dd0000;} - 100% {color: #f75a5a;} -} - -.icon {height: 1em; width: auto;} - -.memo {color: #638500; text-align: center;} -.memoedit {text-align: center; font-size: 125%;} -.abductor {color: #c204c2; font-style: italic;} -.mind_control {color: #df3da9; font-size: 100%; font-weight: bold; font-style: italic;} -.slime {color: #00CED1;} -.drone {color: #848482;} -.monkey {color: #975032;} -.swarmer {color: #2C75FF;} -.resonate {color: #298F85;} - -.monkeyhive {color: #a56408;} -.monkeylead {color: #af6805; font-size: 80%;} - -.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;} -.ml-1 {margin-left: 1em;} -.ml-2 {margin-left: 2em;} -.ml-3 {margin-left: 3em;} diff --git a/code/modules/goonchat/browserassets/css/browserOutput_white.css b/code/modules/goonchat/browserassets/css/browserOutput_white.css deleted file mode 100644 index 1c9d0cdfb29..00000000000 --- a/code/modules/goonchat/browserassets/css/browserOutput_white.css +++ /dev/null @@ -1,415 +0,0 @@ -/***************************************** -* -* GLOBAL STYLES for white theme normies -* -******************************************/ -html, body { - padding: 0; - margin: 0; - height: 100%; - color: #000000; -} -body { - background: #fff; - font-family: Verdana, sans-serif; - font-size: 13px; - line-height: 1.2; - overflow-x: hidden; - overflow-y: scroll; - word-wrap: break-word; -} - -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; - color: white; - text-align: center; - white-space: nowrap; - vertical-align: middle; - background-color: crimson; - border-radius: 10px; -} - -a {color: #0000ff;} -a.visited {color: #ff00ff;} -a:visited {color: #ff00ff;} -a.popt {text-decoration: none;} - -/***************************************** -* -* 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;} - -#messages { - font-size: 13px; - padding: 3px; - margin: 0; - word-wrap: break-word; -} -#newMessages { - position: fixed; - display: block; - bottom: 0; - right: 0; - padding: 8px; - background: #ddd; - text-decoration: none; - font-variant: small-caps; - font-size: 1.1em; - font-weight: bold; - color: #333; -} -#newMessages:hover {background: #ccc;} -#newMessages i {vertical-align: middle; padding-left: 3px;} -#ping { - position: fixed; - top: 0; - right: 135px; - width: 45px; - background: #ddd; - height: 30px; - 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; -} -#userBar .subCell { - background: #ddd; - height: 30px; - padding: 5px 0; - display: block; - color: #333; - text-decoration: none; - line-height: 28px; - border-top: 1px solid #b4b4b4; -} -#userBar .subCell:hover {background: #ccc;} -#userBar .toggle { - width: 45px; - background: #ccc; - border-top: 0; - float: right; - text-align: center; -} -#userBar .sub {clear: both; display: none; width: 180px;} -#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;} - -/* 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 CONTEXT MENU */ -.contextMenu { - background-color: #ddd; - position: fixed; - margin: 2px; - width: 150px; -} -.contextMenu a { - display: block; - padding: 2px 5px; - text-decoration: none; - color: #333; -} - -.contextMenu a:hover { - background-color: #ccc; -} - -/* 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 -* -******************************************/ - -/* MOTD */ -.motd {color: #638500; font-family: Verdana, sans-serif;} -.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;} - -/* ADD HERE FOR BOLD */ -.bold, .name, .prefix, .ooc, .looc, .adminooc, .admin, .medal, .yell {font-weight: bold;} - -/* ADD HERE FOR ITALIC */ -.italic, .italics, .emote {font-style: italic;} - -/* OUTPUT COLORS */ -.highlight {background: yellow;} - -h1, h2, h3, h4, h5, h6 {color: #0000ff;font-family: Georgia, Verdana, sans-serif;} -h1.alert, h2.alert {color: #000000;} - -em {font-style: normal; font-weight: bold;} - -.ooc {color: #002eb8; font-weight: bold;} -.adminobserverooc {color: #0099cc; font-weight: bold;} -.adminooc {color: #700038; font-weight: bold;} - -.adminsay {color: #ff4500; font-weight: bold;} -.admin {color: #4473ff; font-weight: bold;} - -.name { font-weight: bold;} - -.say {} -.deadsay {color: #5c00e6;} -.binarysay {color: #20c20e; background-color: #000000; display: block;} -.binarysay a {color: #00ff00;} -.binarysay a:active, .binarysay a:visited {color: #88ff88;} -.radio {color: #008000;} -.sciradio {color: #993399;} -.comradio {color: #948f02;} -.secradio {color: #a30000;} -.medradio {color: #337296;} -.engradio {color: #fb5613;} -.suppradio {color: #a8732b;} -.servradio {color: #6eaa2c;} -.syndradio {color: #6d3f40;} -.centcomradio {color: #686868;} -.aiprivradio {color: #ff00ff;} -.redteamradio {color: #ff0000;} -.blueteamradio {color: #0000ff;} - -.yell { font-weight: bold;} - -.alert {color: #ff0000;} -h1.alert, h2.alert {color: #000000;} - -.emote { font-style: italic;} - -.userdanger {color: #ff0000; font-weight: bold; font-size: 185%;} -.bolddanger {color: #ff0000; font-weight: bold;} -.danger {color: #ff0000;} -.tinydanger {color: #ff0000; font-size: 85%;} -.smalldanger {color: #ff0000; font-size: 90%;} -.warning {color: #ff0000; font-style: italic;} -.alertwarning {color: #FF0000; font-weight: bold} -.boldwarning {color: #ff0000; font-style: italic; font-weight: bold} -.announce {color: #228b22; font-weight: bold;} -.boldannounce {color: #ff0000; font-weight: bold;} -.greenannounce {color: #00ff00; font-weight: bold;} -.rose {color: #ff5050;} -.info {color: #0000CC;} -.notice {color: #000099;} -.tinynotice {color: #000099; font-style: italic; font-size: 85%;} -.smallnotice {color: #000099; font-size: 90%;} -.smallnoticeital {color: #000099; font-style: italic; font-size: 90%;} -.boldnotice {color: #000099; font-weight: bold;} -.hear {color: #000099; font-style: italic;} -.adminnotice {color: #0000ff;} -.adminhelp {color: #ff0000; font-weight: bold;} -.unconscious {color: #0000ff; font-weight: bold;} -.suicide {color: #ff5050; font-style: italic;} -.green {color: #03ff39;} -.red {color: #FF0000} -.blue {color: #0000FF} -.nicegreen {color: #14a833;} - -.cult {color: #960000;} -.cultitalic {color: #960000; font-style: italic;} -.cultbold {color: #960000; font-style: italic; font-weight: bold;} -.cultboldtalic {color: #960000; font-weight: bold; font-size: 185%;} - -.cultlarge {color: #960000; font-weight: bold; font-size: 185%;} -.narsie {color: #960000; font-weight: bold; font-size: 925%;} -.narsiesmall {color: #960000; font-weight: bold; font-size: 370%;} -.colossus {color: #7F282A; font-size: 310%;} -.hierophant {color: #660099; font-weight: bold; font-style: italic;} -.hierophant_warning {color: #660099; font-style: italic;} -.purple {color: #5e2d79;} -.holoparasite {color: #35333a;} - -.revennotice {color: #1d2953;} -.revenboldnotice {color: #1d2953; font-weight: bold;} -.revenbignotice {color: #1d2953; font-weight: bold; font-size: 185%;} -.revenminor {color: #823abb} -.revenwarning {color: #760fbb; font-style: italic;} -.revendanger {color: #760fbb; font-weight: bold; font-size: 185%;} - -.deconversion_message {color: #5000A0; font-size: 185%; font-style: italic;} - -.ghostalert {color: #5c00e6; font-style: italic; font-weight: bold;} - -.alien {color: #543354;} -.noticealien {color: #00c000;} -.alertalien {color: #00c000; font-weight: bold;} -.changeling {color: #800080; font-style: italic;} -.alertsyndie {color: #FF0000; font-size: 185%; font-weight: bold;} - -.spider {color: #4d004d; font-weight: bold; font-size: 185%;} - -.interface {color: #330033;} - -.sans {font-family: "Comic Sans MS", cursive, sans-serif;} -.papyrus {font-family: "Papyrus", cursive, sans-serif;} -.robot {font-family: "Courier New", cursive, sans-serif;} - -.command_headset {font-weight: bold; font-size: 160%;} -.small {font-size: 60%;} -.big {font-size: 185%;} -.reallybig {font-size: 245%;} -.extremelybig {font-size: 310%;} -.greentext {color: #00FF00; font-size: 185%;} -.redtext {color: #FF0000; font-size: 185%;} -.clown {color: #FF69Bf; font-size: 160%; font-family: "Comic Sans MS", cursive, sans-serif; font-weight: bold;} -.singing {font-family: "Trebuchet MS", cursive, sans-serif; font-style: italic;} -.his_grace {color: #15D512; font-family: "Courier New", cursive, sans-serif; font-style: italic;} -.hypnophrase {color: #0d0d0d; font-weight: bold; animation: hypnocolor 1500ms infinite; animation-direction: alternate;} -@keyframes hypnocolor { - 0% {color: #0d0d0d;} - 25% {color: #410194;} - 50% {color: #7f17d8;} - 75% {color: #410194;} - 100% {color: #3bb5d3;} -} - -.phobia {color: #dd0000; font-weight: bold; animation: phobia 750ms infinite;} -@keyframes phobia { - 0% {color: #0d0d0d;} - 50% {color: #dd0000;} - 100% {color: #0d0d0d;} -} - -.icon {height: 1em; width: auto;} - -.memo {color: #638500; text-align: center;} -.memoedit {text-align: center; font-size: 125%;} -.abductor {color: #800080; font-style: italic;} -.mind_control {color: #A00D6F; font-size: 100%; font-weight: bold; font-style: italic;} -.slime {color: #00CED1;} -.drone {color: #848482;} -.monkey {color: #975032;} -.swarmer {color: #2C75FF;} -.resonate {color: #298F85;} - -.monkeyhive {color: #774704;} -.monkeylead {color: #774704; font-size: 80%;} - -.connectionClosed, .fatalError {background: red; color: white; padding: 5px;} -.connectionClosed.restored {background: green;} -.internal.boldnshit {color: blue; font-weight: bold;} - -/* HELPER CLASSES */ -.text-normal {font-weight: normal; font-style: normal;} -.hidden {display: none; visibility: hidden;} -.ml-1 {margin-left: 1em;} -.ml-2 {margin-left: 2em;} -.ml-3 {margin-left: 3em;} diff --git a/code/modules/goonchat/browserassets/html/browserOutput.html b/code/modules/goonchat/browserassets/html/browserOutput.html deleted file mode 100644 index ff7bff68150..00000000000 --- a/code/modules/goonchat/browserassets/html/browserOutput.html +++ /dev/null @@ -1,57 +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. -
-
-
- -
- - - - - diff --git a/code/modules/goonchat/browserassets/js/browserOutput.js b/code/modules/goonchat/browserassets/js/browserOutput.js deleted file mode 100644 index 0899b3a940e..00000000000 --- a/code/modules/goonchat/browserassets/js/browserOutput.js +++ /dev/null @@ -1,1074 +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 -window.status = 'Output'; -var $messages, $subOptions, $subAudio, $selectedSub, $contextMenu, $filterMessages, $last_message; -var opts = { - //General - 'messageCount': 0, //A count...of messages... - 'messageLimit': 2053, //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': 5, - 'highlightColor': '#FFFF00', //The color of the highlighted message - 'pingDisabled': false, //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': true, - -}; -var replaceRegexes = {}; - -function clamp(val, min, max) { - return Math.max(min, Math.min(val, max)) -} - -//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'); -} - -// Get a highlight markup span -function createHighlightMarkup() { - var extra = ''; - if (opts.highlightColor) { - extra += ' style="background-color: ' + opts.highlightColor + '"'; - } - return ''; -} - -// Get all child text nodes that match a regex pattern -function getTextNodes(elem, pattern) { - var result = $([]); - $(elem).contents().each(function(idx, child) { - if (child.nodeType === 3 && /\S/.test(child.nodeValue) && pattern.test(child.nodeValue)) { - result = result.add(child); - } - else { - result = result.add(getTextNodes(child, pattern)); - } - }); - return result; -} - -// Highlight all text terms matching the registered regex patterns -function highlightTerms(el) { - var pattern = new RegExp("(" + opts.highlightTerms.join('|') + ")", 'gi'); - var nodes = getTextNodes(el, pattern); - - nodes.each(function (idx, node) { - var content = $(node).text(); - var parent = $(node).parent(); - var pre = $(node.previousSibling); - $(node).remove(); - content.split(pattern).forEach(function (chunk) { - // Get our highlighted span/text node - var toInsert = null; - if (pattern.test(chunk)) { - var tmpElem = $(createHighlightMarkup()); - tmpElem.text(chunk); - toInsert = tmpElem; - } - else { - toInsert = document.createTextNode(chunk); - } - - // Insert back into our element - if (pre.length == 0) { - var result = parent.prepend(toInsert); - pre = $(result[0].firstChild); - } - else { - pre.after(toInsert); - pre = $(pre[0].nextSibling); - } - }); - }); -} - -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(); - - message = byondDecode(message).trim(); - - //The behemoth of filter-code (for Admin message filters) - //Note: This is proooobably hella inefficient - var filteredOut = false; - if (opts.hasOwnProperty('showMessagesFilters') && !opts.showMessagesFilters['All'].show) { - //Get this filter type (defined by class on message) - var messageHtml = $.parseHTML(message), - messageClasses; - if (opts.hasOwnProperty('filterHideAll') && opts.filterHideAll) { - var internal = false; - messageClasses = (!!$(messageHtml).attr('class') ? $(messageHtml).attr('class').split(/\s+/) : false); - if (messageClasses) { - for (var i = 0; i < messageClasses.length; i++) { //Every class - if (messageClasses[i] == 'internal') { - internal = true; - break; - } - } - } - if (!internal) { - filteredOut = 'All'; - } - } else { - //If the element or it's child have any classes - if (!!$(messageHtml).attr('class') || !!$(messageHtml).children().attr('class')) { - messageClasses = $(messageHtml).attr('class').split(/\s+/); - if (!!$(messageHtml).children().attr('class')) { - messageClasses = messageClasses.concat($(messageHtml).children().attr('class').split(/\s+/)); - } - var tempCount = 0; - for (var i = 0; i < messageClasses.length; i++) { //Every class - var thisClass = messageClasses[i]; - $.each(opts.showMessagesFilters, function(key, val) { //Every filter - if (key !== 'All' && val.show === false && typeof val.match != 'undefined') { - for (var i = 0; i < val.match.length; i++) { - var matchClass = val.match[i]; - if (matchClass == thisClass) { - filteredOut = key; - break; - } - } - } - if (filteredOut) return false; - }); - if (filteredOut) break; - tempCount++; - } - } else { - if (!opts.showMessagesFilters['Misc'].show) { - filteredOut = 'Misc'; - } - } - } - } - - //Stuff we do along with appending a message - var atBottom = false; - if (!filteredOut) { - var bodyHeight = $('body').height(); - var messagesHeight = $messages.outerHeight(); - var scrollPos = $('body,html').scrollTop(); - - //Should we snap the output to the bottom? - if (bodyHeight + scrollPos >= messagesHeight - opts.scrollSnapTolerance) { - atBottom = true; - if ($('#newMessages').length) { - $('#newMessages').remove(); - } - //If not, put the new messages box in - } else { - if ($('#newMessages').length) { - var messages = $('#newMessages .number').text(); - messages = parseInt(messages); - messages++; - $('#newMessages .number').text(messages); - if (messages == 2) { - $('#newMessages .messageWord').append('s'); - } - } else { - $messages.after('1 new message '); - } - } - } - - opts.messageCount++; - - //Pop the top message off if history limit reached - if (opts.messageCount >= opts.messageLimit) { - $messages.children('div.entry:first-child').remove(); - opts.messageCount--; //I guess the count should only ever equal the limit - } - - // Create the element - if combining is off, we use it, and if it's on, we - // might discard it bug need to check its text content. Some messages vary - // only in HTML markup, have the same text content, and should combine. - var entry = document.createElement('div'); - entry.innerHTML = message; - var trimmed_message = entry.textContent || entry.innerText || ""; - - var handled = false; - if (opts.messageCombining) { - var lastmessages = $messages.children('div.entry:last-child').last(); - if (lastmessages.length && $last_message && $last_message == trimmed_message) { - var badge = lastmessages.children('.r').last(); - if (badge.length) { - badge = badge.detach(); - badge.text(parseInt(badge.text()) + 1); - } else { - badge = $('', {'class': 'r', 'text': 2}); - } - lastmessages.html(message); - lastmessages.find('[replaceRegex]').each(replaceRegex); - lastmessages.append(badge); - badge.animate({ - "font-size": "0.9em" - }, 100, function() { - badge.animate({ - "font-size": "0.7em" - }, 100); - }); - opts.messageCount--; - handled = true; - } - } - - if (!handled) { - //Actually append the message - entry.className = 'entry'; - - if (filteredOut) { - entry.className += ' hidden'; - entry.setAttribute('data-filter', filteredOut); - } - - $(entry).find('[replaceRegex]').each(replaceRegex); - - $last_message = trimmed_message; - $messages[0].appendChild(entry); - $(entry).find("img.icon").error(iconError); - - var to_linkify = $(entry).find(".linkify"); - if (typeof Node === 'undefined') { - // Linkify fallback for old IE - for(var i = 0; i < to_linkify.length; ++i) { - to_linkify[i].innerHTML = linkify_fallback(to_linkify[i].innerHTML); - } - } else { - // Linkify for modern IE versions - for(var i = 0; i < to_linkify.length; ++i) { - linkify_node(to_linkify[i]); - } - } - - //Actually do the snap - //Stuff we can do after the message shows can go here, in the interests of responsiveness - if (opts.highlightTerms && opts.highlightTerms.length > 0) { - highlightTerms($(entry)); - } - } - - if (!filteredOut && atBottom) { - $('body,html').scrollTop($messages.outerHeight()); - } -} - -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_white.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 - } - } - //Lets make sure we obey our limit (can connect from server with higher limit) - while (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'), - lineheight: getCookie('lineheight'), - 'spingDisabled': getCookie('pingdisabled'), - 'shighlightTerms': getCookie('highlightterms'), - 'shighlightColor': getCookie('highlightcolor'), - 'smusicVolume': getCookie('musicVolume'), - 'smessagecombining': getCookie('messagecombining'), - 'sdarkmode': getCookie('darkmode'), - }; - - if (savedConfig.fontsize) { - $messages.css('font-size', savedConfig.fontsize); - internalOutput('Loaded font size setting of: '+savedConfig.fontsize+'', 'internal'); - } - if (savedConfig.lineheight) { - $("body").css('line-height', savedConfig.lineheight); - internalOutput('Loaded line height setting of: '+savedConfig.lineheight+'', '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.shighlightTerms) { - var savedTerms = $.parseJSON(savedConfig.shighlightTerms).filter(function (entry) { - return entry !== null && /\S/.test(entry); - }); - var actualTerms = savedTerms.length != 0 ? savedTerms.join(', ') : null; - if (actualTerms) { - internalOutput('Loaded highlight strings of: ' + actualTerms+'', 'internal'); - opts.highlightTerms = savedTerms; - } - } - if (savedConfig.shighlightColor) { - opts.highlightColor = savedConfig.shighlightColor; - internalOutput('Loaded highlight color of: '+savedConfig.shighlightColor+'', '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 && opts.hasOwnProperty('contextMenuTarget') && opts.contextMenuTarget) { - hideContextMenu(); - 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); - } - runByond('byond://winset?mapwindow.map.focus=true'); - }); - - $('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; - } - runByond('byond://winset?mapwindow.map.focus=true'); - }); - - //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'); - }); - - $('#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'); - }); - - $('#decreaseLineHeight').click(function(e) { - savedConfig.lineheight = Math.max(parseFloat(savedConfig.lineheight || 1.2) - 0.1, 0.1).toFixed(1); - $("body").css({'line-height': savedConfig.lineheight}); - setCookie('lineheight', savedConfig.lineheight, 365); - internalOutput('Line height set to '+savedConfig.lineheight+'', 'internal'); - }); - - $('#increaseLineHeight').click(function(e) { - savedConfig.lineheight = (parseFloat(savedConfig.lineheight || 1.2) + 0.1).toFixed(1); - $("body").css({'line-height': savedConfig.lineheight}); - setCookie('lineheight', savedConfig.lineheight, 365); - internalOutput('Line height set to '+savedConfig.lineheight+'', '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) { - // Requires IE 10+ to issue download commands. Just opening a popup - // window will cause Ctrl+S to save a blank page, ignoring innerHTML. - if (!window.Blob) { - output('This function is only supported on IE 10 and up. Upgrade if possible.', 'internal'); - return; - } - - $.ajax({ - type: 'GET', - url: 'browserOutput_white.css', - success: function(styleData) { - var blob = new Blob(['Chat Log', $messages.html(), '']); - - var fname = 'SS13 Chat Log'; - var date = new Date(), month = date.getMonth(), day = date.getDay(), hours = date.getHours(), mins = date.getMinutes(), secs = date.getSeconds(); - fname += ' ' + date.getFullYear() + '-' + (month < 10 ? '0' : '') + month + '-' + (day < 10 ? '0' : '') + day; - fname += ' ' + (hours < 10 ? '0' : '') + hours + (mins < 10 ? '0' : '') + mins + (secs < 10 ? '0' : '') + secs; - fname += '.html'; - - window.navigator.msSaveBlob(blob, fname); - } - }); - }); - - $('#highlightTerm').click(function(e) { - if ($('.popup .highlightTerm').is(':visible')) {return;} - var termInputs = ''; - for (var i = 0; i < opts.highlightLimit; i++) { - termInputs += '
'; - } - var popupContent = '
String Highlighting
' + - '
' + - '
Choose up to '+opts.highlightLimit+' strings that will highlight the line when they appear in chat.
' + - '
' + - termInputs + - '
' + - '
' + - '' + - '
'; - createPopup(popupContent, 250); - }); - - $('body').on('keyup', '#highlightColor', function() { - var color = $('#highlightColor').val(); - color = color.trim(); - if (!color || color.charAt(0) != '#') return; - $('#highlightColor').css('background-color', color); - }); - - $('body').on('submit', '#highlightTermForm', function(e) { - e.preventDefault(); - - opts.highlightTerms = []; - for (var count = 0; count < opts.highlightLimit; count++) { - var term = $('#highlightTermInput'+count).val(); - if (term !== null && /\S/.test(term)) { - opts.highlightTerms.push(term.trim().toLowerCase()); - } - } - - var color = $('#highlightColor').val(); - color = color.trim(); - if (color == '' || color.charAt(0) != '#') { - opts.highlightColor = '#FFFF00'; - } else { - opts.highlightColor = color; - } - var $popup = $('#highlightPopup').closest('.popup'); - $popup.remove(); - - setCookie('highlightterms', JSON.stringify(opts.highlightTerms), 365); - setCookie('highlightcolor', opts.highlightColor, 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/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/language/language.dm b/code/modules/language/language.dm index 61b8bc6b706..2d87ee141cc 100644 --- a/code/modules/language/language.dm +++ b/code/modules/language/language.dm @@ -30,7 +30,7 @@ return TRUE /datum/language/proc/get_icon() - var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/goonchat) + var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat) return sheet.icon_tag("language-[icon_state]") /datum/language/proc/get_random_name(gender, name_count=2, syllable_count=4, syllable_divisor=2) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index b6b8e79bebd..e051f6ee52a 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -698,6 +698,7 @@ /mob/living/carbon/human/check_self_for_injuries() if(stat == DEAD || stat == UNCONSCIOUS) return + var/list/combined_msg = list() visible_message("[src] examines [p_them()]self.", \ "You check yourself for injuries.") @@ -756,7 +757,7 @@ isdisabled += " but otherwise " else isdisabled += " and " - to_chat(src, "\t Your [LB.name][isdisabled][self_aware ? " has " : " is "][status].") + combined_msg += "\t Your [LB.name][isdisabled][self_aware ? " has " : " is "][status]." for(var/thing in LB.wounds) var/datum/wound/W = thing @@ -770,16 +771,16 @@ msg = "\t Your [LB.name] is suffering [W.a_or_from] [lowertext(W.name)]!" if(WOUND_SEVERITY_CRITICAL) msg = "\t Your [LB.name] is suffering [W.a_or_from] [lowertext(W.name)]!!" - to_chat(src, msg) + combined_msg += msg for(var/obj/item/I in LB.embedded_objects) if(I.isEmbedHarmless()) - to_chat(src, "\t There is \a [I] stuck to your [LB.name]!") + combined_msg += "\t There is \a [I] stuck to your [LB.name]!" else - to_chat(src, "\t There is \a [I] embedded in your [LB.name]!") + combined_msg += "\t There is \a [I] embedded in your [LB.name]!" for(var/t in missing) - to_chat(src, "Your [parse_zone(t)] is missing!") + combined_msg += "Your [parse_zone(t)] is missing!" if(is_bleeding()) var/list/obj/item/bodypart/bleeding_limbs = list() @@ -799,43 +800,43 @@ bleed_text += " [BP.name]," bleed_text += " and [bleeding_limbs[num_bleeds].name]" bleed_text += "!
" - to_chat(src, bleed_text) + combined_msg += bleed_text if(getStaminaLoss()) if(getStaminaLoss() > 30) - to_chat(src, "You're completely exhausted.") + combined_msg += "You're completely exhausted." else - to_chat(src, "You feel fatigued.") + combined_msg += "You feel fatigued." if(HAS_TRAIT(src, TRAIT_SELF_AWARE)) if(toxloss) if(toxloss > 10) - to_chat(src, "You feel sick.") + combined_msg += "You feel sick." else if(toxloss > 20) - to_chat(src, "You feel nauseated.") + combined_msg += "You feel nauseated." else if(toxloss > 40) - to_chat(src, "You feel very unwell!") + combined_msg += "You feel very unwell!" if(oxyloss) if(oxyloss > 10) - to_chat(src, "You feel lightheaded.") + combined_msg += "You feel lightheaded." else if(oxyloss > 20) - to_chat(src, "Your thinking is clouded and distant.") + combined_msg += "Your thinking is clouded and distant." else if(oxyloss > 30) - to_chat(src, "You're choking!") + combined_msg += "You're choking!" if(!HAS_TRAIT(src, TRAIT_NOHUNGER)) switch(nutrition) if(NUTRITION_LEVEL_FULL to INFINITY) - to_chat(src, "You're completely stuffed!") + combined_msg += "You're completely stuffed!" if(NUTRITION_LEVEL_WELL_FED to NUTRITION_LEVEL_FULL) - to_chat(src, "You're well fed!") + combined_msg += "You're well fed!" if(NUTRITION_LEVEL_FED to NUTRITION_LEVEL_WELL_FED) - to_chat(src, "You're not hungry.") + combined_msg += "You're not hungry." if(NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FED) - to_chat(src, "You could use a bite to eat.") + combined_msg += "You could use a bite to eat." if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_HUNGRY) - to_chat(src, "You feel quite hungry.") + combined_msg += "You feel quite hungry." if(0 to NUTRITION_LEVEL_STARVING) - to_chat(src, "You're starving!") + combined_msg += "You're starving!" //Compiles then shows the list of damaged organs and broken organs var/list/broken = list() @@ -867,7 +868,7 @@ //Put the items in that list into a string of text for(var/B in broken) broken_message += B - to_chat(src, "Your [broken_message] [broken_plural ? "are" : "is"] non-functional!") + combined_msg += "Your [broken_message] [broken_plural ? "are" : "is"] non-functional!" if(damaged.len) if(damaged.len > 1) damaged.Insert(damaged.len, "and ") @@ -878,10 +879,12 @@ damaged_plural = TRUE for(var/D in damaged) damaged_message += D - to_chat(src, "Your [damaged_message] [damaged_plural ? "are" : "is"] hurt.") + combined_msg += "Your [damaged_message] [damaged_plural ? "are" : "is"] hurt." if(roundstart_quirks.len) - to_chat(src, "You have these quirks: [get_quirk_string(FALSE, CAT_QUIRK_ALL)].") + combined_msg += "You have these quirks: [get_quirk_string(FALSE, CAT_QUIRK_ALL)]." + + to_chat(src, combined_msg.Join("\n")) /mob/living/carbon/human/damage_clothes(damage_amount, damage_type = BRUTE, damage_flag = 0, def_zone) if(damage_type != BRUTE && damage_type != BURN) diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 46b324e1512..c4515b8a763 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -130,6 +130,13 @@ */ /client/var/list/tgui_windows = list() +/** + * global + * + * TRUE if cache was reloaded by tgui dev server at least once. + */ +/client/var/tgui_cache_reloaded = FALSE + /** * public * @@ -159,16 +166,29 @@ /** * Middleware for /client/Topic. * - * return bool Whether the topic is passed (TRUE), or cancelled (FALSE). + * return bool If TRUE, prevents propagation of the topic call. */ /proc/tgui_Topic(href_list) // Skip non-tgui topics if(!href_list["tgui"]) - return TRUE + return FALSE var/type = href_list["type"] // Unconditionally collect tgui logs if(type == "log") log_tgui(usr, href_list["message"]) + // Reload all tgui windows + if(type == "cacheReloaded") + if(!check_rights(R_ADMIN) || usr.client.tgui_cache_reloaded) + return TRUE + // Mark as reloaded + usr.client.tgui_cache_reloaded = TRUE + // Notify windows + var/list/windows = usr.client.tgui_windows + for(var/window_id in windows) + var/datum/tgui_window/window = windows[window_id] + if (window.status == TGUI_WINDOW_READY) + window.on_message(type, null, href_list) + return TRUE // Locate window var/window_id = href_list["window_id"] var/datum/tgui_window/window @@ -177,7 +197,7 @@ if(!window) log_tgui(usr, "Error: Couldn't find the window datum, force closing.") SStgui.force_close_window(usr, window_id) - return FALSE + return TRUE // Decode payload var/payload if(href_list["payload"]) @@ -185,4 +205,4 @@ // Pass message to window if(window) window.on_message(type, payload, href_list) - return FALSE + return TRUE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index f6c91f515e3..b3b07eb178b 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -80,16 +80,19 @@ opened_at = world.time window.acquire_lock(src) if(!window.is_ready()) - window.initialize(inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui), - )) + window.initialize( + fancy = user.client.prefs.tgui_fancy, + inline_assets = list( + get_asset_datum(/datum/asset/simple/tgui_common), + get_asset_datum(/datum/asset/simple/tgui), + )) else window.send_message("ping") - - var/flushqueue = window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) + var/flush_queue = window.send_asset(get_asset_datum( + /datum/asset/simple/namespaced/fontawesome)) for(var/datum/asset/asset in src_object.ui_assets(user)) - flushqueue |= window.send_asset(asset) - if (flushqueue) + flush_queue |= window.send_asset(asset) + if (flush_queue) user.client.browse_queue_flush() window.send_message("update", get_payload( with_data = TRUE, @@ -206,9 +209,13 @@ "fancy" = user.client.prefs.tgui_fancy, "locked" = user.client.prefs.tgui_lock, ), + "client" = list( + "ckey" = user.client.ckey, + "address" = user.client.address, + "computer_id" = user.client.computer_id, + ), "user" = list( "name" = "[user]", - "ckey" = "[user.ckey]", "observer" = isobserver(user), ), ) diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 9a0a318eae9..b511fe40578 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -8,12 +8,18 @@ var/client/client var/pooled var/pool_index + var/is_browser = FALSE var/status = TGUI_WINDOW_CLOSED var/locked = FALSE var/datum/tgui/locked_by + var/datum/subscriber_object + var/subscriber_delegate var/fatally_errored = FALSE var/message_queue var/sent_assets = list() + // Vars passed to initialize proc (and saved for later) + var/inline_assets + var/fancy /** * public @@ -26,9 +32,9 @@ /datum/tgui_window/New(client/client, id, pooled = FALSE) src.id = id src.client = client + src.client.tgui_windows[id] = src src.pooled = pooled if(pooled) - client.tgui_windows[id] = src src.pool_index = TGUI_WINDOW_INDEX(id) /** @@ -39,18 +45,24 @@ * will be put into the queue until the window finishes loading. * * optional inline_assets list List of assets to inline into the html. + * optional inline_html string Custom HTML to inject. + * optional fancy bool If TRUE, will hide the window titlebar. */ -/datum/tgui_window/proc/initialize(inline_assets = list()) +/datum/tgui_window/proc/initialize( + inline_assets = list(), + inline_html = "", + fancy = FALSE) log_tgui(client, "[id]/initialize") if(!client) return + src.inline_assets = inline_assets + src.fancy = fancy status = TGUI_WINDOW_LOADING fatally_errored = FALSE - message_queue = null // Build window options var/options = "file=[id].html;can_minimize=0;auto_format=0;" // Remove titlebar and resize handles for a fancy window - if(client.prefs.tgui_fancy) + if(fancy) options += "titlebar=0;can_resize=0;" else options += "titlebar=1;can_resize=1;" @@ -72,10 +84,14 @@ asset.send(client) html = replacetextEx(html, "\n", inline_styles) html = replacetextEx(html, "\n", inline_scripts) + // Inject custom HTML + html = replacetextEx(html, "\n", inline_html) // Open the window client << browse(html, "window=[id];[options]") // Instruct the client to signal UI when the window is closed. winset(client, id, "on-close=\"uiclose [id]\"") + // Detect whether the control is a browser + is_browser = winexists(client, id) == "BROWSER" /** * public @@ -107,8 +123,8 @@ * Acquire the window lock. Pool will not be able to provide this window * to other UIs for the duration of the lock. * - * Can be given an optional tgui datum, which will hook its on_message - * callback into the message stream. + * Can be given an optional tgui datum, which will be automatically + * subscribed to incoming messages via the on_message proc. * * optional ui /datum/tgui */ @@ -117,6 +133,8 @@ locked_by = ui /** + * public + * * Release the window lock. */ /datum/tgui_window/proc/release_lock() @@ -126,6 +144,28 @@ locked = FALSE locked_by = null +/** + * public + * + * Subscribes the datum to consume window messages on a specified proc. + * + * Note, that this supports only one subscriber, because code for that + * is simpler and therefore faster. If necessary, this can be rewritten + * to support multiple subscribers. + */ +/datum/tgui_window/proc/subscribe(datum/object, delegate) + subscriber_object = object + subscriber_delegate = delegate + +/** + * public + * + * Unsubscribes the datum. Do not forget to call this when cleaning up. + */ +/datum/tgui_window/proc/unsubscribe(datum/object) + subscriber_object = null + subscriber_delegate = null + /** * public * @@ -159,25 +199,40 @@ * required payload list Message payload * optional force bool Send regardless of the ready status. */ -/datum/tgui_window/proc/send_message(type, list/payload, force) +/datum/tgui_window/proc/send_message(type, payload, force) if(!client) return - var/message = json_encode(list( - "type" = type, - "payload" = payload, - )) - // Strip #255/improper. - message = replacetext(message, "\proper", "") - message = replacetext(message, "\improper", "") - // Pack for sending via output() - message = url_encode(message) + var/message = TGUI_CREATE_MESSAGE(type, payload) // Place into queue if window is still loading if(!force && status != TGUI_WINDOW_READY) if(!message_queue) message_queue = list() message_queue += list(message) return - client << output(message, "[id].browser:update") + client << output(message, is_browser \ + ? "[id]:update" \ + : "[id].browser:update") + +/** + * public + * + * Sends a raw payload to tgui window. + * + * required message string JSON+urlencoded blob to send. + * optional force bool Send regardless of the ready status. + */ +/datum/tgui_window/proc/send_raw_message(message, force) + if(!client) + return + // Place into queue if window is still loading + if(!force && status != TGUI_WINDOW_READY) + if(!message_queue) + message_queue = list() + message_queue += list(message) + return + client << output(message, is_browser \ + ? "[id]:update" \ + : "[id].browser:update") /** * public @@ -191,12 +246,12 @@ /datum/tgui_window/proc/send_asset(datum/asset/asset) if(!client || !asset) return + sent_assets |= list(asset) + . = asset.send(client) if(istype(asset, /datum/asset/spritesheet)) var/datum/asset/spritesheet/spritesheet = asset send_message("asset/stylesheet", spritesheet.css_filename()) send_message("asset/mappings", asset.get_url_mappings()) - sent_assets += list(asset) - return asset.send(client) /** * private @@ -207,7 +262,9 @@ if(!client || !message_queue) return for(var/message in message_queue) - client << output(message, "[id].browser:update") + client << output(message, is_browser \ + ? "[id]:update" \ + : "[id].browser:update") message_queue = null /** @@ -215,26 +272,45 @@ * * Callback for handling incoming tgui messages. */ -/datum/tgui_window/proc/on_message(type, list/payload, list/href_list) - switch(type) - if("ready") - // Status can be READY if user has refreshed the window. - if(status == TGUI_WINDOW_READY) - // Resend the assets - for(var/asset in sent_assets) - send_asset(asset) - status = TGUI_WINDOW_READY - if("log") - if(href_list["fatal"]) - fatally_errored = TRUE +/datum/tgui_window/proc/on_message(type, payload, href_list) + // Status can be READY if user has refreshed the window. + if(type == "ready" && status == TGUI_WINDOW_READY) + // Resend the assets + for(var/asset in sent_assets) + send_asset(asset) + // Mark this window as fatally errored which prevents it from + // being suspended. + if(type == "log" && href_list["fatal"]) + fatally_errored = TRUE + // Mark window as ready since we received this message from somewhere + if(status != TGUI_WINDOW_READY) + status = TGUI_WINDOW_READY + flush_message_queue() // Pass message to UI that requested the lock if(locked && locked_by) - locked_by.on_message(type, payload, href_list) - flush_message_queue() - return + var/prevent_default = locked_by.on_message(type, payload, href_list) + if(prevent_default) + return + // Pass message to the subscriber + else if(subscriber_object) + var/prevent_default = call( + subscriber_object, + subscriber_delegate)(type, payload, href_list) + if(prevent_default) + return // If not locked, handle these message types switch(type) + if("ping") + send_message("pingReply", payload) if("suspend") close(can_be_suspended = TRUE) if("close") close(can_be_suspended = FALSE) + if("openLink") + client << link(href_list["url"]) + if("cacheReloaded") + // Reinitialize + initialize(inline_assets = inline_assets, fancy = fancy) + // Resend the assets + for(var/asset in sent_assets) + send_asset(asset) diff --git a/code/modules/tgui_panel/audio.dm b/code/modules/tgui_panel/audio.dm new file mode 100644 index 00000000000..e62c4b5bc19 --- /dev/null +++ b/code/modules/tgui_panel/audio.dm @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/// Admin music volume, from 0 to 1. +/client/var/admin_music_volume = 1 + +/** + * public + * + * Sends music data to the browser. + * + * Optional settings: + * - pitch: the playback rate + * - start: the start time of the sound + * - end: when the musics stops playing + * + * required url string Must be an https URL. + * optional extra_data list Optional settings. + */ +/datum/tgui_panel/proc/play_music(url, extra_data) + if(!is_ready()) + return + if(!findtext(url, GLOB.is_http_protocol)) + return + var/list/payload = list() + if(length(extra_data) > 0) + for(var/key in extra_data) + payload[key] = extra_data[key] + payload["url"] = url + window.send_message("audio/playMusic", payload) + +/** + * public + * + * Stops playing music through the browser. + */ +/datum/tgui_panel/proc/stop_music() + if(!is_ready()) + return + window.send_message("audio/stopMusic") diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm new file mode 100644 index 00000000000..57c89dc194d --- /dev/null +++ b/code/modules/tgui_panel/external.dm @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/client/var/datum/tgui_panel/tgui_panel + +/** + * tgui panel / chat troubleshooting verb + */ +/client/verb/fix_chat() + set name = "Fix chat" + set category = "OOC" + var/action + log_tgui(src, "tgui_panel: Started fix_chat.") + // Not initialized + if(!tgui_panel || !istype(tgui_panel)) + log_tgui(src, "tgui_panel: datum is missing") + action = alert(src, "tgui panel was not initialized!\nSet it up again?", "", "OK", "Cancel") + if(action != "OK") + return + tgui_panel = new(src) + tgui_panel.initialize() + action = alert(src, "Wait a bit and tell me if it's fixed", "", "Fixed", "Nope") + if(action == "Fixed") + log_tgui(src, "tgui_panel: Fixed by calling 'new' + 'initialize'") + return + // Not ready + if(!tgui_panel?.is_ready()) + log_tgui(src, "tgui_panel: not ready") + action = alert(src, "tgui panel looks like it's waiting for something.\nSend it a ping?", "", "OK", "Cancel") + if(action != "OK") + return + tgui_panel.window.send_message("ping", force = TRUE) + action = alert(src, "Wait a bit and tell me if it's fixed", "", "Fixed", "Nope") + if(action == "Fixed") + log_tgui(src, "tgui_panel: Fixed by sending a ping") + return + // Catch all solution + action = alert(src, "Looks like tgui panel was already setup, but we can always try again.\nSet it up again?", "", "OK", "Cancel") + if(action != "OK") + return + tgui_panel.initialize(force = TRUE) + action = alert(src, "Wait a bit and tell me if it's fixed", "", "Fixed", "Nope") + if(action == "Fixed") + log_tgui(src, "tgui_panel: Fixed by calling 'initialize'") + return + // Failed to fix + action = alert(src, "Welp, I'm all out of ideas. Try closing BYOND and reconnecting.\nWe could also disable tgui_panel and re-enable the old UI", "", "Thanks anyways", "Switch to old UI") + if (action == "Switch to old UI") + winset(src, "output", "on-show=&is-disabled=0&is-visible=1") + winset(src, "browseroutput", "is-disabled=1;is-visible=0") + log_tgui(src, "tgui_panel: Failed to fix.") diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm new file mode 100644 index 00000000000..79087d8500c --- /dev/null +++ b/code/modules/tgui_panel/telemetry.dm @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/** + * Maximum number of connection records allowed to analyze. + * Should match the value set in the browser. + */ +#define TGUI_TELEMETRY_MAX_CONNECTIONS 5 + +/** + * Maximum time allocated for sending a telemetry packet. + */ +#define TGUI_TELEMETRY_RESPONSE_WINDOW 30 SECONDS + +/// Time of telemetry request +/datum/tgui_panel/var/telemetry_requested_at +/// Time of telemetry analysis completion +/datum/tgui_panel/var/telemetry_analyzed_at +/// List of previous client connections +/datum/tgui_panel/var/list/telemetry_connections + +/** + * private + * + * Requests some telemetry from the client. + */ +/datum/tgui_panel/proc/request_telemetry() + telemetry_requested_at = world.time + telemetry_analyzed_at = null + window.send_message("telemetry/request", list( + "limits" = list( + "connections" = TGUI_TELEMETRY_MAX_CONNECTIONS, + ), + )) + +/** + * private + * + * Analyzes a telemetry packet. + * + * Is currently only useful for detecting ban evasion attempts. + */ +/datum/tgui_panel/proc/analyze_telemetry(payload) + if(world.time > telemetry_requested_at + TGUI_TELEMETRY_RESPONSE_WINDOW) + message_admins("[key_name(client)] sent telemetry outside of the allocated time window.") + return + if(telemetry_analyzed_at) + message_admins("[key_name(client)] sent telemetry more than once.") + return + telemetry_analyzed_at = world.time + if(!payload) + return + telemetry_connections = payload["connections"] + var/len = length(telemetry_connections) + if(len == 0) + return + if(len > TGUI_TELEMETRY_MAX_CONNECTIONS) + message_admins("[key_name(client)] was kicked for sending a huge telemetry payload") + qdel(client) + return + var/list/found + for(var/i in 1 to len) + if(QDELETED(client)) + // He got cleaned up before we were done + return + var/list/row = telemetry_connections[i] + // Check for a malformed history object + if (!row || row.len < 3 || (!row["ckey"] || !row["address"] || !row["computer_id"])) + return + if (world.IsBanned(row["ckey"], row["address"], row["computer_id"], real_bans_only = TRUE)) + found = row + break + CHECK_TICK + // This fucker has a history of playing on a banned account. + if(found) + var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])" + message_admins(msg) + log_admin_private(msg) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm new file mode 100644 index 00000000000..b983484046b --- /dev/null +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/** + * tgui_panel datum + * Hosts tgchat and other nice features. + */ +/datum/tgui_panel + var/client/client + var/datum/tgui_window/window + var/broken = FALSE + var/initialized_at + +/datum/tgui_panel/New(client/client) + src.client = client + window = new(client, "browseroutput") + window.subscribe(src, .proc/on_message) + +/datum/tgui_panel/Del() + window.unsubscribe(src) + window.close() + return ..() + +/** + * public + * + * TRUE if panel is initialized and ready to receive messages. + */ +/datum/tgui_panel/proc/is_ready() + return !broken && window.is_ready() + +/** + * public + * + * Initializes tgui panel. + */ +/datum/tgui_panel/proc/initialize(force = FALSE) + initialized_at = world.time + // Perform a clean initialization + window.initialize(inline_assets = list( + get_asset_datum(/datum/asset/simple/tgui_common), + get_asset_datum(/datum/asset/simple/tgui_panel), + )) + window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) + window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat)) + request_telemetry() + addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 2 SECONDS) + +/** + * private + * + * Called when initialization has timed out. + */ +/datum/tgui_panel/proc/on_initialize_timed_out() + // Currently does nothing but sending a message to old chat. + SEND_TEXT(client, "Failed to load fancy chat, reverting to old chat. Certain features won't work.") + +/** + * private + * + * Callback for handling incoming tgui messages. + */ +/datum/tgui_panel/proc/on_message(type, payload) + if(type == "ready") + broken = FALSE + window.send_message("update", list( + "config" = list( + "client" = list( + "ckey" = client.ckey, + "address" = client.address, + "computer_id" = client.computer_id, + ), + "window" = list( + "fancy" = FALSE, + "locked" = FALSE, + ), + ), + )) + return TRUE + if(type == "audio/setAdminMusicVolume") + client.admin_music_volume = payload["volume"] + return TRUE + if(type == "telemetry") + analyze_telemetry(payload) + return TRUE + +/** + * public + * + * Sends a round restart notification. + */ +/datum/tgui_panel/proc/send_roundrestart() + window.send_message("roundrestart") diff --git a/code/modules/tgui_panel/to_chat.dm b/code/modules/tgui_panel/to_chat.dm new file mode 100644 index 00000000000..aad27d48727 --- /dev/null +++ b/code/modules/tgui_panel/to_chat.dm @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/** + * global + * + * Circumvents the message queue and sends the message + * to the recipient (target) as soon as possible. + */ +/proc/to_chat_immediate( + target, + text, + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE) + if(!target || !text) + return + if(target == world) + target = GLOB.clients + var/flags = handle_whitespace \ + | trailing_newline << 1 \ + | confidential << 2 + var/message = TGUI_CREATE_MESSAGE("chat/message", list( + "text" = text, + "flags" = flags, + )) + if(islist(target)) + for(var/_target in target) + var/client/client = CLIENT_FROM_VAR(_target) + if(client) + // Send to tgchat + client.tgui_panel?.window.send_raw_message(message) + // Send to old chat + SEND_TEXT(client, text) + return + var/client/client = CLIENT_FROM_VAR(target) + if(client) + // Send to tgchat + client.tgui_panel?.window.send_raw_message(message) + // Send to old chat + SEND_TEXT(client, text) + +/** + * global + * + * Sends the message to the recipient (target). + */ +/proc/to_chat( + target, + text, + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE) + if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) + to_chat_immediate( + target, + text, + handle_whitespace, + trailing_newline, + confidential) + return + if(!target || !text) + return + if(target == world) + target = GLOB.clients + var/flags = handle_whitespace \ + | trailing_newline << 1 \ + | confidential << 2 + SSchat.queue(target, text, flags) diff --git a/config/resources.txt b/config/resources.txt index 034b6bec770..5ec63876b01 100644 --- a/config/resources.txt +++ b/config/resources.txt @@ -15,7 +15,7 @@ EXTERNAL_RSC_URLS http://tgstation13.download/byond/tgstationv2.zip # Asset Transport # The normal way of getting assets to clients is to use the internal byond system. This can be slow and delay the opening of interface windows. It also doesn't allow the internal IE windows byond uses to cache anything. # You can instead have the server save them to a website via a folder within the game server that the web server can read. This could be a simple webserver or something backed by a CDN. -# Valid values: simple, webroot. Simple is the default. +# Valid values: simple, webroot. Simple is the default. #ASSET_TRANSPORT webroot @@ -28,7 +28,7 @@ EXTERNAL_RSC_URLS http://tgstation13.download/byond/tgstationv2.zip # Webroot asset transport configurable values. -# Local folder to save assets to. +# Local folder to save assets to. # Assets will be saved in the format of asset.MD5HASH.EXT or in namespaces/hash/ as ASSET_FILE_NAME or asset.MD5HASH.EXT #ASSET_CDN_WEBROOT data/asset-store/ diff --git a/code/modules/goonchat/browserassets/js/jquery.min.js b/html/jquery.min.js similarity index 100% rename from code/modules/goonchat/browserassets/js/jquery.min.js rename to html/jquery.min.js diff --git a/tgstation.dme b/tgstation.dme index 1c96fa1a047..5fab26d3362 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -290,7 +290,6 @@ #include "code\controllers\subsystem\parallax.dm" #include "code\controllers\subsystem\pathfinder.dm" #include "code\controllers\subsystem\persistence.dm" -#include "code\controllers\subsystem\ping.dm" #include "code\controllers\subsystem\profiler.dm" #include "code\controllers\subsystem\radiation.dm" #include "code\controllers\subsystem\radio.dm" @@ -1724,7 +1723,6 @@ #include "code\modules\client\client_colour.dm" #include "code\modules\client\client_defines.dm" #include "code\modules\client\client_procs.dm" -#include "code\modules\client\darkmode.dm" #include "code\modules\client\message.dm" #include "code\modules\client\player_details.dm" #include "code\modules\client\preferences.dm" @@ -1968,7 +1966,6 @@ #include "code\modules\games\cas.dm" #include "code\modules\games\kotahi.dm" #include "code\modules\games\tarot.dm" -#include "code\modules\goonchat\browserOutput.dm" #include "code\modules\holiday\easter.dm" #include "code\modules\holiday\foreign_calendar.dm" #include "code\modules\holiday\holidays.dm" @@ -3123,6 +3120,11 @@ #include "code\modules\tgui\states\physical.dm" #include "code\modules\tgui\states\self.dm" #include "code\modules\tgui\states\zlevel.dm" +#include "code\modules\tgui_panel\audio.dm" +#include "code\modules\tgui_panel\external.dm" +#include "code\modules\tgui_panel\telemetry.dm" +#include "code\modules\tgui_panel\tgui_panel.dm" +#include "code\modules\tgui_panel\to_chat.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\unit_tests\_unit_tests.dm" #include "code\modules\uplink\uplink_devices.dm" diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 9fd4db9fd2e..e3c3144e5b2 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -114,7 +114,7 @@ rules: ## Require return statements to either always or never specify values # consistent-return: error ## Enforce consistent brace style for all control statements - curly: [error, all] + curly: [error, multi-line] ## Require default cases in switch statements # default-case: error ## Enforce default parameters to be last @@ -374,6 +374,7 @@ rules: ignorePattern: '^(import\s.+\sfrom\s|.*require\()', ignoreUrls: true, ignoreRegExpLiterals: true, + ignoreStrings: true, }] ## Enforce a maximum number of lines per file # max-lines: error diff --git a/tgui/.gitattributes b/tgui/.gitattributes index 9382416e69f..d9cdc20fbdb 100644 --- a/tgui/.gitattributes +++ b/tgui/.gitattributes @@ -17,3 +17,4 @@ bin/tgui text eol=lf ## Treat bundles as binary and ignore them during conflicts *.bundle.* binary merge=tgui-merge-bundle +*.chunk.* binary merge=tgui-merge-bundle diff --git a/tgui/bin/tgui b/tgui/bin/tgui index 97a86159e6b..5627b404135 100755 --- a/tgui/bin/tgui +++ b/tgui/bin/tgui @@ -67,7 +67,7 @@ task-clean() { task-validate-build() { cd "${base_dir}" local diff - diff="$(git diff packages/tgui/public/tgui.bundle.*)" + diff="$(git diff packages/tgui/public/*)" if [[ -n ${diff} ]]; then echo "Error: our build differs from the build committed into git." echo "Please rebuild tgui." diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md index 549956738fa..34c6347b997 100644 --- a/tgui/docs/component-reference.md +++ b/tgui/docs/component-reference.md @@ -227,7 +227,7 @@ be truncated with an ellipsis. Be careful however, because this prop breaks the baseline alignment. - `title: string` - A native browser tooltip, which appears when hovering over the button. -- `content/children: any` - Content to render inside the button. +- `children: any` - Content to render inside the button. - `onClick: function` - Called when element is clicked. ### `Button.Checkbox` @@ -389,7 +389,9 @@ to the left, and certain elements to the right: - ``` @@ -625,7 +627,9 @@ to perform some sort of action), there is a way to do that: + )}> Content @@ -643,7 +647,7 @@ to perform some sort of action), there is a way to do that: - `label: string` - Item label. - `color: string` - Sets the color of the text. - `buttons: any` - Buttons to render aside the content. -- `content/children: any` - Content of this labeled item. +- `children: any` - Content of this labeled item. ### `LabeledList.Divider` @@ -748,7 +752,7 @@ percentage and how filled the bar is. - `ranges: { color: [from, to] }` - Applies a `color` to the progress bar based on whether the value lands in the range between `from` and `to`. - `color: string` - Color of the progress bar. -- `content/children: any` - Content to render inside the progress bar. +- `children: any` - Content to render inside the progress bar. ### `Section` @@ -773,7 +777,9 @@ If you want to have a button on the right side of an section title
+ )}> Here you can order supply crates.
@@ -784,7 +790,10 @@ If you want to have a button on the right side of an section title - `level: number` - Section level in hierarchy. Default is 1, higher number means deeper level of nesting. Must be an integer number. - `buttons: any` - Buttons to render aside the section title. -- `content/children: any` - Content of this section. +- `fill: boolean` - If true, fills all available vertical space. +- `fitted: boolean` - If true, removes all section padding. +- `scrollable: boolean` - Shows or hides the scrollbar. +- `children: any` - Content of this section. ### `Slider` @@ -953,7 +962,7 @@ Usage: **Props:** - `position: string` - Tooltip position. -- `content/children: string` - Content of the tooltip. Must be a plain string. +- `content: string` - Content of the tooltip. Must be a plain string. Fragments or other elements are **not** supported. ## `tgui/layouts` @@ -978,6 +987,7 @@ Example: **Props:** +- See inherited props: [Box](#box) - `className: string` - Applies a CSS class to the element. - `theme: string` - A name of the theme. - For a list of themes, see `packages/tgui/styles/themes`. @@ -995,6 +1005,8 @@ Can be scrollable. **Props:** +- See inherited props: [Box](#box) - `className: string` - Applies a CSS class to the element. +- `fitted: boolean` - If true, removes all padding. - `scrollable: boolean` - Shows or hides the scrollbar. - `children: any` - Main content of your window. diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js new file mode 100644 index 00000000000..510579bc266 --- /dev/null +++ b/tgui/packages/common/color.js @@ -0,0 +1,62 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +const EPSILON = 0.0001; + +export class Color { + constructor(r = 0, g = 0, b = 0, a = 1) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + toString() { + return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${this.a | 0})`; + } +} + +/** + * Creates a color from the CSS hex color notation. + */ +Color.fromHex = hex => ( + new Color( + parseInt(hex.substr(1, 2), 16), + parseInt(hex.substr(3, 2), 16), + parseInt(hex.substr(5, 2), 16)) +); + +/** + * Linear interpolation of two colors. + */ +Color.lerp = (c1, c2, n) => ( + new Color( + (c2.r - c1.r) * n + c1.r, + (c2.g - c1.g) * n + c1.g, + (c2.b - c1.b) * n + c1.b, + (c2.a - c1.a) * n + c1.a) +); + +/** + * Loops up the color in the provided list of colors + * with linear interpolation. + */ +Color.lookup = (value, colors = []) => { + const len = colors.length; + if (len < 2) { + throw new Error('Needs at least two colors!'); + } + const scaled = value * (len - 1); + if (value < EPSILON) { + return colors[0]; + } + if (value >= 1 - EPSILON) { + return colors[len - 1]; + } + const ratio = scaled % 1; + const index = scaled | 0; + return Color.lerp(colors[index], colors[index + 1], ratio); +}; diff --git a/tgui/packages/common/events.js b/tgui/packages/common/events.js new file mode 100644 index 00000000000..6d590a34453 --- /dev/null +++ b/tgui/packages/common/events.js @@ -0,0 +1,42 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +export class EventEmitter { + constructor() { + this.listeners = {}; + } + + on(name, listener) { + this.listeners[name] = this.listeners[name] || []; + this.listeners[name].push(listener); + } + + off(name, listener) { + const listeners = this.listeners[name]; + if (!listeners) { + throw new Error(`There is no listeners for "${name}"`); + } + this.listeners[name] = listeners + .filter(existingListener => { + return existingListener !== listener; + }); + } + + emit(name, ...params) { + const listeners = this.listeners[name]; + if (!listeners) { + return; + } + for (let i = 0, len = listeners.length; i < len; i += 1) { + const listener = listeners[i]; + listener(...params); + } + } + + clear() { + this.listeners = {}; + } +} diff --git a/tgui/packages/common/keycodes.js b/tgui/packages/common/keycodes.js new file mode 100644 index 00000000000..8f18b154b56 --- /dev/null +++ b/tgui/packages/common/keycodes.js @@ -0,0 +1,86 @@ +/** + * All possible browser keycodes, in one file. + * + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +export const KEY_BACKSPACE = 8; +export const KEY_TAB = 9; +export const KEY_ENTER = 13; +export const KEY_SHIFT = 16; +export const KEY_CTRL = 17; +export const KEY_ALT = 18; +export const KEY_PAUSE = 19; +export const KEY_CAPSLOCK = 20; +export const KEY_ESCAPE = 27; +export const KEY_SPACE = 32; +export const KEY_PAGEUP = 33; +export const KEY_PAGEDOWN = 34; +export const KEY_END = 35; +export const KEY_HOME = 36; +export const KEY_LEFT = 37; +export const KEY_UP = 38; +export const KEY_RIGHT = 39; +export const KEY_DOWN = 40; +export const KEY_INSERT = 45; +export const KEY_DELETE = 46; +export const KEY_0 = 48; +export const KEY_1 = 49; +export const KEY_2 = 50; +export const KEY_3 = 51; +export const KEY_4 = 52; +export const KEY_5 = 53; +export const KEY_6 = 54; +export const KEY_7 = 55; +export const KEY_8 = 56; +export const KEY_9 = 57; +export const KEY_A = 65; +export const KEY_B = 66; +export const KEY_C = 67; +export const KEY_D = 68; +export const KEY_E = 69; +export const KEY_F = 70; +export const KEY_G = 71; +export const KEY_H = 72; +export const KEY_I = 73; +export const KEY_J = 74; +export const KEY_K = 75; +export const KEY_L = 76; +export const KEY_M = 77; +export const KEY_N = 78; +export const KEY_O = 79; +export const KEY_P = 80; +export const KEY_Q = 81; +export const KEY_R = 82; +export const KEY_S = 83; +export const KEY_T = 84; +export const KEY_U = 85; +export const KEY_V = 86; +export const KEY_W = 87; +export const KEY_X = 88; +export const KEY_Y = 89; +export const KEY_Z = 90; +export const KEY_F1 = 112; +export const KEY_F2 = 113; +export const KEY_F3 = 114; +export const KEY_F4 = 115; +export const KEY_F5 = 116; +export const KEY_F6 = 117; +export const KEY_F7 = 118; +export const KEY_F8 = 119; +export const KEY_F9 = 120; +export const KEY_F10 = 121; +export const KEY_F11 = 122; +export const KEY_F12 = 123; +export const KEY_SEMICOLON = 186; +export const KEY_EQUAL = 187; +export const KEY_COMMA = 188; +export const KEY_MINUS = 189; +export const KEY_PERIOD = 190; +export const KEY_SLASH = 191; +export const KEY_LEFT_BRACKET = 219; +export const KEY_BACKSLASH = 220; +export const KEY_RIGHT_BRACKET = 221; +export const KEY_QUOTE = 222; diff --git a/tgui/packages/common/perf.js b/tgui/packages/common/perf.js index 319b77cea39..8414971f93b 100644 --- a/tgui/packages/common/perf.js +++ b/tgui/packages/common/perf.js @@ -1,21 +1,32 @@ /** * Ghetto performance measurement tools. * - * Uses NODE_ENV to redact itself from production bundles. + * Uses NODE_ENV to remove itself from production builds. * * @file * @copyright 2020 Aleksej Komarov * @license MIT */ -let markersByLabel = {}; +const FPS = 60; +const FRAME_DURATION = 1000 / FPS; + +// True if Performance API is supported +const supportsPerf = !!window.performance?.now; +// High precision markers +let hpMarkersByName = {}; +// Low precision markers +let lpMarkersByName = {}; /** * Marks a certain spot in the code for later measurements. */ -const mark = (label, timestamp) => { +const mark = (name, timestamp) => { if (process.env.NODE_ENV !== 'production') { - markersByLabel[label] = timestamp || Date.now(); + if (supportsPerf && !timestamp) { + hpMarkersByName[name] = performance.now(); + } + lpMarkersByName[name] = timestamp || Date.now(); } }; @@ -24,18 +35,23 @@ const mark = (label, timestamp) => { * * Use logger.log() to print the measurement. */ -const measure = (markerA, markerB) => { +const measure = (markerNameA, markerNameB) => { if (process.env.NODE_ENV !== 'production') { - return timeDiff( - markersByLabel[markerA], - markersByLabel[markerB]); + let markerA = hpMarkersByName[markerNameA]; + let markerB = hpMarkersByName[markerNameB]; + if (!markerA || !markerB) { + markerA = lpMarkersByName[markerNameA]; + markerB = lpMarkersByName[markerNameB]; + } + const duration = Math.abs(markerB - markerA); + return formatDuration(duration); } }; -const timeDiff = (startedAt, finishedAt) => { - const diff = Math.abs(finishedAt - startedAt); - const diffFrames = (diff / 16.6667).toFixed(2); - return `${diff}ms (${diffFrames} frames)`; +const formatDuration = duration => { + const durationInFrames = duration / FRAME_DURATION; + return duration.toFixed(duration < 10 ? 1 : 0) + 'ms ' + + '(' + durationInFrames.toFixed(2) + ' frames)'; }; export const perf = { diff --git a/tgui/packages/common/react.js b/tgui/packages/common/react.js index dba84b7b101..c0b24563f14 100644 --- a/tgui/packages/common/react.js +++ b/tgui/packages/common/react.js @@ -64,10 +64,10 @@ export const pureComponentHooks = { }; /** - * A helper to determine whether to render an item. + * A helper to determine whether the object is renderable by React. */ -export const isFalsy = value => { - return value === undefined - || value === null - || value === false; +export const canRender = value => { + return value !== undefined + && value !== null + && typeof value !== 'boolean'; }; diff --git a/tgui/packages/common/redux.js b/tgui/packages/common/redux.js index dc486ff7b86..12aacadb5fc 100644 --- a/tgui/packages/common/redux.js +++ b/tgui/packages/common/redux.js @@ -4,6 +4,7 @@ * @license MIT */ +import { Component } from 'inferno'; import { compose } from './fp'; /** @@ -26,7 +27,9 @@ export const createStore = (reducer, enhancer) => { const dispatch = action => { currentState = reducer(currentState, action); - listeners.forEach(fn => fn()); + for (let i = 0; i < listeners.length; i++) { + listeners[i](); + } }; // This creates the initial store by causing each reducer to be called @@ -81,7 +84,7 @@ export const applyMiddleware = (...middlewares) => { export const combineReducers = reducersObj => { const keys = Object.keys(reducersObj); let hasChanged = false; - return (prevState, action) => { + return (prevState = {}, action) => { const nextState = { ...prevState }; for (let key of keys) { const reducer = reducersObj[key]; @@ -97,3 +100,55 @@ export const combineReducers = reducersObj => { : prevState; }; }; + +/** + * A utility function to create an action creator for the given action + * type string. The action creator accepts a single argument, which will + * be included in the action object as a field called payload. The action + * creator function will also have its toString() overriden so that it + * returns the action type, allowing it to be used in reducer logic that + * is looking for that action type. + * + * @param type The action type to use for created actions. + * @param prepare (optional) a method that takes any number of arguments + * and returns { payload } or { payload, meta }. If this is given, the + * resulting action creator will pass it's arguments to this method to + * calculate payload & meta. + * + * @public + */ +export const createAction = (type, prepare) => { + const actionCreator = (...args) => { + if (!prepare) { + return { type, payload: args[0] }; + } + const prepared = prepare(...args); + if (!prepared) { + throw new Error('prepare function did not return an object'); + } + const action = { type }; + if ('payload' in prepared) { + action.payload = prepared.payload; + } + if ('meta' in prepared) { + action.meta = prepared.meta; + } + return action; + }; + actionCreator.toString = () => '' + type; + actionCreator.type = type; + actionCreator.match = action => action.type === type; + return actionCreator; +}; + + +// Implementation specific +// -------------------------------------------------------- + +export const useDispatch = context => { + return context.store.dispatch; +}; + +export const useSelector = (context, selector) => { + return selector(context.store.getState()); +}; diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.js index 8e1d2183e42..33397f33a17 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.js @@ -6,71 +6,152 @@ * @license MIT */ -export const STORAGE_NONE = 0; -export const STORAGE_LOCAL_STORAGE = 1; -export const STORAGE_INDEXED_DB = 2; +export const IMPL_MEMORY = 0; +export const IMPL_LOCAL_STORAGE = 1; +export const IMPL_INDEXED_DB = 2; -const createMock = () => { - let storage = {}; - const get = key => storage[key]; - const set = (key, value) => { - storage[key] = value; - }; - const remove = key => { - storage[key] = undefined; - }; - const clear = () => { - // NOTE: On IE8, this will probably leak memory if used often. - storage = {}; - }; - return { - get, - set, - remove, - clear, - engine: STORAGE_NONE, - }; -}; +const INDEXED_DB_VERSION = 1; +const INDEXED_DB_NAME = 'tgui'; +const INDEXED_DB_STORE_NAME = 'storage-v1'; -const createLocalStorage = () => { - const get = key => { - const value = localStorage.getItem(key); - if (typeof value !== 'string') { - return; - } - return JSON.parse(value); - }; - const set = (key, value) => { - localStorage.setItem(key, JSON.stringify(value)); - }; - const remove = key => { - localStorage.removeItem(key); - }; - const clear = () => { - localStorage.clear(); - }; - return { - get, - set, - remove, - clear, - engine: STORAGE_LOCAL_STORAGE, - }; -}; +const READ_ONLY = 'readonly'; +const READ_WRITE = 'readwrite'; -const testLocalStorage = () => { - // Localstorage can sometimes throw an error, even if DOM storage is not - // disabled in IE11 settings. - // See: https://superuser.com/questions/1080011 +const testGeneric = testFn => () => { try { - return Boolean(window.localStorage && window.localStorage.getItem); + return Boolean(testFn()); } catch { return false; } }; +// Localstorage can sometimes throw an error, even if DOM storage is not +// disabled in IE11 settings. +// See: https://superuser.com/questions/1080011 +const testLocalStorage = testGeneric(() => ( + window.localStorage && window.localStorage.getItem +)); + +const testIndexedDb = testGeneric(() => ( + (window.indexedDB || window.msIndexedDB) + && (window.IDBTransaction || window.msIDBTransaction) +)); + +class MemoryBackend { + constructor() { + this.impl = IMPL_MEMORY; + this.store = {}; + } + + async get(key) { + return this.store[key]; + } + + async set(key, value) { + this.store[key] = value; + } + + async remove(key) { + this.store[key] = undefined; + } + + async clear() { + this.store = {}; + } +} + +class LocalStorageBackend { + constructor() { + this.impl = IMPL_LOCAL_STORAGE; + this.store = {}; + } + + async get(key) { + const value = localStorage.getItem(key); + if (typeof value === 'string') { + return JSON.parse(value); + } + } + + async set(key, value) { + localStorage.setItem(key, JSON.stringify(value)); + } + + async remove(key) { + localStorage.removeItem(key); + } + + async clear() { + localStorage.clear(); + } +} + +class IndexedDbBackend { + constructor() { + this.impl = IMPL_INDEXED_DB; + /** @type {Promise} */ + this.dbPromise = new Promise((resolve, reject) => { + const indexedDB = window.indexedDB || window.msIndexedDB; + const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION); + req.onupgradeneeded = () => { + try { + req.result.createObjectStore(INDEXED_DB_STORE_NAME); + } + catch (err) { + reject(new Error('Failed to upgrade IDB: ' + req.error)); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => { + reject(new Error('Failed to open IDB: ' + req.error)); + }; + }); + } + + getStore(mode) { + return this.dbPromise.then(db => db + .transaction(INDEXED_DB_STORE_NAME, mode) + .objectStore(INDEXED_DB_STORE_NAME)); + } + + async get(key) { + const store = await this.getStore(READ_ONLY); + return new Promise((resolve, reject) => { + const req = store.get(key); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + } + + async set(key, value) { + // The reason we don't _save_ null is because IE 10 does + // not support saving the `null` type in IndexedDB. How + // ironic, given the bug below! + // See: https://github.com/mozilla/localForage/issues/161 + if (value === null) { + value = undefined; + } + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.put(value, key); + } + + async remove(key) { + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.delete(key); + } + + async clear() { + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.clear(); + } +} + export const storage = ( - testLocalStorage() && createLocalStorage() - || createMock() + testIndexedDb() && new IndexedDbBackend() + || testLocalStorage() && new LocalStorageBackend() + || new MemoryBackend() ); diff --git a/tgui/packages/common/timer.js b/tgui/packages/common/timer.js index f4e26fa5aa7..1177071b9c6 100644 --- a/tgui/packages/common/timer.js +++ b/tgui/packages/common/timer.js @@ -27,3 +27,12 @@ export const debounce = (fn, time, immediate = false) => { } }; }; + +/** + * Suspends an asynchronous function for N milliseconds. + * + * @param {number} time + */ +export const sleep = time => ( + new Promise(resolve => setTimeout(resolve, time)) +); diff --git a/tgui/packages/common/uuid.js b/tgui/packages/common/uuid.js new file mode 100644 index 00000000000..7721af64949 --- /dev/null +++ b/tgui/packages/common/uuid.js @@ -0,0 +1,19 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +/** + * Creates a UUID v4 string + * + * @return {string} + */ +export const createUuid = () => { + let d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +}; diff --git a/tgui/packages/tgui-dev-server/dreamseeker.js b/tgui/packages/tgui-dev-server/dreamseeker.js new file mode 100644 index 00000000000..c36fe55f28d --- /dev/null +++ b/tgui/packages/tgui-dev-server/dreamseeker.js @@ -0,0 +1,87 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import axios from 'axios'; +import { exec } from 'child_process'; +import { createLogger } from 'common/logging.js'; +import { promisify } from 'util'; + +const logger = createLogger('dreamseeker'); + +const instanceByPid = new Map(); + +export class DreamSeeker { + constructor(pid, addr) { + this.pid = pid; + this.addr = addr; + this.client = axios.create({ + baseURL: `http://${addr}/`, + }); + } + + topic(params = {}) { + const query = Object.keys(params) + .map(key => encodeURIComponent(key) + + '=' + encodeURIComponent(params[key])) + .join('&'); + return this.client.get('/dummy?' + query); + } +} + +/** + * @param {number[]} pids + * @returns {DreamSeeker[]} + */ +DreamSeeker.getInstancesByPids = async pids => { + if (process.platform !== 'win32') { + return []; + } + const instances = []; + const pidsToResolve = []; + for (let pid of pids) { + const instance = instanceByPid.get(pid); + if (instance) { + instances.push(instance); + } + else { + pidsToResolve.push(pid); + } + } + if (pidsToResolve.length > 0) { + try { + const command = 'netstat -a -n -o'; + const { stdout } = await promisify(exec)(command); + // Line format: + // proto addr mask mode pid + const entries = stdout + .split('\r\n') + .filter(line => line.includes('LISTENING')) + .map(line => { + const words = line.match(/\S+/g); + return { + addr: words[1], + pid: parseInt(words[4], 10), + }; + }) + .filter(entry => pidsToResolve.includes(entry.pid)); + const len = entries.length; + logger.log('found', len, plural('instance', len)); + for (let entry of entries) { + const { pid, addr } = entry; + const instance = new DreamSeeker(pid, addr); + instances.push(instance); + instanceByPid.set(pid, instance); + } + } + catch (err) { + logger.error(err); + return []; + } + } + return instances; +}; + +const plural = (word, n) => n !== 1 ? word + 's' : word; diff --git a/tgui/packages/tgui-dev-server/index.js b/tgui/packages/tgui-dev-server/index.js index f4e8155d29f..43acd87474b 100644 --- a/tgui/packages/tgui-dev-server/index.js +++ b/tgui/packages/tgui-dev-server/index.js @@ -8,12 +8,15 @@ import { setupWebpack, getWebpackConfig } from './webpack.js'; import { reloadByondCache } from './reloader.js'; const noHot = process.argv.includes('--no-hot'); +const noTmp = process.argv.includes('--no-tmp'); const reloadOnce = process.argv.includes('--reload'); const setupServer = async () => { const config = await getWebpackConfig({ mode: 'development', hot: !noHot, + devServer: true, + useTmpFolder: !noTmp, }); // Reload cache once if (reloadOnce) { diff --git a/tgui/packages/tgui-dev-server/link/client.js b/tgui/packages/tgui-dev-server/link/client.js index 4671b340c53..5a4d2f61122 100644 --- a/tgui/packages/tgui-dev-server/link/client.js +++ b/tgui/packages/tgui-dev-server/link/client.js @@ -37,7 +37,7 @@ if (process.env.NODE_ENV !== 'production') { window.onunload = () => socket && socket.close(); } -const subscribe = fn => subscribers.push(fn); +export const subscribe = fn => subscribers.push(fn); /** * A json serializer which handles circular references and other junk. @@ -68,7 +68,10 @@ const serializeObject = obj => { } refs.push(value); // Error object - if (value instanceof Error) { + const isError = value instanceof Error || ( + value.code && value.message && value.message.includes('Error') + ); + if (isError) { return { __error__: true, string: String(value), @@ -88,7 +91,7 @@ const serializeObject = obj => { return json; }; -const sendRawMessage = msg => { +export const sendMessage = msg => { if (process.env.NODE_ENV !== 'production') { const json = serializeObject(msg); // Send message using WebSocket @@ -109,8 +112,8 @@ const sendRawMessage = msg => { else { const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1'; const req = new XMLHttpRequest(); - req.open('POST', `http://${DEV_SERVER_IP}:3001`); - req.timeout = 500; + req.open('POST', `http://${DEV_SERVER_IP}:3001`, true); + req.timeout = 250; req.send(json); } } @@ -119,7 +122,7 @@ const sendRawMessage = msg => { export const sendLogEntry = (level, ns, ...args) => { if (process.env.NODE_ENV !== 'production') { try { - sendRawMessage({ + sendMessage({ type: 'log', payload: { level, diff --git a/tgui/packages/tgui-dev-server/link/retrace.js b/tgui/packages/tgui-dev-server/link/retrace.js index e0b17a01d6d..4bcb8e40c6a 100644 --- a/tgui/packages/tgui-dev-server/link/retrace.js +++ b/tgui/packages/tgui-dev-server/link/retrace.js @@ -39,6 +39,10 @@ export const loadSourceMaps = async bundleDir => { }; export const retrace = stack => { + if (typeof stack !== 'string') { + logger.log('ERROR: Stack is not a string!', stack); + return stack; + } const header = stack.split(/\n\s.*at/)[0]; const mappedStack = StackTraceParser.parse(stack) .map(frame => { diff --git a/tgui/packages/tgui-dev-server/link/server.js b/tgui/packages/tgui-dev-server/link/server.js index 94a79c9ad5f..2586d777798 100644 --- a/tgui/packages/tgui-dev-server/link/server.js +++ b/tgui/packages/tgui-dev-server/link/server.js @@ -16,37 +16,108 @@ const DEBUG = process.argv.includes('--debug'); export { loadSourceMaps }; -export const setupLink = () => { - logger.log('setting up'); - const wss = setupWebSocketLink(); - setupHttpLink(); - return { - wss, - }; -}; +export const setupLink = () => new LinkServer(); -export const broadcastMessage = (link, msg) => { - const { wss } = link; - const clients = [...wss.clients]; - logger.log(`broadcasting ${msg.type} to ${clients.length} clients`); - for (let client of clients) { - const json = JSON.stringify(msg); - client.send(json); +class LinkServer { + constructor() { + logger.log('setting up'); + this.wss = null; + this.setupWebSocketLink(); + this.setupHttpLink(); } -}; + + // WebSocket-based client link + setupWebSocketLink() { + const port = 3000; + this.wss = new WebSocket.Server({ port }); + this.wss.on('connection', ws => { + logger.log('client connected'); + ws.on('message', json => { + const msg = deserializeObject(json); + this.handleLinkMessage(ws, msg); + }); + ws.on('close', () => { + logger.log('client disconnected'); + }); + }); + logger.log(`listening on port ${port} (WebSocket)`); + } + + // One way HTTP-based client link for IE8 + setupHttpLink() { + const port = 3001; + this.httpServer = http.createServer((req, res) => { + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + req.on('end', () => { + const msg = deserializeObject(body); + this.handleLinkMessage(null, msg); + res.end(); + }); + return; + } + res.write('Hello'); + res.end(); + }); + this.httpServer.listen(port); + logger.log(`listening on port ${port} (HTTP)`); + } + + handleLinkMessage(ws, msg) { + const { type, payload } = msg; + if (type === 'log') { + const { level, ns, args } = payload; + // Skip debug messages + if (level <= 0 && !DEBUG) { + return; + } + directLog(ns, ...args.map(arg => { + if (typeof arg === 'object') { + return inspect(arg, { + depth: Infinity, + colors: true, + compact: 8, + }); + } + return arg; + })); + return; + } + if (type === 'relay') { + for (let client of this.wss.clients) { + if (client === ws) { + continue; + } + this.sendMessage(client, msg); + } + return; + } + logger.log('unhandled message', msg); + } + + sendMessage(ws, msg) { + ws.send(JSON.stringify(msg)); + } + + broadcastMessage(msg) { + const clients = [...this.wss.clients]; + if (clients.length === 0) { + return; + } + logger.log(`broadcasting ${msg.type} to ${clients.length} clients`); + for (let client of clients) { + const json = JSON.stringify(msg); + client.send(json); + } + } +} const deserializeObject = str => { return JSON.parse(str, (key, value) => { if (typeof value === 'object' && value !== null) { - if (value.__error__) { - if (!value.stack) { - return value.string; - } - return retrace(value.stack); - } - if (value.__number__) { - return parseFloat(value.__number__); - } if (value.__undefined__) { // NOTE: You should not rely on deserialized object's undefined, // this is purely for inspection purposes. @@ -54,80 +125,17 @@ const deserializeObject = str => { [inspect.custom]: () => undefined, }; } + if (value.__number__) { + return parseFloat(value.__number__); + } + if (value.__error__) { + if (!value.stack) { + return value.string; + } + return retrace(value.stack); + } return value; } return value; }); }; - -const handleLinkMessage = msg => { - const { type, payload } = msg; - - if (type === 'log') { - const { level, ns, args } = payload; - // Skip debug messages - if (level <= 0 && !DEBUG) { - return; - } - directLog(ns, ...args.map(arg => { - if (typeof arg === 'object') { - return inspect(arg, { - depth: Infinity, - colors: true, - compact: 8, - }); - } - return arg; - })); - return; - } - - logger.log('unhandled message', msg); -}; - -// WebSocket-based client link -const setupWebSocketLink = () => { - const port = 3000; - const wss = new WebSocket.Server({ port }); - - wss.on('connection', ws => { - logger.log('client connected'); - - ws.on('message', json => { - const msg = deserializeObject(json); - handleLinkMessage(msg); - }); - - ws.on('close', () => { - logger.log('client disconnected'); - }); - }); - - logger.log(`listening on port ${port} (WebSocket)`); - return wss; -}; - -// One way HTTP-based client link for IE8 -const setupHttpLink = () => { - const port = 3001; - - const server = http.createServer((req, res) => { - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - req.on('end', () => { - const msg = deserializeObject(body); - handleLinkMessage(msg); - res.end(); - }); - return; - } - res.write('Hello'); - res.end(); - }); - - server.listen(port); - logger.log(`listening on port ${port} (HTTP)`); -}; diff --git a/tgui/packages/tgui-dev-server/package.json b/tgui/packages/tgui-dev-server/package.json index bcb02196e65..3e50a97f0eb 100644 --- a/tgui/packages/tgui-dev-server/package.json +++ b/tgui/packages/tgui-dev-server/package.json @@ -4,6 +4,7 @@ "version": "4.1.0", "type": "module", "dependencies": { + "axios": "^0.19.2", "glob": "^7.1.4", "source-map": "^0.7.3", "stacktrace-parser": "^0.1.7", diff --git a/tgui/packages/tgui-dev-server/reloader.js b/tgui/packages/tgui-dev-server/reloader.js index e33f7226b9e..1271e67d2a4 100644 --- a/tgui/packages/tgui-dev-server/reloader.js +++ b/tgui/packages/tgui-dev-server/reloader.js @@ -11,6 +11,7 @@ import { basename } from 'path'; import { promisify } from 'util'; import { resolveGlob, resolvePath } from './util.js'; import { regQuery } from './winreg.js'; +import { DreamSeeker } from './dreamseeker.js'; const logger = createLogger('reloader'); @@ -43,7 +44,7 @@ export const findCacheRoot = async () => { const paths = await resolveGlob(pattern); if (paths.length > 0) { cacheRoot = paths[0]; - logger.log(`found cache at '${cacheRoot}'`); + onCacheRootFound(cacheRoot); return cacheRoot; } } @@ -58,13 +59,19 @@ export const findCacheRoot = async () => { .replace(/\\$/, '') .replace(/\\/g, '/') + '/cache'; - logger.log(`found cache at '${cacheRoot}'`); + onCacheRootFound(cacheRoot); return cacheRoot; } } logger.log('found no cache directories'); }; +const onCacheRootFound = cacheRoot => { + logger.log(`found cache at '${cacheRoot}'`); + // Plant dummy + fs.closeSync(fs.openSync(cacheRoot + '/dummy', 'w')); +}; + export const reloadByondCache = async bundleDir => { const cacheRoot = await findCacheRoot(); if (!cacheRoot) { @@ -76,10 +83,16 @@ export const reloadByondCache = async bundleDir => { logger.log('found no tmp folder in cache'); return; } - const assets = await resolveGlob(bundleDir, './*.+(bundle|hot-update).*'); + // Get dreamseeker instances + const pids = cacheDirs.map(cacheDir => ( + parseInt(cacheDir.split('/cache/tmp').pop(), 10) + )); + const dssPromise = DreamSeeker.getInstancesByPids(pids); + // Copy assets + const assets = await resolveGlob(bundleDir, './*.+(bundle|chunk|hot-update).*'); for (let cacheDir of cacheDirs) { // Clear garbage - const garbage = await resolveGlob(cacheDir, './*.+(bundle|hot-update).*'); + const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*'); for (let file of garbage) { await promisify(fs.unlink)(file); } @@ -90,4 +103,15 @@ export const reloadByondCache = async bundleDir => { } logger.log(`copied ${assets.length} files to '${cacheDir}'`); } + // Notify dreamseeker + const dss = await dssPromise; + if (dss.length > 0) { + logger.log(`notifying dreamseeker`); + for (let dreamseeker of dss) { + dreamseeker.topic({ + tgui: 1, + type: 'cacheReloaded', + }); + } + } }; diff --git a/tgui/packages/tgui-dev-server/webpack.js b/tgui/packages/tgui-dev-server/webpack.js index c6258274097..7a1f6002532 100644 --- a/tgui/packages/tgui-dev-server/webpack.js +++ b/tgui/packages/tgui-dev-server/webpack.js @@ -9,7 +9,7 @@ import fs from 'fs'; import { createRequire } from 'module'; import { promisify } from 'util'; import webpack from 'webpack'; -import { broadcastMessage, loadSourceMaps, setupLink } from './link/server.js'; +import { loadSourceMaps, setupLink } from './link/server.js'; import { reloadByondCache } from './reloader.js'; import { resolveGlob } from './util.js'; @@ -44,7 +44,7 @@ export const setupWebpack = async config => { // Reload cache await reloadByondCache(bundleDir); // Notify all clients that update has happened - broadcastMessage(link, { + link.broadcastMessage({ type: 'hotUpdate', }); }); @@ -55,6 +55,9 @@ export const setupWebpack = async config => { logger.error('compilation error', err); return; } - logger.log(stats.toString(config.devServer.stats)); + stats + .toString(config.devServer.stats) + .split('\n') + .forEach(line => logger.log(line)); }); }; diff --git a/tgui/packages/tgui-panel/Notifications.js b/tgui/packages/tgui-panel/Notifications.js new file mode 100644 index 00000000000..a64ddd8e306 --- /dev/null +++ b/tgui/packages/tgui-panel/Notifications.js @@ -0,0 +1,41 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { Flex } from 'tgui/components'; + +export const Notifications = props => { + const { children } = props; + return ( +
+ {children} +
+ ); +}; + +const NotificationsItem = props => { + const { + rightSlot, + children, + } = props; + return ( + + + {children} + + {rightSlot && ( + + {rightSlot} + + )} + + ); +}; + +Notifications.Item = NotificationsItem; diff --git a/tgui/packages/tgui-panel/Panel.js b/tgui/packages/tgui-panel/Panel.js new file mode 100644 index 00000000000..d86a1fc8e86 --- /dev/null +++ b/tgui/packages/tgui-panel/Panel.js @@ -0,0 +1,142 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { Button, Flex, Section } from 'tgui/components'; +import { Pane } from 'tgui/layouts'; +import { NowPlayingWidget, useAudio } from './audio'; +import { ChatPanel, ChatTabs } from './chat'; +import { useGame } from './game'; +import { Notifications } from './Notifications'; +import { PingIndicator } from './ping'; +import { SettingsPanel, useSettings } from './settings'; + +export const Panel = (props, context) => { + if (Byond.IS_LTE_IE8) { + return ( + + ); + } + const audio = useAudio(context); + const settings = useSettings(context); + const game = useGame(context); + if (process.env.NODE_ENV !== 'production') { + const { useDebug, KitchenSink } = require('tgui/debug'); + const debug = useDebug(context); + if (debug.kitchenSink) { + return ( + + ); + } + } + return ( + + + +
+ + + + + + + + +
+
+ {audio.visible && ( + +
+ +
+
+ )} + {settings.visible && ( + + + + )} + +
+ + + + + {game.connectionLostAt && ( + Byond.command('.reconnect')}> + Reconnect + + )}> + You are either AFK, experiencing lag or the connection + has closed. + + )} + {game.roundRestartedAt && ( + + The connection has been closed because the server is + restarting. Please wait while you automatically reconnect. + + )} + +
+
+
+
+ ); +}; + +// IE8: Needs special treatment +const HoboIE8Panel = (props, context) => { + const settings = useSettings(context); + return ( + + + + {settings.visible && ( + + + + ) || ( + + )} + + + ); +}; diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js new file mode 100644 index 00000000000..654f2a6b7cf --- /dev/null +++ b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js @@ -0,0 +1,69 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { toFixed } from 'common/math'; +import { useDispatch, useSelector } from 'common/redux'; +import { Fragment } from 'inferno'; +import { Button, Flex, Knob } from 'tgui/components'; +import { useSettings } from '../settings'; +import { selectAudio } from './selectors'; + +export const NowPlayingWidget = (props, context) => { + const audio = useSelector(context, selectAudio); + const dispatch = useDispatch(context); + const settings = useSettings(context); + const title = audio.meta?.title; + return ( + + {audio.playing && ( + + + Now playing: + + + {title || 'Unknown Track'} + + + ) || ( + + Nothing to play. + + )} + {audio.playing && ( + + + + + +
+ {MESSAGE_TYPES + .filter(typeDef => !typeDef.important && !typeDef.admin) + .map(typeDef => ( + dispatch(toggleAcceptedType({ + pageId: page.id, + type: typeDef.type, + }))}> + {typeDef.name} + + ))} + + {MESSAGE_TYPES + .filter(typeDef => !typeDef.important && typeDef.admin) + .map(typeDef => ( + dispatch(toggleAcceptedType({ + pageId: page.id, + type: typeDef.type, + }))}> + {typeDef.name} + + ))} + +
+ + ); +}; diff --git a/tgui/packages/tgui-panel/chat/ChatPanel.js b/tgui/packages/tgui-panel/chat/ChatPanel.js new file mode 100644 index 00000000000..bf7eb90e6fb --- /dev/null +++ b/tgui/packages/tgui-panel/chat/ChatPanel.js @@ -0,0 +1,71 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { shallowDiffers } from 'common/react'; +import { Component, createRef, Fragment } from 'inferno'; +import { Button } from 'tgui/components'; +import { chatRenderer } from './renderer'; + +export class ChatPanel extends Component { + constructor() { + super(); + this.ref = createRef(); + this.state = { + scrollTracking: true, + }; + this.handleScrollTrackingChange = value => this.setState({ + scrollTracking: value, + }); + } + + componentDidMount() { + chatRenderer.mount(this.ref.current); + chatRenderer.events.on('scrollTrackingChanged', + this.handleScrollTrackingChange); + this.componentDidUpdate(); + } + + componentWillUnmount() { + chatRenderer.events.off('scrollTrackingChanged', + this.handleScrollTrackingChange); + } + + componentDidUpdate(prevProps) { + requestAnimationFrame(() => { + chatRenderer.ensureScrollTracking(); + }); + const shouldUpdateStyle = ( + !prevProps || shallowDiffers(this.props, prevProps) + ); + if (shouldUpdateStyle) { + chatRenderer.assignStyle({ + 'width': '100%', + 'white-space': 'pre-wrap', + 'font-size': this.props.fontSize, + 'line-height': this.props.lineHeight, + }); + } + } + + render() { + const { + scrollTracking, + } = this.state; + return ( + +
+ {!scrollTracking && ( + + )} + + ); + } +} diff --git a/tgui/packages/tgui-panel/chat/ChatTabs.js b/tgui/packages/tgui-panel/chat/ChatTabs.js new file mode 100644 index 00000000000..a0e6cc59e52 --- /dev/null +++ b/tgui/packages/tgui-panel/chat/ChatTabs.js @@ -0,0 +1,61 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { useDispatch, useSelector } from 'common/redux'; +import { Box, Tabs, Flex, Button } from 'tgui/components'; +import { changeChatPage, addChatPage } from './actions'; +import { selectChatPages, selectCurrentChatPage } from './selectors'; +import { openChatSettings } from '../settings/actions'; + +const UnreadCountWidget = ({ value }) => ( + + {Math.min(value, 99)} + +); + +export const ChatTabs = (props, context) => { + const pages = useSelector(context, selectChatPages); + const currentPage = useSelector(context, selectCurrentChatPage); + const dispatch = useDispatch(context); + return ( + + + + {pages.map(page => ( + 0 && ( + + )} + onClick={() => dispatch(changeChatPage({ + pageId: page.id, + }))}> + {page.name} + + ))} + + + +