VChat: Redone chat output in Vue.js

Co-authored-by: Leshana <Leshana@users.noreply.github.com>
This commit is contained in:
Arokha Sieyes
2020-02-11 23:08:57 -05:00
parent 314c290392
commit d5b820fc58
32 changed files with 2545 additions and 1338 deletions
+2
View File
@@ -1,5 +1,7 @@
#ignore misc BYOND files
Thumbs.db
vchat.db
vchat.db*
*.log
*.int
*.rsc
+1
View File
@@ -6,6 +6,7 @@
//---------------
#define isatom(D) istype(D, /atom)
#define isclient(D) istype(D, /client)
//---------------
//#define isobj(D) istype(D, /obj) //Built in
+1 -1
View File
@@ -79,7 +79,7 @@
#define COLOR_DEEP_SKY_BLUE "#00e1ff"
#define CLIENT_FROM_VAR(I) (ismob(I) ? I:client : (istype(I, /client) ? I : null))
// Shuttles.
+2
View File
@@ -68,6 +68,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define INIT_ORDER_XENOARCH -20
#define INIT_ORDER_CIRCUIT -21
#define INIT_ORDER_AI -22
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
// Subsystem fire priority, from lowest to highest priority
@@ -87,6 +88,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define FIRE_PRIORITY_PLANETS 75
#define FIRE_PRIORITY_MACHINES 100
#define FIRE_PRIORITY_PROJECTILES 150
#define FIRE_PRIORITY_CHAT 400
#define FIRE_PRIORITY_OVERLAYS 500
// Macro defining the actual code applying our overlays lists to the BYOND overlays list. (I guess a macro for speed)
+2 -1
View File
@@ -4,7 +4,8 @@
#define RANDOM_BLOOD_TYPE pick(4;"O-", 36;"O+", 3;"A-", 28;"A+", 1;"B-", 20;"B+", 1;"AB-", 5;"AB+")
#define to_chat(target, message) target << message
// #define to_chat(target, message) target << message Not anymore!
#define to_chat to_chat_filename=__FILE__;to_chat_line=__LINE__;to_chat_src=src;__to_chat
#define to_world(message) to_chat(world, message)
#define to_world_log(message) world.log << message
// TODO - Baystation has this log to crazy places. For now lets just world.log, but maybe look into it later.
+87
View File
@@ -0,0 +1,87 @@
SUBSYSTEM_DEF(chat)
name = "Chat"
flags = SS_TICKER
wait = 1 // SS_TICKER means this runs every tick
priority = FIRE_PRIORITY_CHAT
init_order = INIT_ORDER_CHAT
var/list/msg_queue = list()
/datum/controller/subsystem/chat/Initialize(timeofday)
init_vchat()
..()
/datum/controller/subsystem/chat/fire()
var/list/msg_queue = src.msg_queue // Local variable for sanic speed.
for(var/i in msg_queue)
var/client/C = i
var/list/messages = msg_queue[C]
msg_queue -= C
if (C)
C << output(jsEncode(messages), "htmloutput:putmessage")
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/chat/stat_entry()
..("C:[msg_queue.len]")
/datum/controller/subsystem/chat/proc/queue(target, time, message, handle_whitespace = TRUE)
if(!target || !message)
return
if(!istext(message))
stack_trace("to_chat called with invalid input type")
return
// Currently to_chat(world, ...) gets sent individually to each client. Consider.
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, "\n", "<br>")
message = replacetext(message, "\improper", "")
message = replacetext(message, "\proper", "")
if(isnull(time))
time = world.time
var/list/messageStruct = list("time" = time, "message" = 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
if(!C?.chatOutput || C.chatOutput.broken) //A player who hasn't updated his skin file.
//Send it to the old style output window.
DIRECT_OUTPUT(C, original_message)
continue
// // Client still loading, put their messages in a queue - Actually don't, logged already in database.
// if(!C.chatOutput.loaded && C.chatOutput.message_queue && islist(C.chatOutput.message_queue))
// C.chatOutput.message_queue[++C.chatOutput.message_queue.len] = messageStruct
// continue
LAZYINITLIST(msg_queue[C])
msg_queue[C][++msg_queue[C].len] = messageStruct
else
var/client/C = CLIENT_FROM_VAR(target) //Grab us a client if possible
if(!C)
return
if(!C?.chatOutput || C.chatOutput.broken) //A player who hasn't updated his skin file.
DIRECT_OUTPUT(C, original_message)
return
// // Client still loading, put their messages in a queue - Actually don't, logged already in database.
// if(!C.chatOutput.loaded && C.chatOutput.message_queue && islist(C.chatOutput.message_queue))
// C.chatOutput.message_queue[++C.chatOutput.message_queue.len] = messageStruct
// return
LAZYINITLIST(msg_queue[C])
msg_queue[C][++msg_queue[C].len] = messageStruct
+3 -3
View File
@@ -97,7 +97,7 @@
//dat += "<a href='?src=\ref[src];item=1'>Recover object</a>.<br>" //VOREStation Removal - Just log them.
//dat += "<a href='?src=\ref[src];allitems=1'>Recover all objects</a>.<br>" //VOREStation Removal
to_chat(user, browse(dat, "window=cryopod_console"))
user << browse(dat, "window=cryopod_console")
onclose(user, "cryopod_console")
/obj/machinery/computer/cryopod/Topic(href, href_list)
@@ -116,7 +116,7 @@
dat += "[person]<br/>"
dat += "<hr/>"
to_chat(user, browse(dat, "window=cryolog"))
user << browse(dat, "window=cryolog")
if(href_list["view"])
if(!allow_items) return
@@ -128,7 +128,7 @@
//VOREStation Edit End
dat += "<hr/>"
to_chat(user, browse(dat, "window=cryoitems"))
user << browse(dat, "window=cryoitems")
else if(href_list["item"])
if(!allow_items) return
@@ -105,7 +105,7 @@
dat += "</font>"
temp = ""
to_chat(user, browse(dat, "window=tcommachine;size=520x500;can_resize=0"))
user << browse(dat, "window=tcommachine;size=520x500;can_resize=0")
onclose(user, "dormitory")
@@ -192,7 +192,7 @@ obj/structure/windoor_assembly/Destroy()
if(src.electronics && istype(src.electronics, /obj/item/weapon/circuitboard/broken))
to_chat(usr,"<span class='warning'>The assembly has broken airlock electronics.</span>")
return
to_chat(usr,browse(null, "window=windoor_access")) //Not sure what this actually does... -Ner
usr << browse(null, "window=windoor_access") //Not sure what this actually does... -Ner
playsound(src, W.usesound, 100, 1)
user.visible_message("[user] pries the windoor into the frame.", "You start prying the windoor into the frame.")
+2 -2
View File
@@ -162,12 +162,12 @@
if(target in admins)
admin_stuff += "/([key])"
to_chat(target, "<span class='ooc'><span class='looc'>" + create_text_tag("looc", "LOOC:", target) + " <EM>[display_name][admin_stuff]:</EM> <span class='message'>[msg]</span></span></span>")
to_chat(target, "<span class='ooc looc'>" + create_text_tag("looc", "LOOC:", target) + " <EM>[display_name][admin_stuff]:</EM> <span class='message'>[msg]</span></span>")
for(var/client/target in r_receivers)
var/admin_stuff = "/([key])([admin_jump_link(mob, target.holder)])"
to_chat(target, "<span class='ooc'><span class='looc'>" + create_text_tag("looc", "LOOC:", target) + " <span class='prefix'>(R)</span><EM>[display_name][admin_stuff]:</EM> <span class='message'>[msg]</span></span></span>")
to_chat(target, "<span class='ooc looc'>" + create_text_tag("looc", "LOOC:", target) + " <span class='prefix'>(R)</span><EM>[display_name][admin_stuff]:</EM> <span class='message'>[msg]</span></span>")
/mob/proc/get_looc_source()
return src
+1
View File
@@ -28,6 +28,7 @@
GLOB.timezoneOffset = text2num(time2text(0,"hh")) * 36000
callHook("startup")
init_vchat()
//Emergency Fix
load_mods()
//end-emergency fix
+2
View File
@@ -24,6 +24,8 @@
var/area = null
var/time_died_as_mouse = null //when the client last died as a mouse
var/datum/tooltip/tooltips = null
var/datum/chatOutput/chatOutput
var/chatOutputLoadedAt
var/adminhelped = 0
+34 -5
View File
@@ -60,8 +60,6 @@
send2adminirc(href_list["irc_msg"])
return
//Logs all hrefs
if(config && config.log_hrefs && href_logfile)
WRITE_LOG(href_logfile, "[src] (usr:[usr])</small> || [hsrc ? "[hsrc] " : ""][href]")
@@ -71,6 +69,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")
src << link(href_list["link"])
..() //redirect to hsrc.Topic()
@@ -105,8 +108,14 @@
del(src)
return
to_chat(src, "<font color='red'>If the title screen is black, resources are still downloading. Please be patient until the title screen appears.</font>")
chatOutput = new /datum/chatOutput(src) //veechat
chatOutput.send_resources()
spawn()
chatOutput.start()
//Only show this if they are put into a new_player mob. Otherwise, "what title screen?"
if(isnewplayer(src.mob))
to_chat(src, "<font color='red'>If the title screen is black, resources are still downloading. Please be patient until the title screen appears.</font>")
GLOB.clients += src
GLOB.directory[ckey] = src
@@ -159,14 +168,14 @@
void.MakeGreed()
screen += void
if(prefs.lastchangelog != changelog_hash) //bolds the changelog button on the interface so we know there are updates.
if((prefs.lastchangelog != changelog_hash) && isnewplayer(src.mob)) //bolds the changelog button on the interface so we know there are updates.
to_chat(src, "<span class='info'>You have unread updates in the changelog.</span>")
winset(src, "rpane.changelog", "background-color=#eaeaea;font-style=bold")
if(config.aggressive_changelog)
src.changes()
hook_vr("client_new",list(src)) //VOREStation Code
if(config.paranoia_logging)
var/alert = FALSE //VOREStation Edit start.
if(isnum(player_age) && player_age == 0)
@@ -441,6 +450,26 @@ client/verb/character_setup()
return FALSE
return ..()
/client/verb/reload_vchat()
set name = "Reload VChat"
set category = "OOC"
//Timing
if(src.chatOutputLoadedAt > (world.time - 10 SECONDS))
alert(src, "You can only try to reload VChat every 10 seconds at most.")
return
//Log, disable
log_debug("[key_name(src)] reloaded VChat.")
//The hard way
qdel_null(src.chatOutput)
chatOutput = new /datum/chatOutput(src) //veechat
chatOutput.send_resources()
spawn()
chatOutput.start()
//This is for getipintel.net.
//You're welcome to replace this proc with your own that does your own cool stuff.
//Just set the client's ip_reputation var and make sure it makes sense with your config settings (higher numbers are worse results)
+10 -10
View File
@@ -106,7 +106,7 @@
if(src.loc == usr)
to_chat(usr, "The access panel is [locked? "locked" : "unlocked"].")
to_chat(usr, "The maintenance panel is [open ? "open" : "closed"].")
to_chat(usr, "Hardsuit systems are [offline ? "<font color='red'>offline</font>" : "<font color='green'>online</font>"].")
to_chat(usr, "Hardsuit systems are [offline ? "<span class='warning'>offline</span>" : "<span class='notice'>online</span>"].")
to_chat(usr, "The cooling stystem is [cooling_on ? "active" : "inactive"].")
if(open)
@@ -265,7 +265,7 @@
if(!failed_to_seal)
if(!instant)
M.visible_message("<font color='blue'>[M]'s suit emits a quiet hum as it begins to adjust its seals.</font>","<font color='blue'>With a quiet hum, the suit begins running checks and adjusting components.</font>")
M.visible_message("<span class='notice'>[M]'s suit emits a quiet hum as it begins to adjust its seals.</span>","<span class='notice'>With a quiet hum, the suit begins running checks and adjusting components.</span>")
if(seal_delay && !do_after(M,seal_delay))
if(M)
to_chat(M, "<span class='warning'>You must remain still while the suit is adjusting the components.</span>")
@@ -297,16 +297,16 @@
piece.icon_state = "[suit_state][!seal_target ? "_sealed" : ""]"
switch(msg_type)
if("boots")
to_chat(M, "<font color='blue'>\The [piece] [!seal_target ? "seal around your feet" : "relax their grip on your legs"].</font>")
to_chat(M, "<span class='notice'>\The [piece] [!seal_target ? "seal around your feet" : "relax their grip on your legs"].</span>")
M.update_inv_shoes()
if("gloves")
to_chat(M, "<font color='blue'>\The [piece] [!seal_target ? "tighten around your fingers and wrists" : "become loose around your fingers"].</font>")
to_chat(M, "<span class='notice'>\The [piece] [!seal_target ? "tighten around your fingers and wrists" : "become loose around your fingers"].</span>")
M.update_inv_gloves()
if("chest")
to_chat(M, "<font color='blue'>\The [piece] [!seal_target ? "cinches tight again your chest" : "releases your chest"].</font>")
to_chat(M, "<span class='notice'>\The [piece] [!seal_target ? "cinches tight again your chest" : "releases your chest"].</span>")
M.update_inv_wear_suit()
if("helmet")
to_chat(M, "<font color='blue'>\The [piece] hisses [!seal_target ? "closed" : "open"].</font>")
to_chat(M, "<span class='notice'>\The [piece] hisses [!seal_target ? "closed" : "open"].</span>")
M.update_inv_head()
if(helmet)
helmet.update_light(wearer)
@@ -341,7 +341,7 @@
// Success!
canremove = seal_target
to_chat(M, "<font color='blue'><b>Your entire suit [canremove ? "loosens as the components relax" : "tightens around you as the components lock into place"].</b></font>")
to_chat(M, "<span class='notice'><b>Your entire suit [canremove ? "loosens as the components relax" : "tightens around you as the components lock into place"].</b></span>")
M.client.screen -= booting_L
qdel(booting_L)
booting_R.icon_state = "boot_done"
@@ -726,7 +726,7 @@
return
if(seal_delay > 0 && istype(M) && (M.back == src || M.belt == src))
M.visible_message("<font color='blue'>[M] starts putting on \the [src]...</font>", "<font color='blue'>You start putting on \the [src]...</font>")
M.visible_message("<span class='notice'>[M] starts putting on \the [src]...</span>", "<span class='notice'>You start putting on \the [src]...</span>")
if(!do_after(M,seal_delay))
if(M && (M.back == src || M.belt == src))
if(!M.unEquip(src))
@@ -735,7 +735,7 @@
return
if(istype(M) && (M.back == src || M.belt == src))
M.visible_message("<font color='blue'><b>[M] struggles into \the [src].</b></font>", "<font color='blue'><b>You struggle into \the [src].</b></font>")
M.visible_message("<span class='notice'><b>[M] struggles into \the [src].</b></span>", "<span class='notice'><b>You struggle into \the [src].</b></span>")
wearer = M
wearer.wearing_rig = src
update_icon()
@@ -785,7 +785,7 @@
holder = use_obj.loc
if(istype(holder))
if(use_obj && check_slot == use_obj)
to_chat(H, "<font color='blue'><b>Your [use_obj.name] [use_obj.gender == PLURAL ? "retract" : "retracts"] swiftly.</b></font>")
to_chat(H, "<span class='notice'><b>Your [use_obj.name] [use_obj.gender == PLURAL ? "retract" : "retracts"] swiftly.</b></span>")
use_obj.canremove = 1
holder.drop_from_inventory(use_obj)
use_obj.forceMove(get_turf(src))
+2 -2
View File
@@ -236,7 +236,7 @@
<A href='?src=\ref[src];action=dispose'>Eject ingredients!<BR>\
"}
to_chat(user, browse("<HEAD><TITLE>Microwave Controls</TITLE></HEAD><TT>[dat]</TT>", "window=microwave"))
user << browse("<HEAD><TITLE>Microwave Controls</TITLE></HEAD><TT>[dat]</TT>", "window=microwave")
onclose(user, "microwave")
return
@@ -422,4 +422,4 @@
/obj/machinery/microwave/advanced/Initialize()
..()
reagents.maximum_volume = 1000
reagents.maximum_volume = 1000
+2 -2
View File
@@ -143,8 +143,8 @@
var/mob/living/carbon/human/H = src
var/datum/gender/T = gender_datums[H.get_visible_gender()]
src.visible_message( \
"<font color='blue'>[src] examines [T.himself].</font>", \
"<font color='blue'>You check yourself for injuries.</font>" \
"<span class='notice'>[src] examines [T.himself].</span>", \
"<span class='notice'>You check yourself for injuries.</span>" \
)
for(var/obj/item/organ/external/org in H.organs)
+2 -2
View File
@@ -17,10 +17,10 @@
spawn() alert("You have logged in already with another key this round, please log out of this one NOW or risk being banned!")
if(matches)
if(M.client)
message_admins("<font color='red'><B>Notice: </B></font><font color='blue'><A href='?src=\ref[usr];priv_msg=\ref[src]'>[key_name_admin(src)]</A> has the same [matches] as <A href='?src=\ref[usr];priv_msg=\ref[M]'>[key_name_admin(M)]</A>.</font>", 1)
message_admins("<font color='red'><B>Notice: </B></font><font color='blue'>[key_name_admin(src)] has the same [matches] as [key_name_admin(M)].</font>", 1)
log_adminwarn("Notice: [key_name(src)] has the same [matches] as [key_name(M)].")
else
message_admins("<font color='red'><B>Notice: </B></font><font color='blue'><A href='?src=\ref[usr];priv_msg=\ref[src]'>[key_name_admin(src)]</A> has the same [matches] as [key_name_admin(M)] (no longer logged in). </font>", 1)
message_admins("<font color='red'><B>Notice: </B></font><font color='blue'>[key_name_admin(src)] has the same [matches] as [key_name_admin(M)] (no longer logged in). </font>", 1)
log_adminwarn("Notice: [key_name(src)] has the same [matches] as [key_name(M)] (no longer logged in).")
/mob/Login()
+2 -2
View File
@@ -331,9 +331,9 @@
if (flavor_text && flavor_text != "")
var/msg = replacetext(flavor_text, "\n", " ")
if(length(msg) <= 40)
return "<font color='blue'>[msg]</font>"
return "<span class='notice'>[msg]</span>"
else
return "<font color='blue'>[copytext_preserve_html(msg, 1, 37)]... <a href='byond://?src=\ref[src];flavor_more=1'>More...</font></a>"
return "<span class='notice'>[copytext_preserve_html(msg, 1, 37)]... <a href='byond://?src=\ref[src];flavor_more=1'>More...</span></a>"
/*
/mob/verb/help()
@@ -91,11 +91,19 @@
. = ..(user,2)
if(.)
switch(construction_stage)
if(2) to_chat(user, "<span class='notice'>It has a metal frame loosely shaped around the stock.</span>")
if(3) to_chat(user, "<span class='notice'>It has a metal frame duct-taped to the stock.</span>")
if(4) to_chat(user, "<span class='notice'>It has a length of pipe attached to the body.</span>")
if(4) to_chat(user, "<span class='notice'>It has a length of pipe welded to the body.</span>")
if(6) to_chat(user, "<span class='notice'>It has a cable mount and capacitor jack wired to the frame.</span>")
if(7) to_chat(user, "<span class='notice'>It has a single superconducting coil threaded onto the barrel.</span>")
if(8) to_chat(user, "<span class='notice'>It has a pair of superconducting coils threaded onto the barrel.</span>")
if(9) to_chat(user, "<span class='notice'>It has three superconducting coils attached to the body, waiting to be secured.</span>")
if(2)
to_chat(user, "<span class='notice'>It has a metal frame loosely shaped around the stock.</span>")
if(3)
to_chat(user, "<span class='notice'>It has a metal frame duct-taped to the stock.</span>")
if(4)
to_chat(user, "<span class='notice'>It has a length of pipe attached to the body.</span>")
if(4)
to_chat(user, "<span class='notice'>It has a length of pipe welded to the body.</span>")
if(6)
to_chat(user, "<span class='notice'>It has a cable mount and capacitor jack wired to the frame.</span>")
if(7)
to_chat(user, "<span class='notice'>It has a single superconducting coil threaded onto the barrel.</span>")
if(8)
to_chat(user, "<span class='notice'>It has a pair of superconducting coils threaded onto the barrel.</span>")
if(9)
to_chat(user, "<span class='notice'>It has three superconducting coils attached to the body, waiting to be secured.</span>")
+86
View File
@@ -0,0 +1,86 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="semantic.css" />
<link rel="stylesheet" type="text/css" href="vchat-font-embedded.css" />
<link rel="stylesheet" type="text/css" href="ss13styles.css" />
</head>
<body>
<div class="ui segment">
<span class='game say'><b>Test Person</b> says, "Testing say message."</span><br>
<span class='notice'>Testing notice message.</span><br>
<span class='danger'>Testing danger message.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message."</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message.</span></span><br>
<span class='admin_channel'>Testing asay message.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 2."</span><br>
<span class='notice'>Testing notice message 2.</span><br>
<span class='danger'>Testing danger message 2.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 2".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 2.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 2.</span></span><br>
<span class='admin_channel'>Testing asay message 2.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 3."</span><br>
<span class='notice'>Testing notice message 3.</span><br>
<span class='danger'>Testing danger message 3.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 3".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 3.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 3.</span></span><br>
<span class='admin_channel'>Testing asay message 3.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 4."</span><br>
<span class='notice'>Testing notice message 4.</span><br>
<span class='danger'>Testing danger message 4.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 4".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 4.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 4.</span></span><br>
<span class='admin_channel'>Testing asay message 4.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 5."</span><br>
<span class='notice'>Testing notice message 5.</span><br>
<span class='danger'>Testing danger message 5.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 5".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 5.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 5.</span></span><br>
<span class='admin_channel'>Testing asay message 5.</span><br>
</div>
<div class="ui segment inverted">
<span class='game say'><b>Test Person</b> says, "Testing say message."</span><br>
<span class='notice'>Testing notice message.</span><br>
<span class='danger'>Testing danger message.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message."</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message.</span></span><br>
<span class='admin_channel'>Testing asay message.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 2."</span><br>
<span class='notice'>Testing notice message 2.</span><br>
<span class='danger'>Testing danger message 2.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 2".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 2.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 2.</span></span><br>
<span class='admin_channel'>Testing asay message 2.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 3."</span><br>
<span class='notice'>Testing notice message 3.</span><br>
<span class='danger'>Testing danger message 3.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 3".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 3.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 3.</span></span><br>
<span class='admin_channel'>Testing asay message 3.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 4."</span><br>
<span class='notice'>Testing notice message 4.</span><br>
<span class='danger'>Testing danger message 4.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 4".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 4.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 4.</span></span><br>
<span class='admin_channel'>Testing asay message 4.</span><br>
<span class='game say'><b>Test Person</b> says, "Testing say message 5."</span><br>
<span class='notice'>Testing notice message 5.</span><br>
<span class='danger'>Testing danger message 5.</span><br>
<span class='secradio'>[Security] <b>Secu Person</b> says, "Testing radio message 5".</span><br>
<span class='ooc'><span class='everyone'>Testing OOC message 5.</span></span><br>
<span class='ooc'><span class='looc'>Testing LOOC message 5.</span></span><br>
<span class='admin_channel'>Testing asay message 5.</span><br>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+227
View File
@@ -0,0 +1,227 @@
/* VChat Styles */
#contentbox {
margin-top: 3rem; /* Make room for the fixed top menu */
overflow-wrap: break-word;
word-wrap: break-word; /* IE, blah */
}
.menu, .item {
padding-top: 0.2em !important;
padding-bottom: 0.2em !important;
}
.ui.menu .item > .label:not(.floating) {
margin-left: 0;
padding-bottom: 0;
padding-top: 0;
text-align: center;
font-size: 10px;
}
#app > .menu {
overflow-x: auto;
overflow-y: hidden;
}
.item {
cursor: pointer;
}
body.inverted {
background-color: #111111;
}
.blinkwarn {
color: #FF0000;
animation: blink-animation 1s steps(5, start) infinite;
}
@keyframes blink-animation {
to {
visibility: hidden;
}
}
/* The chat message display classes */
#messagebox div { display: none; } /* Hide messages by default */
#messagebox.vc_showall div { display: block; } /* Unless we have vc_showall */
#messagebox.vc_localchat .vc_localchat { display: block; }
#messagebox.vc_radio .vc_radio { display: block; }
#messagebox.vc_warnings .vc_warnings { display: block; }
#messagebox.vc_info .vc_info { display: block; }
#messagebox.vc_deadchat .vc_deadchat { display: block; }
#messagebox.vc_globalooc .vc_globalooc { display: block; }
#messagebox.vc_adminpm .vc_adminpm { display: block; }
#messagebox.vc_adminchat .vc_adminchat { display: block; }
#messagebox.vc_modchat .vc_modchat { display: block; }
#messagebox.vc_eventchat .vc_eventchat { display: block; }
#messagebox.vc_system .vc_system { display: block; }
/* SS13 Styles */
#messagebox {font-family: Verdana, sans-serif;}
#messagebox h1, #messagebox h2, #messagebox h3, #messagebox h4, #messagebox h5, #messagebox h6 {color: #0000ff; font-family: Georgia, Verdana, sans-serif;}
#messagebox em {font-style: normal; font-weight: bold;}
.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;}
.prefix {font-weight: bold;}
.log_message {color: #386AFF; font-weight: bold;}
/*.inverted .message {color: #386AFF; font-weight: bold;}*/
/* OOC */
.ooc {font-weight: bold;}
.ooc img.text_tag {width: 32px; height: 10px;}
.ooc .everyone {color: #002eb8;}
.inverted .ooc .everyone {color: #004ed8;} /* Dark mode */
.ooc .looc {color: #3A9696;}
.ooc .elevated {color: #2e78d9;}
.ooc .moderator {color: #184880;}
.ooc .developer {color: #1b521f;}
.ooc .admin {color: #b82e00;}
.ooc .event_manager {color: #660033;}
.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;}
.event_channel {color: #cc3399; font-weight: bold;}
/* Radio: Misc */
.deadsay {color: #530FAD;}
.inverted .deadsay {color: #732FCD;} /* Dark mode */
.radio {color: #008000;}
.deptradio {color: #ff00ff;} /* when all other department colors fail */
.newscaster {color: #750000;}
/* Radio Channels */
.comradio {color: #193A7A;}
.inverted .comradio {color: #395A9A;} /* Dark mode */
.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: #555555;}
/* Miscellaneous */
.name {font-weight: bold;}
.say {}
.alert {color: #ff0000;}
h1.alert, h2.alert {color: #000000;}
.emote {font-style: italic;}
/* Game Messages */
.attack {color: #ff0000;}
.moderate {color: #CC0000;}
.disarm {color: #990000;}
.passive {color: #660000;}
.critical {color: #ff0000; font-weight: bold; font-size: 150%;}
.danger {color: #ff0000; font-weight: bold;}
.warning {color: #ff0000; font-style: italic;}
.rose {color: #ff5050;}
.info {color: #0000CC;}
.inverted .info {color: #6060c9;} /* Dark mode */
.notice {color: #000099;}
.inverted .notice {color: #6060c9;} /* Dark mode */
.alium {color: #00ff00;}
.cult {color: #800080; font-weight: bold; font-style: italic;}
.reflex_shoot {color: #000099; font-style: italic;}
/* Languages */
.alien {color: #543354;}
.tajaran {color: #803B56;}
.tajaran_signlang {color: #941C1C;}
.akhani {color: #AC398C;}
.skrell {color: #00B0B3;}
.skrellfar {color: #70FCFF;}
.soghun {color: #228B22;}
.solcom {color: #22228B;}
.changeling {color: #800080;}
.sergal {color: #0077FF; font-family: "Comic Sans MS";}
.birdsongc {color: #CC9900;}
.vulpkanin {color: #B97A57;}
.enochian {color: #848A33; letter-spacing:-1pt; word-spacing:4pt; font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;}
.daemon {color: #5E339E; letter-spacing:-1pt; word-spacing:0pt; font-family: "Courier New", Courier, monospace;}
.bug {color: #9e9e39;}
.vox {color: #AA00AA;}
.zaddat {color: #941C1C;}
.rough {font-family: "Trebuchet MS", cursive, sans-serif;}
.say_quote {font-family: Georgia, Verdana, sans-serif;}
.terminus {font-family: "Times New Roman", Times, serif, sans-serif}
.interface {color: #330033;}
/*BIG IMG.icon {width: 32px; height: 32px;}*/
img.icon {vertical-align: middle; max-height: 1em;}
img.icon.bigicon {max-height: 32px;}
/* Zoom levels */
.zoom_normal {
font-size: 0.9em;
line-height: 1.35;
}
.zoom_more {
font-size: 1.2em;
line-height: 1.4;
}
.zoom_less {
font-size: 0.8em;
line-height: 1.2;
}
/* Debug Logs */
.debug_error {color:#FF0000; font-weight:bold}
.debug_warning {color:#FF0000;}
.debug_info {}
.debug_debug {color:#0000FF;}
.debug_trace {color:#888888;}
/* Log animations */
.msgsanim-enter-active {
transition: opacity 0.5s, transform 0.5s;
}
.msgsanim-enter {
opacity: 0;
transform: translateX(15px);
}
/* Dog mode */
.woof {
background-color: rgba(255,255,255,0) !important;
background-image: url('dog.gif') !important;
background-size: cover !important;
transition: background-color 15s;
}
.woof div, .woof span {
background-color: rgba(0,0,0,0) !important;
transition: background-color 15s;
}
File diff suppressed because one or more lines are too long
+162
View File
@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html debug="true">
<head>
<title>VChat</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="semantic.min.css" />
<link rel="stylesheet" type="text/css" href="vchat-font-embedded.css" />
<link rel="stylesheet" type="text/css" href="ss13styles.css" />
<!-- Important scripts -->
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="vue.min.js"></script>
<script type="text/javascript" src="vchat.js"></script>
</head>
<body>
<div id="loader">
<p>VChat is still loading. If you see this for a very long time, try the OOC 'Reload VChat' verb, or reconnecting.</p>
<p>Sometimes if you're still caching resources, it will take longer than usual.</p>
</div>
<div id="app" @mouseup="on_mouseup" style="display: none;">
<!-- Top menu -->
<div class="ui top fixed pointing menu" :class="{ inverted: inverted }">
<div v-for="tab in tabs" class="item" :class="{ active: tab.active, inverted: inverted }" @click="switchtab(tab)" @click.ctrl="editmode">
{{tab.name}}
<div class="ui empty circular label" :class="tab_unread_classes(tab)"></div>
</div>
<div class="right menu">
<div class="item" :class="{ inverted: inverted }" @click="newtab" title="New Tab"><i class="icon-folder-add"></i></div>
<div class="item" :class="{ inverted: inverted }" @click="pause" title="Pause Autoscroll">
<i class="icon-pause-outline" :class="{ blinkwarn: paused }"></i>
</div>
<div class="item" :class="{ inverted: inverted }" @click="editmode" title="Edit Mode">
<i class="icon-cog-outline" :class="{ blinkwarn: editing }"></i>
</div>
<div class="item"><span class="ui circular label" title="VChat Latency (Not exactly network latency)" :class="ping_classes">{{latency}}ms</span></div>
</div>
</div>
<!-- Editor box -->
<div id="contentbox">
<div v-show="editing" id="editbox" class="ui segment" :class="{ inverted: inverted }">
<div class="ui internally celled grid">
<div class="row">
<div class="sixteen wide column">
<div class="ui center aligned header" :class="{ inverted: inverted }">
<h2>VChat Settings</h2>
</div>
<div style="text-align: center;"><a href="#" @click="editmode">Close Settings</a></div>
</div>
</div>
<div class="row">
<div class="eight wide column">
<h3>'{{active_tab.name}}' Tab Settings</h3>
<span v-show="active_tab.immutable">This tab is immutable. You cannot make changes.</span>
<div v-show="!active_tab.immutable">
<a href="#" @click="renametab">Rename Tab</a> - <a href="#" @click="deltab">Delete Tab</a>
<h5>Messages to display:</h5>
<div class="ui form">
<div class="grouped fields">
<div class="field" v-for="type in type_table">
<div v-show="!type.admin || is_admin" class="ui slider checkbox" :class="{ inverted: inverted, disabled: type.required || active_tab.immutable, checked: type.required || active_tab.immutable }">
<input type="checkbox" id="type.becomes" :value="type.becomes" v-model="active_tab.classes" :disabled="type.required || active_tab.immutable" :checked="type.required || active_tab.immutable"><label :for="type.becomes">{{type.pretty}}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="eight wide column">
<h3>Global Settings</h3>
<div class="ui form" :class="{ inverted: inverted }">
<div class="grouped fields">
<div class="field">
<div class="ui slider checkbox" :class="{ inverted: inverted }">
<input type="checkbox" id="darkmode" v-model="inverted"><label for="darkmode">Dark Mode</label>
</div>
</div>
<div class="field">
<div class="ui slider checkbox" :class="{ inverted: inverted }">
<input type="checkbox" id="combining" v-model="crushing"><label for="combining">Combine Messages</label>
</div>
</div>
<div class="field">
<div class="ui slider checkbox" :class="{ inverted: inverted }">
<input type="checkbox" id="combining" v-model="animated"><label for="animated">Animate Messages</label>
</div>
</div>
</div>
<div class="inline fields">
<label>Font Size</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="fssmall" name="fontsize" v-model="fontsize" value="zoom_less">
<label for="fssmall">Small</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="fsstd" name="fontsize" v-model="fontsize" value="zoom_normal">
<label for="fsstd">Standard</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="fslarge" name="fontsize" v-model="fontsize" value="zoom_more">
<label for="fslarge">Large</label>
</div>
</div>
</div>
<div class="inline fields">
<div class="field">
<label># Shown Messages</label>
<input :class="{ inverted: inverted }" type="number" name="showingnum" placeholder="200" title="Will affect performance!" v-model.lazy.number="showingnum" required style="width: 5em; padding: 0.1em;">
</div>
</div>
</div>
<button class="ui button" :class="{ inverted: inverted }" @click="save_chatlog">Export Chatlog</button>
</div>
</div>
</div>
</div>
<!-- Messages box -->
<div v-show="!editing" id="messagebox" class="ui segment" :class="active_classes" @click="click_message">
<div v-show="messages.length > showingnum" class="vc_system">
<span class='notice'>[ {{messages.length - showingnum}} previous messages hidden due to display settings. ]</span>
</div>
<transition-group name="msgsanim" tag="span" :css="animated">
<div v-for="message in shown_messages" :class="[message.category, fontsize]" :key="message.id">
<span v-html="message.content"></span>
<span v-show="message.repeats > 1" class="ui grey circular label">x{{message.repeats}}</span>
</div>
</transition-group>
</div>
</div>
<!--
Tab: {{active_tab}}<br>
Classes: {{active_classes}}<br>
Fontsize: {{fontsize}}<br>
-->
</div>
<script type="text/javascript">
//Document Ready
(function(){
start_vchat();
document.getElementById("loader").style.display = 'none';
document.getElementById("app").style.display = 'block';
})();
</script>
</body>
</html>
+78
View File
@@ -0,0 +1,78 @@
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw TypeError('predicate must be a function');
}
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
// 5. Let k be 0.
var k = 0;
// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}
// 7. Return undefined.
return undefined;
},
configurable: true,
writable: true
});
}
if (!Date.now) {
Date.now = function now() {
return new Date().getTime();
};
}
//IE ugh
function storageAvailable(type) {
var storage;
try {
storage = window[type];
var x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch(e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
(storage && storage.length !== 0);
}
}
+766
View File
@@ -0,0 +1,766 @@
//The 'V' is for 'VORE' but you can pretend it's for Vue.js if you really want.
//Options for vchat
var vchat_opts = {
crush_messages: 3, //How many messages back should we try to combine if crushing is on
pingThisOften: 10000, //ms
pingDropsAllowed: 2,
cookiePrefix: "vst-" //If you're another server, you can change this if you want.
};
var DARKMODE_COLORS = {
buttonBgColor: "#40628a",
buttonTextColor: "#FFFFFF",
windowBgColor: "#272727",
highlightColor: "#009900",
tabTextColor: "#FFFFFF",
tabBackgroundColor: "#272727"
};
var LIGHTMODE_COLORS = {
buttonBgColor: "none",
buttonTextColor: "#000000",
windowBgColor: "none",
highlightColor: "#007700",
tabTextColor: "#000000",
tabBackgroundColor: "none"
};
/***********
*
* Setup Methods
*
************/
var set_storage = set_cookie;
var get_storage = get_cookie;
var domparser = new DOMParser();
//Upgrade to LS
if (storageAvailable('localStorage')) {
set_storage = set_localstorage;
get_storage = get_localstorage;
}
//State-tracking variables
var vchat_state = {
ready: false,
//Userinfo as reported by byond
byond_ip: null,
byond_cid: null,
byond_ckey: null,
//Ping status
lastPingAttempt: 0,
lastPingReply: 0,
missedPings: 0,
latency: 0,
reconnecting: false
}
function start_vchat() {
//Instantiate Vue.js
start_vue();
//Inform byond we're done
vchat_state.ready = true;
push_Topic('done_loading');
//I'll do my own winsets
doWinset("htmloutput", {"is-visible": true});
doWinset("oldoutput", {"is-visible": false});
doWinset("chatloadlabel", {"is-visible": false});
//Commence the pingening
send_ping();
setInterval(send_ping, vchat_opts.pingThisOften);
}
//Loads vue for chat usage
var vueapp;
function start_vue() {
vueapp = new Vue({
el: '#app',
data: {
messages: [], //List o messages from byond
tabs: [ //Our tabs
{name: "Main", classes: ["vc_showall"], immutable: true, active: true}
],
always_show: ["vc_looc", "vc_system"], //Classes to always display on every tab
unread_messages: {}, //Message categories that haven't been looked at since we got one of them
editing: false, //If we're in settings edit mode
paused: false, //Autoscrolling
latency: 0, //Not necessarily network latency, since the game server has to align the responses into ticks
ext_styles: "", //Styles for chat downloaded files
is_admin: false,
//Settings
inverted: false, //Dark mode
crushing: true, //Combine similar messages
showingnum: 200, //How many messages to show
animated: true, //Small CSS animations for new messages
fontsize: "zoom_normal", //Font size nudging
//The table to map game css classes to our vchat classes
type_table: [
{
matches: ".say, .emote",
becomes: "vc_localchat",
pretty: "Local Chat",
required: false,
admin: false
},
{
matches: ".alert, .syndradio, .centradio, .airadio, .entradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .supradio, .srvradio, .expradio, .radio, .deptradio, .newscaster",
becomes: "vc_radio",
pretty: "Radio Comms",
required: false,
admin: false
},
{
matches: ".notice, .adminnotice, .info, .sinister, .cult",
becomes: "vc_info",
pretty: "Notices",
required: false,
admin: false
},
{
matches: ".critical, .danger, .userdanger, .warning, .italics",
becomes: "vc_warnings",
pretty: "Warnings",
required: false,
admin: false
},
{
matches: ".deadsay",
becomes: "vc_deadchat",
pretty: "Deadchat",
required: false,
admin: false
},
{
matches: ".ooc:not(.looc)",
becomes: "vc_globalooc",
pretty: "Global OOC",
required: false,
admin: false
},
{
matches: ".pm",
becomes: "vc_adminpm",
pretty: "Admin PMs",
required: false,
admin: false
},
{
matches: ".admin_channel",
becomes: "vc_adminchat",
pretty: "Admin Chat",
required: false,
admin: true
},
{
matches: ".mod_channel",
becomes: "vc_modchat",
pretty: "Mod Chat",
required: false,
admin: true
},
{
matches: ".event_channel",
becomes: "vc_eventchat",
pretty: "Event Chat",
required: false,
admin: true
},
{
matches: ".ooc.looc, .ooc .looc", //Dumb game
becomes: "vc_looc",
pretty: "Local OOC",
required: true
},
{
matches: ".boldannounce",
becomes: "vc_system",
pretty: "System Messages",
required: true
}
],
},
created: function() {
/*Dog mode
setTimeout(function(){
document.body.className += " woof";
},5000);
*/
/* Stress test
var varthis = this;
setInterval( function() {
if(varthis.messages.length > 10000) {
return;
}
var stringymessages = JSON.stringify(varthis.messages);
var unstringy = JSON.parse(stringymessages);
unstringy.forEach( function(message) {
message.id = (varthis.messages.length + 1);
varthis.messages.push(message);
});
varthis.internal_message("Now have " + varthis.messages.length + " messages in array.");
}, 10000);
*/
},
mounted: function() {
//Load our settings
this.load_settings();
var xhr = new XMLHttpRequest();
xhr.open('GET', 'ss13styles.css');
xhr.onreadystatechange = (function() {
this.ext_styles = xhr.responseText;
}).bind(this);
xhr.send();
},
updated: function() {
if(!this.editing && !this.paused) {
window.scrollTo(0,document.getElementById("messagebox").scrollHeight);
}
},
watch: {
//Save the inverted setting to LS
inverted: function (newSetting) {
set_storage("darkmode",newSetting);
if(newSetting) { //Special treatment for <body> which is outside Vue's scope and has custom css
document.body.classList.add("inverted");
switch_ui_mode(DARKMODE_COLORS);
} else {
document.body.classList.remove("inverted");
switch_ui_mode(LIGHTMODE_COLORS);
}
},
crushing: function (newSetting) {
set_storage("crushing",newSetting);
},
animated: function (newSetting) {
set_storage("animated",newSetting);
},
fontsize: function (newSetting) {
set_storage("fontsize",newSetting);
},
showingnum: function (newSetting, oldSetting) {
if(!isFinite(newSetting)) {
this.showingnum = oldSetting;
return;
}
newSetting = Math.floor(newSetting);
if(newSetting <= 50) {
this.showingnum = 50;
} else if(newSetting > 2000) {
this.showingnum = 2000;
}
set_storage("showingnum",this.showingnum);
}
},
computed: {
//Which tab is active?
active_tab: function() {
//Had to polyfill this stupid .find since IE doesn't have EC6
let tab = this.tabs.find( function(tab) {
return tab.active;
});
return tab;
},
//Which classes does the active tab need?
active_classes: function() {
let classarray = this.active_tab.classes;
let classtext = classarray.toString(); //Convert to a string
let classproper = classtext.replace(/,/g," ");
if(this.inverted) classproper += " inverted";
return classproper; //Swap commas for spaces
},
//What color does the latency pip get?
ping_classes: function() {
if(this.latency === 0) { return "grey"; }
else if(this.latency < 0 ) {return "red"; }
else if(this.latency <= 200) { return "green"; }
else if(this.latency <= 400) { return "yellow"; }
else { return "red"; }
},
shown_messages: function() {
if(this.messages.length <= this.showingnum) {
return this.messages;
} else {
return this.messages.slice(-1*this.showingnum);
}
}
},
methods: {
//Load the chat settings
load_settings: function() {
this.inverted = get_storage("darkmode", false);
this.crushing = get_storage("crushing", true);
this.showingnum = get_storage("showingnum", 200);
this.animated = get_storage("animated", true);
this.fontsize = get_storage("fontsize", 'zoom_normal');
},
//Change to another tab
switchtab: function(tab) {
if(tab == this.active_tab) return;
this.active_tab.active = false;
tab.active = true;
tab.classes.forEach( function(cls) {
this.unread_messages[cls] = false;
}, this);
},
//Toggle edit mode
editmode: function() {
this.editing = !this.editing;
},
//Toggle autoscroll
pause: function() {
this.paused = !this.paused;
},
//Create a new tab (stupid lack of classes in ES5...)
newtab: function() {
this.tabs.push({
name: "New Tab",
classes: this.always_show,
immutable: false,
active: false
});
this.switchtab(this.tabs[this.tabs.length - 1]);
},
//Rename an existing tab
renametab: function() {
if(this.active_tab.immutable) {
return;
}
var tabtorename = this.active_tab;
var newname = window.prompt("Type the desired tab name:", tabtorename.name);
if(newname === null || newname === "" || tabtorename === null) {
return;
}
tabtorename.name = newname;
},
//Delete the currently active tab
deltab: function() {
if(this.active_tab.immutable) {
return;
}
var doomed_tab = this.active_tab;
this.switchtab(this.tabs[0]);
this.tabs.splice(this.tabs.indexOf(doomed_tab), 1);
},
tab_unread_classes: function(tab) {
var unreads = false;
var thisum = this.unread_messages;
tab.classes.find( function(cls){
if(thisum[cls]) {
unreads = true;
return true;
}
});
return { red: unreads, grey: !unreads};
},
//Push a new message into our array
add_message: function(message) {
//IE doesn't support the 'class' syntactic sugar so we're left making our own object.
let newmessage = {
time: message.time,
category: "error",
content: message.message,
repeats: 1
};
//Get a category
newmessage.category = this.get_category(newmessage.content);
if(!this.active_tab.classes.some(function(cls) { return (cls == newmessage.category || cls == "vc_showall"); })) {
this.unread_messages[newmessage.category] = true;
}
//Try to crush it with one of the last few
if(this.crushing) {
let crushwith = this.messages.slice(-(vchat_opts.crush_messages));
for (let i = crushwith.length - 1; i >= 0; i--) {
let oldmessage = crushwith[i];
if(oldmessage.content == newmessage.content) {
newmessage.repeats += oldmessage.repeats;
this.messages.splice(this.messages.indexOf(oldmessage), 1);
}
}
}
//Append to vue's messages
newmessage.id = (this.messages.length + 1);
this.messages.push(newmessage);
},
//Push an internally generated message into our array
internal_message: function(message) {
let newmessage = {
time: this.messages.length ? this.messages.slice(-1).time+1 : 0,
category: "vc_system",
content: "<span class='notice'>[VChat Internal] " + message + "</span>"
};
newmessage.id = (this.messages.length + 1);
this.messages.push(newmessage);
},
on_mouseup: function(event) {
// Focus map window on mouseup so hotkeys work. Exception for if they highlighted text or clicked an input.
let ele = event.target;
let textSelected = ('getSelection' in window) && window.getSelection().isCollapsed === false;
if (!textSelected && !(ele && (ele.tagName === 'INPUT' || ele.tagName === 'TEXTAREA'))) {
focusMapWindow();
// Okay focusing map window appears to prevent click event from being fired. So lets do it ourselves.
event.preventDefault();
event.target.click();
}
},
click_message: function(event) {
let ele = event.target;
if(ele.tagName === "A") {
event.stopPropagation();
event.preventDefault ? event.preventDefault() : (event.returnValue = false); //The second one is the weird IE method.
var href = ele.getAttribute('href'); // Gets actual href without transformation into fully qualified URL
if (href[0] == '?' || (href.length >= 8 && href.substring(0,8) == "byond://")) {
window.location = href; //Internal byond link
} else { //It's an external link
window.location = "byond://?action=openLink&link="+encodeURIComponent(href);
}
}
},
//Derive a vchat category based on css classes
get_category: function(message) {
if(!vchat_state.ready) {
push_Topic('not_ready');
return;
}
let doc = domparser.parseFromString(message, 'text/html');
let evaluating = doc.querySelector('span');
let category = "nomatch"; //What we use if the classes aren't anything we know.
if(!evaluating) return category;
this.type_table.find( function(type) {
if(evaluating.msMatchesSelector(type.matches)) {
category = type.becomes;
return true;
}
});
return category;
},
save_chatlog: function() {
var textToSave = "<html><head><style>"+this.ext_styles+"</style></head><body>";
this.messages.forEach( function(message) {
textToSave += message.content;
if(message.repeats > 1) {
textToSave += "(x"+message.repeats+")";
}
textToSave += "<br>\n";
});
textToSave += "</body></html>";
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:attachment/text,' + encodeURI(textToSave);
hiddenElement.target = '_blank';
var filename = "chat_export.html";
//Unlikely to work unfortunately, not supported in any version of IE, only Edge
if (hiddenElement.download !== undefined){
hiddenElement.download = filename;
hiddenElement.click();
//Probably what will end up getting used
} else {
let blob = new Blob([textToSave], {type: 'text/html;charset=utf8;'});
saved = window.navigator.msSaveBlob(blob, filename);
}
}
}
});
}
/***********
*
* Actual Methods
*
************/
//Send a 'ping' to byond and check to see if we got the last one back in a timely manner
function send_ping() {
vchat_state.latency = (Math.min(Math.max(vchat_state.lastPingReply - vchat_state.lastPingAttempt, -1), 999));
//If their last reply was in the previous ping window or earlier.
if(vchat_state.latency < 0) {
vchat_state.missedPings++;
if((vchat_state.missedPings >= vchat_opts.pingDropsAllowed) && !vchat_state.reconnecting) {
system_message("Your client has lost connection with the server. It will reconnect automatically if possible.");
vchat_state.reconnecting = true;
}
}
vueapp.latency = vchat_state.latency;
push_Topic("keepalive_client");
vchat_state.lastPingAttempt = Date.now();
}
//We accept double-url-encoded JSON strings because Byond is garbage and UTF-8 encoded url_encode() text has crazy garbage in it.
function byondDecode(message) {
//Byond encodes spaces as pluses?! This is 1998 I guess.
message = message.replace(/\+/g, "%20");
try {
message = decodeURIComponent(message);
} catch (err) {
message = unescape(message);
}
return JSON.parse(message);
}
//This is the function byond actually communicates with using byond's client << output() method.
function putmessage(messages) {
messages = byondDecode(messages);
if (Array.isArray(messages)) {
messages.forEach(function(message) {
vueapp.add_message(message);
});
} else if (typeof messages === 'object') {
vueapp.add_message(messages);
}
}
//Send an internal message generated in the javascript
function system_message(message) {
vueapp.internal_message(message);
}
//This is the other direction of communication, to push a Topic message back
function push_Topic(topic_uri) {
window.location = '?_src_=chat&proc=' + topic_uri; //Yes that's really how it works.
}
//Tells byond client to focus the main map window.
function focusMapWindow() {
window.location = 'byond://winset?mapwindow.map.focus=true';
}
//A side-channel to send events over that aren't just chat messages, if necessary.
function get_event(event) {
if(!vchat_state.ready) {
push_Topic('not_ready');
return;
}
var parsed_event = {evttype: 'internal_error', event: event};
parsed_event = byondDecode(event);
switch(parsed_event.evttype) {
//We didn't parse it very well
case 'internal_error':
system_message("Event parse error: " + event);
break;
//They provided byond data.
case 'byond_player':
send_client_data();
vueapp.is_admin = (parsed_event.admin === 'true');
vchat_state.byond_ip = parsed_event.address;
vchat_state.byond_cid = parsed_event.cid;
vchat_state.byond_ckey = parsed_event.ckey;
set_storage("ip",vchat_state.byond_ip);
set_storage("cid",vchat_state.byond_cid);
set_storage("ckey",vchat_state.byond_ckey);
break;
//Just a ping.
case 'keepalive_server':
vchat_state.lastPingReply = Date.now();
vchat_state.missedPings = 0;
reconnecting = false;
break;
default:
system_message("Didn't know what to do with event: " + event);
}
}
//Send information retrieved from storage
function send_client_data() {
let client_data = {
ip: get_storage("ip"),
cid: get_storage("cid"),
ckey: get_storage("ckey")
};
push_Topic("ident&param[clientdata]="+JSON.stringify(client_data));
}
//Newer localstorage methods
function set_localstorage(key, value) {
let localstorage = window.localStorage;
localstorage.setItem(vchat_opts.cookiePrefix+key,value);
}
function get_localstorage(key, deffo) {
let localstorage = window.localStorage;
let value = localstorage.getItem(vchat_opts.cookiePrefix+key);
//localstorage only stores strings.
if(value === "null" || value === null) {
value = deffo;
} else if(value === "true") {
value = true;
} else if(value === "false") {
value = false;
}
return value;
}
//Older cookie methods
function set_cookie(key, value) {
let now = new Date();
now.setFullYear(now.getFullYear() + 1);
let then = now.toUTCString();
document.cookie = vchat_opts.cookiePrefix+key+"="+value+";expires="+then+";path=/";
}
function get_cookie(key, deffo) {
var candidates = {cookie: null, localstorage: null, indexeddb: null};
let cookie_array = document.cookie.split(';');
let cookie_object = {};
cookie_array.forEach( function(element) {
let clean = element.replace(vchat_opts.cookiePrefix,"").trim(); //Strip the prefix, trim whitespace
let equals = clean.search("="); //Find the equals
let left = decodeURIComponent(clean.substring(0,equals)); //From start to one char before equals
let right = decodeURIComponent(clean.substring(equals+1)); //From one char after equals to end
//cookies only stores strings.
if(right == "null" || right === null) {
right = deffo;
} else if(right === "true") {
right = true;
} else if(right === "false") {
right = false;
}
cookie_object[left] = right; //Stick into object
});
candidates.cookie = cookie_object[key]; //Return value of that key in our object (or undefined)
}
// Button Controls that need background-color and text-color set.
var SKIN_BUTTONS = [
/* Rpane */ "rpane.textb", "rpane.infob", "rpane.wikib", "rpane.forumb", "rpane.rulesb", "rpane.github", "rpane.mapb", "rpane.changelog",
/* Mainwindow */ "mainwindow.saybutton", "mainwindow.mebutton", "mainwindow.hotkey_toggle"
];
// Windows or controls that need background-color set.
var SKIN_ELEMENTS = [
/* Mainwindow */ "mainwindow", "mainwindow.mainvsplit", "mainwindow.tooltip",
/* Rpane */ "rpane", "rpane.rpanewindow", "rpane.mediapanel",
];
function switch_ui_mode(options) {
doWinset(SKIN_BUTTONS.reduce(function(params, ctl) {params[ctl + ".background-color"] = options.buttonBgColor; return params;}, {}));
doWinset(SKIN_BUTTONS.reduce(function(params, ctl) {params[ctl + ".text-color"] = options.buttonTextColor; return params;}, {}));
doWinset(SKIN_ELEMENTS.reduce(function(params, ctl) {params[ctl + ".background-color"] = options.windowBgColor; return params;}, {}));
doWinset("infowindow", {
"background-color": options.tabBackgroundColor,
"text-color": options.tabTextColor
});
doWinset("infowindow.info", {
"background-color": options.tabBackgroundColor,
"text-color": options.tabTextColor,
"highlight-color": options.highlightColor,
"tab-text-color": options.tabTextColor,
"tab-background-color": options.tabBackgroundColor
});
}
function doWinset(control_id, params) {
if (typeof params === 'undefined') {
params = control_id; // Handle single-argument use case.
control_id = null;
}
var url = "byond://winset?";
if (control_id) {
url += ("id=" + control_id + "&");
}
url += Object.keys(params).map(function(ctl) {
return ctl + "=" + encodeURIComponent(params[ctl]);
}).join("&");
window.location = url;
}
/***********
*
* Vue Components
*
************/
/*
Classes of note (02-06-2020 vorestation):
------ 'Local Chat' ------
.say = say (includes whispers which just have <i>)
.emote = emote (includes subtles which just have <i>)
.ooc .looc = looc
------ 'Radio' ------
.alert = global announcer (join messages, etc, the fake radios)
.syndradio
.centradio
.airadioc
.entradio
.comradio
.secradio
.engradio
.medradio
.sciradio
.supradio
.srvradio
.expradio
.radio = radio fallback
------ 'Warnings' ------
.critical = BIGGEST warnings?? rarely used
.danger = generally BIG warnings
.userdanger = ??
.warning = generally smol warnings
.italics = stupid, should be replaced with warning
------ 'Info' ------
.notice = generally notifications
.adminnotice = server malfunctions
.info = antag role info only?
.sinister = cult things, equivalent of fancy notice
.cult = cult things, equivalent of fancy notice
------ 'Deadchat' ------
.deadsay = dsay
------ 'Global OOC' ------
.ooc :not(.looc) = ooc (global)
------ 'Admin PM' ------
.pm = adminpm
------ 'Admin Chat' ------
.admin_channel = asay
------ 'Mod Chat' ------
.mod_channel = msay
------ 'Event Chat' ------
.event_channel = esay
------ 'System' ------
.boldannounce = server announcements/bootup messages
//Unused for now
.ooc .aooc = aooc
.ooc .admin = admin speaking in ooc
.ooc .event_manager = em speaking in ooc
.ooc .developer = dev speaking in ooc
.ooc .moderator = mod speaking in ooc
.ooc .elevated = staff that it can't find a style for speaking in ooc
*/
File diff suppressed because one or more lines are too long
+322
View File
@@ -0,0 +1,322 @@
#define BACKLOG_LENGTH 20 MINUTES
//The 'V' is for 'VORE' but you can pretend it's for Vue.js if you really want.
//These are sent to the client via browse_rsc() in advance so the HTML can access them.
GLOBAL_LIST_INIT(vchatFiles, list(
"code/modules/vchat/css/vchat-font-embedded.css",
"code/modules/vchat/css/semantic.min.css",
"code/modules/vchat/css/ss13styles.css",
"code/modules/vchat/js/polyfills.js",
"code/modules/vchat/js/vue.min.js",
"code/modules/vchat/js/vchat.js"
))
// The to_chat() macro calls this proc
/proc/__to_chat(var/target, var/message)
// First do logging in database
if(isclient(target))
var/client/C = target
vchat_add_message(C.ckey,message)
else if(ismob(target))
var/mob/M = target
if(M.ckey)
vchat_add_message(M.ckey,message)
// Now lets either queue it for sending, or send it right now
if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.subsystem_initialized)
to_chat_immediate(target, world.time, message)
else
SSchat.queue(target, world.time, message)
//This is used to convert icons to base64 <image> strings, because byond stores icons in base64 in savefiles.
GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of icons for the browser output
//The main object attached to clients, created when they connect, and has start() called on it in client/New()
/datum/chatOutput
var/client/owner = null
var/loaded = FALSE
var/list/message_queue = list()
var/broken = FALSE
var/resources_sent = FALSE
var/last_topic_time = 0
var/too_many_topics = 0
var/topic_spam_limit = 10 //Just enough to get over the startup and such
/datum/chatOutput/New(client/C)
. = ..()
owner = C
update_vis()
/datum/chatOutput/Destroy()
owner = null
. = ..()
/datum/chatOutput/proc/update_vis()
if(!loaded && !broken)
winset(owner, null, "outputwindow.htmloutput.is-visible=false;outputwindow.oldoutput.is-visible=false;outputwindow.chatloadlabel.is-visible=true")
else if(broken)
winset(owner, null, "outputwindow.htmloutput.is-visible=false;outputwindow.oldoutput.is-visible=true;outputwindow.chatloadlabel.is-visible=false")
else if(loaded)
return //It can do it's own winsets from inside the JS if it's working.
//Shove all the assets at them
/datum/chatOutput/proc/send_resources()
for(var/filename in GLOB.vchatFiles)
owner << browse_rsc(file(filename))
resources_sent = TRUE
//Called from client/New() in a spawn()
/datum/chatOutput/proc/start()
if(!owner)
qdel(src)
return FALSE
if(!winexists(owner, "htmloutput"))
spawn()
alert(owner.mob, "Updated chat window does not exist. If you are using a custom skin file please allow the game to update.")
become_broken()
return FALSE
if(!owner) // In case the client vanishes before winexists returns
qdel(src)
return FALSE
if(!resources_sent)
send_resources()
load()
return TRUE
//Attempts to actually load the HTML page into the client's UI
/datum/chatOutput/proc/load()
if(!owner)
qdel(src)
return
owner << browse(file2text("code/modules/vchat/html/vchat.html"), "window=htmloutput")
//Check back later
spawn(60 SECONDS)
if(!src)
return
if(!src.loaded)
src.become_broken()
//var/list/joins = list() //Just for testing with the below
//Called by Topic, when the JS in the HTML page finishes loading
/datum/chatOutput/proc/done_loading()
if(loaded)
return
loaded = TRUE
broken = FALSE
owner.chatOutputLoadedAt = world.time
//update_vis() //It does it's own winsets
send_playerinfo()
load_database()
//Perform DB shenanigans
/datum/chatOutput/proc/load_database()
set waitfor = FALSE
var/list/results = vchat_get_messages(owner.ckey, world.time - BACKLOG_LENGTH)
for(var/list/message in results)
to_chat_immediate(owner, message["time"], message["message"])
CHECK_TICK
//It din work
/datum/chatOutput/proc/become_broken()
broken = TRUE
loaded = FALSE
update_vis()
spawn()
alert(owner,"VChat didn't load after some time. Switching to use oldchat as a fallback. Try using 'Reload VChat' verb in OOC verbs, or reconnecting to try again.")
//Provide the JS with who we are
/datum/chatOutput/proc/send_playerinfo()
if(!owner)
qdel(src)
return
var/list/playerinfo = list("evttype" = "byond_player", "cid" = owner.computer_id, "ckey" = owner.ckey, "address" = owner.address, "admin" = owner.holder ? "true" : "false")
send_event(playerinfo)
//Ugh byond doesn't handle UTF-8 well so we have to do this.
/proc/jsEncode(var/list/message) {
if(!islist(message))
CRASH("Passed a non-list to encode.")
return; //Necessary?
return url_encode(url_encode(json_encode(message)))
}
//Send a side-channel event to the chat window
/datum/chatOutput/proc/send_event(var/event, var/client/C = owner)
C << output(jsEncode(event), "htmloutput:get_event")
//Just produces a message for using in keepalives from the server to the client
/datum/chatOutput/proc/keepalive()
return list("evttype" = "keepalive_server")
//Redirected from client/Topic when the user clicks a link that pertains directly to the chat (when src == "chat")
/datum/chatOutput/Topic(var/href, var/list/href_list)
if(usr.client != owner)
return 1
if(last_topic_time > (world.time - 3 SECONDS))
too_many_topics++
if(too_many_topics >= topic_spam_limit)
log_and_message_admins("Kicking [key_name(owner)] - VChat Topic() spam")
to_chat(owner,"<span class='danger'>You have been kicked due to VChat sending too many messages to the server. Try reconnecting.</span>")
qdel(owner)
qdel(src)
return
else
too_many_topics = 0
last_topic_time = world.time
var/list/params = list()
for(var/key in href_list)
if(length(key) > 7 && findtext(key, "param"))
var/param_name = copytext(key, 7, -1)
var/item = href_list[key]
params[param_name] = item
var/data
switch(href_list["proc"])
if("not_ready")
CRASH("Tried to send a message to [owner.ckey] chatOutput before it was ready!")
if("done_loading")
data = done_loading(arglist(params))
if("keepalive_client")
data = keepalive(arglist(params))
if("ident")
data = bancheck(arglist(params))
if("unloading")
loaded = FALSE
if(data)
send_event(event = data)
//Check relevant client info reported from JS
/datum/chatOutput/proc/bancheck(var/clientdata)
var/list/info = json_decode(clientdata)
var/ckey = info["ckey"]
var/ip = info["ip"]
var/cid = info["cid"]
//Never connected? How sad!
if(!cid && !ip && !ckey)
return
var/list/ban = world.IsBanned(key = ckey, address = ip, computer_id = cid)
if(ban)
log_and_message_admins("[key_name(owner)] has a cookie from a banned account! (Cookie: [ckey], [ip], [cid])")
//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(var/icon/icon, var/iconKey = "misc")
if (!isicon(icon)) return FALSE
GLOB.iconCache[iconKey] << icon
var/iconData = GLOB.iconCache.ExportText(iconKey)
var/list/partial = splittext(iconData, "{")
return replacetext(copytext(partial[2], 3, -5), "\n", "")
/proc/bicon(var/obj, var/use_class = 1, var/custom_classes = "")
var/class = use_class ? "class='icon misc [custom_classes]'" : null
if (!obj)
return
var/static/list/bicon_cache = list()
if (isicon(obj))
if (!bicon_cache["\ref[obj]"]) // Doesn't exist yet, make it.
bicon_cache["\ref[obj]"] = icon2base64(obj)
return "<img [class] src='data:image/png;base64,[bicon_cache["\ref[obj]"]]'>"
// Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with.
var/atom/A = obj
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(obj))
I = getFlatIcon(obj) //Ugly
bicon_cache[key] = icon2base64(I, key)
if(use_class)
class = "class='icon [A.icon_state] [custom_classes]'"
return "<img [class] src='data:image/png;base64,[bicon_cache[key]]'>"
//Checks if the message content is a valid to_chat message
/proc/is_valid_tochat_message(message)
return istext(message)
//Checks if the target of to_chat is something we can send to
/proc/is_valid_tochat_target(target)
return !istype(target, /savefile) && (ismob(target) || islist(target) || isclient(target) || target == world)
var/to_chat_filename
var/to_chat_line
var/to_chat_src
//This proc is only really used if the SSchat subsystem is unavailable (not started yet)
/proc/to_chat_immediate(target, time, message)
if(!is_valid_tochat_message(message) || !is_valid_tochat_target(target))
target << message
// Info about the "message"
if(isnull(message))
message = "(null)"
else if(istype(message, /datum))
var/datum/D = message
message = "([D.type]): '[D]'"
else if(!is_valid_tochat_message(message))
message = "(bad message) : '[message]'"
// Info about the target
var/targetstring = "'[target]'"
if(istype(target, /datum))
var/datum/D = target
targetstring += ", [D.type]"
// The final output
log_debug("to_chat called with invalid message/target: [to_chat_filename], [to_chat_line], [to_chat_src], Message: '[message]', Target: [targetstring]")
return
else if(is_valid_tochat_message(message))
if(istext(target))
log_debug("Somehow, to_chat got a text as a target")
return
var/original_message = message
message = replacetext(message, "\n", "<br>")
message = replacetext(message, "\improper", "")
message = replacetext(message, "\proper", "")
if(isnull(time))
time = world.time
var/client/C = CLIENT_FROM_VAR(target)
if(C && C.chatOutput)
if(C.chatOutput.broken)
DIRECT_OUTPUT(C, original_message)
return
// // Client still loading, put their messages in a queue - Actually don't, logged already in database.
// if(!C.chatOutput.loaded && C.chatOutput.message_queue && islist(C.chatOutput.message_queue))
// C.chatOutput.message_queue[++C.chatOutput.message_queue.len] = list("time" = time, "message" = message)
// return
var/list/tojson = list("time" = time, "message" = message);
target << output(jsEncode(tojson), "htmloutput:putmessage")
#undef BACKLOG_LENGTH
+116
View File
@@ -0,0 +1,116 @@
#define VCHAT_FILENAME "data/vchat.db"
GLOBAL_DATUM(vchatdb, /database)
//Boot up db file
/proc/init_vchat()
//Cleanup previous if exists
fdel(VCHAT_FILENAME)
fdel(VCHAT_FILENAME+"-shm") //Shared memory, and
fdel(VCHAT_FILENAME+"-wal") // write ahead. Only present on unclean stop.
//Create a new one
GLOB.vchatdb = new(VCHAT_FILENAME)
//Build our basic boring tables
vchat_create_tables()
//Check to see if it's init
/proc/check_vchat()
if(istype(GLOB.vchatdb))
return TRUE
else
return FALSE
//For INSERT/CREATE/DELETE, etc that return a RowsAffected.
/proc/vchat_exec_update(var/query)
if(!check_vchat())
log_debug("There's no vchat database open but you tried to query it with: [query]")
return FALSE
//Solidify our query
var/database/query/q = vchat_build_query(query)
//Run it
q.Execute(GLOB.vchatdb)
//Handle errors
if(q.Error())
log_debug("Query \"[islist(query)?query[1]:query]\" ended in error [q.ErrorMsg()]")
return FALSE
return q.RowsAffected()
//For SELECT, that return results.
/proc/vchat_exec_query(var/query)
if(!check_vchat())
log_debug("There's no vchat database open but you tried to query it!")
return FALSE
//Solidify our query
var/database/query/q = vchat_build_query(query)
//Run it
q.Execute(GLOB.vchatdb)
//Handle errors
if(q.Error())
log_debug("Query \"[islist(query)?query[1]:query]\" ended in error [q.ErrorMsg()]")
return FALSE
//Return any results
var/list/results = list()
//Return results if any.
while(q.NextRow())
results[++results.len] = q.GetRowData()
return results
//Create a query from string or list with params
/proc/vchat_build_query(var/query)
var/database/query/q
if(islist(query))
q = new(arglist(query))
else
q = new(query)
if(!istype(q))
return
return q
/proc/vchat_create_tables()
//Messages table
var/tabledef = "CREATE TABLE messages(\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
ckey VARCHAR(50) NOT NULL,\
worldtime INTEGER NOT NULL,\
message TEXT NOT NULL)"
vchat_exec_update(tabledef)
//Index on ckey
var/indexdef = "CREATE INDEX ckey_index ON messages (ckey)"
vchat_exec_update(indexdef)
//INSERT a new message
/proc/vchat_add_message(var/ckey, var/message)
if(!ckey || !message)
return
var/list/messagedef = list(
"INSERT INTO messages (ckey,worldtime,message) VALUES (?, ?, ?)",
ckey,
world.time,
message)
return vchat_exec_update(messagedef)
//Get a player's message history
/proc/vchat_get_messages(var/ckey, var/oldest = 0)
if(!ckey)
return
var/list/getdef = list("SELECT * FROM messages WHERE ckey = ? AND worldtime >= ?", ckey, oldest)
return vchat_exec_query(getdef)
#undef VCHAT_FILENAME
+31 -1298
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -230,6 +230,7 @@
#include "code\controllers\subsystems\atoms.dm"
#include "code\controllers\subsystems\bellies_vr.dm"
#include "code\controllers\subsystems\character_setup.dm"
#include "code\controllers\subsystems\chat.dm"
#include "code\controllers\subsystems\circuits.dm"
#include "code\controllers\subsystems\events.dm"
#include "code\controllers\subsystems\garbage.dm"
@@ -3237,6 +3238,8 @@
#include "code\modules\turbolift\turbolift_map.dm"
#include "code\modules\turbolift\turbolift_process.dm"
#include "code\modules\turbolift\turbolift_turfs.dm"
#include "code\modules\vchat\vchat_client.dm"
#include "code\modules\vchat\vchat_db.dm"
#include "code\modules\vehicles\bike.dm"
#include "code\modules\vehicles\boat.dm"
#include "code\modules\vehicles\cargo_train.dm"