Greasy Fork is available in English.

TagPro Userscript Library

Functions that any TagPro script could benefit from

Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/371240/727770/TagPro%20Userscript%20Library.js.

// ==UserScript==
// @name         TagPro Userscript Library
// @description  Functions that any TagPro script could benefit from
// @author       Ko </u/Wilcooo> (https://greasyfork.org/users/152992)
// @version      4.13
// @license      MIT
// @match        *://*.koalabeast.com/*
// @match        *://*.jukejuice.com/*
// @match        *://*.newcompte.fr/*
// @downloadURL  https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/tpul.lib.js
// @supportURL   https://www.reddit.com/message/compose/?to=Wilcooo
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      koalabeast.com
// ==/UserScript==



// ==UserLibrary==
// @name         TagPro Userscript Library
// @description  Functions that any TagPro script could benefit from
// @version      4.12
// @license      MIT
// ==/UserLibrary==


var version = 4.12;
console.log('Loading TPUL (TagPro Userscript Library) version '+version);



////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////

// To use this library, include these 5 lines in your userscripts' metadata block:

// @require      https://greasyfork.org/scripts/371240/code/TagPro%20Userscript%20Library.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      koalabeast.com

////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////






/* TODO

compatibility with SWJ (done I think)

Option to change the layout of the settings

Notify "options cancld" when scrolling away

margin beneath buttons on scoreboard

option to disable notifications.

ESC cancels, option to Save when canceld (scroll away, esc)

*/







