mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-12 03:02:54 +00:00
You can mute looc anyways and it makes sense for admin pms to appear in all windows so you cant say you did not notice it.
935 lines
28 KiB
JavaScript
935 lines
28 KiB
JavaScript
//The 'V' is for 'VORE' but you can pretend it's for Vue.js if you really want.
|
|
|
|
(function(){
|
|
var oldLog = console.log;
|
|
console.log = function (message) {
|
|
send_debug(message);
|
|
oldLog.apply(console, arguments);
|
|
};
|
|
var oldError = console.error;
|
|
console.error = function (message) {
|
|
send_debug(message);
|
|
oldError.apply(console, arguments);
|
|
}
|
|
window.onerror = function (message, url, line, col, error) {
|
|
var stacktrace = "";
|
|
if(error && error.stack) {
|
|
stacktrace = error.stack;
|
|
}
|
|
send_debug(message+" ("+url+"@"+line+":"+col+") "+error+"|UA: "+navigator.userAgent+"|Stack: "+stacktrace);
|
|
return true;
|
|
}
|
|
})();
|
|
|
|
|
|
//Options for vchat
|
|
var vchat_opts = {
|
|
msBeforeDropped: 30000, //No ping for this long, and the server must be gone
|
|
cookiePrefix: "vst-", //If you're another server, you can change this if you want.
|
|
alwaysShow: ["vc_adminpm", "vc_system"], //Categories to always display on every tab. YW Edit: removes vc_looc adds: vc_adminpm
|
|
vchatTabsVer: 1.0 //Version of vchat tabs save 'file'
|
|
};
|
|
|
|
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
|
|
lastPingReceived: 0,
|
|
latency_sent: 0,
|
|
|
|
//Last ID
|
|
lastId: 0
|
|
}
|
|
|
|
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
|
|
setInterval(check_ping, vchat_opts.msBeforeDropped);
|
|
|
|
//For fun
|
|
send_debug("VChat Loaded!");
|
|
//throw new Error("VChat Loaded!");
|
|
|
|
}
|
|
|
|
//Loads vue for chat usage
|
|
var vueapp;
|
|
function start_vue() {
|
|
vueapp = new Vue({
|
|
el: '#app',
|
|
data: {
|
|
messages: [], //List o messages from byond
|
|
shown_messages: [], //Used on filtered tabs, but not "Main" because it has 0len categories list, which bypasses filtering for speed
|
|
unshown_messages: 0, //How many messages in archive would be shown but aren't
|
|
archived_messages: [], //Too old to show
|
|
tabs: [ //Our tabs
|
|
{name: "Main", categories: [], immutable: true, active: true}
|
|
],
|
|
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
|
|
reconnecting: false, //If we've lost our connection
|
|
ext_styles: "", //Styles for chat downloaded files
|
|
is_admin: false,
|
|
|
|
//Settings
|
|
inverted: false, //Dark mode
|
|
crushing: 3, //Combine similar messages
|
|
animated: false, //Small CSS animations for new messages
|
|
fontsize: 0.9, //Font size nudging
|
|
lineheight: 130,
|
|
showingnum: 200, //How many messages to show
|
|
|
|
//The table to map game css classes to our vchat categories
|
|
type_table: [
|
|
{
|
|
matches: ".say, .emote",
|
|
becomes: "vc_localchat",
|
|
pretty: "Local Chat",
|
|
tooltip: "In-character local messages (say, emote, etc)",
|
|
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",
|
|
tooltip: "All departments of radio messages",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
{
|
|
matches: ".notice:not(.pm), .adminnotice, .info, .sinister, .cult",
|
|
becomes: "vc_info",
|
|
pretty: "Notices",
|
|
tooltip: "Non-urgent messages from the game and items",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
{
|
|
matches: ".critical, .danger, .userdanger, .warning:not(.pm), .italics",
|
|
becomes: "vc_warnings",
|
|
pretty: "Warnings",
|
|
tooltip: "Urgent messages from the game and items",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
{
|
|
matches: ".deadsay",
|
|
becomes: "vc_deadchat",
|
|
pretty: "Deadchat",
|
|
tooltip: "All of deadchat",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
{
|
|
matches: ".ooc:not(.looc)",
|
|
becomes: "vc_globalooc",
|
|
pretty: "Global OOC",
|
|
tooltip: "The bluewall of global OOC messages",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
//VOREStation Add Start
|
|
{
|
|
matches: ".nif",
|
|
becomes: "vc_nif",
|
|
pretty: "NIF Messages",
|
|
tooltip: "Messages from the NIF itself and people inside",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
//VOREStation Add End
|
|
{
|
|
matches: ".pm",
|
|
becomes: "vc_adminpm",
|
|
pretty: "Admin PMs",
|
|
tooltip: "Messages to/from admins ('adminhelps')",
|
|
required: false,
|
|
admin: false
|
|
},
|
|
{
|
|
matches: ".admin_channel",
|
|
becomes: "vc_adminchat",
|
|
pretty: "Admin Chat",
|
|
tooltip: "ASAY messages",
|
|
required: false,
|
|
admin: true
|
|
},
|
|
{
|
|
matches: ".mod_channel",
|
|
becomes: "vc_modchat",
|
|
pretty: "Mod Chat",
|
|
tooltip: "MSAY messages",
|
|
required: false,
|
|
admin: true
|
|
},
|
|
{
|
|
matches: ".event_channel",
|
|
becomes: "vc_eventchat",
|
|
pretty: "Event Chat",
|
|
tooltip: "ESAY messages",
|
|
required: false,
|
|
admin: true
|
|
},
|
|
{
|
|
matches: ".ooc.looc, .ooc .looc", //Dumb game
|
|
becomes: "vc_looc",
|
|
pretty: "Local OOC",
|
|
tooltip: "Local OOC messages, always enabled",
|
|
required: true
|
|
},
|
|
{
|
|
matches: ".boldannounce",
|
|
becomes: "vc_system",
|
|
pretty: "System Messages",
|
|
tooltip: "Messages from your client, always enabled",
|
|
required: true
|
|
}
|
|
],
|
|
},
|
|
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: {
|
|
reconnecting: function(newSetting, oldSetting) {
|
|
if(newSetting == true && oldSetting == false) {
|
|
this.internal_message("Your client has lost connection to the server, or there is severe lag. Your client will reconnect if possible.");
|
|
} else if (newSetting == false && oldSetting == true) {
|
|
this.internal_message("Your client has reconnected to the server.");
|
|
}
|
|
},
|
|
//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, oldSetting) {
|
|
if(isNaN(newSetting)) { //Numbers only
|
|
this.fontsize = oldSetting;
|
|
return;
|
|
}
|
|
if(newSetting < 0.2) {
|
|
this.fontsize = 0.2;
|
|
} else if(newSetting > 5) {
|
|
this.fontsize = 5;
|
|
}
|
|
set_storage("fontsize",newSetting);
|
|
},
|
|
lineheight: function (newSetting, oldSetting) {
|
|
if(!isFinite(newSetting)) { //Integers only
|
|
this.lineheight = oldSetting;
|
|
return;
|
|
}
|
|
if(newSetting < 100) {
|
|
this.lineheight = 100;
|
|
} else if(newSetting > 200) {
|
|
this.lineheight = 200;
|
|
}
|
|
set_storage("lineheight",newSetting);
|
|
},
|
|
showingnum: function (newSetting, oldSetting) {
|
|
if(!isFinite(newSetting)) { //Integers only
|
|
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);
|
|
this.attempt_archive();
|
|
},
|
|
current_categories: function(newSetting, oldSetting) {
|
|
if(newSetting.length) {
|
|
this.apply_filter(newSetting);
|
|
}
|
|
}
|
|
},
|
|
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;
|
|
},
|
|
//What color does the latency pip get?
|
|
ping_classes: function() {
|
|
if(!this.latency) {
|
|
return this.reconnecting ? "red" : "green"; //Standard
|
|
}
|
|
|
|
if (this.latency == "?") { return "grey"; } //Waiting for latency test reply
|
|
else if(this.latency < 0 ) {return "red"; }
|
|
else if(this.latency <= 200) { return "green"; }
|
|
else if(this.latency <= 400) { return "yellow"; }
|
|
else { return "grey"; }
|
|
},
|
|
current_categories: function() {
|
|
if(this.active_tab == this.tabs[0]) {
|
|
return []; //Everything, no filtering, special case for speed.
|
|
} else {
|
|
return this.active_tab.categories.concat(vchat_opts.alwaysShow);
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
//Load the chat settings
|
|
load_settings: function() {
|
|
this.inverted = get_storage("darkmode", false);
|
|
this.crushing = get_storage("crushing", 3);
|
|
this.animated = get_storage("animated", false);
|
|
this.fontsize = get_storage("fontsize", 0.9);
|
|
this.lineheight = get_storage("lineheight", 130);
|
|
this.showingnum = get_storage("showingnum", 200);
|
|
|
|
if(isNaN(this.crushing)){this.crushing = 3;} //This used to be a bool (03-02-2020)
|
|
if(isNaN(this.fontsize)){this.fontsize = 0.9;} //This used to be a string (03-02-2020)
|
|
|
|
this.load_tabs();
|
|
},
|
|
load_tabs: function() {
|
|
var loadstring = get_storage("tabs")
|
|
if(!loadstring)
|
|
return;
|
|
var loadfile = JSON.parse(loadstring);
|
|
//Malformed somehow.
|
|
if(!loadfile.version || !loadfile.tabs) {
|
|
this.internal_message("There was a problem loading your tabs. Any new ones you make will be saved, however.");
|
|
return;
|
|
}
|
|
//Version is old? Sorry.
|
|
if(!loadfile.version == vchat_opts.vchatTabsVer) {
|
|
this.internal_message("Your saved tabs are for an older version of VChat and must be recreated, sorry.");
|
|
return;
|
|
}
|
|
|
|
this.tabs.push.apply(this.tabs, loadfile.tabs);
|
|
},
|
|
save_tabs: function() {
|
|
var savefile = {
|
|
version: vchat_opts.vchatTabsVer,
|
|
tabs: []
|
|
}
|
|
|
|
//The tabs contain a bunch of vue stuff that gets funky when you try to serialize it with stringify, so we 'purify' it
|
|
this.tabs.forEach(function(tab){
|
|
if(tab.immutable)
|
|
return;
|
|
|
|
var name = tab.name;
|
|
|
|
var categories = [];
|
|
tab.categories.forEach(function(category){categories.push(category);});
|
|
|
|
var cleantab = {name: name, categories: categories, immutable: false, active: false}
|
|
|
|
savefile.tabs.push(cleantab);
|
|
});
|
|
|
|
var savestring = JSON.stringify(savefile);
|
|
set_storage("tabs", savestring);
|
|
},
|
|
//Change to another tab
|
|
switchtab: function(tab) {
|
|
if(tab == this.active_tab) return;
|
|
this.active_tab.active = false;
|
|
tab.active = true;
|
|
|
|
tab.categories.forEach( function(cls) {
|
|
this.unread_messages[cls] = 0;
|
|
}, this);
|
|
|
|
this.apply_filter(this.current_categories);
|
|
},
|
|
//Toggle edit mode
|
|
editmode: function() {
|
|
this.editing = !this.editing;
|
|
this.save_tabs();
|
|
},
|
|
//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",
|
|
categories: [],
|
|
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(tab) {
|
|
if(!tab) {
|
|
tab = this.active_tab;
|
|
}
|
|
if(tab.immutable) {
|
|
return;
|
|
}
|
|
this.switchtab(this.tabs[0]);
|
|
this.tabs.splice(this.tabs.indexOf(tab), 1);
|
|
},
|
|
movetab: function(tab, shift) {
|
|
if(!tab || tab.immutable) {
|
|
return;
|
|
}
|
|
var at = this.tabs.indexOf(tab);
|
|
var to = at + shift;
|
|
this.tabs.splice(to, 0, this.tabs.splice(at, 1)[0]);
|
|
},
|
|
tab_unread_count: function(tab) {
|
|
var unreads = 0;
|
|
var thisum = this.unread_messages;
|
|
tab.categories.find( function(cls){
|
|
if(thisum[cls]) {
|
|
unreads += thisum[cls];
|
|
}
|
|
});
|
|
return unreads;
|
|
},
|
|
tab_unread_categories: function(tab) {
|
|
var unreads = false;
|
|
var thisum = this.unread_messages;
|
|
tab.categories.find( function(cls){
|
|
if(thisum[cls]) {
|
|
unreads = true;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
return { red: unreads, grey: !unreads};
|
|
},
|
|
attempt_archive: function() {
|
|
var wiggle = 20; //Wiggle room to prevent hysterisis effects. Slice off 20 at a time.
|
|
//Pushing out old messages
|
|
if(this.messages.length > this.showingnum) {//Time to slice off old messages
|
|
var too_old = this.messages.splice(0,wiggle); //We do a few at a time to avoid doing it too often
|
|
Array.prototype.push.apply(this.archived_messages, too_old); //ES6 adds spread operator. I'd use it if I could.
|
|
}/*
|
|
//Pulling back old messages
|
|
} else if(this.messages.length < (this.showingnum - wiggle)) { //Sigh, repopulate old messages
|
|
var too_new = this.archived_messages.splice(this.messages.length - (this.showingnum - wiggle));
|
|
Array.prototype.shift.apply(this.messages, too_new);
|
|
}
|
|
*/
|
|
},
|
|
apply_filter: function(cat_array) {
|
|
//Clean up the array
|
|
this.shown_messages.splice(0);
|
|
this.unshown_messages = 0;
|
|
|
|
//For each message, try to find it's category in the categories we're showing
|
|
this.messages.forEach( function(msg){
|
|
if(cat_array.indexOf(msg.category) > -1) { //Returns the position in the array, and -1 for not found
|
|
this.shown_messages.push(msg);
|
|
}
|
|
}, this);
|
|
|
|
//For each message, try to find it's category in the categories we're showing
|
|
this.archived_messages.forEach( function(msg){
|
|
if(cat_array.indexOf(msg.category) > -1) { //Returns the position in the array, and -1 for not found
|
|
this.unshown_messages++;
|
|
}
|
|
}, this);
|
|
},
|
|
//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);
|
|
|
|
//Try to crush it with one of the last few
|
|
if(this.crushing) {
|
|
let crushwith = this.messages.slice(-(this.crushing));
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Unread indicator and insertion into current tab shown messages if sensible
|
|
if(this.current_categories.length && (this.current_categories.indexOf(newmessage.category) < 0)) { //Not in the current categories
|
|
if (isNaN(this.unread_messages[newmessage.category])) {
|
|
this.unread_messages[newmessage.category] = 0;
|
|
}
|
|
this.unread_messages[newmessage.category] += 1;
|
|
} else if(this.current_categories.length) { //Is in the current categories
|
|
this.shown_messages.push(newmessage);
|
|
}
|
|
|
|
//Append to vue's messages
|
|
newmessage.id = ++vchat_state.lastId;
|
|
this.attempt_archive();
|
|
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 = ++vchat_state.lastId;
|
|
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>";
|
|
|
|
var messagesToSave = this.archived_messages.concat(this.messages);
|
|
|
|
messagesToSave.forEach( function(message) {
|
|
textToSave += message.content;
|
|
if(message.repeats > 1) {
|
|
textToSave += "(x"+message.repeats+")";
|
|
}
|
|
textToSave += "<br>\n";
|
|
});
|
|
textToSave += "</body></html>";
|
|
|
|
var fileprefix = "log";
|
|
var extension =".html";
|
|
|
|
var now = new Date();
|
|
var hours = String(now.getHours());
|
|
if(hours.length < 2) {
|
|
hours = "0" + hours;
|
|
}
|
|
var minutes = String(now.getMinutes());
|
|
if(minutes.length < 2) {
|
|
minutes = "0" + minutes;
|
|
}
|
|
var dayofmonth = String(now.getDate());
|
|
if(dayofmonth.length < 2) {
|
|
dayofmonth = "0" + dayofmonth;
|
|
}
|
|
var month = String(now.getMonth()+1); //0-11
|
|
if(month.length < 2) {
|
|
month = "0" + month;
|
|
}
|
|
var year = String(now.getFullYear());
|
|
var datesegment = " "+year+"-"+month+"-"+dayofmonth+" ("+hours+" "+minutes+")";
|
|
|
|
var filename = fileprefix+datesegment+extension;
|
|
|
|
//Unlikely to work unfortunately, not supported in any version of IE, only Edge
|
|
var hiddenElement = document.createElement('a');
|
|
if (hiddenElement.download !== undefined) {
|
|
hiddenElement.href = 'data:attachment/text,' + encodeURI(textToSave); //Has a problem in byond 512 due to weird unicode handling
|
|
hiddenElement.target = '_blank';
|
|
hiddenElement.download = filename;
|
|
hiddenElement.click();
|
|
//Probably what will end up getting used
|
|
} else {
|
|
var blob = new Blob([textToSave], {type: 'text/html;charset=utf8;'});
|
|
saved = window.navigator.msSaveOrOpenBlob(blob, filename);
|
|
}
|
|
},
|
|
do_latency_test: function() {
|
|
send_latency_check();
|
|
},
|
|
blur_this: function(event) {
|
|
event.target.blur();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/***********
|
|
*
|
|
* Actual Methods
|
|
*
|
|
************/
|
|
function check_ping() {
|
|
var time_ago = Date.now() - vchat_state.lastPingReceived;
|
|
if(time_ago > vchat_opts.msBeforeDropped)
|
|
vueapp.reconnecting = true;
|
|
}
|
|
|
|
//Send a 'ping' to byond
|
|
function send_latency_check() {
|
|
if(vchat_state.latency_sent)
|
|
return;
|
|
|
|
vchat_state.latency_sent = Date.now();
|
|
vueapp.latency = "?";
|
|
push_Topic("ping");
|
|
setTimeout(function() {
|
|
if(vchat_state.latency_ms == "?") {
|
|
vchat_state.latency_ms = 999;
|
|
}
|
|
}, 1000); // 1 second to reply otherwise we mark it as bad
|
|
setTimeout(function() {
|
|
vchat_state.latency_sent = 0;
|
|
vueapp.latency = 0;
|
|
}, 5000); //5 seconds to display ping time overall
|
|
}
|
|
|
|
function get_latency_check() {
|
|
if(!vchat_state.latency_sent) {
|
|
return; //Too late
|
|
}
|
|
|
|
vueapp.latency = Date.now() - vchat_state.latency_sent;
|
|
}
|
|
|
|
//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';
|
|
}
|
|
|
|
//Debug event
|
|
function send_debug(message) {
|
|
push_Topic("debug¶m[message]="+encodeURIComponent(message));
|
|
}
|
|
|
|
//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':
|
|
vchat_state.lastPingReceived = Date.now();
|
|
vueapp.reconnecting = false;
|
|
break;
|
|
|
|
//Response to a latency test.
|
|
case 'pong':
|
|
get_latency_check();
|
|
break;
|
|
|
|
//The server doesn't know if we're loaded or not (we bail above if we're not, so we must be).
|
|
case 'availability':
|
|
push_Topic("done_loading");
|
|
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;
|
|
//Coerce bools back into their native forms
|
|
} else if(value === "true") {
|
|
value = true;
|
|
} else if(value === "false") {
|
|
value = false;
|
|
//Coerce numbers back into numerical form
|
|
} else if(!isNaN(value)) {
|
|
value = +value;
|
|
}
|
|
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;
|
|
} else if(!isNaN(right)) {
|
|
right = +right;
|
|
}
|
|
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;
|
|
}
|