/***************************************** * * 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, $subOptions, $subAudio, $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? 'colorPreset': 0, // index in the color presets list. //'darkmode':false, //Are we using darkmode? If not WHY ARE YOU LIVING IN 2009??? <- /tg/ take on darktheme //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': 35000, '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': [], //Admin music volume update 'volumeUpdateDelay': 5000, //Time from when the volume updates to data being sent to the server 'volumeUpdating': false, //True if volume update function set to fire 'updatedVolume': 0, //The volume level that is sent to the server 'musicStartAt': 0, //The position the music starts playing 'musicEndAt': 0, //The position the music... stops playing... if null, doesn't apply (so the music runs through) 'defaultMusicVolume': 25, 'messageCombining': true, }; var replaceRegexes = {}; // Array of names for chat display color presets. CIT SPECIFIC. // If not set to normal, a CSS file `browserOutput_${name}.css` will be added to the head. var colorPresets = [ 'normal', 'light', 'dark' ] 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, ''); }; } // CIT SPECIFIC. function updateColorPreset() { var el = $("#colorPresetLink")[0]; el.href = "browserOutput_"+colorPresets[opts.colorPreset]+".css"; runByond('?_src_=chat&proc=colorPresetPost&preset='+colorPresets[opts.colorPreset]); } // 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); } //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); 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); $(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 swap() { //Swap to darkmode if (opts.darkmode){ document.getElementById("sheetofstyles").href = "browserOutput_white.css"; opts.darkmode = false; runByond('?_src_=chat&proc=swaptolightmode'); } else { document.getElementById("sheetofstyles").href = "browserOutput.css"; opts.darkmode = true; runByond('?_src_=chat&proc=swaptodarkmode'); } setCookie('darkmode', (opts.darkmode ? 'true' : 'false'), 365); } */ 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('