var GM_configStruct = (function(){


////////////////////////////////////////////////////////////////
//    START OF ORIGINAL GM_CONFIG                             //
////////////////////////////////////////////////////////////////

/*
Copyright 2009+, GM_config Contributors (https://github.com/sizzlemctwizzle/GM_config)

GM_config Contributors:
    Mike Medley <medleymind@gmail.com>
    Joe Simmons
    Izzy Soft
    Marti Martz

GM_config is distributed under the terms of the GNU Lesser General Public License.

    GM_config is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

function GM_configStruct(){if(arguments.length){GM_configInit(this,arguments);this.onInit()}}
function GM_configInit(config,args){if(typeof config.fields=="undefined"){config.fields={};config.onInit=config.onInit||function(){};config.onOpen=config.onOpen||function(){};config.onSave=config.onSave||function(){};config.onClose=config.onClose||function(){};config.onReset=config.onReset||function(){};config.isOpen=false;config.title="User Script Settings";config.css={basic:["#GM_config * { font-family: arial,tahoma,myriad pro,sans-serif; }","#GM_config { background: #FFF; }","#GM_config input[type='radio'] { margin-right: 8px; }",
"#GM_config .indent40 { margin-left: 40%; }","#GM_config .field_label { font-size: 12px; font-weight: bold; margin-right: 6px; }","#GM_config .radio_label { font-size: 12px; }","#GM_config .block { display: block; }","#GM_config .saveclose_buttons { margin: 16px 10px 10px; padding: 2px 12px; }","#GM_config .reset, #GM_config .reset a,"+" #GM_config_buttons_holder { color: #000; text-align: right; }","#GM_config .config_header { font-size: 20pt; margin: 0; }","#GM_config .config_desc, #GM_config .section_desc, #GM_config .reset { font-size: 9pt; }",
"#GM_config .center { text-align: center; }","#GM_config .section_header_holder { margin-top: 8px; }","#GM_config .config_var { margin: 0 0 4px; }","#GM_config .section_header { background: #414141; border: 1px solid #000; color: #FFF;"," font-size: 13pt; margin: 0; }","#GM_config .section_desc { background: #EFEFEF; border: 1px solid #CCC; color: #575757;"+" font-size: 9pt; margin: 0 0 6px; }"].join("\n")+"\n",basicPrefix:"GM_config",stylish:""}}if(args.length==1&&typeof args[0].id=="string"&&typeof args[0].appendChild!=
"function")var settings=args[0];else{var settings={};for(var i=0,l=args.length,arg;i<l;++i){arg=args[i];if(typeof arg.appendChild=="function"){settings.frame=arg;continue}switch(typeof arg){case "object":for(var j in arg){if(typeof arg[j]!="function"){settings.fields=arg;break}if(!settings.events)settings.events={};settings.events[j]=arg[j]}break;case "function":settings.events={onOpen:arg};break;case "string":if(/\w+\s*\{\s*\w+\s*:\s*\w+[\s|\S]*\}/.test(arg))settings.css=arg;else settings.title=
arg;break}}}if(settings.id)config.id=settings.id;else if(typeof config.id=="undefined")config.id="GM_config";if(settings.title)config.title=settings.title;if(settings.css)config.css.stylish=settings.css;if(settings.frame)config.frame=settings.frame;if(settings.events){var events=settings.events;for(var e in events)config["on"+e.charAt(0).toUpperCase()+e.slice(1)]=events[e]}if(settings.fields){var stored=config.read(),fields=settings.fields,customTypes=settings.types||{},configId=config.id;for(var id in fields){var field=
fields[id];if(field)config.fields[id]=new GM_configField(field,stored[id],id,customTypes[field.type],configId);else if(config.fields[id])delete config.fields[id]}}if(config.id!=config.css.basicPrefix){config.css.basic=config.css.basic.replace(new RegExp("#"+config.css.basicPrefix,"gm"),"#"+config.id);config.css.basicPrefix=config.id}}
GM_configStruct.prototype={init:function(){GM_configInit(this,arguments);this.onInit()},open:function(){var match=document.getElementById(this.id);if(match&&(match.tagName=="IFRAME"||match.childNodes.length>0))return;var config=this;function buildConfigWin(body,head){var create=config.create,fields=config.fields,configId=config.id,bodyWrapper=create("div",{id:configId+"_wrapper"});head.appendChild(create("style",{type:"text/css",textContent:config.css.basic+config.css.stylish}));bodyWrapper.appendChild(create("div",
{id:configId+"_header",className:"config_header block center"},config.title));var section=bodyWrapper,secNum=0;for(var id in fields){var field=fields[id],settings=field.settings;if(settings.section){section=bodyWrapper.appendChild(create("div",{className:"section_header_holder",id:configId+"_section_"+secNum}));if(Object.prototype.toString.call(settings.section)!=="[object Array]")settings.section=[settings.section];if(settings.section[0])section.appendChild(create("div",{className:"section_header center",
id:configId+"_section_header_"+secNum},settings.section[0]));if(settings.section[1])section.appendChild(create("p",{className:"section_desc center",id:configId+"_section_desc_"+secNum},settings.section[1]));++secNum}section.appendChild(field.wrapper=field.toNode())}bodyWrapper.appendChild(create("div",{id:configId+"_buttons_holder"},create("button",{id:configId+"_saveBtn",textContent:"Save",title:"Save settings",className:"saveclose_buttons",onclick:function(){config.save()}}),create("button",{id:configId+
"_closeBtn",textContent:"Close",title:"Close window",className:"saveclose_buttons",onclick:function(){config.close()}}),create("div",{className:"reset_holder block"},create("a",{id:configId+"_resetLink",textContent:"Reset to defaults",href:"#",title:"Reset fields to default values",className:"reset",onclick:function(e){e.preventDefault();config.reset()}}))));body.appendChild(bodyWrapper);config.center();window.addEventListener("resize",config.center,false);config.onOpen(config.frame.contentDocument||
config.frame.ownerDocument,config.frame.contentWindow||window,config.frame);window.addEventListener("beforeunload",function(){config.close()},false);config.frame.style.display="block";config.isOpen=true}var defaultStyle="bottom: auto; border: 1px solid #000; display: none; height: 75%;"+" left: 0; margin: 0; max-height: 95%; max-width: 95%; opacity: 0;"+" overflow: auto; padding: 0; position: fixed; right: auto; top: 0;"+" width: 75%; z-index: 9999;";if(this.frame){this.frame.id=this.id;this.frame.setAttribute("style",
defaultStyle);buildConfigWin(this.frame,this.frame.ownerDocument.getElementsByTagName("head")[0])}else{document.body.appendChild(this.frame=this.create("iframe",{id:this.id,style:defaultStyle}));this.frame.src="about:blank";this.frame.addEventListener("load",function(e){var frame=config.frame;var body=frame.contentDocument.getElementsByTagName("body")[0];body.id=config.id;buildConfigWin(body,frame.contentDocument.getElementsByTagName("head")[0])},false)}},save:function(){var forgotten=this.write();
this.onSave(forgotten)},close:function(){if(this.frame.contentDocument){this.remove(this.frame);this.frame=null}else{this.frame.innerHTML="";this.frame.style.display="none"}var fields=this.fields;for(var id in fields){var field=fields[id];field.wrapper=null;field.node=null}this.onClose();this.isOpen=false},set:function(name,val){this.fields[name].value=val;if(this.fields[name].node)this.fields[name].reload()},get:function(name,getLive){var field=this.fields[name],fieldVal=null;if(getLive&&field.node)fieldVal=
field.toValue();return fieldVal!=null?fieldVal:field.value},write:function(store,obj){if(!obj){var values={},forgotten={},fields=this.fields;for(var id in fields){var field=fields[id];var value=field.toValue();if(field.save)if(value!=null){values[id]=value;field.value=value}else values[id]=field.value;else forgotten[id]=value}}try{this.setValue(store||this.id,this.stringify(obj||values))}catch(e){this.log("GM_config failed to save settings!")}return forgotten},read:function(store){try{var rval=this.parser(this.getValue(store||
this.id,"{}"))}catch(e){this.log("GM_config failed to read saved settings!");var rval={}}return rval},reset:function(){var fields=this.fields;for(var id in fields)fields[id].reset();this.onReset()},create:function(){switch(arguments.length){case 1:var A=document.createTextNode(arguments[0]);break;default:var A=document.createElement(arguments[0]),B=arguments[1];for(var b in B)if(b.indexOf("on")==0)A.addEventListener(b.substring(2),B[b],false);else if(",style,accesskey,id,name,src,href,which,for".indexOf(","+
b.toLowerCase())!=-1)A.setAttribute(b,B[b]);else A[b]=B[b];if(typeof arguments[2]=="string")A.innerHTML=arguments[2];else for(var i=2,len=arguments.length;i<len;++i)A.appendChild(arguments[i])}return A},center:function(){var node=this.frame;if(!node)return;var style=node.style,beforeOpacity=style.opacity;if(style.display=="none")style.opacity="0";style.display="";style.top=Math.floor(window.innerHeight/2-node.offsetHeight/2)+"px";style.left=Math.floor(window.innerWidth/2-node.offsetWidth/2)+"px";
style.opacity="1"},remove:function(el){if(el&&el.parentNode)el.parentNode.removeChild(el)}};
(function(){var isGM=typeof GM_getValue!="undefined"&&typeof GM_getValue("a","b")!="undefined",setValue,getValue,stringify,parser;if(!isGM){setValue=function(name,value){return localStorage.setItem(name,value)};getValue=function(name,def){var s=localStorage.getItem(name);return s==null?def:s};stringify=JSON.stringify;parser=JSON.parse}else{setValue=GM_setValue;getValue=GM_getValue;stringify=typeof JSON=="undefined"?function(obj){return obj.toSource()}:JSON.stringify;parser=typeof JSON=="undefined"?
function(jsonData){return(new Function("return "+jsonData+";"))()}:JSON.parse}GM_configStruct.prototype.isGM=isGM;GM_configStruct.prototype.setValue=setValue;GM_configStruct.prototype.getValue=getValue;GM_configStruct.prototype.stringify=stringify;GM_configStruct.prototype.parser=parser;GM_configStruct.prototype.log=window.console?console.log:isGM&&typeof GM_log!="undefined"?GM_log:window.opera?opera.postError:function(){}})();
function GM_configDefaultValue(type,options){var value;if(type.indexOf("unsigned ")==0)type=type.substring(9);switch(type){case "radio":case "select":value=options[0];break;case "checkbox":value=false;break;case "int":case "integer":case "float":case "number":value=0;break;default:value=""}return value}
function GM_configField(settings,stored,id,customType,configId){this.settings=settings;this.id=id;this.configId=configId;this.node=null;this.wrapper=null;this.save=typeof settings.save=="undefined"?true:settings.save;if(settings.type=="button")this.save=false;this["default"]=typeof settings["default"]=="undefined"?customType?customType["default"]:GM_configDefaultValue(settings.type,settings.options):settings["default"];this.value=typeof stored=="undefined"?this["default"]:stored;if(customType){this.toNode=
customType.toNode;this.toValue=customType.toValue;this.reset=customType.reset}}
GM_configField.prototype={create:GM_configStruct.prototype.create,toNode:function(){var field=this.settings,value=this.value,options=field.options,type=field.type,id=this.id,configId=this.configId,labelPos=field.labelPos,create=this.create;function addLabel(pos,labelEl,parentNode,beforeEl){if(!beforeEl)beforeEl=parentNode.firstChild;switch(pos){case "right":case "below":if(pos=="below")parentNode.appendChild(create("br",{}));parentNode.appendChild(labelEl);break;default:if(pos=="above")parentNode.insertBefore(create("br",
{}),beforeEl);parentNode.insertBefore(labelEl,beforeEl)}}var retNode=create("div",{className:"config_var",id:configId+"_"+id+"_var",title:field.title||""}),firstProp;for(var i in field){firstProp=i;break}var label=field.label&&type!="button"?create("label",{id:configId+"_"+id+"_field_label","for":configId+"_field_"+id,className:"field_label"},field.label):null;switch(type){case "textarea":retNode.appendChild(this.node=create("textarea",{innerHTML:value,id:configId+"_field_"+id,className:"block",cols:field.cols?
field.cols:20,rows:field.rows?field.rows:2}));break;case "radio":var wrap=create("div",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var radLabel=create("label",{className:"radio_label"},options[i]);var rad=wrap.appendChild(create("input",{value:options[i],type:"radio",name:id,checked:options[i]==value}));var radLabelPos=labelPos&&(labelPos=="left"||labelPos=="right")?labelPos:firstProp=="options"?"left":"right";addLabel(radLabelPos,radLabel,wrap,rad)}retNode.appendChild(wrap);
break;case "select":var wrap=create("select",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var option=options[i];wrap.appendChild(create("option",{value:option,selected:option==value},option))}retNode.appendChild(wrap);break;default:var props={id:configId+"_field_"+id,type:type,value:type=="button"?field.label:value};switch(type){case "checkbox":props.checked=value;break;case "button":props.size=field.size?field.size:25;if(field.script)field.click=field.script;
if(field.click)props.onclick=field.click;break;case "hidden":break;default:props.type="text";props.size=field.size?field.size:25}retNode.appendChild(this.node=create("input",props))}if(label){if(!labelPos)labelPos=firstProp=="label"||type=="radio"?"left":"right";addLabel(labelPos,label,retNode)}return retNode},toValue:function(){var node=this.node,field=this.settings,type=field.type,unsigned=false,rval=null;if(!node)return rval;if(type.indexOf("unsigned ")==0){type=type.substring(9);unsigned=true}switch(type){case "checkbox":rval=
node.checked;break;case "select":rval=node[node.selectedIndex].value;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=radios.length;i<len;++i)if(radios[i].checked)rval=radios[i].value;break;case "button":break;case "int":case "integer":case "float":case "number":var num=Number(node.value);var warn='Field labeled "'+field.label+'" expects a'+(unsigned?" positive ":"n ")+"integer value";if(isNaN(num)||type.substr(0,3)=="int"&&Math.ceil(num)!=Math.floor(num)||unsigned&&
num<0){alert(warn+".");return null}if(!this._checkNumberRange(num,warn))return null;rval=num;break;default:rval=node.value;break}return rval},reset:function(){var node=this.node,field=this.settings,type=field.type;if(!node)return;switch(type){case "checkbox":node.checked=this["default"];break;case "select":for(var i=0,len=node.options.length;i<len;++i)if(node.options[i].textContent==this["default"])node.selectedIndex=i;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=
radios.length;i<len;++i)if(radios[i].value==this["default"])radios[i].checked=true;break;case "button":break;default:node.value=this["default"];break}},remove:function(el){GM_configStruct.prototype.remove(el||this.wrapper);this.wrapper=null;this.node=null},reload:function(){var wrapper=this.wrapper;if(wrapper){var fieldParent=wrapper.parentNode;fieldParent.insertBefore(this.wrapper=this.toNode(),wrapper);this.remove(wrapper)}},_checkNumberRange:function(num,warn){var field=this.settings;if(typeof field.min==
"number"&&num<field.min){alert(warn+" greater than or equal to "+field.min+".");return null}if(typeof field.max=="number"&&num>field.max){alert(warn+" less than or equal to "+field.max+".");return null}return true}};var GM_config=new GM_configStruct;


////////////////////////////////////////////////////////////////
//    END OF ORIGINAL GM_CONFIG                               //
////////////////////////////////////////////////////////////////




    // I'm going to edit GM_config slightly.
    // Mostly to get rid of the 'alerts' when something is wrong.
    // (alerts pause the window, which causes you to disconnect from a game)


    // This function will return true when no errors were found.

    GM_configStruct.prototype.valid = function() {


        for (var id in this.fields) {

            var node = this.fields[id].node;

            if (node.validity && !node.validity.valid) return false;

            /*
            var field = this.fields[id],
                type = field.settings.type,
                unsigned = false;

            if (type.indexOf('unsigned ') == 0) {
                type = type.substring(9);
                unsigned = true;
            }

            if (['int','integer','float','number'].includes(type)) {

                var num = Number(field.node.value);

                var warn = 'Field labeled "' + field.label + '" expects a' +
                    (unsigned ? ' positive ' : 'n ') + 'integer value';

                if (isNaN(num) ||
                    (type.substr(0, 3) == 'int' && Math.ceil(num) != Math.floor(num)) ||
                    (unsigned && num < 0)) {
                    // Add a few ways for scripters to know that there is an error
                    field.error = true;
                    field.wrapper.classList.add('error');
                    correct = false;
                }

                else if (typeof field.settings.min == "number" && num < field.settings.min) {
                    // Add a few ways for scripters to know that there is an error
                    field.error = true;
                    field.wrapper.classList.add('error');
                    correct = false;
                }

                else if (typeof field.settings.max == "number" && num > field.settings.max) {
                    // Add a few ways for scripters to know that there is an error
                    field.error = true;
                    field.wrapper.classList.add('error');
                    correct = false;
                }

                else {
                    // Add a few ways for scripters to know that there is NO error
                    field.error = false;
                    field.wrapper.classList.remove('error');
                }
            }*/
        }

        return true;
    };


    // Change the field prototype

    var org_toNode = GM_configField.prototype.toNode;

    GM_configField.prototype.toNode = function(){

        var retNode = org_toNode.apply(this, ...arguments);

        var unsigned = false,
            type = this.settings.type;

        if (type.indexOf('unsigned ') === 0) {
            type = type.substring(9);
            unsigned = true;
        }

        if (this.node.validity) {
            // Validity checks will work for ANY input, not only numbers.
            // For example, if you want a text field to have at least 3 characters,
            // manually set the 'minLength' tag to 3 and the rest will be done
            // automagically.

            // Immediately show a validity report while typing / clicking
            this.node.addEventListener('input', this.node.reportValidity);
            this.node.addEventListener('click', this.node.reportValidity);

            // The autocomplete covers the validity report (at least in Chrome)
            this.node.autocomplete = 'off';
        }

        if (['int','integer','float','number'].includes(type)) {

            // By default, GM_config makes most inputs a text field, even numbers.
            // Lets fix that, to be able to check min and max values better.

            this.node.type = 'number';

            if (this.settings.min) this.node.min = this.settings.min;
            if (this.settings.max) this.node.max = this.settings.max;

            // unsigned means non-negative
            if (unsigned) this.node.min = Math.max(0,this.settings.min);

            // integers are only whole numbers
            if (type.substr(0, 3) == 'int') this.node.step = 1;
        }

        if (!['radio','select','checkbox','button','hidden'].includes(type)) {
            // Disable TagPro's controls when typing inside a field you can type in
            // You can set tpul.rollingChat.enable = true to make the Arrow keys move your ball, even when typing text.
            this.node.addEventListener('focus', function(){typeof tagpro != 'undefined' && (tagpro.disableControls = true)});
            this.node.addEventListener('blur', function(){typeof tagpro != 'undefined' && (tagpro.disableControls = false)});
        }

        return retNode;
    };



    return GM_configStruct;
})();








