Greasy Fork is available in English.

TagPro Userscript Library

Functions that any TagPro script could benefit from

נכון ליום 03-10-2018. ראה הגרסה האחרונה.

אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/371240/633764/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      3.1
// @license      MIT
// @include      *.koalabeast.com*
// @include      *.jukejuice.com*
// @include      *.newcompte.fr*
// @downloadURL  https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/tpul.lib.js
// @icon         https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/icon.png
// @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      3.0
// @license      MIT
// ==/UserLibrary==


var version = 3.0;
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

Option to change the layout of the settings

Show a "scroll down" reminder

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(){tagpro.disableControls = true;});
            this.node.addEventListener('blur', function(){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);

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;
}`);


    /*

    //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=====





    // =====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(location.port) SettingsFrame.classList.add('in-game');
    document.body.appendChild(SettingsFrame);


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





    // =====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;},

        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');

                            last_opened = settings;

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

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

                            SettingsFrame.removeEventListener('scroll', settings.scroll);

                            //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 (immediately && tagpro) {
                        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){

                // 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();
                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.');
                tagpro.rollingChat.enabled = Boolean(e);
            },
        },

        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 ) {
                if (tagpro && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {

                    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 (tagpro && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {
                    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];
                });
            }
        }
    };

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





    // 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('scroll', function(wheel) {

        // Open when scrolling down (only in game)
        if (location.port && wheel.deltaY > 0 && last_opened && !last_opened.isOpen) last_opened.open();

        // 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);
    });




    // 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 (tagpro && 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 functions

    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){}
    }


    return tpul;
})();