/*****************************************
*
* FUNCTION AND VAR DECLARATIONS
*
******************************************/
//DEBUG STUFF
var escaper = encodeURIComponent || escape;
var decoder = decodeURIComponent || unescape;
window.onerror = function(msg, url, line, col, error) {
if (document.location.href.indexOf("proc=debug") <= 0) {
var extra = !col ? '' : ' | column: ' + col;
extra += !error ? '' : ' | error: ' + error;
extra += !navigator.userAgent ? '' : ' | user agent: ' + navigator.userAgent;
var debugLine = 'Error: ' + msg + ' | url: ' + url + ' | line: ' + line + extra;
window.location = '?_src_=chat&proc=debug¶m[error]='+escaper(debugLine);
}
return true;
};
//Globals
window.status = 'Output';
var $messages, $subTheme, $subOptions, $subFont, $selectedSub, $contextMenu, $filterMessages, $last_message;
var opts = {
//General
'messageCount': 0, //A count...of messages...
'messageLimit': 2053, //A limit...for the messages...
'scrollSnapTolerance': 10, //If within x pixels of bottom
'clickTolerance': 10, //Keep focus if outside x pixels of mousedown position on mouseup
'imageRetryDelay': 50, //how long between attempts to reload images (in ms)
'imageRetryLimit': 50, //how many attempts should we make?
'popups': 0, //Amount of popups opened ever
'wasd': false, //Is the user in wasd mode?
'priorChatHeight': 0, //Thing for height-resizing detection
'restarting': false, //Is the round restarting?
'iconsize': 12,
//Options menu
'selectedSubLoop': null, //Contains the interval loop for closing the selected sub menu
'suppressSubClose': false, //Whether or not we should be hiding the selected sub menu
'highlightTerms': [],
'highlightLimit': 5,
'highlightColor': '#FFFF00', //The color of the highlighted message
'pingDisabled': false, //Has the user disabled the ping counter
//Ping display
'lastPang': 0, //Timestamp of the last response from the server.
'pangLimit': 70000,
'pingTime': 0, //Timestamp of when ping sent
'pongTime': 0, //Timestamp of when ping received
'noResponse': false, //Tracks the state of the previous ping request
'noResponseCount': 0, //How many failed pings?
//Clicks
'mouseDownX': null,
'mouseDownY': null,
'preventFocus': false, //Prevents switching focus to the game window
//Client Connection Data
'clientDataLimit': 5,
'clientData': [],
'font': 'Arial',
'messageCombining': true,
};
var replaceRegexes = {};
function clamp(val, min, max) {
return Math.max(min, Math.min(val, max))
}
//Polyfill for fucking date now because of course IE8 and below don't support it
if (!Date.now) {
Date.now = function now() {
return new Date().getTime();
};
}
//Polyfill for trim() (IE8 and below)
if (typeof String.prototype.trim !== 'function') {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
// Linkify the contents of a node, within its parent.
function linkify(parent, insertBefore, text) {
var start = 0;
var match;
var regex = /(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#\/%?=~_|$!:,.;()]+/ig;
while ((match = regex.exec(text)) !== null) {
// add the unmatched text
parent.insertBefore(document.createTextNode(text.substring(start, match.index)), insertBefore);
var href = match[0];
if (!/^https?:\/\//i.test(match[0])) {
href = "http://" + match[0];
}
// add the link
var link = document.createElement("a");
link.href = href;
link.textContent = match[0];
parent.insertBefore(link, insertBefore);
start = regex.lastIndex;
}
if (start !== 0) {
// add the remaining text and remove the original text node
parent.insertBefore(document.createTextNode(text.substring(start)), insertBefore);
parent.removeChild(insertBefore);
}
}
// Recursively linkify the children of a given node.
function linkify_node(node) {
var children = node.childNodes;
// work backwards to avoid the risk of looping forever on our own output
for (var i = children.length - 1; i >= 0; --i) {
var child = children[i];
if (child.nodeType == Node.TEXT_NODE) {
// text is to be linkified
linkify(node, child, child.textContent);
} else if (child.nodeName != "A" && child.nodeName != "a") {
// do not linkify existing links
linkify_node(child);
}
}
}
//Shit fucking piece of crap that doesn't work god fuckin damn it
function linkify_fallback(text) {
var rex = /((?:'+$0+'';
}
else {
return $1 ? $0: ''+$0+'';
}
});
}
function byondDecode(message) {
// Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8.
// The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b.
// Marvelous.
message = message.replace(/\+/g, "%20");
try {
// This is a workaround for the above not always working when BYOND's shitty url encoding breaks. (byond bug id:2399401)
if (decodeURIComponent) {
message = decodeURIComponent(message);
} else {
throw new Error("Easiest way to trigger the fallback")
}
} catch (err) {
message = unescape(message);
}
return message;
}
function replaceRegex() {
var selectedRegex = replaceRegexes[$(this).attr('replaceRegex')];
if (selectedRegex) {
var replacedText = $(this).html().replace(selectedRegex[0], selectedRegex[1]);
$(this).html(replacedText);
}
$(this).removeAttr('replaceRegex');
}
// Get a highlight markup span
function createHighlightMarkup() {
var extra = '';
if (opts.highlightColor) {
extra += ' style="background-color: ' + opts.highlightColor + '"';
}
return '';
}
// Get all child text nodes that match a regex pattern
function getTextNodes(elem, pattern) {
var result = $([]);
$(elem).contents().each(function(idx, child) {
if (child.nodeType === 3 && /\S/.test(child.nodeValue) && pattern.test(child.nodeValue)) {
result = result.add(child);
}
else {
result = result.add(getTextNodes(child, pattern));
}
});
return result;
}
// Highlight all text terms matching the registered regex patterns
function highlightTerms(el) {
var pattern = new RegExp("(" + opts.highlightTerms.join('|') + ")", 'gi');
var nodes = getTextNodes(el, pattern);
nodes.each(function (idx, node) {
var content = $(node).text();
var parent = $(node).parent();
var pre = $(node.previousSibling);
$(node).remove();
content.split(pattern).forEach(function (chunk) {
// Get our highlighted span/text node
var toInsert = null;
if (pattern.test(chunk)) {
var tmpElem = $(createHighlightMarkup());
tmpElem.text(chunk);
toInsert = tmpElem;
}
else {
toInsert = document.createTextNode(chunk);
}
// Insert back into our element
if (pre.length == 0) {
var result = parent.prepend(toInsert);
pre = $(result[0].firstChild);
}
else {
pre.after(toInsert);
pre = $(pre[0].nextSibling);
}
});
});
}
function iconError(E) {
var that = this;
setTimeout(function() {
var attempts = $(that).data('reload_attempts');
if (typeof attempts === 'undefined' || !attempts) {
attempts = 1;
}
if (attempts > opts.imageRetryLimit)
return;
var src = that.src;
that.src = null;
that.src = src+'#'+attempts;
$(that).data('reload_attempts', ++attempts);
}, opts.imageRetryDelay);
}
function updateIconsSize(html) {
$(html).find(".icon").not('.text_tag').css({'height': opts.iconsize, 'width': opts.iconsize});
}
//Send a message to the client
function output(message, flag) {
if (typeof message === 'undefined') {
return;
}
if (typeof flag === 'undefined') {
flag = '';
}
if (flag !== 'internal')
opts.lastPang = Date.now();
message = byondDecode(message).trim();
//The behemoth of filter-code (for Admin message filters)
//Note: This is proooobably hella inefficient
var filteredOut = false;
if (opts.hasOwnProperty('showMessagesFilters') && !opts.showMessagesFilters['All'].show) {
//Get this filter type (defined by class on message)
var messageHtml = $.parseHTML(message),
messageClasses;
if (opts.hasOwnProperty('filterHideAll') && opts.filterHideAll) {
var internal = false;
messageClasses = (!!$(messageHtml).attr('class') ? $(messageHtml).attr('class').split(/\s+/) : false);
if (messageClasses) {
for (var i = 0; i < messageClasses.length; i++) { //Every class
if (messageClasses[i] == 'internal') {
internal = true;
break;
}
}
}
if (!internal) {
filteredOut = 'All';
}
} else {
//If the element or it's child have any classes
if (!!$(messageHtml).attr('class') || !!$(messageHtml).children().attr('class')) {
messageClasses = $(messageHtml).attr('class').split(/\s+/);
if (!!$(messageHtml).children().attr('class')) {
messageClasses = messageClasses.concat($(messageHtml).children().attr('class').split(/\s+/));
}
var tempCount = 0;
for (var i = 0; i < messageClasses.length; i++) { //Every class
var thisClass = messageClasses[i];
$.each(opts.showMessagesFilters, function(key, val) { //Every filter
if (key !== 'All' && val.show === false && typeof val.match != 'undefined') {
for (var i = 0; i < val.match.length; i++) {
var matchClass = val.match[i];
if (matchClass == thisClass) {
filteredOut = key;
break;
}
}
}
if (filteredOut) return false;
});
if (filteredOut) break;
tempCount++;
}
} else {
if (!opts.showMessagesFilters['Misc'].show) {
filteredOut = 'Misc';
}
}
}
}
//Stuff we do along with appending a message
var atBottom = false;
if (!filteredOut) {
var bodyHeight = $('body').height();
var messagesHeight = $messages.outerHeight();
var scrollPos = $('body,html').scrollTop();
//Should we snap the output to the bottom?
if (bodyHeight + scrollPos >= messagesHeight - opts.scrollSnapTolerance) {
atBottom = true;
if ($('#newMessages').length) {
$('#newMessages').remove();
}
//If not, put the new messages box in
} else {
if ($('#newMessages').length) {
var messages = $('#newMessages .number').text();
messages = parseInt(messages);
messages++;
$('#newMessages .number').text(messages);
if (messages == 2) {
$('#newMessages .messageWord').append('s');
}
} else {
$messages.after('1 new ');
}
}
}
opts.messageCount++;
//Pop the top message off if history limit reached
if (opts.messageCount >= opts.messageLimit) {
$messages.children('div.entry:first-child').remove();
opts.messageCount--; //I guess the count should only ever equal the limit
}
// Create the element - if combining is off, we use it, and if it's on, we
// might discard it bug need to check its text content. Some messages vary
// only in HTML markup, have the same text content, and should combine.
var entry = document.createElement('div');
entry.innerHTML = message;
var trimmed_message = entry.textContent || entry.innerText || "";
var handled = false;
if (opts.messageCombining) {
var lastmessages = $messages.children('div.entry:last-child').last();
if (lastmessages.length && $last_message && $last_message == trimmed_message) {
var badge = lastmessages.children('.r').last();
if (badge.length) {
badge = badge.detach();
badge.text(parseInt(badge.text()) + 1);
} else {
badge = $('', {'class': 'r', 'text': 2});
}
lastmessages.html(message.replace(/
\s*$/g,' '));
lastmessages.find('[replaceRegex]').each(replaceRegex);
lastmessages.append(badge);
badge.animate({
"font-size": "0.9em"
}, 100, function() {
badge.animate({
"font-size": "0.7em"
}, 100);
});
opts.messageCount--;
handled = true;
}
}
if (!handled) {
//Actually append the message
entry.className = 'entry';
if (filteredOut) {
entry.className += ' hidden';
entry.setAttribute('data-filter', filteredOut);
}
$(entry).find('[replaceRegex]').each(replaceRegex);
$last_message = trimmed_message;
$messages[0].appendChild(entry);
updateIconsSize(entry);
$(entry).find("img.icon").error(iconError);
var to_linkify = $(entry).find(".linkify");
if (typeof Node === 'undefined') {
// Linkify fallback for old IE
for(var i = 0; i < to_linkify.length; ++i) {
to_linkify[i].innerHTML = linkify_fallback(to_linkify[i].innerHTML);
}
} else {
// Linkify for modern IE versions
for(var i = 0; i < to_linkify.length; ++i) {
linkify_node(to_linkify[i]);
}
}
//Actually do the snap
//Stuff we can do after the message shows can go here, in the interests of responsiveness
if (opts.highlightTerms && opts.highlightTerms.length > 0) {
highlightTerms($(entry));
}
}
if (!filteredOut && atBottom) {
$('body,html').scrollTop($messages.outerHeight());
}
}
function internalOutput(message, flag)
{
output(escaper(message), flag)
}
//Runs a route within byond, client or server side. Consider this "ehjax" for byond.
function runByond(uri) {
window.location = uri;
}
function setCookie(cname, cvalue, exdays) {
cvalue = escaper(cvalue);
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = 'expires='+d.toUTCString();
document.cookie = cname + '=' + cvalue + '; ' + expires + "; path=/";
}
function getCookie(cname) {
var name = cname + '=';
var ca = document.cookie.split(';');
for(var i=0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1);
if (c.indexOf(name) === 0) {
return decoder(c.substring(name.length,c.length));
}
}
return '';
}
function rgbToHex(R,G,B) {return toHex(R)+toHex(G)+toHex(B);}
function toHex(n) {
n = parseInt(n,10);
if (isNaN(n)) return "00";
n = Math.max(0,Math.min(n,255));
return "0123456789ABCDEF".charAt((n-n%16)/16) + "0123456789ABCDEF".charAt(n%16);
}
function setTheme(theme) {
if (theme === 'white') {
document.getElementById("sheetofstyles").href = "browserOutput_white.css";
runByond('?_src_=chat&proc=swaptolightmode');
} else if (theme === 'dark') {
document.getElementById("sheetofstyles").href = "browserOutput.css";
runByond('?_src_=chat&proc=swaptodarkmode');
}
setCookie('theme', theme, 365);
internalOutput('Set theme: '+theme+'', 'internal');
}
function handleClientData(ckey, ip, compid) {
//byond sends player info to here
var currentData = {'ckey': ckey, 'ip': ip, 'compid': compid};
if (opts.clientData && !$.isEmptyObject(opts.clientData)) {
runByond('?_src_=chat&proc=analyzeClientData¶m[cookie]='+JSON.stringify({'connData': opts.clientData}));
for (var i = 0; i < opts.clientData.length; i++) {
var saved = opts.clientData[i];
if (currentData.ckey == saved.ckey && currentData.ip == saved.ip && currentData.compid == saved.compid) {
return; //Record already exists
}
}
//Lets make sure we obey our limit (can connect from server with higher limit)
while (opts.clientData.length >= opts.clientDataLimit) {
opts.clientData.shift();
}
} else {
runByond('?_src_=chat&proc=analyzeClientData¶m[cookie]=none');
}
//Update the cookie with current details
opts.clientData.push(currentData);
setCookie('connData', JSON.stringify(opts.clientData), 365);
}
//Server calls this on ehjax response
//Or, y'know, whenever really
function ehjaxCallback(data) {
opts.lastPang = Date.now();
if (data == 'softPang') {
return;
} else if (data == 'pang') {
opts.pingCounter = 0; //reset
opts.pingTime = Date.now();
runByond('?_src_=chat&proc=ping');
} else if (data == 'pong') {
if (opts.pingDisabled) {return;}
opts.pongTime = Date.now();
var pingDuration = Math.ceil((opts.pongTime - opts.pingTime) / 2);
$('#pingMs').text(pingDuration+'ms');
pingDuration = Math.min(pingDuration, 255);
var red = pingDuration;
var green = 255 - pingDuration;
var blue = 0;
var hex = rgbToHex(red, green, blue);
$('#pingDot').css('color', '#'+hex);
} else if (data == 'roundrestart') {
opts.restarting = true;
internalOutput('