mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-07-18 18:46:24 +01:00
VChat: Redone chat output in Vue.js
Co-authored-by: Leshana <Leshana@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
#ignore misc BYOND files
|
||||
Thumbs.db
|
||||
vchat.db
|
||||
vchat.db*
|
||||
*.log
|
||||
*.int
|
||||
*.rsc
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
//---------------
|
||||
#define isatom(D) istype(D, /atom)
|
||||
#define isclient(D) istype(D, /client)
|
||||
|
||||
//---------------
|
||||
//#define isobj(D) istype(D, /obj) //Built in
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
GLOB.timezoneOffset = text2num(time2text(0,"hh")) * 36000
|
||||
|
||||
callHook("startup")
|
||||
init_vchat()
|
||||
//Emergency Fix
|
||||
load_mods()
|
||||
//end-emergency fix
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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>
|
||||
+404
File diff suppressed because one or more lines are too long
+110
File diff suppressed because one or more lines are too long
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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¶m[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
|
||||
*/
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user