var tpul = (function(){




    // =====STYLE SECTION=====



    // Create our own stylesheet to define the styles in:

    var style = document.getElementById('tpul-style') || document.createElement('style');
    document.head.appendChild(style);
    style.id = 'tpul-style';

    // Remove all existing rules of any previous TPUL version.

    var styleSheet = style.sheet;
    Array.from(styleSheet.cssRules).forEach(rule => styleSheet.deleteRule(rule));

    // THE SETTINGS MENU BUTTONS

    // Container for settings buttons
    styleSheet.insertRule(` #tpul-settings-menu {
text-align: center;
margin: 0 10%;
}`);

    // A settings button
    styleSheet.insertRule(` .tpul-settings-btn {
position: relative;
width: 64px;
height: 64px;
padding: 10px;
margin: 20px;
background-size: contain !important;
background-origin: content-box !important;
background-repeat: no-repeat !important;
outline: none;
}`);

    // Blue line around button when focussed
    styleSheet.insertRule(` .tpul-settings-btn:focus::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
border: 2px solid Highlight;
top: 0;
left: 0;
}`);

    // Tooltip of button
    styleSheet.insertRule(` .tpul-settings-btn span {
position: absolute;
z-index: 1;
border-radius: 10px;
margin-top: 10px;
padding: 10px;
background: #0E8AE0;
border: 1px solid #095C96;
box-shadow: 0 3px #095C96;
font-size: small;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: max-content;
min-width: 64px;
max-width: 128px;
overflow-wrap: break-word;
word-wrap: break-word;
pointer-events: none;
opacity: 0;
transition: opacity .3s;
}`);


    // Arrow of tooltip
    styleSheet.insertRule(` .tpul-settings-btn span::after {
content: "";
position: absolute;
left: 50%;
bottom: 100%;
margin-left: -20px;
border-width: 20px;
border-style: solid;
border-color: transparent transparent #0E8AE0 transparent;
}`);

    // Show tooltip when hovering/focussing
    styleSheet.insertRule(`.tpul-settings-btn:hover span, .tpul-settings-btn:focus span {
opacity: 1;
}`);



    // THE SETTINGS PANEL

    // The frame (gray, spans full page)
    styleSheet.insertRule(` .tpul-settings-frame {
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
scroll-behavior: smooth;

transition: opacity .5s;
opacity: 0;
pointer-events: none;
}`);

    // The frame when shown
    styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame {
opacity: 1;
pointer-events: auto;
}`);

    // The settings window itself
    styleSheet.insertRule(` .tpul-settings-frame > div {
width: 80%;
max-width: 800px;
margin: auto;
margin-bottom: 10%;

position: relative;
padding: 20px;

border: 1px solid #888;
border-radius: 15px;
background: #353535;

font-size: 16px;

top: 200%;
transition: top .5s;
}`);

    styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame         > div { top: 120%; }`);
    // In a game we want to have an 80% gap to be able to keep playing.
    styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame.in-game > div { top: 180%; }`);


    styleSheet.insertRule(`.tpul-settings-frame .config_header {
font-size: 2em;
font-weight: bold;
}`);

    styleSheet.insertRule(`.tpul-settings-frame .section_header {
font-size: 1.5em;
font-weight: bold;
}`);

    styleSheet.insertRule(`.tpul-settings-frame .config_var {
}`);


    // ERRORS in fields:
    styleSheet.insertRule(`.tpul-settings-frame .config_var input:invalid {
box-shadow: inset 0 0 10px rgba(255,0,0,1), 0 0 10px rgba(255, 0, 0, 1);
}`);

    /*styleSheet.insertRule(`.tpul-settings-frame .config_var.error:before {
content: attr(data-min) ' - ' attr(data-max);
display: block;
text-align: right;
margin: 5px 20px;
color: #FFA9A2;
font-style: italic;
}`);*/

    styleSheet.insertRule(`.tpul-settings-frame .field_label {
font-weight: bold;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .form-control {
background: #212121;
border-color: #5f5f5f;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .form-control[type="checkbox"] {
width: auto;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .btn-default {
border-color: #888888;
}`);
    styleSheet.insertRule(`.tpul-settings-frame textarea.form-control {
resize: vertical;
}`);

    styleSheet.insertRule(`.tpul-settings-frame .btn-primary {
margin-left: 10px;
}`);



    styleSheet.insertRule(`.tpul-settings-frame .tab-list {
border-bottom-color: #888888;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .tab-list li {
cursor: pointer;
color: #8BC34A;
font-size: 1.5em;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .tab-list li:hover {
color: #689F38;
}`);
    styleSheet.insertRule(`.tpul-settings-frame .tab-list li.active {
border-color: #888888;
border-bottom-color: transparent;
background-color: #353535;
}`);



    // save/close/etc buttons

    styleSheet.insertRule(`.tpul-settings-frame-buttons-holder {
height: 0;
text-align: right;
}`);

    styleSheet.insertRule(`.tpul-settings-frame-buttons-holder button {
padding: 4px .5em;
}`);


    styleSheet.insertRule(`@keyframes bounce {
0%, 20% {transform: translate(-50px,50%)scale(.06)}
10% {transform: translate(-50px,55%)scale(.06)}
}`);

    styleSheet.insertRule(`.tpul-settings-scroll-down-arrow {
position: fixed;
width: 80%;
max-width: 800px;
left: 50%;
transform: translate(-50px,50%)scale(.06);
bottom: 30px;
z-index: 1;
transition: opacity .5s;
animation-name: bounce;
animation-delay: 1s;
animation-duration: 3s;
animation-iteration-count: infinite;
animation-direction: alternate;
transition: opacity .5s;
cursor: pointer;
}`);

    styleSheet.insertRule(`
@media screen and (max-width: 1000) {
.tpul-settings-scroll-down-arrow {
right: calc(10% + 30px);
width: 5px;
}
}`);


    /*

    //Bad design notice:

    styleSheet.insertRule(` .tpul-settings-frame > div::after {
content: "Sorry for the bad design, I'm working on it!";
font-style: italic;
color: gray;
}`);

    */

    // Stop the body from scrolling when the settings panel is shown
    styleSheet.insertRule(`body.tpul-settings-shown {
overflow:hidden !important;
}`);




    // Notifications
    styleSheet.insertRule(` .tpul-notification-success {
border-color: #8BC34A;
background: #4C6D25;
color: black;
}`);

    styleSheet.insertRule(` .tpul-notification-error {
border-color: #BD0E0B;
background: #6B2121;
color: #FFA9A2;
}`);

    styleSheet.insertRule(` .tpul-notification-warning {
border-color: Olive;
background: DarkKhaki;
color: black;
}`);

    styleSheet.insertRule(` .tpul-notification {
position: fixed;
bottom: 0px;

padding: 10px;

width: 100%;

text-align: center;

cursor: pointer;
z-index: 2;

border-top: 1px solid #404040;
background: #353535;
color: #fff;

animation: slideUp 1s;
transform: translateY(0);
transition: transform 1s;
}`);

    styleSheet.insertRule(` .tpul-notification.vanish {
transform: translateY(100%);
}`);

    styleSheet.insertRule(` @keyframes slideUp {
0% { transform: translateY(100%); }
100% { transform: translateY(0%); }
}`);



    // =====NOITCES ELYTS=====





    // =====LOGIC SECTION=====



    var GM_storage = typeof GM_setValue === 'function' && typeof GM_getValue === 'function',
        all_settings = [],
        profileId = null,
        last_opened = null,
        rollingChatEnabled = false;


    // THE TPUL OBJECT!!

    var tpul = {
        get version(){return version},

        get noscript(){return typeof tagpro != 'object'},

        settings: {
            addSettings: function({id, title, fields, icon, tooltipText, buttonText}) {

                var config = arguments[0];

                if (config.allowLocal && !id && !GM_storage) throw "TPUL: A unique id is required, because localStorage will be used! By the way; it is better to @grant GM_getValue and GM_setValue and set 'allowLocal:false' to use private storage instead.";

                if (!config.allowLocal && !GM_storage) throw "TPUL: Please @grant GM_setValue and GM_getValue in your userscripts metadata (recommended) or use 'allowLocal:true' (not recommended)";


                if (arguments.length != 1 || typeof config != 'object')
                    throw Error("addSettings() takes one object as an argument! Example: addSettings( {id:'MySettings', title:'Hello World'} )");

                // Create a new GM_config instance
                let settings = new GM_configStruct({

                    frame: SettingsFrame,

                    ...config,

                    id: String(config.id) || 'defaultId',

                    events: {
                        ...(config.events||{}),

                        open: function(){

                            //Remove the default inline style of the GM_config frame
                            this.frame.setAttribute('style', '');

                            //Apply some TagPro/Bootstrap styles
                            SettingsFrame.firstChild.classList.add('form-horizontal');
                            for (let el of SettingsFrame.getElementsByClassName('config_header')) el.classList.add('header-title');
                            for (let el of SettingsFrame.getElementsByClassName('config_var')) el.classList.add('form-group');
                            for (let el of SettingsFrame.getElementsByClassName('field_label')) {
                                el.classList.add('col-xs-4');
                                el.classList.add('control-label');
                            }
                            for (let el of SettingsFrame.getElementsByClassName('radio_label')) el.classList.add('radio');
                            for (let el of [...SettingsFrame.getElementsByTagName('input'),
                                            ...SettingsFrame.getElementsByTagName('select'),
                                            ...SettingsFrame.getElementsByTagName('textarea')]) {

                                switch (el.type) {
                                    case 'radio':
                                        el.parentElement.classList.add('col-xs-8');
                                        el.parentElement.style.paddingLeft = '30px';
                                        el.nextElementSibling.prepend(el);
                                        continue;
                                    case 'button':
                                        el.classList.add('btn');
                                        el.classList.add('btn-default');
                                        break;
                                    default:
                                        el.classList.add('form-control');

                                }

                                var div = document.createElement('div');
                                el.parentElement.appendChild(div);
                                div.appendChild(el);

                                div.classList.add('col-xs-8');
                                div.classList.add('pull-right');
                            }

                            // The footer with the buttons:

                            var buttonsHolder = SettingsFrame.firstElementChild.lastElementChild;
                            buttonsHolder.classList.add('col-sm-12');
                            buttonsHolder.classList.add('tpul-settings-frame-buttons-holder');

                            // Place the "footer" on top
                            buttonsHolder.parentElement.insertBefore(buttonsHolder, buttonsHolder.parentElement.firstElementChild);

                            for (var btn of [...buttonsHolder.getElementsByClassName('saveclose_buttons'),
                                             ...buttonsHolder.getElementsByClassName('reset')]) {
                                btn.classList.add('btn');
                                btn.classList.add('btn-primary');
                            }

                            buttonsHolder.innerHTML = '';

                            for (var type of this.buttons || ['ok','cancel','reset']) {
                                var button = document.createElement('button');
                                button.className = 'btn btn-primary';
                                button.settings = settings;
                                buttonsHolder.appendChild(button);

                                switch(type.toLowerCase()) {
                                    case 'ok':
                                        button.onclick = function(){
                                            if(this.settings.valid()) {this.settings.save(); this.settings.close(); tpul.notify('Options saved!','success');}
                                            else {tpul.notify('Please fix any issues before saving', 'error');}
                                        };
                                        button.innerText = 'Ok';
                                        break;
                                    case 'cancel':
                                        button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
                                        button.innerText = 'Cancel';
                                        break;
                                    case 'reset':
                                        button.onclick = function(){ this.settings.reset(); tpul.notify('All options are reset to their defaults','');};
                                        button.innerText = 'Reset';
                                        break;
                                    case 'save':
                                        button.onclick = function(){
                                            if(this.settings.valid()) {this.settings.save(); tpul.notify('Options saved!','success');}
                                            else {tpul.notify('Please fix any issues before saving', 'error');}
                                        };
                                        button.innerText = 'Save';
                                        break;
                                    case 'close':
                                        button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
                                        button.innerText = 'Close';
                                        break;
                                }
                            }


                            if (this.tabs) {

                                var tablist = document.createElement('ul');
                                tablist.classList.add('tab-list');
                                SettingsFrame.firstElementChild.insertBefore(tablist, SettingsFrame.firstElementChild.lastElementChild);

                                var tabcontent = document.createElement('div');
                                tabcontent.classList.add('tab-content');
                                SettingsFrame.firstElementChild.insertBefore(tabcontent, SettingsFrame.firstElementChild.lastElementChild);

                                for (let el of [...SettingsFrame.getElementsByClassName('section_header_holder')]) {

                                    var header = el.getElementsByClassName('section_header')[0];

                                    tablist.innerHTML += '<li data-target="#'+el.id+'">' + header.innerText;

                                    tabcontent.appendChild(el);
                                    el.classList.add('tab-pane');

                                    el.removeChild(header);
                                }

                                tablist.firstElementChild.click();

                            } else {
                                for (let el of SettingsFrame.getElementsByClassName('section_header')) el.classList.add('header-title');
                            }

                            //Open the settings on our way (animated, blocking scroll of body etc.)
                            this.frame.style.display = '';
                            SettingsFrame.scrollTop = SettingsFrame.offsetHeight;
                            document.body.classList.add('tpul-settings-shown');

                            // Add an arrow, indicating the user to scroll down for more settings
                            var arrow = document.createElement('img');
                            arrow.classList.add("tpul-settings-scroll-down-arrow");
                            arrow.src = "https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/arrow.png";
                            this.frame.appendChild(arrow);
                            arrow.onclick = function(){
                                arrow.style.opacity = 0;
                                SettingsFrame.scrollTo(0,SettingsFrame.scrollHeight)
                            }

                            last_opened = settings;

                            // If the userscript adds an 'open' event as well, run it as well
                            if (this.events && typeof this.events.open == "function")
                                this.events.open.call(this,...arguments);
                        },

                        close: function(){
                            if(this.isOpen){}//TODO: Check whether unsaved?

                            //close the settings in our way (animated)
                            this.frame.style.display = '';
                            document.body.classList.remove('tpul-settings-shown');

                            if (this.events && typeof this.events.close == "function")
                                this.events.close.call(this,...arguments);
                        },
                    }
                });

                // Remove all other default styles of GM_config
                delete settings.css.basic;

                // Create a button using the function below
                var button = tpul.settings.addButton({
                    onclick: ()=>settings.open(),
                    icon: icon,
                    tooltipText: tooltipText,
                    buttonText: buttonText,
                });

                settings.button = button;

                for (let c in config) if(settings[c] === undefined) settings[c] = config[c];

                all_settings.push(settings);

                return settings;

            },
            addButton: function({onclick, icon, tooltipText, buttonText}) {

                if (!SettingsMenu) {
                    console.error('TPUL: Could not find a place to add the settings button for '+name);
                    return null;
                }

                var button = document.createElement('button');
                button.className = 'btn tpul-settings-btn';

                if (icon) {
                    if (icon.search(/^url\((.*)\)$/) == -1) icon = 'url("'+icon+'")';
                    button.style.backgroundImage = icon;
                    button.innerHTML = '&nbsp;';
                } else button.innerText = buttonText || '?';

                var tooltip = document.createElement('span');
                tooltip.innerText = tooltipText || "Configure this script's settings" ;
                button.appendChild(tooltip);

                SettingsMenu.appendChild(button);

                button.addEventListener('click',function(click){
                    button.blur();
                    onclick(click);
                });

                return button;
            },
            get parent() {return SettingsMenu.parentElement;},
            set parent(container) {

                if (container) console.warn('You are repositioning the tpul settings menu. This will affect all settings buttons, not only for your script!');

                container = container ||
                    document.getElementById('tpul-settings-container') || // Try to add it to a position pre-defined by another script (such as ModFather)
                    document.getElementById('userscript-top') || // Try to add it on top of any page on the server
                    document.getElementById('options'); // Try to add it to the scoreboard in-game

                if (container) {
                    container.classList.remove('hidden');
                    container.appendChild(SettingsMenu);
                } else console.error('Couldn\'t find a parent element.');

                return container;
            },
            get menu(){ return SettingsMenu; },
            set menu(_){ throw "You can't change the TPUL settings menu object. You might mean to change the tpul.settings.parent"; },
        },

        profile: {
            getId: function() {

                if (!tpul_promises.getProfileId) {
                    tpul_promises.getProfileId = new Promise(function(resolve,reject) {

                        GM_xmlhttpRequest({
                            method: "GET",
                            url: "http://"+document.location.hostname+"/",
                            onload: function(){
                                var match = this.responseText.match(/profile\/([0-9a-f]+)/i);
                                if (match) {
                                    profileId = match[1];
                                    resolve(profileId);
                                } else reject({error:"not logged in"});
                            },
                            onerror: ()=> reject({error:"request error", request:this}),
                        });
                    });
                }

                return tpul_promises.getProfileId;
            },

            getInfo: function() {

                if (!tpul_promises.getProfileInfo) {

                    tpul_promises.getProfileInfo = new Promise(function(resolve,reject) {

                        tpul.profile.getId().then( function(id){

                            GM_xmlhttpRequest({
                                method: "GET",
                                url: "http://"+document.location.hostname+"/profiles/"+id,
                                onload: function(r){
                                    // 'r' is the response that we get back from the TP server, lets do some error handling with it:

                                    var arr;
                                    try{  arr = JSON.parse(r.response);  }
                                    catch(e){  reject({error:"/profiles/ responded invalid JSON", request:this});  }

                                    if(arr.error) reject(arr);

                                    if(Array.isArray( arr ) && arr.length == 1) {
                                        resolve(arr[0]);
                                    }
                                    else reject({error:"unknown error", response:arr, request:this});
                                },
                                onerror: ()=> reject({error:"request error", request:this}),
                            });
                        });

                        tpul.profile.getId().catch( reject );

                    });
                }

                return tpul_promises.getProfileInfo;
            },

            getPage: function() {

                if (!tpul_promises.getProfilePage) {

                    tpul_promises.getProfilePage = new Promise(function(resolve,reject) {

                        tpul.profile.getId().then( function(id){

                            GM_xmlhttpRequest({
                                method: "GET",
                                url: "http://"+document.location.hostname+"/profile/"+id,
                                onload: function(r){
                                    // 'r' is the response that we get back from the TP server, lets do some error handling with it:
                                    if(r.response.error) reject(r.response);

                                    var match,
                                        profile = {
                                            settings: {
                                                allChat: undefined,
                                                teamChat: undefined,
                                                groupChat: undefined,
                                                systemChat: undefined,
                                                tutorialChat: undefined,

                                                names: undefined,
                                                degrees: undefined,
                                                matchState: undefined,
                                                performanceInfo: undefined,
                                                spectatorInfo: undefined,

                                                stats: undefined,
                                            },

                                            flair: [],
                                        };

                                    // If the 'settings' div cannot be found, assume to not be logged in.
                                    if( !/<div(?: [^>]*)? id="settings"/i.test(this.responseText) ) return reject({error:"not logged in", request:this});

                                    // Get the global settings
                                    // (ball spin, respawn warnings and video settings are NOT stored on the TP server,
                                    //     only in a cookie on your device)
                                    for (var setting in profile.settings) {
                                        match = RegExp('<input(?: [^>]*)? id="' +setting+ '"(?: [^>]*)? (checked)?', 'i').exec(this.responseText);
                                        if (match) {
                                            profile.settings[setting] = Boolean(match[1]);
                                        } else return reject({error:"unknown error", request:this});
                                    }

                                    // Get the 'Custom Team Names' setting (the only non-boolean setting)
                                    /*
                                    <select id="teamNames" name="teamNames" class="form-control">
                                        <option value="always" >Always</option>
                                        <option value="spectating" >When Spectating</option>
                                        <option value="never" selected>Never</option>
                                    </select>
                                    */

                                    var teamNamesOptions = /<select(?: [^>]*)? id="teamNames"(?: [^>]*)?>((?:\s*?.*?)*?)<\/select>/i.exec(this.responseText);
                                    if (teamNamesOptions) {
                                        var teamNamesOpt_rgx = /<option(?: [^>]*)? value="([^>]*)"(?: [^>]*)? (selected)?(?: [^>]*)?>/ig;
                                        while ( (match = teamNamesOpt_rgx.exec(teamNamesOptions[1])) ){

                                            if (match[2]) {
                                                profile.settings.teamNames = match[1];
                                                break;
                                            }
                                        }
                                    } else return reject({error:"unknown error", request:this});

                                    // Get both names
                                    for (var name of ['reservedName','displayedName']) {
                                        match = RegExp('<input(?: [^>]*)? id="' +name+ '"(?: [^>]*)? value="(.*?)"', 'i').exec(this.responseText);
                                        if (match) {
                                            profile[name] = match[1];
                                        } else return reject({error:"unknown error", request:this});
                                    }

                                    // Get your email
                                    match = /<span(?: [^>]*)? class="hidden-email"(?: [^>]*)?>[^<]*?\b([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})\b<\/span>/i.exec(this.responseText);
                                    if (match) {
                                        profile.email = match[1];
                                    } else return reject({error:"unknown error", request:this});

                                    // Get all flairs, and whether they are available, and which one is selected
                                    var flair_rgx = /<li class="(.*?)" data-flair="(.*?)">/ig;
                                    while ( (match = flair_rgx.exec(this.responseText)) ) {
                                        var i = profile.flair.push({
                                            id: match[2],
                                            selected: match[1].includes('selected'),
                                            available: match[1].includes('flair-available'),
                                        });
                                        if (profile.flair[i-1]) profile.selectedFlair = profile.flair[i-1];
                                    }

                                    // Remove duplicate flairs (because there are 3 tabs)
                                    var flair_ids = [];
                                    profile.flair = profile.flair.filter(flair => !flair_ids.includes(flair.id) && flair_ids.push(flair.id));

                                    resolve(profile);

                                },

                                onerror: ()=> reject({error:"request error", request:this}),
                            });
                        });

                        tpul.profile.getId().catch( reject );
                    });
                }

                return tpul_promises.getProfilePage;
            },

            getRolling: function() {

                if (!tpul_promises.getProfileRolling) {

                    tpul_promises.getProfileRolling = new Promise(function(resolve,reject) {

                        tpul.profile.getId().then( function(id){

                            GM_xmlhttpRequest({
                                method: "GET",
                                url: "http://"+document.location.hostname+"/profile_rolling/"+id,
                                onload: function(r){
                                    // 'r' is the response that we get back from the TP server, lets do some error handling with it:
                                    if(r.response.error) reject(r.response);
                                    if(Array.isArray( r.response )) {
                                        resolve(r.response);
                                    }
                                    else reject({error:"unknown error", request:this});
                                },
                                onerror: ()=> reject({error:"request error", request:this}),
                            });
                        });

                        tpul.profile.getId().catch( reject );

                    });
                }

                return tpul_promises.getProfileRolling;
            },

            getReservedName: function(fallbackTimeout=5e3) {

                /*

                Where to get the Reserved name from?

                    - in-game when auth
                    - getInfo /profiles/...
                    - getPage /profile/...

                Logic:

                    1. if getInfo was called before: use that
                    2. if getPage was called before: use that
                    3. if in-game and auth: get it that way
                    4. call getInfo() to get the name

                */

                if (!tpul_promises.getReservedName) {

                    tpul_promises.getReservedName = new Promise(function(resolve,reject) {

                        // The fallback: get the reserved name using getInfo()
                        var fallback = function(){

                            done = true;

                            tpul.profile.getInfo().then(function(profileInfo) {

                                resolve(profileInfo.reservedName);
                            });

                            tpul.profile.getInfo().catch( reject );
                        };

                        if (tpul_promises.getProfileInfo) {

                            tpul_promises.getProfileInfo.then(function(profileInfo){
                                resolve(profileInfo.reservedName);
                            });

                            tpul_promises.getProfileInfo.catch( reject );

                        } else if (tpul_promises.getProfilePage) {

                            tpul_promises.getProfilePage.then(function(profilePage){
                                resolve(profilePage.reservedName);
                            });

                            tpul_promises.getProfilePage.catch( reject );

                        } else if (typeof tagpro != 'undefined' && tagpro.ready) {
                            tagpro.ready(function(){

                                if (tagpro.players) {
                                    if (tagpro.players[tagpro.playerId]) {

                                        if (tagpro.players[tagpro.playerId].auth) {
                                            resolve (tagpro.players[tagpro.playerId].name);
                                        } else fallback();

                                    } else {

                                        tagpro.socket.on('p',function(playerId) {
                                            if (tagpro.players[tagpro.playerId]) {

                                                if (tagpro.players[tagpro.playerId].auth) {
                                                    resolve (tagpro.players[tagpro.playerId].name);
                                                } else fallback();

                                            }
                                        });

                                    }
                                } else fallback();
                            });
                        } else fallback();

                        var done = false;
                        setTimeout(function(){
                            tpul_promises.getReservedName.then(()=>done=true);
                        });

                        setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );

                    });
                }

                return tpul_promises.getReservedName;
            },

            getDisplayedName: function(fallbackTimeout = 5e3) {

                 /*

                Where to get the Displayed name from?

                    - in-game
                    - getProfile /profile/...

                Logic:

                    1. if getPage was called before: use that
                    2. if in-game: get it that way
                    3. call getPage() to get the name

                */

                if (!tpul_promises.getDisplayedName) {

                    tpul_promises.getDisplayedName = new Promise(function(resolve,reject) {

                        // The fallback: get the displayed name using getPage()
                        var fallback = function(){

                            done = true;

                            tpul.profile.getPage().then(function(profilePage) {

                                resolve(profilePage.displayedName);
                            });

                            tpul.profile.getPage().catch( reject );
                        };

                        if (tpul_promises.getProfilePage) {

                            tpul_promises.getProfilePage.then(function(profilePage){
                                resolve(tpul_promises.getProfilePage.displayedName);
                            });

                            tpul_promises.getProfilePage.catch( reject );

                        } else if (typeof tagpro != 'undefined' && tagpro.ready) {
                            tagpro.ready(function(){

                                if (tagpro.players) {
                                    if (tagpro.players[tagpro.playerId]) {

                                        resolve (tagpro.players[tagpro.playerId].name);

                                    } else {

                                        tagpro.socket.on('p',function(playerId) {
                                            if (tagpro.players[tagpro.playerId]) {

                                                resolve (tagpro.players[tagpro.playerId].name);

                                            }
                                        });

                                    }
                                } else fallback();
                            });
                        } else fallback();

                        var done = false;
                        setTimeout(function(){
                            tpul_promises.getDisplayedName.then(()=>done=true);
                        });

                        setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );

                    });
                }

                return tpul_promises.getDisplayedName;

            },

            getSettings: function(fallbackTimeout = 5e3) {

                /*

                Where to get the settings from?

                    - in-game
                    - getPage /profile/...

                Logic:

                    1. if in-game: get it that way
                    2. call getPage() to get the settings

                */

                var top_args = arguments;

                if (!tpul_promises.getProfileSettings) {

                    tpul_promises.getProfileSettings = new Promise(function(resolve,reject) {

                        var fallback = function(){

                            done = true;

                            tpul.profile.getPage().then(function(profilePage){
                                resolve(profilePage.settings);
                            });

                            tpul.profile.getPage().catch( reject );
                        };

                        if (top_args[0] && top_args[0].__settings) {
                            resolve(top_args[0].__settings);
                        } else if (tpul_promises.getProfilePage) {

                            tpul_promises.getProfilePage.then(function(profilePage){
                                resolve(profilePage.settings);
                            });

                            tpul_promises.getProfilePage.catch( reject );

                        } else if (typeof tagpro != 'undefined' && tagpro.ready) {
                            tagpro.ready(function(){

                                if (tagpro.socket && tagpro.socket.on) {

                                    tagpro.socket.on('settings', function(settings) {
                                        resolve(Object.assign(settings.ui, {stats:settings.stats}));
                                    });

                                } else fallback();
                            });
                        } else fallback();

                        var done = false;
                        setTimeout(function(){
                            tpul_promises.getProfileSettings.then(()=>done=true);
                        });

                        setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );

                    });

                }

                return tpul_promises.getProfileSettings;

            },

            setSettings: function(newSettings, persistent=true, immediately=false) {

                if (immediately) console.warn("Most settings will NOT take effect immediately, I might add this functionality in the future. Only chat settings work at the moment.");

                return new Promise(function(resolve, reject){

                    // Step 1: set any local (cookie) settings
                    // These don't have to be send to the server, easy!

                    if (persistent) {

                        for (let setting in newSettings) {
                            if (['sound',
                                 'music',
                                 'volume',

                                 'textures',

                                 'disableBallSpin',
                                 'tileRespawnWarnings',
                                 'disableTutorialChat', // This cookie seems to be unused
                                 // Setting it anyway \(^.^)/

                                 'disableParticles',
                                 'forceCanvasRenderer',
                                 'disableViewportScaling',
                                ].includes(setting)) {

                                var expires = new Date(Date.now() + 31536e8).toUTCString(); // A century from now (same as TagPro uses)
                                document.cookie = setting + '=' + newSettings[setting] + '; expires='+expires+'; path=/; domain=.koalabeast.com';
                            }
                        }

                        // Step 2: send any server-sided settings to the server

                        if (['reservedName',
                             'displayedName',

                             'allChat',
                             'teamChat',
                             'groupChat',
                             'systemChat',
                             'tutorialChat',

                             'names',
                             'degrees',
                             'matchState',
                             'performanceInfo',
                             'spectatorInfo',

                             'teamNames',
                             'stats',
                            ].some( s => s in newSettings ) ){

                            // Call these to let them run in parallel
                            tpul.profile.getSettings();
                            tpul.profile.getReservedName();
                            tpul.profile.getDisplayedName();

                            tpul.profile.getSettings().then( function(settings){
                                tpul.profile.getReservedName().then( function(reservedName){
                                    tpul.profile.getDisplayedName().then( function(displayedName){

                                        console.log(param({reservedName: reservedName, // Your reservedName
                                                           displayedName: displayedName,  // Your displayedName
                                                           //...settings, // The current settings
                                                           //...newSettings}));
                                                          }));
                                        var req = GM_xmlhttpRequest({
                                            data: param(Object.assign({},
                                                                      settings, // The current settings
                                                                      {reservedName: reservedName, // Your reservedName
                                                                       displayedName: displayedName},  // Your displayedName
                                                                      newSettings // Overwrite with the settings that you want to edit.
                                                                     )),
                                            method: "POST",
                                            headers: {"Content-Type": "application/x-www-form-urlencoded"},
                                            url: "http://"+document.location.hostname+"/profile/update",
                                            onload: function(r){
                                                // 'r' is the response that we get back from the TP server, lets do some error handling with it:

                                                var arr;
                                                try{  arr = JSON.parse(r.response);  }
                                                catch(e){  reject({error:"/profile/update responded invalid JSON", request:this});  }

                                                if(arr.error) reject(arr);
                                                else if(arr.success) {
                                                    resolve(arr);
                                                } else reject({error:'unknown error',response: arr, request:this});
                                            },
                                            onerror: reject,
                                        });

                                    });
                                });
                            });

                        }

                    }

                    // Step 3: In case we are in-game, let the settings go into effect immediately.
                    // To update the reserved name, a refresh is required. TPUL won't do this!

                    if (typeof tagpro != 'undefined' && immediately) {
                        if (!tagpro.settings) tagpro.settings = {ui:{}};
                        if (!tagpro.settings.ui) tagpro.settings.ui = {};

                        for (let setting in newSettings) {
                            if (['allChat',
                                 'teamChat',
                                 'groupChat',
                                 'systemChat',
                                 'tutorialChat',
                                ].includes(setting)){

                                tagpro.settings.ui[setting] = newSettings[setting];
                            }
                        }

                        if (setting == 'tutorialChat') {
                            var tutorialButton = document.getElementById('tutorialButton');

                            if (tutorialButton) {
                                var action = tutorialButton.innerText === "Enable Tips";
                                if (newSettings[setting] == action) tutorialButton.click();
                            }
                        }


                    }

                });
            }

        },

        rollingChat: {

            _init: function initRollingChat(enable = false){

                if (typeof tagpro == 'undefined') return console.error( "The `tagpro` object does not exist. Is this a no-script match?" )

                // In case you don't want to load the full TPUL library,
                // You can add RollingChat to your own script by copying this function
                // Usage:
                //     initRollingChat(true);

                if (!tagpro.rollingChat) {

                    tagpro.rollingChat = {
                        enabled: false,
                        get handler() {
                            return function(event) {

                                // Return if not enabled
                                if (!tagpro.rollingChat.enabled) return;

                                // Whether you are releasing instead of pressing the key:
                                var releasing = event.type == 'keyup';

                                // Check if any modifier keys where held down during a keyDown
                                if (!releasing && (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)) return;

                                // The key that is pressed/released (undefined when it is any other key)
                                var arrow = ['left','up','right','down'][[37,38,39,40].indexOf(event.keyCode)];

                                // Only if the controls are disabled (usually while composing a message)
                                // AND the key is indeed an arrow (not undefined)
                                if (tagpro.disableControls && arrow) {

                                    // Prevent the 'default' thing to happen, which is the cursor moving through the message you are typing
                                    event.preventDefault();

                                    // Return if already pressed/released
                                    if (tagpro.players[tagpro.playerId].pressing[arrow] != releasing) return;

                                    // Send the key press/release to the server!
                                    tagpro.sendKeyPress(arrow, releasing);

                                    // Not necesarry, but useful for other scripts to 'hook onto'
                                    if (!releasing && tagpro.events.keyDown) tagpro.events.keyDown.forEach(f => f.keyDown(arrow));
                                    if (releasing && tagpro.events.keyUp) tagpro.events.keyUp.forEach(f => f.keyUp(arrow));
                                    if(tagpro.ping.avg)setTimeout(()=>(tagpro.players[tagpro.playerId][arrow]=!releasing),tagpro.ping.avg/2);
                                }
                            };
                        }
                    };

                    // intercept all key presses and releases:
                    document.addEventListener('keydown', tagpro.rollingChat.handler);
                    document.addEventListener('keyup', tagpro.rollingChat.handler);
                }

                if (enable) tagpro.rollingChat.enabled = true;
            },

            get enabled(){
                tpul.rollingChat._init();
                return tagpro.rollingChat.enabled;
            },

            set enabled(e){
                tpul.rollingChat._init( Boolean(e) );
                if (!e) console.warn('Disabling Rolling Chat! This will disable Rolling Chat for all scripts, not only yours! Please enable it again asap to not get users confused.');
            },
        },

        notify: function(text, type="message", timeout=Math.max(4000, 50*text.length) ){

            // Accepted types: message, success, error, warning
            //              (  white    green    red    yellow )
            // For more types, the only thing you need to add is some CSS

            var notification = document.createElement('div');
            notification.className = 'tpul-notification tpul-notification-' + type;
            notification.innerText = text;
            document.body.appendChild(notification);

            // Hide after a while (timeout)
            setTimeout(function(notification){
                if(notification)notification.classList.add('vanish');
            }, timeout, notification);

            // Hide on click
            notification.onclick = function(){ this.classList.add('vanish'); };

            // Clear up the DOM once the notification is vanished
            notification.addEventListener('transitionend',function(){ this.remove(); });

            // Return the element, for scripters to "play" with
            return notification;

        },

        groupcomm: {

            emit: function ( script, command, ...args ) {
                // Example: tpul.groupcomm.emit('gropro', 'desc', 'welcome to my awesome group')
                if (typeof tagpro != 'undefined' && tagpro.group && tagpro.group.socket) {

                    var full_command = "/" + [...arguments].map(a=>(a||"").replace(/([\^\/:;])/g,"^$1")).join("/") + ";";

                    tagpro.group.socket.emit( "touch", full_command.substr( 0,12 ) );
                    for (var i = 12; i < full_command.length; i += 11) {
                        tagpro.group.socket.emit( "touch", ":" + full_command.substr( i,11 ) );
                    }

                    tagpro.group.socket.emit( "touch", tagpro.group.socket.playerLocation );
                }
                else throw "Not connected to a group";
            },

            oncommand: function oncommand( callback ) {
                if (typeof tagpro != 'undefined' && tagpro.group && tagpro.group.socket) {
                    if (!tpul.groupcomm._active) tpul.groupcomm._init();
                    tpul.groupcomm._callbacks.push(callback);
                }
                else throw "Not connected to a group";
            },

            _callbacks: [],

            _commands: {},

            _active: false,

            _init: function (){

                if (tpul.groupcomm._active) return;

                tpul.groupcomm._active = true;

                tagpro.group.socket.on( "member", function(member) {

                    function handleCommand(command){
                        var args = [...command
                                    .replace(/\^(.)/g, "$1^")
                                    .match(/\/(.*);(?=(?:\^\^)*(?!\^))/,1)[1]
                                    .split(/\/(?=(?:\^\^)*(?!\^))/)
                                    .map(a=>a.replace(/(.)\^(?=(?:\^\^)*(?!\^))/g, "$1"))
                                   ];

                        for (var c in tpul.groupcomm._callbacks) {
                            var callback = tpul.groupcomm._callbacks[c];
                            try { callback({
                                member: member,
                                script: args.shift() || null,
                                command: args.shift() || null,
                                args: args,
                                raw:command } ); }
                            catch(e) {
                                console.error("Unhandled GroupComm error. Mod makers, handle your errors!", e);
                                tpul.groupcomm._callbacks.splice(c,1);
                            }
                        }
                    }

                    var raw = member.location,
                        commands = tpul.groupcomm._commands;

                    if (typeof raw !== "string") return;

                    // A full one-line command:  / ... ;
                    if ( raw.match(/^\/.*[^^];/) ) {
                        handleCommand( raw );
                        delete commands[member.id];
                    }

                    // The start of a multi-line command:  / ...
                    else if ( raw.match(/^\//) ) {
                        commands[member.id] = raw;
                    }

                    // The end of a multi-line command:  : ... ;
                    else if ( raw.match(/^:.*[^^];/) ) {
                        if (!commands[member.id]) throw "Did not receive start of command.";
                        var com = commands[member.id] + raw.slice(1);
                        handleCommand( com );
                        delete commands[member.id];
                    }

                    // A middle part of a multi-line command:  : ...
                    else if ( raw.match(/^:/) ) {
                        if (!commands[member.id]) throw "Did not receive start of command.";
                        commands[member.id] += raw.slice(1);
                    }

                    // Not a GroupComm command:
                    else delete commands[member.id];
                });
            }
        },

        chat: {

            emit: function(message, type='all'){
                // type: all/team/group/mod

                if (tpul.playerLocation != 'game') {
                    console.error( "TPUL wasn't able to send this chat, as we haven't joined a game", message )
                    throw "TPUL: can't send a chat when not in a game";
                }

                if (!tpul.noscript && tagpro.socket) {
                    // Method 1: Emit a message using the socket.
                    // Preferable since it has the most chance
                    // to work with other scripts.

                    if (type == 'group') tagpro.group && tagpro.group.socket.emit('chat', message);

                    else tagpro.socket.emit('chat',{
                        message:message,
                        toAll:type!='team',
                        asMod:type=='mod',
                    });

                } else if (typeof $ == 'function') {
                    // Method 2: Send a message "manually" (because no-script)
                    // This takes about 15ms on my computer (TagPro's chat function isn't really performant)
                    // So expect a frame or two to drop while sending a macro (still better than typing it out yourself though)
                    // On top of that, your input will be delayed by the same amount of time.
                    // Of course, THESE DELAYS WILL ONLY HAPPEN AT THE MOMENT YOU USE ONE OF YOUR MACROS!

                     // The chatbox and name input box
                    var chat = document.getElementById('chat'),
                        name = document.getElementById('name')

                    // Blur (unfocus) the 'name' input, in case you are in the middle of changing your name
                    if (name == document.activeElement) name.blur()

                    // Close the box in case it is already opened
                    if (chat.style.display != 'none' && chat.style.display != '' ) {
                        chat.value = ""
                        this._handler( { type: 'keydown', keyCode: 13, preventDefault: ()=>0 } )
                    }

                    // Trick the handler in opening the box
                    var keyCode = { team: 84, group: 71, mod: 77, all: 13 }[type]
                    this._handler( { type: 'keydown', keyCode: keyCode, preventDefault: ()=>0 } )

                    // Type out the message:
                    document.getElementById('chat').value = message;

                    // Trick the handler in closing the box and sending the message
                    this._handler( { type: 'keydown', keyCode: 13, preventDefault: ()=>0 } )

                } else console.error( "TPUL wasn't able to send the chat message", message )
            },

            get _handler(){ // Self-overwriting getter
                // Find the keydown handler that opens/closes the chat box
                delete this._handler;
                return this._handler = $._data( document, "events" ).keydown.find(
                    listener => listener.handler.toString().includes('tagpro.keys.chatToTeam')
                ).handler || function(){$(document).trigger(...arguments)};
            },
        },

        get playerLocation(){

            // This function seems to be pointless, but I'll make
            // sure it'll keep working even when the site architechture changes,
            // so that you don't have to update your script too often :)
            // (for example, when the SWJ got introduced)

            if ( location.pathname.startsWith('/games/find') ) return 'find';
            if ( location.pathname.match(/^\/groups\/[a-z]{8}$/) ) return 'group';
            var path = location.pathname.match(/\w+/);
            if (path) return path[0];
            if ( location.port ) return 'game';
            if ( location.pathname == '/' ) return 'home';

            throw 'Player location unknown';
        },

        /*events: {
            on: function(event, callback) {

                if( !tpul.events._listeners[event] ) tpul.events._listeners[event] = []
                tpul.events._listener.push(callback)

                //if (event in deepEvents) enableDeepEvents();
            },

            on: function(event, callback) {

                if (event == 'register') throw "You can't use 'register' as an event"

                if (typeof tagpro != 'undefined') {
                    if ( !tagpro.events ) tagpro.events = {}
                    if ( !tagpro.events[event] ) tagpro.events[event] = []


                if (!tagpro.events[event]) tagpro.events[event] = [];

                var eventFunc = {};
                eventFunc[event] = callback;
                tagpro.events[event].push(eventFunc);

                //if (event in deepEvents) enableDeepEvents();
            },

            emit: function(event, data) {

                if ( tpul.events._listeners[event] ) for (let callback of tpul.events._listeners[
                if (tagpro.events[event]) for (let listener of tagpro.events[event]) {
                    try { listener[event](data); }
                    catch (e) {
                        console.error("Unhandled tagpro.events.on('"+event+"') error. Mod makers, handle your errors!");
                        console.error(e);
                        console.error( listener[event]);
                    }
                }
            },

            _listeners: {}
        }*/
    };






    // =====DOM SECTION=====



    var SettingsMenu = document.getElementById('tpul-settings-menu') || document.createElement('div');
    SettingsMenu.id = 'tpul-settings-menu';

    var SettingsFrame = document.getElementsByClassName('tpul-settings-frame')[0] || document.createElement('div');
    SettingsFrame.className = 'tpul-settings-frame';
    if(tpul.playerLocation == 'game') SettingsFrame.classList.add('in-game');
    document.body.appendChild(SettingsFrame);


    if (!SettingsMenu.parentElement) tpul.settings.parent = null;

    // =====NOITCES MOD=====





    // OPENING AND CLOSING

    SettingsFrame.onclick = function(click) {

        // Close all settings when clicking outside the panel
        if (SettingsFrame == click.target) for (var settings of all_settings) settings.close();

    };

    SettingsFrame.addEventListener('wheel', function(wheel) {

        // Close all settings when scrolling up far enough
        setTimeout(function(){
            if (SettingsFrame.firstElementChild &&
                SettingsFrame.scrollTop + SettingsFrame.offsetHeight <= SettingsFrame.firstElementChild.offsetTop + 20)
                for (var settings of all_settings) settings.close();
        },200);

        // Open when scrolling down (only in game) DOESN'T WORK PROPERLY
        // if (tpul.playerLocation == 'game' && wheel.deltaY > 0 && last_opened && !last_opened.isOpen) last_opened.open();

        if (wheel.deltaY > 0) {
            // Hide the scrolldown arrow TODO
            for (var arrow of document.getElementsByClassName('tpul-settings-scroll-down-arrow')) {
                arrow.style.opacity = 0;
            }
        }
    })




    // Section tabs

    SettingsFrame.addEventListener('click', function(click) {
        var tablist = click.target.parentElement;
        if (tablist.classList.contains('tab-list')) {

            var scrollTop = SettingsFrame.scrollTop;
            console.log(scrollTop);

            for (let li of tablist.getElementsByTagName('li'))
                li.classList.remove('active');
            for (let pane of tablist.parentElement.getElementsByClassName('tab-pane'))
                pane.classList.remove('active');
            click.target.classList.add('active');
            document.querySelector(click.target.dataset.target).classList.add('active');

            SettingsFrame.scrollTop = scrollTop;
        }
    }, true);



    // Get settings from socket:

    if (typeof tagpro != 'undefined' && tagpro.ready) {
        tagpro.ready(function(){
            if (tagpro.socket && tagpro.socket.on) {
                tagpro.socket.on('settings', function(settings) {
                    // Don't try to tamper with this, or copy this in your own script.
                    // It will affect all scripts using TPUL.
                    tpul.profile.getSettings( {__settings:Object.assign(settings.ui, {stats: settings.stats})} );
                });
            }
        });
    }

    // Some helper function(s)

    function param(o){

        return Object.keys(o).map(function(k) {
            return encodeURIComponent(k) + '=' + encodeURIComponent(o[k.replace(' ','+')]);
        }).join('&').replace(/%20/g, '+');
    }




    // =====NOITCES CIGOL=====





    if (typeof tpul_promises == 'undefined') {
        try{
            window.tpul_promises = {};
            unsafeWindow.tpul_promises = window.tpul_promises;
        }catch(e){}
    }

    var tpul_common = {}

    if (typeof tpul_common == 'undefined') {
        if (window) window.tpul_common = tpul_common
        if (unsafeWindow) unsaeWindow.tpul_common = tpul_common
    }


    // If running independently (not @required by another script)
    // only good for modders or while debugging
    if (typeof GM_info == 'undefined' || GM_info.script.name == 'TagPro Userscript Library') {
        if (typeof tagpro != 'undefined') tagpro.tpul = tpul;
        if (window) window.tpul = tpul;
        if (unsafeWindow) unsafeWindow.tpul = tpul;
    }

    return tpul;
})();