diff --git a/baystation12.dme b/baystation12.dme
index 58bdac0084088..f2e1be2e60e8f 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -180,6 +180,7 @@
#include "code\controllers\ProcessScheduler\core\process.dm"
#include "code\controllers\ProcessScheduler\core\processScheduler.dm"
#include "code\controllers\subsystems\atoms.dm"
+#include "code\controllers\subsystems\chat.dm"
#include "code\controllers\subsystems\event.dm"
#include "code\controllers\subsystems\garbage.dm"
#include "code\controllers\subsystems\inactivity.dm"
@@ -1196,6 +1197,7 @@
#include "code\modules\client\client_defines.dm"
#include "code\modules\client\client_helpers.dm"
#include "code\modules\client\client_procs.dm"
+#include "code\modules\client\darkmode.dm"
#include "code\modules\client\movement.dm"
#include "code\modules\client\preferences.dm"
#include "code\modules\client\preferences_factions.dm"
@@ -1427,6 +1429,8 @@
#include "code\modules\games\tarot.dm"
#include "code\modules\genetics\side_effects.dm"
#include "code\modules\ghosttrap\trap.dm"
+#include "code\modules\goonchat\_helpers.dm"
+#include "code\modules\goonchat\browserOutput.dm"
#include "code\modules\halo\_defs_radio_speech_sizes.dm"
#include "code\modules\halo\difficulty_setting.dm"
#include "code\modules\halo\languages.dm"
diff --git a/code/__defines/colors.dm b/code/__defines/colors.dm
index 9f8685b2a24ca..6f96417667d7f 100644
--- a/code/__defines/colors.dm
+++ b/code/__defines/colors.dm
@@ -160,4 +160,9 @@
#define CODEX_COLOR_MECHANICS "#9ebcd8"
#define CODEX_COLOR_ANTAG "#e5a2a2"
-#define COLOR_WEBHOOK_DEFAULT 0x8bbbd5
\ No newline at end of file
+
+#define COLOR_WEBHOOK_DEFAULT 0x8bbbd5
+
+#define COLOR_DARKMODE_BACKGROUND "#202020"
+#define COLOR_DARKMODE_DARKBACKGROUND "#171717"
+#define COLOR_DARKMODE_TEXT "#a4bad6"
diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index a49ead149916b..1e9db4c02a26f 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -207,4 +207,11 @@
#define REQUEST_LIBRARY_LOCATION (world.system_type == MS_WINDOWS ? "lib/gotbyond.dll" : "lib/gotbyond.so")
//Elevation Defines//
-#define BASE_ELEVATION 0
\ No newline at end of file
+#define BASE_ELEVATION 0
+#ifndef HTTP_POST_DLL_LOCATION
+#define HTTP_POST_DLL_LOCATION (world.system_type == MS_WINDOWS ? WINDOWS_HTTP_POST_DLL_LOCATION : UNIX_HTTP_POST_DLL_LOCATION)
+#endif
+
+//Misc text define. Does 4 spaces. Used as a makeshift tabulator.
+#define FOURSPACES " "
+#define CLIENT_FROM_VAR(I) (ismob(I) ? I:client : (isclient(I) ? I : (istype(I, /datum/mind) ? I:current?:client : null)))
diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm
index 22cdb478bea2e..fcb76eb0da20f 100644
--- a/code/__defines/subsystems.dm
+++ b/code/__defines/subsystems.dm
@@ -86,6 +86,8 @@
#define SS_INIT_TICKER -20
#define SS_INIT_AI -21
#define SS_INIT_AIFAST -22
+
+
#define SS_INIT_CHAT -90 // Should be lower to ensure chat remains smooth during init.
#define SS_INIT_UNIT_TESTS -100
diff --git a/code/_helpers/game.dm b/code/_helpers/game.dm
index 8b82e6d179825..4ce5729c41990 100644
--- a/code/_helpers/game.dm
+++ b/code/_helpers/game.dm
@@ -573,3 +573,16 @@ datum/projectile_data
/proc/round_is_spooky(var/spookiness_threshold = config.cult_ghostwriter_req_cultists)
return (cult.current_antagonists.len > spookiness_threshold)
+
+/proc/getviewsize(view)
+ var/viewX
+ var/viewY
+ if(isnum(view))
+ var/totalviewrange = 1 + 2 * view
+ viewX = totalviewrange
+ viewY = totalviewrange
+ else
+ var/list/viewrangelist = splittext(view,"x")
+ viewX = text2num(viewrangelist[1])
+ viewY = text2num(viewrangelist[2])
+ return list(viewX, viewY)
diff --git a/code/_helpers/icons.dm b/code/_helpers/icons.dm
index 62649f7181bd1..94221581c6d5a 100644
--- a/code/_helpers/icons.dm
+++ b/code/_helpers/icons.dm
@@ -920,4 +920,3 @@ proc/generate_image(var/tx as num, var/ty as num, var/tz as num, var/range as nu
cap.Blend(img, blendMode2iconMode(A.blend_mode), A.pixel_x + xoff, A.pixel_y + yoff)
return cap
-
diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm
index a0bee74087573..55092c36a5f75 100644
--- a/code/_helpers/text.dm
+++ b/code/_helpers/text.dm
@@ -320,7 +320,7 @@ proc/TextPreview(var/string,var/len=40)
/proc/create_text_tag(var/tagname, var/tagdesc = tagname, var/client/C = null)
if(!(C && C.is_preference_enabled(/datum/client_preference/chat_tags)))
return tagdesc
- return ""
+ return icon2html(icon('./icons/chattags.dmi', tagname), world, realsize=TRUE, class="text_tag")
/proc/contains_az09(var/input)
for(var/i=1, i<=length(input), i++)
diff --git a/code/_macros.dm b/code/_macros.dm
index 76849bb0634d2..086274881a52e 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -79,15 +79,21 @@
#define to_target(target, payload) target << (payload)
#define from_target(target, receiver) target >> (receiver)
-#define to_chat(target, message) target << message
-#define to_world(message) world << message
-#define to_world_log(message) world.log << message
-#define sound_to(target, sound) target << sound
-#define to_file(file_entry, source_var) file_entry << source_var
-#define from_file(file_entry, target_var) file_entry >> target_var
-#define show_browser(target, browser_content, browser_name) target << browse(browser_content, browser_name)
-#define show_image(target, image) target << image
-#define send_rsc(target, rsc_content, rsc_name) target << browse_rsc(rsc_content, rsc_name)
+/// Common use
+#define legacy_chat(target, message) to_target(target, message)
+#define to_world(message) to_chat(world, message)
+#define to_world_log(message) to_target(world.log, message)
+#define sound_to(target, sound) to_target(target, sound)
+#define image_to(target, image) to_target(target, image)
+#define show_browser(target, content, title) to_target(target, browse(content, title))
+#define close_browser(target, title) to_target(target, browse(null, title))
+#define send_rsc(target, content, title) to_target(target, browse_rsc(content, title))
+#define send_link(target, url) to_target(target, link(url))
+#define send_output(target, msg, control) to_target(target, output(msg, control))
+#define from_file(file_entry, target_var) file_entry >> target_var
+#define to_file(handle, value) to_target(handle, value)
+#define to_save(handle, value) to_target(handle, value) //semantics
+#define from_save(handle, target_var) from_target(handle, target_var)
#define MAP_IMAGE_PATH "nano/images/[GLOB.using_map.path]/"
diff --git a/code/controllers/subsystems/chat.dm b/code/controllers/subsystems/chat.dm
new file mode 100644
index 0000000000000..db36885989807
--- /dev/null
+++ b/code/controllers/subsystems/chat.dm
@@ -0,0 +1,84 @@
+SUBSYSTEM_DEF(chat)
+ name = "Chat"
+ wait = 1
+ runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
+ priority = SS_PRIORITY_CHAT
+ init_order = SS_INIT_CHAT
+ var/list/payload = list()
+ var/initialized = 0
+
+/datum/controller/subsystem/chat/Initialize(timeofday)
+ initialized = 1
+ return ..()
+
+
+/datum/controller/subsystem/chat/fire()
+ for(var/i in payload)
+ var/client/C = i
+ to_target(C, output(payload[C], "browseroutput:output"))
+ payload -= C
+
+ if(MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/chat/proc/queue(target, message, handle_whitespace = TRUE, trailing_newline = TRUE)
+ if(!target || !message)
+ return
+
+ if(!istext(message))
+ CRASH("to_chat called with invalid input type")
+
+ 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))
+
+ if(islist(target))
+ for(var/I in target)
+ var/client/C = CLIENT_FROM_VAR(I) //Grab us a client if possible
+
+ if(!C)
+ return
+
+ //Send it to the old style output window.
+ legacy_chat(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.
+ legacy_chat(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
diff --git a/code/datums/communication/aooc.dm b/code/datums/communication/aooc.dm
index d01cd42b899b2..6bb708d8dd725 100644
--- a/code/datums/communication/aooc.dm
+++ b/code/datums/communication/aooc.dm
@@ -25,8 +25,8 @@
for(var/client/target in GLOB.clients)
if(target.holder)
- receive_communication(C, target, "[create_text_tag("aooc", "Antag-OOC:", target)] [get_options_bar(C, 0, 1, 1)]:[message]")
+ receive_communication(C, target, "[create_text_tag("aooc", "Antag-OOC:", target)] [get_options_bar(C, 0, 1, 1)]:[message]")
else if(target.mob && target.mob.mind && target.mob.mind.special_role)
var/display_name = C.key
var/player_display = holder ? "[display_name]([usr.client.holder.rank])" : display_name
- receive_communication(C, target, "[create_text_tag("aooc", "Antag-OOC:", target)] [player_display]:[message]")
\ No newline at end of file
+ receive_communication(C, target, "[create_text_tag("aooc", "Antag-OOC:", target)] [player_display]:[message]")
\ No newline at end of file
diff --git a/code/datums/communication/dsay.dm b/code/datums/communication/dsay.dm
index e5ea6cfc1b904..51c4742fd7006 100644
--- a/code/datums/communication/dsay.dm
+++ b/code/datums/communication/dsay.dm
@@ -89,10 +89,10 @@
/decl/dsay_communication/proc/get_message(var/client/C, var/mob/M, var/message)
var say_verb = pick("complains","moans","whines","laments","blubbers","copes","seethes","malds")
- return "[get_name(C, M)] [say_verb], \"[message]\""
+ return "[get_name(C, M)] [say_verb], \"[message]\""
/decl/dsay_communication/emote/get_message(var/client/C, var/mob/M, var/message)
- return "[get_name(C, M)] [message]"
+ return "[get_name(C, M)] [message]"
/decl/dsay_communication/proc/adjust_channel(var/decl/communication_channel/dsay)
dsay.flags |= COMMUNICATION_ADMIN_FOLLOW|COMMUNICATION_GHOST_FOLLOW // Add admin and ghost follow
@@ -115,7 +115,7 @@
/decl/dsay_communication/admin/get_message(var/client/communicator, var/mob/M, var/message)
var/stafftype = uppertext(communicator.holder.rank)
- return "[stafftype]([communicator.key]) says, \"[message]\""
+ return "[stafftype]([communicator.key]) says, \"[message]\""
/decl/dsay_communication/admin/adjust_channel(var/decl/communication_channel/dsay)
dsay.log_proc = /proc/log_say
diff --git a/code/datums/communication/ooc.dm b/code/datums/communication/ooc.dm
index 576c9446be929..605d8f480c2b6 100644
--- a/code/datums/communication/ooc.dm
+++ b/code/datums/communication/ooc.dm
@@ -41,7 +41,7 @@
for(var/client/target in GLOB.clients)
if(target.is_key_ignored(C.key)) // If we're ignored by this person, then do nothing.
continue
- var/sent_message = "[create_text_tag("ooc", "OOC:", target)] [C.key]:[message]"
+ var/sent_message = "[create_text_tag("ooc", "OOC:", target)] [C.key]:[message]"
if(can_badmin)
receive_communication(C, target, "[sent_message]")
else
diff --git a/code/datums/communication/pray.dm b/code/datums/communication/pray.dm
index 21ef30cb2a314..058f3266c67bc 100644
--- a/code/datums/communication/pray.dm
+++ b/code/datums/communication/pray.dm
@@ -11,10 +11,10 @@
var/mob/M = m
if(!M.client)
continue
- if(M.client.holder && M.client.is_preference_enabled(/datum/client_preference/admin/show_chat_prayers))
- receive_communication(communicator, M, "\[SC\] \[TAKE\]\icon[cross] PRAY: [key_name(communicator, 1)]: [message]")
+ if(M.client.holder)
+ receive_communication(communicator, M, "\[SC\] \[DN\][icon2html(cross, M)] PRAY: [key_name(communicator, 1)]: [message]")
else if(communicator == M) //Give it to ourselves
- receive_communication(communicator, M, "\icon[cross] You send the prayer, \"[message]\" out into the heavens.")
+ receive_communication(communicator, M, "[icon2html(cross, M)] You send the prayer, \"[message]\" out into the heavens.")
/decl/communication_channel/pray/receive_communication(var/mob/communicator, var/mob/receiver, var/message)
..()
diff --git a/code/datums/wires/camera.dm b/code/datums/wires/camera.dm
index 1724b9469cb9f..d50864435e963 100644
--- a/code/datums/wires/camera.dm
+++ b/code/datums/wires/camera.dm
@@ -61,7 +61,7 @@ var/const/CAMERA_WIRE_NOTHING2 = 32
C.light_disabled = !C.light_disabled
if(CAMERA_WIRE_ALARM)
- C.visible_message("\icon[C] *beep*", "\icon[C] *beep*")
+ C.visible_message("[icon2html(C,viewers(C))] *beep*", "[icon2html(C,viewers(C))] *beep*")
return
/datum/wires/camera/proc/CanDeconstruct()
diff --git a/code/datums/wires/particle_accelerator.dm b/code/datums/wires/particle_accelerator.dm
index 83562386922df..2eb3589c312a6 100644
--- a/code/datums/wires/particle_accelerator.dm
+++ b/code/datums/wires/particle_accelerator.dm
@@ -28,7 +28,7 @@ var/const/PARTICLE_LIMIT_POWER_WIRE = 8 // Determines how strong the PA can be.
C.interface_control = !C.interface_control
if(PARTICLE_LIMIT_POWER_WIRE)
- C.visible_message("\icon[C][C] makes a large whirring noise.")
+ C.visible_message("[icon2html(C,viewers(C))][C] makes a large whirring noise.")
/datum/wires/particle_acc/control_box/UpdateCut(var/index, var/mended)
var/obj/machinery/particle_accelerator/control_box/C = holder
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 6262d6017d76c..387830af07e3a 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -252,7 +252,7 @@ its easier to just keep the beam vertical.
else
f_name += "oil-stained [name][infix]."
- to_chat(user, "\icon[src] That's [f_name] [suffix]")
+ to_chat(user, "[icon2html(src, user)] That's [f_name] [suffix]")
to_chat(user, desc)
return distance == -1 || (get_dist(src, user) <= distance)
diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index ace3322caa86d..894b3a95128ce 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -290,7 +290,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/
end_call()
if (caller_id&&sourcepad)
if(caller_id.loc!=sourcepad.loc)
- sourcepad.to_chat(caller_id, "Severing connection to distant holopad.")
+ to_chat(sourcepad.caller_id, "Severing connection to distant holopad.")
end_call()
audible_message("The connection has been terminated by the caller.")
return 1
diff --git a/code/game/machinery/kitchen/icecream.dm b/code/game/machinery/kitchen/icecream.dm
index a63720c1ecc9b..3c3a288dfe645 100644
--- a/code/game/machinery/kitchen/icecream.dm
+++ b/code/game/machinery/kitchen/icecream.dm
@@ -104,7 +104,7 @@
var/obj/item/weapon/reagent_containers/food/snacks/icecream/I = O
if(!I.ice_creamed)
if(product_types[dispense_flavour] > 0)
- src.visible_message("\icon[src] [user] scoops delicious [flavour_name] icecream into [I].")
+ src.visible_message("[icon2html(src, viewers(src))] [user] scoops delicious [flavour_name] icecream into [I].")
product_types[dispense_flavour] -= 1
I.add_ice_cream(flavour_name)
// if(beaker)
diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm
index af30de08d578f..093d39d2db1cd 100644
--- a/code/game/machinery/requests_console.dm
+++ b/code/game/machinery/requests_console.dm
@@ -169,7 +169,8 @@ var/list/obj/machinery/requests_console/allConsoles = list()
screen = RCS_SENTPASS
message_log += "Message sent to [recipient] [message]"
else
- audible_message(text("\icon[src] *The Requests Console beeps: 'NOTICE: No server detected!'"),,4)
+ audible_message(text("[icon2html(src, viewers(src))] *The Requests Console beeps: 'NOTICE: No server detected!'"),,4)
+
//Handle screen switching
if(href_list["setScreen"])
diff --git a/code/game/machinery/vending.dm b/code/game/machinery/vending.dm
index 1797ddc726edb..305628ebac0cc 100644
--- a/code/game/machinery/vending.dm
+++ b/code/game/machinery/vending.dm
@@ -235,7 +235,7 @@
if(currently_vending.price > cashmoney.worth)
// This is not a status display message, since it's something the character
// themselves is meant to see BEFORE putting the money in
- to_chat(usr, "\icon[cashmoney] That is not enough money.")
+ to_chat(usr, "[icon2html(cashmoney, usr)] That is not enough money.")
return 0
visible_message("\The [usr] inserts some cash into \the [src].")
diff --git a/code/game/objects/effects/mines.dm b/code/game/objects/effects/mines.dm
index 6eee1b8c93b4d..ca263b1bc90cd 100644
--- a/code/game/objects/effects/mines.dm
+++ b/code/game/objects/effects/mines.dm
@@ -22,7 +22,7 @@
if(istype(M, /mob/living/carbon/human))
for(var/mob/O in viewers(world.view, src.loc))
- to_chat(O, "\The [M] triggered the \icon[src] [src]")
+ to_chat(O, "\The [M] triggered the [icon2html(src, O)] [src]")
triggered = 1
call(src,triggerproc)(M)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index dcd5a7b79603f..3b8c083375a20 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -705,5 +705,20 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
/obj/item/proc/can_use_when_prone()
return (w_class <= ITEM_SIZE_NORMAL)
+
/obj/item/proc/can_embed()
return 1
+
+/obj/item/proc/get_examine_line()
+ if(blood_color)
+ . = "[icon2html(src, viewers(src))] [gender==PLURAL?"some":"a"] stained [src]"
+ else
+ . = "[icon2html(src, viewers(src))] \a [src]"
+ var/ID = GetIdCard()
+ if(ID)
+ . += " \[Look at ID\]"
+
+/obj/item/proc/on_active_hand()
+
+/obj/item/proc/has_embedded()
+ return
diff --git a/code/game/objects/items/devices/geiger.dm b/code/game/objects/items/devices/geiger.dm
index 31a6853a471cd..4d81cd905f74c 100644
--- a/code/game/objects/items/devices/geiger.dm
+++ b/code/game/objects/items/devices/geiger.dm
@@ -33,7 +33,7 @@
/obj/item/device/geiger/attack_self(var/mob/user)
scanning = !scanning
update_icon()
- to_chat(user, "\icon[src] You switch [scanning ? "on" : "off"] [src].")
+ to_chat(user, "[icon2html(src, user)] You switch [scanning ? "on" : "off"] [src].")
/obj/item/device/geiger/update_icon()
if(!scanning)
diff --git a/code/game/objects/items/devices/hacktool.dm b/code/game/objects/items/devices/hacktool.dm
index eb7d5012c3489..6207699cba6e2 100644
--- a/code/game/objects/items/devices/hacktool.dm
+++ b/code/game/objects/items/devices/hacktool.dm
@@ -44,7 +44,7 @@
to_chat(user, "You are already hacking!")
return 1
if(!is_type_in_list(target, supported_types))
- to_chat(user, "\icon[src] Unable to hack this target.")
+ to_chat(user, "[icon2html(src, user)] Unable to hack this target.")
return 0
var/found = known_targets.Find(target)
if(found)
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index 83f445f8be164..daf9b0e6204eb 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -489,7 +489,7 @@ GLOBAL_LIST_EMPTY(all_radios)
var/image/I = image('icons/effects/effects.dmi', src, "empdisable")
overlays += I
- show_image(src.loc, I)
+ image_to(src.loc, I)
spawn(10)
overlays -= I
qdel(I)
diff --git a/code/game/objects/items/weapons/cards_ids.dm b/code/game/objects/items/weapons/cards_ids.dm
index dde46cb72e7d8..b95ebf8875b9e 100644
--- a/code/game/objects/items/weapons/cards_ids.dm
+++ b/code/game/objects/items/weapons/cards_ids.dm
@@ -211,8 +211,8 @@ var/const/NO_EMAG_ACT = -50
return jointext(dat,null)
/obj/item/weapon/card/id/attack_self(mob/user as mob)
- user.visible_message("\The [user] shows you: \icon[src] [src.name]. The assignment on the card: [src.assignment]",\
- "You flash your ID card: \icon[src] [src.name]. The assignment on the card: [src.assignment]")
+ user.visible_message("\The [user] shows you: [icon2html(src, viewers(src))] [src.name]. The assignment on the card: [src.assignment]",\
+ "You flash your ID card: [icon2html(src, viewers(src))] [src.name]. The assignment on the card: [src.assignment]")
src.add_fingerprint(user)
return
@@ -228,7 +228,7 @@ var/const/NO_EMAG_ACT = -50
set category = "Object"
set src in usr
- to_chat(usr, text("\icon[] []: The current assignment on the card is [].", src, src.name, src.assignment))
+ to_chat(usr, text("[icon2html(src, usr)] []: The current assignment on the card is [].", src.name, src.assignment))
to_chat(usr, "The blood type on the card is [blood_type].")
to_chat(usr, "The DNA hash on the card is [dna_hash].")
to_chat(usr, "The fingerprint hash on the card is [fingerprint_hash].")
diff --git a/code/game/objects/items/weapons/extinguisher.dm b/code/game/objects/items/weapons/extinguisher.dm
index 1283f8d379798..a354b096e98bf 100644
--- a/code/game/objects/items/weapons/extinguisher.dm
+++ b/code/game/objects/items/weapons/extinguisher.dm
@@ -39,10 +39,10 @@
reagents.add_reagent(/datum/reagent/water, max_water)
..()
-/obj/item/weapon/extinguisher/examine(mob/user)
- if(..(user, 0))
- to_chat(user, text("\icon[] [] contains [] units of water left!", src, src.name, src.reagents.total_volume))
- return
+/obj/item/weapon/extinguisher/examine(mob/user, distance)
+ . = ..()
+ if(distance <= 0)
+ to_chat(user, text("[icon2html(src, viewers(src))] [] contains [] units of water left!", src, src.reagents.total_volume))
/obj/item/weapon/extinguisher/attack_self(mob/user as mob)
safety = !safety
@@ -61,10 +61,10 @@
src.last_use = world.time
reagents.splash(M, min(reagents.total_volume, spray_amount))
-
+
user.visible_message("\The [user] sprays \the [M] with \the [src].")
playsound(src.loc, 'sound/effects/extinguish.ogg', 75, 1, -3)
-
+
return 1 // No afterattack
return ..()
diff --git a/code/game/objects/items/weapons/tanks/tanks.dm b/code/game/objects/items/weapons/tanks/tanks.dm
index a3ec570e987aa..bd74ff5511980 100644
--- a/code/game/objects/items/weapons/tanks/tanks.dm
+++ b/code/game/objects/items/weapons/tanks/tanks.dm
@@ -459,7 +459,7 @@ var/list/global/tank_gauge_cache = list()
return
T.assume_air(air_contents)
playsound(get_turf(src), 'sound/weapons/gunshot/shotgun.ogg', 20, 1)
- visible_message("\icon[src] \The [src] flies apart!", "You hear a bang!")
+ visible_message("[icon2html(src, viewers(src))] \The [src] flies apart!", "You hear a bang!")
T.hotspot_expose(air_contents.temperature, 70, 1)
@@ -496,10 +496,12 @@ var/list/global/tank_gauge_cache = list()
//dynamic air release based on ambient pressure
T.assume_air(leaked_gas)
+
if(!leaking)
visible_message("\icon[src] \The [src] relief valve flips open with a hiss!", "You hear hissing.")
playsound(src.loc, 'sound/effects/spray.ogg', 10, 1, -3)
leaking = 1
+
#ifdef FIREDBG
log_debug("[x],[y] tank is leaking: [pressure] kPa, integrity [integrity]")
#endif
diff --git a/code/game/objects/items/weapons/weldbackpack.dm b/code/game/objects/items/weapons/weldbackpack.dm
index cdc3526543e17..61135f42c3936 100644
--- a/code/game/objects/items/weapons/weldbackpack.dm
+++ b/code/game/objects/items/weapons/weldbackpack.dm
@@ -50,6 +50,5 @@
return
/obj/item/weapon/weldpack/examine(mob/user)
- . = ..(user)
- to_chat(user, text("\icon[] [] units of fuel left!", src, src.reagents.total_volume))
- return
+ . = ..()
+ to_chat(user, text("[icon2html(src, user)] [] units of fuel left!", src.reagents.total_volume))
diff --git a/code/game/objects/structures/janicart.dm b/code/game/objects/structures/janicart.dm
index 672f4d85a13cf..ad20648a27c02 100644
--- a/code/game/objects/structures/janicart.dm
+++ b/code/game/objects/structures/janicart.dm
@@ -18,11 +18,10 @@
/obj/structure/janitorialcart/New()
create_reagents(180)
-
-/obj/structure/janitorialcart/examine(mob/user)
- if(..(user, 1))
- to_chat(user, "[src] \icon[src] contains [reagents.total_volume] unit\s of liquid!")
- //everything else is visible, so doesn't need to be mentioned
+/obj/structure/janitorialcart/examine(mob/user, distance)
+ . = ..()
+ if(distance <= 1)
+ to_chat(user, "[src] [icon2html(src, viewers(src))] contains [reagents.total_volume] unit\s of liquid!")
/obj/structure/janitorialcart/attackby(obj/item/I, mob/user)
@@ -184,7 +183,7 @@
if(!..(user, 1))
return
- to_chat(user, "\icon[src] This [callme] contains [reagents.total_volume] unit\s of water!")
+ to_chat(user, "[icon2html(src, user)] This [callme] contains [reagents.total_volume] unit\s of water!")
if(mybag)
to_chat(user, "\A [mybag] is hanging on the [callme].")
diff --git a/code/game/objects/structures/mop_bucket.dm b/code/game/objects/structures/mop_bucket.dm
index 6c98097d96cb8..d3994deb1b31b 100644
--- a/code/game/objects/structures/mop_bucket.dm
+++ b/code/game/objects/structures/mop_bucket.dm
@@ -13,9 +13,11 @@
create_reagents(180)
..()
-/obj/structure/mopbucket/examine(mob/user)
- if(..(user, 1))
- to_chat(user, "[src] \icon[src] contains [reagents.total_volume] unit\s of water!")
+/obj/structure/mopbucket/examine(mob/user, distance)
+ . = ..()
+ if(distance <= 1)
+ to_chat(user, "[src] [icon2html(src, user)] contains [reagents.total_volume] unit\s of water!")
+
/obj/structure/mopbucket/attackby(obj/item/I, mob/user)
if(istype(I, /obj/item/weapon/mop))
diff --git a/code/game/objects/weapons.dm b/code/game/objects/weapons.dm
index 44d582249f9f6..649d9efa24b98 100644
--- a/code/game/objects/weapons.dm
+++ b/code/game/objects/weapons.dm
@@ -63,6 +63,10 @@
if(isnull(item_to_disintegrate))
return 1
+ //Once we have all the references, let's make sure we're not cutting an npc in half.
+ if(item_to_disintegrate == mob_holding_disintegrated)
+ return 1
+
if(!isnull(item_to_disintegrate) && istype(item_to_disintegrate,/obj/item/weapon/gun) && !prob(BASE_PARRY_PLASMA_DESTROY))
force_half_damage = 1
diff --git a/code/game/verbs/ooc.dm b/code/game/verbs/ooc.dm
index c3267e7d23dad..e5500d99c0471 100644
--- a/code/game/verbs/ooc.dm
+++ b/code/game/verbs/ooc.dm
@@ -10,3 +10,84 @@
set category = "OOC"
sanitize_and_communicate(/decl/communication_channel/ooc/looc, src, message)
+
+/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/chatOutput(src)
+ chatOutput.start()
+ action = alert(src, "Goon chat reloading, wait a bit and tell me if it's fixed", "", "Fixed", "Nope")
+ if (action == "Fixed")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by re-creating the chatOutput datum")
+ else
+ chatOutput.load()
+ action = alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No")
+ if (action == "Yes")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by re-creating the chatOutput datum and forcing a load()")
+ else
+ action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat")
+ if (action == "Switch to old chat")
+ winset(src, "output", "is-visible=true;is-disabled=false")
+ winset(src, "browseroutput", "is-visible=false")
+ log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window after recreating the chatOutput and forcing a load()")
+
+ else if (chatOutput.loaded)
+ var/action = alert(src, "ChatOutput seems to be loaded\nDo you want me to force a reload, wiping the chat log or just refresh the chat window because it broke/went away?", "Hmmm", "Force Reload", "Refresh", "Cancel")
+ switch (action)
+ if ("Force Reload")
+ chatOutput.loaded = FALSE
+ chatOutput.start() //this is likely to fail since it asks , but we should try it anyways so we know.
+ action = alert(src, "Goon chat reloading, wait a bit and tell me if it's fixed", "", "Fixed", "Nope")
+ if (action == "Fixed")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a start()")
+ else
+ chatOutput.load()
+ action = alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No")
+ if (action == "Yes")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a load()")
+ else
+ action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat")
+ if (action == "Switch to old chat")
+ winset(src, "output", "is-visible=true;is-disabled=false")
+ winset(src, "browseroutput", "is-visible=false")
+ log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window forcing a start() and forcing a load()")
+
+ if ("Refresh")
+ chatOutput.showChat()
+ action = alert(src, "Goon chat refreshing, wait a bit and tell me if it's fixed", "", "Fixed", "Nope, force a reload")
+ if (action == "Fixed")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a show()")
+ else
+ chatOutput.loaded = FALSE
+ chatOutput.load()
+ action = alert(src, "How about now? (give it a moment)", "", "Yes", "No")
+ if (action == "Yes")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by forcing a load()")
+ else
+ action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat")
+ if (action == "Switch to old chat")
+ winset(src, "output", "is-visible=true;is-disabled=false")
+ winset(src, "browseroutput", "is-visible=false")
+ log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window forcing a show() and forcing a load()")
+ return
+
+ else
+ chatOutput.start()
+ var/action = alert(src, "Manually loading Chat, wait a bit and tell me if it's fixed", "", "Fixed", "Nope")
+ if (action == "Fixed")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by manually calling start()")
+ else
+ chatOutput.load()
+ alert(src, "How about now? (give it a moment (it may also try to load twice))", "", "Yes", "No")
+ if (action == "Yes")
+ log_game("GOONCHAT: [key_name(src)] Had to fix their goonchat by manually calling start() and forcing a load()")
+ else
+ action = alert(src, "Welp, I'm all out of ideas. Try closing byond and reconnecting.\nWe could also disable fancy chat and re-enable oldchat", "", "Thanks anyways", "Switch to old chat")
+ if (action == "Switch to old chat")
+ winset(src, "output", list2params(list("on-show" = "", "is-disabled" = "false", "is-visible" = "true")))
+ winset(src, "browseroutput", "is-disabled=true;is-visible=false")
+ log_game("GOONCHAT: [key_name(src)] Failed to fix their goonchat window after manually calling start() and forcing a load()")
diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index 75e4299a830b0..8dd9bac66ab36 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -125,13 +125,17 @@
var/sender_message = "" + create_text_tag("pm_out_alt", "PM", src) + " to [get_options_bar(C, holder ? 1 : 0, holder ? 1 : 0, 1)]"
if(holder)
sender_message += " ([(ticket.status == TICKET_OPEN) ? "TAKE" : "JOIN"]) (CLOSE)"
+
sender_message += ": [msg]"
+
to_chat(src, sender_message)
var/receiver_message = "" + create_text_tag("pm_in", "", C) + " \[[recieve_pm_type] PM\][get_options_bar(src, C.holder ? 1 : 0, C.holder ? 1 : 0, 1)]"
if(C.holder)
receiver_message += " ([(ticket.status == TICKET_OPEN) ? "TAKE" : "JOIN"]) (CLOSE)"
+
receiver_message += ": [msg]"
+
to_chat(C, receiver_message)
//play the recieving admin the adminhelp sound (if they have them enabled)
@@ -150,8 +154,10 @@
//check client/X is an admin and isn't the sender or recipient
if(X == C || X == src)
continue
- if(X.key != key && X.key != C.key && (X.holder.rights & R_ADMIN|R_MOD|R_MENTOR))
- to_chat(X, "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0, ticket)] to [key_name(C, X, 0, ticket)] ([(ticket.status == TICKET_OPEN) ? "TAKE" : "JOIN"]) (CLOSE): [msg]")
+
+ if(X.key != key && X.key != C.key && (X.holder.rights & R_ADMIN|R_MOD))
+ to_chat(X, "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0, ticket)] to [key_name(C, X, 0, ticket)] ([(ticket.status == TICKET_OPEN) ? "TAKE" : "JOIN"]) (CLOSE): [msg]")
+
/client/proc/cmd_admin_irc_pm(sender)
if(prefs.muted & MUTE_ADMINHELP)
@@ -173,9 +179,9 @@
ahelp2discord(src, sender, html_decode(msg))
admin_pm_repository.store_pm(src, "IRC-[sender]", msg)
- to_chat(src, "" + create_text_tag("pm_out_alt", "PM", src) + " to [sender]: [msg]")
+ to_chat(src, "" + create_text_tag("pm_out_alt", "PM", src) + " to [sender]: [msg]")
for(var/client/X in GLOB.admins)
if(X == src)
continue
if(X.holder.rights & R_ADMIN|R_MOD)
- to_chat(X, "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0)] to [sender]: [msg]")
+ to_chat(X, "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0)] to [sender]: [msg]")
diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm
index 932c0b74c8c8d..0705723a2a471 100644
--- a/code/modules/admin/verbs/adminsay.dm
+++ b/code/modules/admin/verbs/adminsay.dm
@@ -12,7 +12,7 @@
if(check_rights(R_ADMIN,0))
for(var/client/C in GLOB.admins)
if(R_ADMIN & C.holder.rights)
- to_chat(C, "" + create_text_tag("admin", "ADMIN:", C) + " [key_name(usr, 1)]([admin_jump_link(mob, src)]): [msg]")
+ to_chat(C, "" + create_text_tag("admin", "ADMIN:", C) + " [key_name(usr, 1)]([admin_jump_link(mob, src)]): [msg]")
feedback_add_details("admin_verb","M") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -33,6 +33,6 @@
if(check_rights(R_ADMIN, 0))
sender_name = "[sender_name]"
for(var/client/C in GLOB.admins)
- to_chat(C, "" + create_text_tag("mod", "MOD:", C) + " [sender_name]([admin_jump_link(mob, C.holder)]): [msg]")
+ to_chat(C, "" + create_text_tag("mod", "MOD:", C) + " [sender_name]([admin_jump_link(mob, C.holder)]): [msg]")
feedback_add_details("admin_verb","MS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/assembly/holder.dm b/code/modules/assembly/holder.dm
index 0480be4b625fb..d3b80559b4f06 100644
--- a/code/modules/assembly/holder.dm
+++ b/code/modules/assembly/holder.dm
@@ -197,7 +197,7 @@
process_activation(var/obj/D, var/normal = 1, var/special = 1)
if(!D) return 0
if(!secured)
- visible_message("\icon[src] *beep* *beep*", "*beep* *beep*")
+ visible_message("[icon2html(src, viewers(src))] *beep* *beep*", "*beep* *beep*")
if((normal) && (a_right) && (a_left))
if(a_right != D)
a_right.pulsed(0)
diff --git a/code/modules/assembly/infrared.dm b/code/modules/assembly/infrared.dm
index 9d73160bcd6d4..5a36878ffd912 100644
--- a/code/modules/assembly/infrared.dm
+++ b/code/modules/assembly/infrared.dm
@@ -112,7 +112,7 @@
pulse(0)
if(!holder)
- visible_message("\icon[src] *beep* *beep*")
+ visible_message("[icon2html(src, viewers(src))] *beep* *beep*")
cooldown = 2
spawn(10)
process_cooldown()
diff --git a/code/modules/assembly/signaler.dm b/code/modules/assembly/signaler.dm
index f7d4c649e801f..1a13dd646f104 100644
--- a/code/modules/assembly/signaler.dm
+++ b/code/modules/assembly/signaler.dm
@@ -136,7 +136,7 @@
if(!holder)
for(var/mob/O in hearers(1, src.loc))
- O.show_message(text("\icon[] *beep* *beep*", src), 3, "*beep* *beep*", 2)
+ O.show_message(text("[icon2html(src, O)] *beep* *beep*"), 3, "*beep* *beep*", 2)
return
diff --git a/code/modules/assembly/timer.dm b/code/modules/assembly/timer.dm
index 4c41ef31bd552..4e72f68feab93 100644
--- a/code/modules/assembly/timer.dm
+++ b/code/modules/assembly/timer.dm
@@ -39,7 +39,7 @@
if(!secured) return 0
pulse(0)
if(!holder)
- visible_message("\icon[src] *beep* *beep*", "*beep* *beep*")
+ visible_message("[icon2html(src, viewers(src))] *beep* *beep*", "*beep* *beep*")
cooldown = 2
spawn(10)
process_cooldown()
diff --git a/code/modules/assembly/voice.dm b/code/modules/assembly/voice.dm
index d2416545ed75f..998fd604c5d8a 100644
--- a/code/modules/assembly/voice.dm
+++ b/code/modules/assembly/voice.dm
@@ -20,7 +20,7 @@
recorded = msg
listening = 0
var/turf/T = get_turf(src) //otherwise it won't work in hand
- T.visible_message("\icon[src] beeps, \"Activation message is '[recorded]'.\"")
+ T.visible_message("[icon2html(src, viewers(src))] beeps, \"Activation message is '[recorded]'.\"")
else
if(findtext(msg, recorded))
pulse(0)
@@ -30,7 +30,7 @@
if(!holder)
listening = !listening
var/turf/T = get_turf(src)
- T.visible_message("\icon[src] beeps, \"[listening ? "Now" : "No longer"] recording input.\"")
+ T.visible_message("[icon2html(src, viewers(src))] beeps, \"[listening ? "Now" : "No longer"] recording input.\"")
/obj/item/device/assembly/voice/attack_self(mob/user)
diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm
index 0433b833da6cf..51acb1538bd05 100644
--- a/code/modules/client/asset_cache.dm
+++ b/code/modules/client/asset_cache.dm
@@ -138,6 +138,12 @@ You can set verify to TRUE if you want send() to sleep until the client has the
/proc/register_asset(var/asset_name, var/asset)
asset_cache.cache[asset_name] = asset
+//Generated names do not include file extention.
+//Used mainly for code that deals with assets in a generic way
+//The same asset will always lead to the same asset name
+/proc/generate_asset_name(file)
+ return "asset.[md5(fcopy_rsc(file))]"
+
//These datums are used to populate the asset cache, the proc "register()" does this.
//all of our asset datums, used for referring to these later
@@ -151,6 +157,7 @@ You can set verify to TRUE if you want send() to sleep until the client has the
/datum/asset/New()
asset_datums[type] = src
+ register()
/datum/asset/proc/register()
return
@@ -166,9 +173,22 @@ You can set verify to TRUE if you want send() to sleep until the client has the
/datum/asset/simple/register()
for(var/asset_name in assets)
register_asset(asset_name, assets[asset_name])
+
/datum/asset/simple/send(client)
send_asset_list(client,assets,verify)
+// For registering or sending multiple others at once
+/datum/asset/group
+ var/list/children
+
+/datum/asset/group/register()
+ for(var/type in children)
+ get_asset_datum(type)
+
+/datum/asset/group/send(client/C)
+ for(var/type in children)
+ var/datum/asset/A = get_asset_datum(type)
+ A.send(C)
//DEFINITIONS FOR ASSET DATUMS START HERE.
@@ -257,6 +277,39 @@ You can set verify to TRUE if you want send() to sleep until the client has the
send_asset_list(client, uncommon, FALSE)
send_asset_list(client, common, TRUE)
+/datum/asset/group/goonchat
+ children = list(
+ /datum/asset/simple/jquery,
+ /datum/asset/simple/goonchat,
+ /datum/asset/simple/fontawesome
+ )
+
+/datum/asset/simple/jquery
+ verify = FALSE
+ assets = list(
+ "jquery.min.js" = 'code/modules/goonchat/browserassets/js/jquery.min.js',
+ )
+
+/datum/asset/simple/goonchat
+ verify = 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'
+ )
+
+/datum/asset/simple/fontawesome
+ verify = FALSE
+ assets = list(
+ "fa-regular-400.eot" = 'html/font-awesome/webfonts/fa-regular-400.eot',
+ "fa-regular-400.woff" = 'html/font-awesome/webfonts/fa-regular-400.woff',
+ "fa-solid-900.eot" = 'html/font-awesome/webfonts/fa-solid-900.eot',
+ "fa-solid-900.woff" = 'html/font-awesome/webfonts/fa-solid-900.woff',
+ "font-awesome.css" = 'html/font-awesome/css/all.min.css',
+ "v4shim.css" = 'html/font-awesome/css/v4-shims.min.css'
+ )
+
/*
Asset cache
*/
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 7eda267d0897a..82922c8cf12bd 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -45,5 +45,19 @@
var/related_accounts_ip = "Requires database" //So admins know why it isn't working - Used to determine what other accounts previously logged in from this ip
var/related_accounts_cid = "Requires database" //So admins know why it isn't working - Used to determine what other accounts previously logged in from this computer id
- preload_rsc = 0 // This is 0 so we can set it to an URL once the player logs in and have them download the resources from a different server.
+
var/static/obj/screen/click_catcher/void
+
+ /*
+ As of byond 512, due to how broken preloading is, preload_rsc MUST be set to 1 at compile time if resource URLs are *not* in use,
+ BUT you still want resource preloading enabled (from the server itself). If using resource URLs, it should be set to 0 and
+ changed to a URL at runtime (see client_procs.dm for procs that do this automatically). More information about how goofy this broken setting works at
+ http://www.byond.com/forum/post/1906517?page=2#comment23727144
+ */
+ preload_rsc = 0
+
+ ///goonchat chatoutput of the client
+ var/datum/chatOutput/chatOutput
+
+ var/fullscreen = FALSE
+
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 6cc83a7dc2a5c..d3839dee70438 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -143,6 +143,11 @@
if("usr") hsrc = mob
if("prefs") return prefs.process_link(usr,href_list)
if("vars") return view_var_Topic(href,href_list,hsrc)
+ if("chat") return chatOutput.Topic(href, href_list)
+
+ switch(href_list["action"])
+ if("openLink")
+ send_link(src, href_list["link"])
..() //redirect to hsrc.Topic()
@@ -167,10 +172,27 @@
/client/New(TopicData)
TopicData = null //Prevent calls to client.Topic from connect
- if(!(connection in list("seeker", "web"))) //Invalid connection type.
- return null
- if(byond_version < MIN_CLIENT_VERSION) //Out of date client.
- return null
+ // Load goonchat
+ chatOutput = new(src)
+
+ switch (connection)
+ if ("seeker", "web") // check for invalid connection type. do nothing if valid
+ else return null
+ #if DM_VERSION >= 512
+ var/bad_version = config.minimum_byond_version && byond_version < config.minimum_byond_version
+ var/bad_build = config.minimum_byond_build && byond_build < config.minimum_byond_build
+ if (bad_build || bad_version)
+ to_chat(src, "You are attempting to connect with a out of date version of BYOND. Please update to the latest version at http://www.byond.com/ before trying again.")
+ qdel(src)
+ return
+
+ /*if("[byond_version].[byond_build]" in config.forbidden_versions)
+ _DB_staffwarn_record(ckey, "Tried to connect with broken and possibly exploitable BYOND build.")
+ to_chat(src, "You are attempting to connect with a broken and possibly exploitable BYOND build. Please update to the latest version at http://www.byond.com/ before trying again.")
+ qdel(src)
+ return*/
+
+ #endif
if(!config.guests_allowed && IsGuestKey(key))
alert(src,"This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.","Guest","OK")
@@ -254,8 +276,12 @@
if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them.
to_chat(src, "Unable to access asset cache browser, if you are using a custom skin file, please allow DS to download the updated version, if you are not, then make a bug report. This is not a critical issue but can cause issues with resource downloading, as it is impossible to know when extra resources arrived to you.")
+ if(is_preference_enabled(/datum/client_preference/goonchat))
+ chatOutput.start()
+
if(holder)
src.control_freak = 0 //Devs need 0 for profiler access
+
//////////////
//DISCONNECT//
//////////////
@@ -428,3 +454,80 @@ client/verb/character_setup()
/client/proc/apply_fps(var/client_fps)
if(world.byond_version >= 511 && byond_version >= 511 && client_fps >= CLIENT_MIN_FPS && client_fps <= CLIENT_MAX_FPS)
vars["fps"] = prefs.clientfps
+
+
+/client/MouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params)
+ . = ..()
+/* var/mob/living/M = mob
+ if(istype(M))
+ M.OnMouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params)
+*/
+
+/client/verb/toggle_fullscreen()
+ set name = "Toggle Fullscreen"
+ set category = "OOC"
+
+ fullscreen = !fullscreen
+
+ if (fullscreen)
+ winset(usr, "mainwindow", "titlebar=false")
+ winset(usr, "mainwindow", "can-resize=false")
+ winset(usr, "mainwindow", "is-maximized=false")
+ winset(usr, "mainwindow", "is-maximized=true")
+ winset(usr, "mainwindow", "statusbar=false")
+ winset(usr, "mainwindow", "menu=")
+// winset(usr, "mainwindow.mainvsplit", "size=0x0")
+ else
+ winset(usr, "mainwindow", "is-maximized=false")
+ winset(usr, "mainwindow", "titlebar=true")
+ winset(usr, "mainwindow", "can-resize=true")
+ winset(usr, "mainwindow", "statusbar=true")
+ winset(usr, "mainwindow", "menu=menu")
+
+ fit_viewport()
+
+/client/verb/fit_viewport()
+ set name = "Fit Viewport"
+ set category = "OOC"
+ set desc = "Fit the width of the map window to match the viewport"
+
+ // Fetch aspect ratio
+ var/view_size = getviewsize(view)
+ var/aspect_ratio = view_size[1] / view_size[2]
+
+ // Calculate desired pixel width using window size and aspect ratio
+ var/sizes = params2list(winget(src, "mainwindow.mainvsplit;mapwindow", "size"))
+ var/map_size = splittext(sizes["mapwindow.size"], "x")
+ var/height = text2num(map_size[2])
+ var/desired_width = round(height * aspect_ratio)
+ if (text2num(map_size[1]) == desired_width)
+ // Nothing to do
+ return
+
+ var/split_size = splittext(sizes["mainwindow.mainvsplit.size"], "x")
+ var/split_width = text2num(split_size[1])
+
+ // Calculate and apply a best estimate
+ // +4 pixels are for the width of the splitter's handle
+ var/pct = 100 * (desired_width + 4) / split_width
+ winset(src, "mainwindow.mainvsplit", "splitter=[pct]")
+
+ // Apply an ever-lowering offset until we finish or fail
+ var/delta
+ for(var/safety in 1 to 10)
+ var/after_size = winget(src, "mapwindow", "size")
+ map_size = splittext(after_size, "x")
+ var/got_width = text2num(map_size[1])
+
+ if (got_width == desired_width)
+ // success
+ return
+ else if (isnull(delta))
+ // calculate a probable delta value based on the difference
+ delta = 100 * (desired_width - got_width) / split_width
+ else if ((delta > 0 && got_width > desired_width) || (delta < 0 && got_width < desired_width))
+ // if we overshot, halve the delta and reverse direction
+ delta = -delta/2
+
+ pct += delta
+ winset(src, "mainwindow.mainvsplit", "splitter=[pct]")
diff --git a/code/modules/client/darkmode.dm b/code/modules/client/darkmode.dm
new file mode 100644
index 0000000000000..4edd5c0112050
--- /dev/null
+++ b/code/modules/client/darkmode.dm
@@ -0,0 +1,132 @@
+//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, "rpane", "background-color = [COLOR_DARKMODE_DARKBACKGROUND];background-color = none")
+ winset(src, "rpane", "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, "rpanewindow", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none")
+ winset(src, "rpanewindow", "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")
+ winset(src, "mainvsplit", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none")
+ //Buttons
+ winset(src, "textb", "background-color = #494949;background-color = none")
+ winset(src, "textb", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "infob", "background-color = #494949;background-color = none")
+ winset(src, "infob", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "rulesb", "background-color = #494949;background-color = none")
+ winset(src, "rulesb", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "Lore", "background-color = #494949;background-color = none")
+ winset(src, "Lore", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "wikib", "background-color = #494949;background-color = none")
+ winset(src, "wikib", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "forumb", "background-color = #494949;background-color = none")
+ winset(src, "forumb", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "changelog", "background-color = #494949;background-color = none")
+ winset(src, "changelog", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "github", "background-color = #494949;background-color = none")
+ winset(src, "github", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "BugReport", "background-color = #494949;background-color = none")
+ winset(src, "BugReport", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "hotkey_toggle", "background-color = #494949;background-color = none")
+ winset(src, "hotkey_toggle", "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, "info", "background-color = [COLOR_DARKMODE_DARKBACKGROUND];background-color = #FFFFFF")
+ winset(src, "info", "tab-background-color = [COLOR_DARKMODE_BACKGROUND];tab-background-color = none")
+ winset(src, "info", "text-color = [COLOR_DARKMODE_TEXT];text-color = #000000")
+ winset(src, "info", "tab-text-color = [COLOR_DARKMODE_TEXT];tab-text-color = #000000")
+ winset(src, "info", "prefix-color = [COLOR_DARKMODE_TEXT];prefix-color = #000000")
+ winset(src, "info", "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, "asset_cache_browser", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none")
+ winset(src, "asset_cache_browser", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none")
+ //winset(src, "input", "background-color = [COLOR_DARKMODE_BACKGROUND];background-color = none")
+ //winset(src, "input", "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, "rpane", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]")
+ winset(src, "rpane", "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, "rpanewindow", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]")
+ winset(src, "rpanewindow", "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]")
+ winset(src, "mainvsplit", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]")
+ //Buttons
+ winset(src, "textb", "background-color = none;background-color = #494949")
+ winset(src, "textb", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "infob", "background-color = none;background-color = #494949")
+ winset(src, "infob", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "rulesb", "background-color = none;background-color = #494949")
+ winset(src, "rulesb", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "Lore", "background-color = none;background-color = #494949")
+ winset(src, "Lore", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "wikib", "background-color = none;background-color = #494949")
+ winset(src, "wikib", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "forumb", "background-color = none;background-color = #494949")
+ winset(src, "forumb", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "changelog", "background-color = none;background-color = #494949")
+ winset(src, "changelog", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "github", "background-color = none;background-color = #494949")
+ winset(src, "github", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "BugReport", "background-color = none;background-color = #494949")
+ winset(src, "BugReport", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "hotkey_toggle", "background-color = none;background-color = #494949")
+ winset(src, "hotkey_toggle", "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, "info", "background-color = #FFFFFF;background-color = [COLOR_DARKMODE_DARKBACKGROUND]")
+ winset(src, "info", "tab-background-color = none;tab-background-color = [COLOR_DARKMODE_BACKGROUND]")
+ winset(src, "info", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "info", "tab-text-color = #000000;tab-text-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "info", "prefix-color = #000000;prefix-color = [COLOR_DARKMODE_TEXT]")
+ winset(src, "info", "suffix-color = #000000;suffix-color = [COLOR_DARKMODE_TEXT]")
+ //Say, OOC, me Buttons etc.
+ winset(src, "saybutton", "background-color = none;background-color = #494949")
+ winset(src, "saybutton", "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, "input", "background-color = none;background-color = [COLOR_DARKMODE_BACKGROUND]")
+ //winset(src, "input", "text-color = #000000;text-color = [COLOR_DARKMODE_TEXT]")
diff --git a/code/modules/client/preference_setup/global/preference_datums.dm b/code/modules/client/preference_setup/global/preference_datums.dm
index 4dd276713da66..ae7877090c13d 100644
--- a/code/modules/client/preference_setup/global/preference_datums.dm
+++ b/code/modules/client/preference_setup/global/preference_datums.dm
@@ -162,6 +162,10 @@ var/list/_client_preferences_by_type
enabled_description = "Fancy"
disabled_description = "Plain"
+/datum/client_preference/goonchat
+ description = "Use Goon Chat"
+ key = "USE_GOONCHAT"
+
/********************
* Admin Preferences *
********************/
diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm
index 2f73f1084189e..cf70987fb5b33 100644
--- a/code/modules/clothing/clothing.dm
+++ b/code/modules/clothing/clothing.dm
@@ -943,7 +943,7 @@ BLIND // can't see anything
if(new_mode != sensor_mode)
var/image/I = image('icons/effects/effects.dmi', src, "empdisable")
overlays += I
- show_image(src.loc, I)
+ image_to(src.loc, I)
spawn(30)
overlays -= I
qdel(I)
diff --git a/code/modules/clothing/rings/rings.dm b/code/modules/clothing/rings/rings.dm
index 33b8a3ec26028..f2cbda971e227 100644
--- a/code/modules/clothing/rings/rings.dm
+++ b/code/modules/clothing/rings/rings.dm
@@ -56,7 +56,7 @@
/obj/item/clothing/ring/reagent/equipped(var/mob/living/carbon/human/H)
..()
if(istype(H) && H.gloves==src)
- to_chat(H, "You feel a prick as you slip on the ring.")
+ to_chat(H, "You feel a prick as you slip on the ring.")
if(reagents.total_volume)
if(H.reagents)
diff --git a/code/modules/clothing/spacesuits/rig/modules/combat.dm b/code/modules/clothing/spacesuits/rig/modules/combat.dm
index e65a37a13bfa5..802eb998a838a 100644
--- a/code/modules/clothing/spacesuits/rig/modules/combat.dm
+++ b/code/modules/clothing/spacesuits/rig/modules/combat.dm
@@ -58,7 +58,7 @@
to_chat(user, "Another grenade of that type will not fit into the module.")
return 0
- to_chat(user, "You slot \the [input_device] into the suit module.")
+ to_chat(user, "You slot \the [input_device] into the suit module.")
user.drop_from_inventory(input_device)
qdel(input_device)
accepted_item.charges++
@@ -257,7 +257,7 @@
else
var/obj/item/new_weapon = new fabrication_type()
new_weapon.forceMove(H)
- to_chat(H, "You quickly fabricate \a [new_weapon].")
+ to_chat(H, "You quickly fabricate \a [new_weapon].")
H.put_in_hands(new_weapon)
return 1
diff --git a/code/modules/clothing/spacesuits/rig/modules/computer.dm b/code/modules/clothing/spacesuits/rig/modules/computer.dm
index 4c6c761909026..cc2506b5a8bd8 100644
--- a/code/modules/clothing/spacesuits/rig/modules/computer.dm
+++ b/code/modules/clothing/spacesuits/rig/modules/computer.dm
@@ -265,7 +265,7 @@
var/obj/item/weapon/disk/tech_disk/disk = input_device
if(disk.stored)
if(load_data(disk.stored))
- to_chat(user, "Download successful; disk erased.")
+ to_chat(user, "Download successful; disk erased.")
disk.stored = null
else
to_chat(user, "The disk is corrupt. It is useless to you.")
@@ -291,7 +291,7 @@
else
// Maybe consider a way to drop all your data into a target repo in the future.
if(load_data(incoming_files.known_tech))
- to_chat(user, "Download successful; local and remote repositories synchronized.")
+ to_chat(user, "Download successful; local and remote repositories synchronized.")
else
to_chat(user, "Scan complete. There is nothing useful stored on this terminal.")
return 1
@@ -467,9 +467,9 @@
/obj/item/rig_module/power_sink/proc/drain_complete(var/mob/living/M)
if(!interfaced_with)
- if(M) to_chat(M, "Total power drained: [round(total_power_drained*CELLRATE)] Wh.")
+ if(M) to_chat(M, "Total power drained: [round(total_power_drained*CELLRATE)] Wh.")
else
- if(M) to_chat(M, "Total power drained from [interfaced_with]: [round(total_power_drained*CELLRATE)] Wh.")
+ if(M) to_chat(M, "Total power drained from [interfaced_with]: [round(total_power_drained*CELLRATE)] Wh.")
interfaced_with.drain_power(0,1,0) // Damage the victim.
drain_loc = null
diff --git a/code/modules/clothing/spacesuits/rig/modules/utility.dm b/code/modules/clothing/spacesuits/rig/modules/utility.dm
index 2eacec16ccfef..e5d0a15be3e03 100644
--- a/code/modules/clothing/spacesuits/rig/modules/utility.dm
+++ b/code/modules/clothing/spacesuits/rig/modules/utility.dm
@@ -215,7 +215,7 @@
break
if(total_transferred)
- to_chat(user, "You transfer [total_transferred] units into the suit reservoir.")
+ to_chat(user, "You transfer [total_transferred] units into the suit reservoir.")
else
to_chat(user, "None of the reagents seem suitable.")
return 1
@@ -330,17 +330,17 @@
if("Enable")
active = 1
voice_holder.active = 1
- to_chat(usr, "You enable the speech synthesiser.")
+ to_chat(usr, "You enable the speech synthesiser.")
if("Disable")
active = 0
voice_holder.active = 0
- to_chat(usr, "You disable the speech synthesiser.")
+ to_chat(usr, "You disable the speech synthesiser.")
if("Set Name")
var/raw_choice = sanitize(input(usr, "Please enter a new name.") as text|null, MAX_NAME_LEN)
if(!raw_choice)
return 0
voice_holder.voice = raw_choice
- to_chat(usr, "You are now mimicking [voice_holder.voice].")
+ to_chat(usr, "You are now mimicking [voice_holder.voice].")
return 1
/obj/item/rig_module/maneuvering_jets
diff --git a/code/modules/clothing/spacesuits/rig/modules/vision.dm b/code/modules/clothing/spacesuits/rig/modules/vision.dm
index 8bd4dcc0a88c6..aa9312617b8b9 100644
--- a/code/modules/clothing/spacesuits/rig/modules/vision.dm
+++ b/code/modules/clothing/spacesuits/rig/modules/vision.dm
@@ -162,7 +162,7 @@
// Don't cycle if this engage() is being called by activate().
if(starting_up)
- to_chat(holder.wearer, "You activate your visual sensors.")
+ to_chat(holder.wearer, "You activate your visual sensors.")
return 1
if(vision_modes.len > 1)
@@ -171,9 +171,9 @@
vision_index = 1
vision = vision_modes[vision_index]
- to_chat(holder.wearer, "You cycle your sensors to [vision.mode] mode.")
+ to_chat(holder.wearer, "You cycle your sensors to [vision.mode] mode.")
else
- to_chat(holder.wearer, "Your sensors only have one mode.")
+ to_chat(holder.wearer, "Your sensors only have one mode.")
return 1
/obj/item/rig_module/vision/New()
diff --git a/code/modules/clothing/spacesuits/rig/rig.dm b/code/modules/clothing/spacesuits/rig/rig.dm
index 18f70376ffac4..508f53f68fd65 100644
--- a/code/modules/clothing/spacesuits/rig/rig.dm
+++ b/code/modules/clothing/spacesuits/rig/rig.dm
@@ -18,7 +18,9 @@
center_of_mass = null
// These values are passed on to all component pieces.
+
armor = list(melee = 40, bullet = 5, laser = 20,energy = 5, bomb = 35, bio = 100, rad = 20)
+
min_cold_protection_temperature = SPACE_SUIT_MIN_COLD_PROTECTION_TEMPERATURE
max_heat_protection_temperature = SPACE_SUIT_MAX_HEAT_PROTECTION_TEMPERATURE
siemens_coefficient = 0.2
@@ -92,7 +94,9 @@
for(var/obj/item/piece in list(helmet,gloves,chest,boots))
if(!piece || piece.loc != wearer)
continue
- to_chat(usr, "\icon[piece] \The [piece] [piece.gender == PLURAL ? "are" : "is"] deployed.")
+
+ to_chat(usr, "[icon2html(piece, usr)] \The [piece] [piece.gender == PLURAL ? "are" : "is"] deployed.")
+
if(src.loc == usr)
to_chat(usr, "The access panel is [locked? "locked" : "unlocked"].")
@@ -233,13 +237,19 @@
sealing = 1
if(!seal_target && !suit_is_deployed())
- wearer.visible_message("[wearer]'s suit flashes an error light.","Your suit flashes an error light. It can't function properly without being fully deployed.")
+ wearer.visible_message(\
+ "[wearer]'s suit flashes an error light.", \
+ "Your suit flashes an error light. It can't function properly without being fully deployed.")
+
failed_to_seal = 1
if(!failed_to_seal)
if(!instant)
- wearer.visible_message("[wearer]'s suit emits a quiet hum as it begins to adjust its seals.","With a quiet hum, the suit begins running checks and adjusting components.")
+ wearer.visible_message(\
+ "[wearer]'s suit emits a quiet hum as it begins to adjust its seals.", \
+ "With a quiet hum, the suit begins running checks and adjusting components.")
+
if(seal_delay && !do_after(wearer,seal_delay, src))
if(wearer) to_chat(wearer, "You must remain still while the suit is adjusting the components.")
failed_to_seal = 1
@@ -271,16 +281,16 @@
//piece.icon_state = "[initial(icon_state)][!seal_target ? "_sealed" : ""]"
switch(msg_type)
if("boots")
- to_chat(wearer, "\The [piece] [!seal_target ? "seal around your feet" : "relax their grip on your legs"].")
+ to_chat(wearer, "\The [piece] [!seal_target ? "seal around your feet" : "relax their grip on your legs"].")
wearer.update_inv_shoes()
if("gloves")
- to_chat(wearer, "\The [piece] [!seal_target ? "tighten around your fingers and wrists" : "become loose around your fingers"].")
+ to_chat(wearer, "\The [piece] [!seal_target ? "tighten around your fingers and wrists" : "become loose around your fingers"].")
wearer.update_inv_gloves()
if("chest")
- to_chat(wearer, "\The [piece] [!seal_target ? "cinches tight again your chest" : "releases your chest"].")
+ to_chat(wearer, "\The [piece] [!seal_target ? "cinches tight again your chest" : "releases your chest"].")
wearer.update_inv_wear_suit()
if("helmet")
- to_chat(wearer, "\The [piece] hisses [!seal_target ? "closed" : "open"].")
+ to_chat(wearer, "\The [piece] hisses [!seal_target ? "closed" : "open"].")
wearer.update_inv_head()
if(helmet)
helmet.update_light(wearer)
@@ -311,10 +321,12 @@
// Success!
canremove = seal_target
- to_chat(wearer, "Your entire suit [canremove ? "loosens as the components relax" : "tightens around you as the components lock into place"].")
+
+ to_chat(wearer, "Your entire suit [canremove ? "loosens as the components relax" : "tightens around you as the components lock into place"].")
+
if(wearer != initiator)
- to_chat(initiator, "Suit adjustment complete. Suit is now [canremove ? "unsealed" : "sealed"].")
+ to_chat(initiator, "Suit adjustment complete. Suit is now [canremove ? "unsealed" : "sealed"].")
if(canremove)
for(var/obj/item/rig_module/module in installed_modules)
@@ -616,7 +628,10 @@
..()
if(seal_delay > 0 && istype(M) && M.back == src)
- M.visible_message("[M] starts putting on \the [src]...", "You start putting on \the [src]...")
+ M.visible_message(\
+ "[M] starts putting on \the [src]...", \
+ "You start putting on \the [src]...")
+
if(!do_after(M,seal_delay,src))
if(M && M.back == src)
if(!M.unEquip(src))
@@ -625,7 +640,10 @@
return
if(istype(M) && M.back == src)
- M.visible_message("[M] struggles into \the [src].", "You struggle into \the [src].")
+ M.visible_message(\
+ "[M] struggles into \the [src].", \
+ "You struggle into \the [src].")
+
wearer = M
wearer.wearing_rig = src
update_icon()
@@ -675,7 +693,7 @@
holder = use_obj.loc
if(istype(holder))
if(use_obj && check_slot == use_obj)
- to_chat(wearer, "Your [use_obj.name] [use_obj.gender == PLURAL ? "retract" : "retracts"] swiftly.")
+ to_chat(wearer, "Your [use_obj.name] [use_obj.gender == PLURAL ? "retract" : "retracts"] swiftly.")
use_obj.canremove = 1
holder.drop_from_inventory(use_obj)
use_obj.canremove = 0
diff --git a/code/modules/clothing/spacesuits/rig/rig_verbs.dm b/code/modules/clothing/spacesuits/rig/rig_verbs.dm
index 3e27ae5340a92..d38421f25fe40 100644
--- a/code/modules/clothing/spacesuits/rig/rig_verbs.dm
+++ b/code/modules/clothing/spacesuits/rig/rig_verbs.dm
@@ -197,7 +197,7 @@
if(malfunction_check(usr))
return
-
+
if(!check_power_cost(usr, 0, 0, 0, 0))
return
@@ -222,7 +222,8 @@
return
selected_module = module
- to_chat(usr, "Primary system is now: [selected_module.interface_name].")
+ to_chat(usr, "Primary system is now: [selected_module.interface_name].")
+
/obj/item/weapon/rig/verb/toggle_module()
@@ -256,10 +257,10 @@
return
if(module.active)
- to_chat(usr, "You attempt to deactivate \the [module.interface_name].")
+ to_chat(usr, "You attempt to deactivate \the [module.interface_name].")
module.deactivate()
else
- to_chat(usr, "You attempt to activate \the [module.interface_name].")
+ to_chat(usr, "You attempt to activate \the [module.interface_name].")
module.activate()
/obj/item/weapon/rig/verb/engage_module()
@@ -293,5 +294,5 @@
if(!istype(module))
return
- to_chat(usr, "You attempt to engage the [module.interface_name].")
+ to_chat(usr, "You attempt to engage the [module.interface_name].")
module.engage()
\ No newline at end of file
diff --git a/code/modules/detectivework/microscope/dnascanner.dm b/code/modules/detectivework/microscope/dnascanner.dm
index 79d9d02bdef28..792fcd521d1ed 100644
--- a/code/modules/detectivework/microscope/dnascanner.dm
+++ b/code/modules/detectivework/microscope/dnascanner.dm
@@ -99,7 +99,7 @@
last_process_worldtime = world.time
/obj/machinery/dnaforensics/proc/complete_scan()
- src.visible_message("\icon[src] makes an insistent chime.", 2)
+ src.visible_message("[icon2html(src, viewers(src))] makes an insistent chime.", 2)
update_icon()
if(bloodsamp)
var/obj/item/weapon/paper/P = new(src)
diff --git a/code/modules/economy/ATM.dm b/code/modules/economy/ATM.dm
index 934a90b2e1aa5..7e509c2885134 100644
--- a/code/modules/economy/ATM.dm
+++ b/code/modules/economy/ATM.dm
@@ -65,14 +65,15 @@
//display a message to the user
var/response = pick("Initiating withdraw. Have a nice day!", "CRITICAL ERROR: Activating cash chamber panic siphon.","PIN Code accepted! Emptying account balance.", "Jackpot!")
- to_chat(user, "\icon[src] The [src] beeps: \"[response]\"")
+
+ to_chat(user, "[icon2html(src, user)] [src] beeps: \"[response]\"")
return 1
/obj/machinery/atm/attackby(obj/item/I as obj, mob/user as mob)
if(istype(I, /obj/item/weapon/card))
if(emagged > 0)
//prevent inserting id into an emagged ATM
- to_chat(user, "\icon[src] CARD READER ERROR. This system has been compromised!")
+ to_chat(user, "[icon2html(src, user)] CARD READER ERROR. This system has been compromised!")
return
var/obj/item/weapon/card/id/idcard = I
@@ -109,7 +110,7 @@
/obj/machinery/atm/interact(mob/user)
if(istype(user, /mob/living/silicon))
- to_chat(user, "\icon[src] Artificial unit recognized. Artificial units do not currently receive monetary compensation, as per system banking regulation #1005.")
+ to_chat(user, "[icon2html(src, user)] Artificial unit recognized. Artificial units do not currently receive monetary compensation, as per system banking regulation #1005.")
return
if(get_dist(src,user) <= 1)
@@ -244,10 +245,10 @@
var/datum/transaction/T = new("Account #[target_account_number]", transfer_purpose, -transfer_amount, machine_id)
authenticated_account.do_transaction(T)
else
- to_chat(usr, "\icon[src]Funds transfer failed.")
+ to_chat(usr, "[icon2html(src, usr)]Funds transfer failed.")
else
- to_chat(usr, "\icon[src]You don't have enough funds to do that!")
+ to_chat(usr, "[icon2html(src, usr)]You don't have enough funds to do that!")
if("view_screen")
view_screen = text2num(href_list["view_screen"])
if("change_security_level")
@@ -294,11 +295,11 @@
var/datum/transaction/T = new(failed_account.owner_name, "Unauthorised login attempt", 0, machine_id)
failed_account.do_transaction(T)
else
- to_chat(usr, "\icon[src] Incorrect pin/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.")
+ to_chat(usr, "[icon2html(src, usr)] Incorrect pin/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.")
previous_account_number = tried_account_num
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 1)
else
- to_chat(usr, "\icon[src] Unable to log in to account, additional information may be required.")
+ to_chat(usr, "[icon2html(src, usr)] Unable to log in to account, additional information may be required.")
number_incorrect_tries = 0
else
playsound(src, 'sound/machines/twobeep.ogg', 50, 1)
@@ -309,7 +310,7 @@
var/datum/transaction/T = new(authenticated_account.owner_name, "Remote terminal access", 0, machine_id)
authenticated_account.do_transaction(T)
- to_chat(usr, "\icon[src] Access granted. Welcome user '[authenticated_account.owner_name].'")
+ to_chat(usr, "[icon2html(src, usr)] Access granted. Welcome user '[authenticated_account.owner_name].'")
previous_account_number = tried_account_num
if("e_withdrawal")
@@ -326,7 +327,7 @@
var/datum/transaction/T = new(authenticated_account.owner_name, "Credit withdrawal", -amount, machine_id)
authenticated_account.do_transaction(T)
else
- to_chat(usr, "\icon[src]You don't have enough funds to do that!")
+ to_chat(usr, "[icon2html(src, usr)]You don't have enough funds to do that!")
if("withdrawal")
var/amount = max(text2num(href_list["funds_amount"]),0)
amount = round(amount, 0.01)
@@ -341,7 +342,7 @@
var/datum/transaction/T = new(authenticated_account.owner_name, "Credit withdrawal", -amount, machine_id)
authenticated_account.do_transaction(T)
else
- to_chat(usr, "\icon[src]You don't have enough funds to do that!")
+ to_chat(usr, "[icon2html(src, usr)]You don't have enough funds to do that!")
if("balance_statement")
if(authenticated_account)
var/obj/item/weapon/paper/R = new(src.loc)
@@ -413,7 +414,7 @@
if(!held_card)
//this might happen if the user had the browser window open when somebody emagged it
if(emagged > 0)
- to_chat(usr, "\icon[src] The ATM card reader rejected your ID because this machine has been sabotaged!")
+ to_chat(usr, "[icon2html(src, usr)] The ATM card reader rejected your ID because this machine has been sabotaged!")
else
var/obj/item/I = usr.get_active_hand()
if (istype(I, /obj/item/weapon/card/id))
diff --git a/code/modules/economy/EFTPOS.dm b/code/modules/economy/EFTPOS.dm
index 403818542dede..15976bf19b473 100644
--- a/code/modules/economy/EFTPOS.dm
+++ b/code/modules/economy/EFTPOS.dm
@@ -118,7 +118,7 @@
if(linked_account)
scan_card(I, O)
else
- to_chat(usr, "\icon[src]Unable to connect to linked account.")
+ to_chat(usr, "[icon2html(src, usr)]Unable to connect to linked account.")
else if (istype(O, /obj/item/weapon/spacecash/ewallet))
var/obj/item/weapon/spacecash/ewallet/E = O
if (linked_account)
@@ -126,7 +126,7 @@
if(transaction_locked && !transaction_paid)
if(transaction_amount <= E.worth)
playsound(src, 'sound/machines/chime.ogg', 50, 1)
- src.visible_message("\icon[src] \The [src] chimes.")
+ src.visible_message("[icon2html(src, viewers(src))] \The [src] chimes.")
transaction_paid = 1
//transfer the money
@@ -134,11 +134,12 @@
var/datum/transaction/T = new(E.owner_name, (transaction_purpose ? transaction_purpose : "None supplied."), transaction_amount, machine_id)
linked_account.do_transaction(T)
else
- to_chat(usr, "\icon[src]\The [O] doesn't have that much money!")
+ to_chat(usr, "[icon2html(src,usr)]\The [O] doesn't have that much money!")
else
- to_chat(usr, "\icon[src]Connected account has been suspended.")
+ to_chat(usr, "[icon2html(src,usr)]Connected account has been suspended.")
+
else
- to_chat(usr, "\icon[src]EFTPOS is not connected to an account.")
+ to_chat(usr, "[icon2html(src, usr)]EFTPOS is not connected to an account.")
else
..()
@@ -156,14 +157,14 @@
alert("That is not a valid code!")
print_reference()
else
- to_chat(usr, "\icon[src]Incorrect code entered.")
+ to_chat(usr, "[icon2html(src, usr)]Incorrect code entered.")
if("change_id")
var/attempt_code = text2num(input("Re-enter the current EFTPOS access code", "Confirm EFTPOS code"))
if(attempt_code == access_code)
eftpos_name = sanitize(input("Enter a new terminal ID for this device", "Enter new EFTPOS ID"), MAX_NAME_LEN) + " EFTPOS scanner"
print_reference()
else
- to_chat(usr, "\icon[src]Incorrect code entered.")
+ to_chat(usr, "[icon2html(src, usr)]Incorrect code entered.")
if("link_account")
var/attempt_account_num = input("Enter account number to pay EFTPOS charges into", "New account number") as num
var/attempt_pin = input("Enter pin code", "Account pin") as num
@@ -171,9 +172,9 @@
if(linked_account)
if(linked_account.suspended)
linked_account = null
- to_chat(usr, "\icon[src]Account has been suspended.")
+ to_chat(usr, "[icon2html(src, usr)]Account has been suspended.")
else
- to_chat(usr, "\icon[src]Account not found.")
+ to_chat(usr, "[icon2html(src, usr)]Account not found.")
if("trans_purpose")
var/choice = sanitize(input("Enter reason for EFTPOS transaction", "Transaction purpose"))
if(choice) transaction_purpose = choice
@@ -196,14 +197,14 @@
else if(linked_account)
transaction_locked = 1
else
- to_chat(usr, "\icon[src]No account connected to send transactions to.")
+ to_chat(usr, "[icon2html(src, usr)]No account connected to send transactions to.")
if("scan_card")
if(linked_account)
var/obj/item/I = usr.get_active_hand()
if (istype(I, /obj/item/weapon/card))
scan_card(I)
else
- to_chat(usr, "\icon[src]Unable to link accounts.")
+ to_chat(usr, "[icon2html(src, usr)]Unable to link accounts.")
if("reset")
//reset the access code - requires HoP/captain access
var/obj/item/I = usr.get_active_hand()
@@ -211,10 +212,10 @@
var/obj/item/weapon/card/id/C = I
if(access_cent_captain in C.access || access_hop in C.access || access_captain in C.access)
access_code = 0
- to_chat(usr, "\icon[src]Access code reset to 0.")
+ to_chat(usr, "[icon2html(src, usr)]Access code reset to 0.")
else if (istype(I, /obj/item/weapon/card/emag))
access_code = 0
- to_chat(usr, "\icon[src]Access code reset to 0.")
+ to_chat(usr, "[icon2html(src, usr)]Access code reset to 0.")
src.attack_self(usr)
@@ -248,25 +249,25 @@
T = new(D.owner_name, transaction_purpose, transaction_amount, machine_id)
linked_account.do_transaction(T)
else
- to_chat(usr, "\icon[src]You don't have that much money!")
+ to_chat(usr, "[icon2html(src,usr)]You don't have that much money!")
else
- to_chat(usr, "\icon[src]Your account has been suspended.")
+ to_chat(usr, "[icon2html(src,usr)]Your account has been suspended.")
else
- to_chat(usr, "\icon[src]Unable to access account. Check security settings and try again.")
+ to_chat(usr, "[icon2html(src, usr)]Unable to access account. Check security settings and try again.")
else
- to_chat(usr, "\icon[src]Connected account has been suspended.")
+ to_chat(usr, "[icon2html(src, usr)]Connected account has been suspended.")
else
- to_chat(usr, "\icon[src]EFTPOS is not connected to an account.")
+ to_chat(usr, "[icon2html(src, usr)]EFTPOS is not connected to an account.")
else if (istype(I, /obj/item/weapon/card/emag))
if(transaction_locked)
if(transaction_paid)
- to_chat(usr, "\icon[src]You stealthily swipe \the [I] through \the [src].")
+ to_chat(usr, "[icon2html(src, usr)]You stealthily swipe \the [I] through \the [src].")
transaction_locked = 0
transaction_paid = 0
else
usr.visible_message("\The [usr] swipes a card through \the [src].")
playsound(src, 'sound/machines/chime.ogg', 50, 1)
- src.visible_message("\icon[src] \The [src] chimes.")
+ src.visible_message("[icon2html(src, usr)] \The [src] chimes.")
transaction_paid = 1
else
..()
diff --git a/code/modules/goonchat/_helpers.dm b/code/modules/goonchat/_helpers.dm
new file mode 100644
index 0000000000000..af6a7fbc4e067
--- /dev/null
+++ b/code/modules/goonchat/_helpers.dm
@@ -0,0 +1,112 @@
+GLOBAL_DATUM_INIT(is_http_protocol, /regex, regex("^https?://"))
+
+
+//Converts an icon to base64. Operates by putting the icon in the iconCache savefile,
+// exporting it as text, and then parsing the base64 from that.
+// (This relies on byond automatically storing icons in savefiles as base64)
+/proc/icon2base64(icon/icon, iconKey = "misc")
+ if (!isicon(icon))
+ return FALSE
+ to_save(GLOB.iconCache[iconKey], icon)
+ var/iconData = GLOB.iconCache.ExportText(iconKey)
+ var/list/partial = splittext(iconData, "{")
+ return replacetext(copytext(partial[2], 3, -5), "\n", "")
+
+/proc/icon2html(thing, target, icon_state, dir, frame = 1, moving = FALSE, realsize = FALSE, class = null)
+ if (!thing)
+ return
+
+ var/key
+ var/icon/I = thing
+ if (!target)
+ return
+ if (target == world)
+ target = GLOB.clients
+
+ var/list/targets
+ if (!islist(target))
+ targets = list(target)
+ else
+ targets = target
+ if (!targets.len)
+ return
+ if (!isicon(I))
+ if (isfile(thing)) //special snowflake
+ var/name = "[generate_asset_name(thing)].png"
+ register_asset(name, thing)
+ for (var/thing2 in targets)
+ send_asset(thing2, key, FALSE)
+ return ""
+ var/atom/A = thing
+ if (isnull(dir))
+ dir = A.dir
+ if (isnull(icon_state))
+ icon_state = A.icon_state
+ I = A.icon
+ if (ishuman(thing)) // Shitty workaround for a BYOND issue.
+ var/icon/temp = I
+ I = icon()
+ I.Insert(temp, dir = SOUTH)
+ dir = SOUTH
+ else
+ if (isnull(dir))
+ dir = SOUTH
+ if (isnull(icon_state))
+ icon_state = ""
+
+ I = icon(I, icon_state, dir, frame, moving)
+
+ key = "[generate_asset_name(I)].png"
+ register_asset(key, I)
+ for (var/thing2 in targets)
+ send_asset(thing2, key, FALSE)
+
+ if(realsize)
+ return ""
+
+
+ return ""
+
+/proc/icon2base64html(thing)
+ if (!thing)
+ return
+ var/static/list/bicon_cache = list()
+ if (isicon(thing))
+ var/icon/I = thing
+ var/icon_base64 = icon2base64(I)
+
+ if (I.Height() > world.icon_size || I.Width() > world.icon_size)
+ var/icon_md5 = md5(icon_base64)
+ icon_base64 = bicon_cache[icon_md5]
+ if (!icon_base64) // Doesn't exist yet, make it.
+ bicon_cache[icon_md5] = icon_base64 = icon2base64(I)
+
+
+ return ""
+
+ // Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with.
+ var/atom/A = thing
+ var/key = "[istype(A.icon, /icon) ? "\ref[A.icon]" : A.icon]:[A.icon_state]"
+
+
+ if (!bicon_cache[key]) // Doesn't exist, make it.
+ var/icon/I = icon(A.icon, A.icon_state, SOUTH, 1)
+ if (ishuman(thing)) // Shitty workaround for a BYOND issue.
+ var/icon/temp = I
+ I = icon()
+ I.Insert(temp, dir = SOUTH)
+
+ bicon_cache[key] = icon2base64(I, key)
+
+ return ""
+
+//Costlier version of icon2html() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs.
+/proc/costly_icon2html(thing, target)
+ if (!thing)
+ return
+
+ if (isicon(thing))
+ return icon2html(thing, target)
+
+ var/icon/I = getFlatIcon(thing)
+ return icon2html(I, target)
diff --git a/code/modules/goonchat/browserOutput.dm b/code/modules/goonchat/browserOutput.dm
new file mode 100644
index 0000000000000..2a464806ac0b8
--- /dev/null
+++ b/code/modules/goonchat/browserOutput.dm
@@ -0,0 +1,295 @@
+/*********************************
+For the main html chat area
+*********************************/
+
+//Precaching a bunch of shit
+GLOBAL_DATUM_INIT(iconCache, /savefile, new("tmp/iconCache.sav")) //Cache of icons for the browser output
+
+//Should match the value set in the browser js
+#define MAX_COOKIE_LENGTH 5
+#define SPAM_TRIGGER_AUTOMUTE 10
+
+//On client, created on login
+/datum/chatOutput
+ var/client/owner //client ref
+ // 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
+ var/loaded = FALSE // Has the client loaded the browser output area?
+ var/list/messageQueue //If they haven't loaded chat, this is where messages will go until they do
+ var/cookieSent = FALSE // Has the client sent a cookie for analysis
+ var/broken = FALSE
+ var/list/connectionHistory //Contains the connection history passed from chat cookie
+
+/datum/chatOutput/New(client/C)
+ owner = C
+ messageQueue = list()
+ connectionHistory = list()
+
+/datum/chatOutput/proc/start()
+ //Check for existing chat
+ if(!owner)
+ return FALSE
+
+ if(!winexists(owner, "browseroutput")) // Oh goddamnit.
+ set waitfor = FALSE
+ broken = TRUE
+ message_admins("Couldn't start chat for [key_name_admin(owner)]!")
+ . = FALSE
+ 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
+
+/datum/chatOutput/proc/load()
+ set waitfor = FALSE
+ if(!owner)
+ return
+
+ var/datum/asset/stuff = get_asset_datum(/datum/asset/group/goonchat)
+ stuff.send(owner)
+
+ show_browser(owner, file('code/modules/goonchat/browserassets/html/browserOutput.html'), "window=browseroutput")
+
+/datum/chatOutput/Topic(href, list/href_list)
+ if(usr.client != owner)
+ return TRUE
+
+ // Build arguments.
+ // Arguments are in the form "param[paramname]=thing"
+ var/list/params = list()
+ for(var/key in href_list)
+ if(length(key) > 7 && findtext(key, "param")) // 7 is the amount of characters in the basic param key template.
+ var/param_name = copytext(key, 7, -1)
+ var/item = href_list[key]
+
+ params[param_name] = item
+
+ var/data // Data to be sent back to the chat.
+ switch(href_list["proc"])
+ if("doneLoading")
+ data = doneLoading(arglist(params))
+
+ if("debug")
+ data = debug(arglist(params))
+
+ if("ping")
+ data = ping(arglist(params))
+
+ if("analyzeClientData")
+ data = analyzeClientData(arglist(params))
+
+ if("swaptodarkmode")
+ swaptodarkmode()
+
+ if("swaptolightmode")
+ swaptolightmode()
+
+ if(data)
+ ehjax_send(data = data)
+
+
+//Called on chat output done-loading by JS.
+/datum/chatOutput/proc/doneLoading()
+ if(loaded)
+ 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()
+ legacy_chat(owner, "Failed to load fancy chat, reverting to old chat. Certain features won't work.")
+
+ pingLoop()
+
+/datum/chatOutput/proc/showChat()
+ winset(owner, "output", "is-visible=false")
+ winset(owner, "browseroutput", "is-disabled=false;is-visible=true")
+
+/datum/chatOutput/proc/pingLoop()
+ set waitfor = FALSE
+
+ while (owner)
+ ehjax_send(data = owner.is_afk(29) ? "softPang" : "pang") // SoftPang isn't handled anywhere but it'll always reset the opts.lastPang.
+ sleep(30)
+
+/proc/syncChatRegexes()
+ for (var/user in GLOB.clients)
+ var/client/C = user
+ var/datum/chatOutput/Cchat = C.chatOutput
+ if (Cchat && !Cchat.broken && Cchat.loaded)
+ Cchat.syncRegex()
+
+/datum/chatOutput/proc/syncRegex()
+ var/list/regexes = list()
+
+ if (regexes.len)
+ ehjax_send(data = list("syncRegex" = regexes))
+
+/datum/chatOutput/proc/ehjax_send(client/C = owner, window = "browseroutput", data)
+ if(islist(data))
+ data = json_encode(data)
+ send_output(C, "[data]", "[window]:ehjaxCallback")
+
+//Sends client connection details to the chat to handle and save
+/datum/chatOutput/proc/sendClientData()
+ //Get dem deets
+ var/list/deets = list("clientData" = list())
+ deets["clientData"]["ckey"] = owner.ckey
+ deets["clientData"]["ip"] = owner.address
+ deets["clientData"]["compid"] = owner.computer_id
+ var/data = json_encode(deets)
+ ehjax_send(data = data)
+
+//Called by client, sent data to investigate (cookie history so far)
+/datum/chatOutput/proc/analyzeClientData(cookie = "")
+ //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"]))
+ found = row
+ break
+ CHECK_TICK
+
+ //Uh oh this fucker has a history of playing on a banned account!!
+ if (found.len > 0)
+ var/msg = "[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])"
+ //TODO: add a new evasion ban for the CURRENT client details, using the matched row details
+ message_admins(msg)
+ log_admin(msg)
+
+ cookieSent = TRUE
+
+//Called by js client every 60 seconds
+/datum/chatOutput/proc/ping()
+ return "pong"
+
+//Called by js client on js error
+/datum/chatOutput/proc/debug(error)
+ log_world("\[[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]\] Client: [(src.owner.key ? src.owner.key : src.owner)] triggered JS error: [error]")
+
+//Global chat procs
+/proc/to_chat_immediate(target, message, handle_whitespace = TRUE, trailing_newline = TRUE)
+ if(!target || !message)
+ return
+
+ if(target == world)
+ target = GLOB.clients
+
+ var/original_message = message
+ if(handle_whitespace)
+ message = replacetext(message, "\n", " ")
+ message = replacetext(message, "\t", "[FOURSPACES][FOURSPACES]")
+
+ //Replace expanded \icon macro with icon2html
+ //regex/Replace with a proc won't work here because icon2html takes target as an argument and there is no way to pass it to the replacement proc
+ //not even hacks with reassigning usr work
+ var/regex/i = new(@//, "g")
+ while(i.Find(message))
+ message = copytext(message,1,i.index)+icon2html(locate(i.group[1]), target, icon_state=i.group[2])+copytext(message,i.next)
+
+ 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.
+ legacy_chat(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
+
+ send_output(C, 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.
+ legacy_chat(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.
+ send_output(C, url_encode(url_encode(message)), "browseroutput:output")
+
+/proc/to_chat(target, message, handle_whitespace = TRUE, trailing_newline = TRUE)
+ if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized)
+ to_chat_immediate(target, message, handle_whitespace, trailing_newline)
+ return
+ SSchat.queue(target, message, handle_whitespace, trailing_newline)
+
+/datum/chatOutput/proc/swaptolightmode() //Dark mode light mode stuff. Yell at KMC if this breaks! (See darkmode.dm for documentation)
+ owner.force_white_theme()
+
+/datum/chatOutput/proc/swaptodarkmode()
+ owner.force_dark_theme()
+
+#undef MAX_COOKIE_LENGTH
diff --git a/code/modules/goonchat/browserassets/css/browserOutput.css b/code/modules/goonchat/browserassets/css/browserOutput.css
new file mode 100644
index 0000000000000..f9b020e29d88d
--- /dev/null
+++ b/code/modules/goonchat/browserassets/css/browserOutput.css
@@ -0,0 +1,381 @@
+/*****************************************
+*
+* GLOBAL STYLES
+*
+******************************************/
+html, body {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+ color: #a4bad6;
+}
+body {
+ background: #171717;
+ font-family: Verdana, sans-serif;
+ font-size: 13px;
+ 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: 1em;
+ 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;}
+
+em {font-style: normal; font-weight: bold;}
+
+/* LOG */
+.log_message {color: #386aff; font-weight: bold;}
+
+/* OOC */
+.ooc {font-weight: bold;}
+.ooc img.text_tag {width: 32px; height: 10px;}
+
+.ooc .everyone {color: #5353ff;}
+.ooc .looc {color: #3a9696;}
+.ooc .elevated {color: #2e78d9;}
+.ooc .moderator {color: #184880;}
+.ooc .developer {color: #1b521f;}
+.ooc .admin {color: #b82e00;}
+.ooc .aooc {color: #960018;}
+
+/* Admin: Private Messages */
+.pm .howto {color: #ff0000; font-weight: bold; font-size: 200%;}
+.pm .in {color: #ff0000;}
+.pm .out {color: #ff0000;}
+.pm .other {color: #5353ff;}
+
+/* Admin: Channels */
+.mod_channel {color: #735638; font-weight: bold;}
+.mod_channel .admin {color: #b82e00; font-weight: bold;}
+.admin_channel {color: #9611d4; font-weight: bold;}
+
+/* Radio: Misc */
+.deadsay {color: #e2c1ff}
+.radio {color: #1ecc43;}
+.deptradio {color: #ff00ff;} /* when all other department colors fail */
+.newscaster {color: #750000;}
+
+/* Radio Channels */
+.comradio {color: #193a7a;}
+.syndradio {color: #6d3f40;}
+.centradio {color: #5c5c8a;}
+.airadio {color: #ff00ff;}
+.entradio {color: #339966;}
+
+.secradio {color: #b41c1c;}
+.engradio {color: #a66300;}
+.medradio {color: #008160;}
+.sciradio {color: #993399;}
+.supradio {color: #5f4519;}
+.srvradio {color: #6eaa2c;}
+.expradio {color: #a3a332;}
+
+/* Miscellaneous */
+.name {font-weight: bold;}
+.alert {color: #d82020;}
+h1.alert, h2.alert {color: #a4bad6;}
+
+.emote {font-style: italic;}
+
+/* Game Messages */
+.attack {color: #ff0000;}
+.moderate {color: #cc0000;}
+.disarm {color: #990000;}
+.passive {color: #660000;}
+
+.danger {color: #c51e1e;}
+.warning {color: #c51e1e; font-style: italic;}
+.boldannounce {color: #c51e1e; font-weight: bold;}
+.rose {color: #ff5050;}
+.info {color: #6685f5;}
+.notice {color: #6685f5;}
+.alium {color: #00ff00;}
+.cult {color: #aa1c1c;}
+
+/* Languages */
+.alien {color: #855d85;}
+.changeling {color: #059223; font-style: italic;}
+.tajaran {color: #803b56;}
+.tajaran_signlang {color: #941c1c;}
+.skrell {color: #00ced1;}
+.soghun {color: #228b22;}
+.nabber_lang {color: #525252;}
+.solcom {color: #22228b;}
+.vox {color: #aa00aa;}
+.rough {font-family: "Trebuchet MS", cursive, sans-serif;}
+.say_quote {font-family: Georgia, Verdana, sans-serif;}
+.terran {color: #9c250b;}
+.moon {color: #422863;}
+.spacer {color: #ff6600;}
+
+.interface {color: #750e75;}
+
+.good {color: #4f7529; font-weight: bold;}
+.bad {color: #ee0000; font-weight: bold;}
+
+@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;}
+
+.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
new file mode 100644
index 0000000000000..bd0b1eba47417
--- /dev/null
+++ b/code/modules/goonchat/browserassets/css/browserOutput_white.css
@@ -0,0 +1,378 @@
+/*****************************************
+*
+* 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: 1em;
+ 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;}
+
+em {font-style: normal;font-weight: bold;}
+
+/* LOG */
+.log_message {color: #386aff;font-weight: bold;}
+
+/* OOC */
+.ooc {font-weight: bold;}
+.ooc img.text_tag {width: 32px; height: 10px;}
+
+.ooc .everyone {color: #002eb8;}
+.ooc .looc {color: #3a9696;}
+.ooc .elevated {color: #2e78d9;}
+.ooc .moderator {color: #184880;}
+.ooc .developer {color: #1b521f;}
+.ooc .admin {color: #b82e00;}
+.ooc .aooc {color: #960018;}
+
+/* Admin: Private Messages */
+.pm .howto {color: #ff0000;font-weight: bold;font-size: 200%;}
+.pm .in {color: #ff0000;}
+.pm .out {color: #ff0000;}
+.pm .other {color: #0000ff;}
+
+/* Admin: Channels */
+.mod_channel {color: #735638;font-weight: bold;}
+.mod_channel .admin {color: #b82e00;font-weight: bold;}
+.admin_channel {color: #9611d4;font-weight: bold;}
+
+/* Radio: Misc */
+.deadsay {color: #530fad;}
+.radio {color: #008000;}
+.deptradio {color: #ff00ff;} /* when all other department colors fail */
+.newscaster {color: #750000;}
+
+/* Radio Channels */
+.comradio {color: #193a7a;}
+.syndradio {color: #6d3f40;}
+.centradio {color: #5c5c8a;}
+.airadio {color: #ff00ff;}
+.entradio {color: #339966;}
+
+.secradio {color: #a30000;}
+.engradio {color: #a66300;}
+.medradio {color: #008160;}
+.sciradio {color: #993399;}
+.supradio {color: #5f4519;}
+.srvradio {color: #6eaa2c;}
+.expradio {color: #a3a332;}
+
+/* Miscellaneous */
+.name {font-weight: bold;}
+.alert {color: #ff0000;}
+h1.alert, h2.alert {color: #000080;}
+
+.emote {font-style: italic;}
+
+/* Game Messages */
+.attack {color: #ff0000;}
+.moderate {color: #cc0000;}
+.disarm {color: #990000;}
+.passive {color: #660000;}
+
+.danger {color: #ff0000;}
+.warning {color: #ff0000; font-style: italic;}
+.boldannounce {color: #ff0000; font-weight: bold;}
+.rose {color: #ff5050;}
+.info {color: #0000CC;}
+.notice {color: #000099;}
+.alium {color: #00ff00;}
+.cult {color: #800080; font-weight: bold; font-style: italic;}
+
+/* Languages */
+.alien {color: #855d85;}
+.changeling {color: #059223; font-style: italic;}
+.tajaran {color: #803b56;}
+.tajaran_signlang {color: #941c1c;}
+.skrell {color: #00ced1;}
+.soghun {color: #228b22;}
+.nabber_lang {color: #525252;}
+.solcom {color: #22228b;}
+.vox {color: #aa00aa;}
+.rough {font-family: "Trebuchet MS", cursive, sans-serif;}
+.say_quote {font-family: Georgia, Verdana, sans-serif;}
+.terran {color: #9c250b;}
+.moon {color: #422863;}
+.spacer {color: #ff6600;}
+
+.interface {color: #750e75;}
+
+.good {color: #4f7529; font-weight: bold;}
+.bad {color: #ee0000; font-weight: bold;}
+
+@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;}
+
+.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/css/styles_template.css b/code/modules/goonchat/browserassets/css/styles_template.css
new file mode 100644
index 0000000000000..5bd2c2affec60
--- /dev/null
+++ b/code/modules/goonchat/browserassets/css/styles_template.css
@@ -0,0 +1,363 @@
+/*****************************************
+*
+* 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: 1em;
+ 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;}
+#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;}
+
+em {font-style: normal; font-weight: bold;}
+
+/* LOG */
+.log_message {color: #386aff; font-weight: bold;}
+
+/* OOC */
+.ooc img.text_tag {width: 32px; height: 10px;}
+
+.ooc .everyone {color: #002eb8;}
+.ooc .looc {color: #3a9696;}
+.ooc .elevated {color: #2e78d9;}
+.ooc .moderator {color: #184880;}
+.ooc .developer {color: #1b521f;}
+.ooc .admin {color: #b82e00;}
+.ooc .aooc {color: #960018;}
+
+/* Admin: Private Messages */
+.pm .howto {color: #ff0000; font-weight: bold; font-size: 200%;}
+.pm .in {color: #ff0000;}
+.pm .out {color: #ff0000;}
+.pm .other {color: #0000ff;}
+
+/* Admin: Channels */
+.mod_channel {color: #735638; font-weight: bold;}
+.mod_channel .admin {color: #b82e00; font-weight: bold;}
+.admin_channel {color: #9611d4; font-weight: bold;}
+
+/* Radio: Misc */
+.deadsay {color: #530fad;}
+.radio {color: #008000;}
+.deptradio {color: #ff00ff;} /* when all other department colors fail */
+.newscaster {color: #750000;}
+
+/* Radio Channels */
+.comradio {color: #193a7a;}
+.syndradio {color: #6d3f40;}
+.centradio {color: #5c5c8a;}
+.airadio {color: #ff00ff;}
+.entradio {color: #339966;}
+
+.secradio {color: #a30000;}
+.engradio {color: #a66300;}
+.medradio {color: #008160;}
+.sciradio {color: #993399;}
+.supradio {color: #5f4519;}
+.srvradio {color: #6eaa2c;}
+.expradio {color: #a3a332;}
+
+/* Miscellaneous */
+.name {font-weight: bold;}
+.alert {color: #ff0000;}
+h1.alert, h2.alert {color: #000080;}
+
+.emote {font-style: italic;}
+
+/* Game Messages */
+.attack {color: #ff0000;}
+.moderate {color: #cc0000;}
+.disarm {color: #990000;}
+.passive {color: #660000;}
+
+.danger {color: #ff0000; font-weight: bold;}
+.warning {color: #ff0000; font-style: italic;}
+.boldannounce {color: #ff0000; font-weight: bold;}
+.rose {color: #ff5050;}
+.info {color: #0000cc;}
+.notice {color: #000099;}
+.alium {color: #00ff00;}
+.cult {color: #800080; font-weight: bold; font-style: italic;}
+
+
+/* Languages */
+.alien {color: #855d85;}
+.changeling {color: #059223; font-style: italic;}
+.tajaran {color: #803b56;}
+.tajaran_signlang {color: #941c1c;}
+.skrell {color: #00ced1;}
+.soghun {color: #228b22;}
+.nabber_lang {color: #525252;}
+.solcom {color: #22228b;}
+.vox {color: #aa00aa;}
+.rough {font-family: "Trebuchet MS", cursive, sans-serif;}
+.say_quote {font-family: Georgia, Verdana, sans-serif;}
+.terran {color: #9c250b;}
+.moon {color: #422863;}
+.spacer {color: #ff6600;}
+
+.interface {color: #750e75;}
+
+.good {color: #4f7529; font-weight: bold;}
+.bad {color: #ee0000; font-weight: bold;}
+
+@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;}
+
+.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;}
diff --git a/code/modules/goonchat/browserassets/html/browserOutput.html b/code/modules/goonchat/browserassets/html/browserOutput.html
new file mode 100644
index 0000000000000..11840cc9a78a1
--- /dev/null
+++ b/code/modules/goonchat/browserassets/html/browserOutput.html
@@ -0,0 +1,71 @@
+
+
+
+ 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.
+