Better Chat

Userscript to improve bonk.io chat.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Better Chat
// @version      1.2.0
// @author       Apex, (apx)
// @namespace    https://greasyfork.org/users/1272759-apx
// @description  Userscript to improve bonk.io chat.
// @license      MIT
// @match        https://bonk.io/gameframe-release.html
// @match        https://bonkisback.io/gameframe-release.html
// @match        https://multiplayer.gg/physics/gameframe-release.html
// @require      https://cdnjs.cloudflare.com/ajax/libs/emojione/4.5.0/lib/js/emojione.min.js
// @run-at       document_end
// @grant        none
// ==/UserScript==


window.betterChat = {};

const userscriptName = 'bonk-chat';

const linkRegex = /https:\/\/[a-zA-Z0-9\/\._%-]{1,}(?:\?[a-zA-Z0-9\_\.%=&-]{1,})?/;
const chat = document.getElementById('newbonklobby_chat_content');
const chatBox = document.getElementById('newbonklobby_chatbox');
const chatInput = document.getElementById('newbonklobby_chat_input');
const ingameChat = document.getElementById('ingamechatcontent');
const ingameChatBox = document.getElementById('ingamechatbox');
const ingameChatInput = document.getElementById('ingamechatinputtext');

const ownEmojis = [];
const customEmojis = [];

let filter = true;

const addClass = (name, ingame = isIngame()) => ingame? 'bchat_msg_ingame' + name : 'bchat_msg_' + name;
const isIngame = () => document.getElementById('gamerenderer').style.visibility == 'inherit';
const isHost = () => !document.getElementById('newbonklobby_startbutton').classList.contains('brownButtonDisabled');

const copyToClipboard = (text) => {
    navigator.clipboard.writeText(text).catch(err => {
        console.error('Failed to copy text: ' + text, err);
    });
}

let modifiedKeydown = false;
let bonkCommandsDetected = false;
const onLobbyJoin = () => {
    generatedSkins = [];
    while (typeof customEmojis[0] != 'undefined')
        customEmojis.pop();
    closeContextMenu();
    helpContainer.showing = false;
    targetCommandId = -1;
    if (!isIngame()) {
        ingameChatBox.style.opacity = 0;
        ingameChat.style.visibility = 'hidden';
    }
    else {
        ingameChatBox.style.opacity = 0;
        ingameChat.style.visibility = 'hidden';
    }
    onReturnLobby();

    // for bonk commands compability
    if (!modifiedKeydown) {
        modifiedKeydown = true;
        if (window.Gwindow && typeof window.Gwindow.BonkCommandsScriptInjector == 'function') {
            bonkCommandsDetected = true;
            if (!window.chatCommands)
                window.chatCommands = [];
            let commands = Object.entries(window.Gwindow.adv_help)
            .filter( command => !/\W+/.test(command[0]) )
            .map( command => {
                return {
                    name: command[0],
                    description: command[1],
                    source: 'Bonk Commands',
                    callback: () => true
                };
            });
            window.chatCommands = window.chatCommands.concat(commands);

            let _commandhandle = window.Gwindow.commandhandle;
            window.Gwindow.commandhandle = function (chat_val) {
                let result = _commandhandle.apply(this, arguments);
                document.activeElement.value = result == ''? '' : chat_val;
                return '';
            }
            let _flag_manage = window.Gwindow.flag_manage;
            window.Gwindow.flag_manage = function (chat_val) {
                let result = _flag_manage.apply(this, arguments);
                document.activeElement.value = result == ''? '' : chat_val;
                return '';
            }
        }
        // modify onkeydown here because bcommands is ******
        _onkeydown = document.getElementById('newbonklobby_chat_input').onkeydown ?? new Function();
        _oningamekeydown = document.getElementById('ingamechatinputtext').onkeydown ?? new Function();
        document.getElementById('newbonklobby_chat_input').onkeydown = chatKeydown;
        document.getElementById('ingamechatinputtext').onkeydown = document.getElementById('newbonklobby_chat_input').onkeydown;

        _documentKeydown = document.onkeydown ?? new Function();
        document.onkeydown = DocumentKeydown;
    }
}
const settings = {
    autoShowImages: true,
    ingameImages: true,
    notify: true,
    maxMessages: 500,
    emojiLimit: 10,
    emojiNameMaxLength: 32,
    whiteList: [
        'https://upload.wikimedia.org/',
        'https://media.discordapp.net/attachments/',
        'https://i.pinimg.com/',
        //'https://avatars.mds.yandex.net/',
    ],
};
const CSS = document.createElement('style');
CSS.id = 'bonkChatUserscript';
CSS.innerHTML = `
#newbonklobby_chat_content {
   padding: 2px 4px;
}

/* LOBBY CONTENT & MESSAGE */
.bchat_content {
    position: relative;
    margin: 2px 0;
    scroll-margin-top: 60px;
}
.bchat_content:hover {
    background-color: rgba(0, 0, 0, 0.06);
}
.bchat_contentselected {
    background-color: rgba(13, 125, 120, 0.12) !important;
}
.bchat_msg_skinbox {
    position: absolute;
    user-select: none;
    margin-top: 2px;
}
.bchat_msg_href {
    font-family: "futurept_book";
    color: #0955c7;
    cursor: pointer;
    text-decoration: none;
}
.bchat_msg_href:hover {
    color: #5893ec;
}
.bchat_msg_name {
    font-family: "futurept_b1";
    color: #484848;
    font-size: 12px;
    margin-top: 0;
    display: block;
    margin-left: 40px;
    user-select: none;
}
.bchat_msg_txtcontainer {
    margin-left: 39px;
    min-height: 21px;
}
.bchat_msg_txt {
    font-family: "futurept_b1";
    color: #000000;
    font-size: 16px;
    word-break: break-all;
}
.bchat_msg_time {
    font-family: "futurept_b1";
    color: #999999;
    font-size: 9px;
    display: inline-block;
    user-select: none;
    bottom: .5px;
    position: relative;
    visibility: hidden;
}
.bchat_msg_reply {
    font-family: "futurept_b1";
    margin-bottom: 1px;
}
.bchat_msg_replyhref {
    color: #181818;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    opacity: 0.6;
}
.bchat_msg_replyhref:hover {
    opacity: 0.9;
}
.bchat_msg_replyarrow {
    display: inline-block;
    height: 7px;
    width: 20px;
    pointer-events: none;
    margin: 9px 0 -5px 17px;
    border-left: 2px solid #b3b3b3;
    border-top: 2px solid #b3b3b3;
    border-radius: 7px 0 0;
}
.bchat_msg_replyauthor {
    display: inline;
    color: #505050;
    pointer-events: none;
    font-size: 14px;
}
.bchat_msg_replytext {
    display: inline;
    pointer-events: none;
    font-size: 14px;
}
.bchat_msg_imagearea {
    padding: 4px 0;
    margin-left: 39px;
    display: block;
}
.bchat_msg_imagecontainer {
    position: relative;
    display: inline-block;
    max-width: 50%;
    max-height: fit-content;
}
.bchat_msg_image {
    max-width: 100%;
    max-height: 100px;
    border-radius: 10px;
    cursor: pointer;
}
.bchat_msg_imageshow {
    cursor: pointer;
    display: inline-block;
    position: relative;
    width: 20px;
    height: 20px;
    background-image: url("data:image/svg+xml,%3Csvg width='18px' height='18px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.0007 12C15.0007 13.6569 13.6576 15 12.0007 15C10.3439 15 9.00073 13.6569 9.00073 12C9.00073 10.3431 10.3439 9 12.0007 9C13.6576 9 15.0007 10.3431 15.0007 12Z' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M12.0012 5C7.52354 5 3.73326 7.94288 2.45898 12C3.73324 16.0571 7.52354 19 12.0012 19C16.4788 19 20.2691 16.0571 21.5434 12C20.2691 7.94291 16.4788 5 12.0012 5Z' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 1 1;
}
.bchat_msg_imageshow:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
.bchat_msg_imagehide {
    cursor: pointer;
    display: inline-block;
    position: absolute;
    width: 20px;
    height: 20px;
    background-image: url("data:image/svg+xml,%3Csvg width='18px' height='18px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99902 3L20.999 21M9.8433 9.91364C9.32066 10.4536 8.99902 11.1892 8.99902 12C8.99902 13.6569 10.3422 15 11.999 15C12.8215 15 13.5667 14.669 14.1086 14.133M6.49902 6.64715C4.59972 7.90034 3.15305 9.78394 2.45703 12C3.73128 16.0571 7.52159 19 11.9992 19C13.9881 19 15.8414 18.4194 17.3988 17.4184M10.999 5.04939C11.328 5.01673 11.6617 5 11.9992 5C16.4769 5 20.2672 7.94291 21.5414 12C21.2607 12.894 20.8577 13.7338 20.3522 14.5' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 1 1;
    background-color: rgba(0, 0, 0, 0.2);
    bottom: 5px;
    right: 5px;
    border-radius: 5px;
}
.bchat_msg_imagehide:hover {
    background-color: rgba(100, 100, 100, 0.2);
}

/* INGAME CONTENT & MESSAGE */
.bchat_ingamecontent {
    position: relative;
    margin: 2px 0;
}
.bchat_ingamecontent:hover {
    background-color: rgba(255, 255, 255, 0.06);
}
.bchat_ingamecontentselected {
    background-color: rgba(13, 125, 120, 0.12) !important;
}
.bchat_msg_ingamehref {
    color: #0955c7;
    cursor: pointer;
    text-decoration: none;
}
.bchat_msg_ingamehref:hover {
    color: #5893ec;
}
.bchat_msg_ingamename {
    display: inline;
    color: #e3e3e3a0;
    padding-left: 2px;
    user-select: none;
}
.bchat_msg_ingametxtcontainer {
    display: inline-block;
}
.bchat_msg_ingametxt {
    display: inline;
    color: #ffffffd6;
    user-select: text;
    word-break: break-all;
}
.bchat_msg_ingametime {
    font-size: 12px;
    color: #ffffff8d;
    user-select: none;
    visibility: hidden;
}
.bchat_msg_ingamereply {
    font-family: "futurept_b1";
    margin-bottom: 1px;
}
.bchat_msg_ingamereplyhref {
    font-family: "futurept_b1";
    color: #dddddd;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    opacity: 0.6;
}
.bchat_msg_ingamereplyhref:hover {
    opacity: 0.9;
}
.bchat_msg_ingamereplyarrow {
    display: inline-block;
    height: 7px;
    width: 20px;
    pointer-events: none;
    margin: 9px 0 -5px 5px;
    border-left: 2px solid #b3b3b3;
    border-top: 2px solid #b3b3b3;
    border-radius: 7px 0 0;
}
.bchat_msg_ingamereplyauthor {
    font-family: "futurept_b1";
    display: inline;
    color: #acacac;
    pointer-events: none;
    font-size: 14px;
}
.bchat_msg_ingamereplytext {
    display: inline;
    pointer-events: none;
    font-size: 14px;
}
.bchat_msg_ingamechatbutton {
    font-family: "futurept_b1";
    letter-spacing: 0.4px;
    text-align: center;
    cursor: pointer;
    height: 21px;
    color: white;
    margin-top: 5px;
    pointer-events: all;
    border: 1px solid;
    border-radius: 5px;
}
.bchat_msg_ingamechatbutton:hover {
    background-color: rgba(0, 0, 0, 0.4);
}
.bchat_msg_ingamechatbuttondisabled {
    background-color: rgba(209, 82, 82, 0.3);
    pointer-events: none !important;
}
.bchat_msg_ingameimagearea {
    padding: 4px 0 4px 2px;
    display: block;
}
.bchat_msg_ingameimagecontainer {
    position: relative;
    display: inline-block;
    max-width: 50%;
    max-height: fit-content;
}
.bchat_msg_ingameimage {
    max-width: 100%;
    max-height: 70px;
    border-radius: 10px;
    cursor: pointer;
}
.bchat_msg_ingameimageshow {
    cursor: pointer;
    display: inline-block;
    position: relative;
    width: 14px;
    height: 14px;
    background-image: url("data:image/svg+xml,%3Csvg width='12px' height='12px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.0007 12C15.0007 13.6569 13.6576 15 12.0007 15C10.3439 15 9.00073 13.6569 9.00073 12C9.00073 10.3431 10.3439 9 12.0007 9C13.6576 9 15.0007 10.3431 15.0007 12Z' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M12.0012 5C7.52354 5 3.73326 7.94288 2.45898 12C3.73324 16.0571 7.52354 19 12.0012 19C16.4788 19 20.2691 16.0571 21.5434 12C20.2691 7.94291 16.4788 5 12.0012 5Z' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 1 1;
    bottom: 3px;
}
.bchat_msg_ingameimageshow:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
.bchat_msg_ingameimagehide {
    cursor: pointer;
    display: inline-block;
    position: absolute;
    width: 18px;
    height: 18px;
    background-image: url("data:image/svg+xml,%3Csvg width='16px' height='16px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99902 3L20.999 21M9.8433 9.91364C9.32066 10.4536 8.99902 11.1892 8.99902 12C8.99902 13.6569 10.3422 15 11.999 15C12.8215 15 13.5667 14.669 14.1086 14.133M6.49902 6.64715C4.59972 7.90034 3.15305 9.78394 2.45703 12C3.73128 16.0571 7.52159 19 11.9992 19C13.9881 19 15.8414 18.4194 17.3988 17.4184M10.999 5.04939C11.328 5.01673 11.6617 5 11.9992 5C16.4769 5 20.2672 7.94291 21.5414 12C21.2607 12.894 20.8577 13.7338 20.3522 14.5' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 1 1;
    background-color: rgba(0, 0, 0, 0.2);
    bottom: 5px;
    right: 5px;
    border-radius: 5px;
}
.bchat_msg_ingameimagehide:hover {
    background-color: rgba(100, 100, 100, 0.2);
}

.bchat_ingamebutton {
    font-family: "futurept_b1";
    letter-spacing: 0.4px;
    text-align: center;
    cursor: pointer;
    color: white;
    margin-top: 5px;
    pointer-events: all;
    border: 1px solid;
    border-radius: 5px;
}
.bchat_ingamebutton:hover {
    background-color: rgba(0, 0, 0, 0.4);
}
.bchat_ingamebutton_disabled {
    background-color: rgba(209, 82, 82, 0.3);
    pointer-events: none !important;
}

/* CONTEXT MENU */
#bchat_contextmenu_menu {
    background-color: #b8cdd0;
    position: absolute;
    padding: 0 5px 5px 5px;
    border-radius: 5px;
    z-index: 99;
    box-shadow: 2px 3px 5px -2px rgb(0 0 0 / 63%);
    display: none;
}
.bchat_contextmenu_button {
    margin-top: 5px;
    width: 145px;
}
.bchat_contextmenu_overline {
    margin-top: 5px;
    width: 145px;
    height: 1px;
    background-color: #a5acb0;
}
#bchat_ingamecontextmenu_menu {
    background-color: rgba(0, 0, 0, 0.2);
    position: absolute;
    padding: 0 5px 5px 5px;
    border: 1px solid white;
    border-radius: 10px;
    z-index: 99;
    box-shadow: 2px 3px 5px -2px rgb(0 0 0 / 63%);
    display: none;
    backdrop-filter: blur(5px);
}
.bchat_ingamecontextmenu_overline {
    margin-top: 5px;
    width: 145px;
    height: 1px;
    background-color: white;
}

/* IMAGE PREVIEW */
#bchat_imagepreview_container {
    width: 100%;
    height: 100%;
    position: absolute;
    visibility: hidden;
    z-index: 2;
}
#bchat_imagepreview_behindblocker {
    width: 100%;
    height: 100%;
    position: absolute;
    background-color: rgba(0, 0, 0, 0.70);
}
.bchat_imagepreview_image {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    max-width: 80%;
    max-height: 80%;
    border-radius: 10px;
    cursor: pointer;
}
.bchat_imagepreview_button {
    cursor: pointer;
    position: absolute;
    top: 10px;
    width: 40px;
    height: 40px;
    background-repeat: no-repeat;
    background-position: 5 5;
    background-color: rgba(64, 64, 64, 0.33);
    border: 2px solid rgba(102, 102, 102, 0.33);
    border-radius: 30px;
}
.bchat_imagepreview_button:hover {
    background-color: rgba(90, 90, 90, 0.33);
}
#bchat_imagepreview_close {
    right: 10px;
    background-image: url("data:image/svg+xml,%3Csvg width='30px' height='30px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.7457 3.32851C20.3552 2.93798 19.722 2.93798 19.3315 3.32851L12.0371 10.6229L4.74275 3.32851C4.35223 2.93798 3.71906 2.93798 3.32854 3.32851C2.93801 3.71903 2.93801 4.3522 3.32854 4.74272L10.6229 12.0371L3.32856 19.3314C2.93803 19.722 2.93803 20.3551 3.32856 20.7457C3.71908 21.1362 4.35225 21.1362 4.74277 20.7457L12.0371 13.4513L19.3315 20.7457C19.722 21.1362 20.3552 21.1362 20.7457 20.7457C21.1362 20.3551 21.1362 19.722 20.7457 19.3315L13.4513 12.0371L20.7457 4.74272C21.1362 4.3522 21.1362 3.71903 20.7457 3.32851Z' fill='%23888'/%3E%3C/svg%3E");
}
#bchat_imagepreview_tab {
    right: 60px;
    background-image: url("data:image/svg+xml,%3Csvg width='30px' height='30px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 4C11.4477 4 11 3.55228 11 3C11 2.44772 11.4477 2 12 2L20 2C21.1046 2 22 2.89543 22 4V12C22 12.5523 21.5523 13 21 13C20.4477 13 20 12.5523 20 12V5.39343L3.72798 21.6655C3.33746 22.056 2.70429 22.056 2.31377 21.6655C1.92324 21.2749 1.92324 20.6418 2.31377 20.2512L18.565 4L12 4Z' fill='%23888'/%3E%3C/svg%3E");
}
#bchat_imagepreview_link {
    right: 110px;
    background-image: url("data:image/svg+xml,%3Csvg width='30px' height='30px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.2218 3.32234C15.3697 1.17445 18.8521 1.17445 21 3.32234C23.1479 5.47022 23.1479 8.95263 21 11.1005L17.4645 14.636C15.3166 16.7839 11.8342 16.7839 9.6863 14.636C9.48752 14.4373 9.30713 14.2271 9.14514 14.0075C8.90318 13.6796 8.97098 13.2301 9.25914 12.9419C9.73221 12.4688 10.5662 12.6561 11.0245 13.1435C11.0494 13.1699 11.0747 13.196 11.1005 13.2218C12.4673 14.5887 14.6834 14.5887 16.0503 13.2218L19.5858 9.6863C20.9526 8.31947 20.9526 6.10339 19.5858 4.73655C18.219 3.36972 16.0029 3.36972 14.636 4.73655L13.5754 5.79721C13.1849 6.18774 12.5517 6.18774 12.1612 5.79721C11.7706 5.40669 11.7706 4.77352 12.1612 4.383L13.2218 3.32234Z' fill='%23888'/%3E%3Cpath d='M6.85787 9.6863C8.90184 7.64233 12.2261 7.60094 14.3494 9.42268C14.7319 9.75083 14.7008 10.3287 14.3444 10.685C13.9253 11.1041 13.2317 11.0404 12.7416 10.707C11.398 9.79292 9.48593 9.88667 8.27209 11.1005L4.73655 14.636C3.36972 16.0029 3.36972 18.219 4.73655 19.5858C6.10339 20.9526 8.31947 20.9526 9.6863 19.5858L10.747 18.5251C11.1375 18.1346 11.7706 18.1346 12.1612 18.5251C12.5517 18.9157 12.5517 19.5488 12.1612 19.9394L11.1005 21C8.95263 23.1479 5.47022 23.1479 3.32234 21C1.17445 18.8521 1.17445 15.3697 3.32234 13.2218L6.85787 9.6863Z' fill='%23888'/%3E%3C/svg%3E");
}
#bchat_imagepreview_infocontainer {
    position: absolute;
    max-width: 50%;
    top: 10px;
    left: 10px;
    color: #dbdbdb;
    font-family: "futurept_b1";
    text-shadow: 1px 1px 3px black;
}

/* HELP CONTAINER */
.bchat_help_skinimage {
    vertical-align: middle;
    margin: 0 5px 0 -5px;
}
.bchat_helpcontainer {
    bottom: 26px;
    position: absolute;
    width: calc(100% - 2px);
    max-height: 142px;
    overflow-y: scroll;
    background-color: rgba(207, 216, 220, 0.9);
    left: 0;
    right: 0;
    margin: auto;
    border-radius: 10px 10px 5px 5px;
    border-bottom: 0;
    box-shadow: 1px 1px 5px -2px rgba(0, 0, 0, 0.63);
    backdrop-filter: blur(1px);
}
.bchat_helpcontainer::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
.bchat_helpcontainer::-webkit-scrollbar-track {
    background-color: transparent;
    margin-top: 5px;
}
.bchat_helpcontainer::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.bchat_helptable tr:nth-last-child(n + 2) {
	border-bottom: 1px solid #c2ccd1;
}
.bchat_helptable tr:hover {
    background-color: rgba(100,100,100,0.10);
}
.bchat_helpcontainer_optionleft {
    padding: 5px;
    padding-left: 10px;
}
.bchat_helpcontainer_optionright {
    padding-right: 10px;
    text-align: right;
    color: #4e4e4e;
    width: 35%;
}
.bchat_helpcontainer_optiondesc {
    display: block;
    color: #4e4e4e;
    font-size: 12px;
    pointer-events: none;
}

.bchat_ingamehelpcontainer {
    bottom: 26px;
    position: absolute;
    width: calc(98% - 1px);
    max-height: 142px;
    overflow-y: scroll;
    background-color: rgba(0, 0, 0, 0.2);
    left: 7px;
    border-radius: 10px 10px 5px 5px;
    border-bottom: 0;
    box-shadow: 4px 1.5px 5px -4px rgb(0 0 0 / 63%);
    backdrop-filter: blur(5px);
    height: fit-content;
    pointer-events: all;
}
.bchat_ingamehelpcontainer::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
.bchat_ingamehelpcontainer::-webkit-scrollbar-track {
    background-color: transparent;
    margin-top: 5px;
}
.bchat_ingamehelpcontainer::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.bchat_ingamehelptable tr:nth-last-child(n + 2) {
	border-bottom: 1px solid white;
}
.bchat_ingamehelptable tr:hover {
    background-color: rgba(0, 0, 0, 0.2);
}
.bchat_ingamehelpcontainer_optionleft {
    color: white;
    padding: 5px;
    padding-left: 10px;
}
.bchat_ingamehelpcontainer_optionright {
    padding-right: 10px;
    text-align: right;
    color: #e3e3e3a0;
    width: 35%;
}
.bchat_ingamehelpcontainer_optiondesc {
    display: block;
    color: #e3e3e3a0;
    font-size: 12px;
    pointer-events: none;
}

.bchat_helptable, .bchat_ingamehelptable {
    border-collapse: collapse;
    font-family: "futurept_b1";
    width: 97%;
    margin-left: 2.5%;
}
.bchat_helptable tr, .bchat_ingamehelptable tr {
    cursor: pointer;
    font-size: 14px;
    height: 30px;
}

/* COMMAND BAR */
#bchat_commandbar {
    bottom: 26px;
    position: absolute;
    width: calc(97% - 12px);
    height: max-content;
    background-color: rgb(190 198 201 / 90%);
    left: 0;
    right: 0;
    margin: auto;
    border-radius: 10px 10px 0 0;
    box-shadow: 1px 1px 5px -2px rgba(0, 0, 0, 0.63);
    font-family: "futurept_b1";
    line-height: 30px;
    padding: 0 6px;
}
#bchat_commandbar_top {
    font-size: 14px;
    height: 16px;
    line-height: 21px;
    opacity: 0.7;
    width: 100%;
}
.bchat_commandbar_text {
    height: 21px;
    display: inline;
    word-break: break-all;
}
#bchat_ingamecommandbar {
    bottom: 28px;
    position: absolute;
    width: calc(98% - 12px);
    height: max-content;
    background-color: rgba(0, 0, 0, 0.25);
    backdrop-filter: blur(2px);
    left: 5px;
    right: 0;
    margin: auto;
    border-radius: 10px;
    font-family: "futurept_b1";
    line-height: 30px;
    padding: 0 6px;
}


#pretty_bottom {
    display: none;
}
#ingamechatcontent {
    pointer-events: all;
    overflow-y: unset !important;
    max-height: unset !important;
    height: unset !important;
    margin-right: 2px;
    position: static;
}
#ingamechatbox {
    height: 152px !important;
    overflow: visible;
}

#bchat_ingamechatscroll {
    overflow-y: scroll;
    max-height: 125px;
    visibility: hidden;
}

#ingamechatcontent,
#bchat_ingamechatscroll:hover,
#bchat_ingamechatscroll:focus {
  visibility: visible;
}

#bchat_ingamechatscroll::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
#bchat_ingamechatscroll::-webkit-scrollbar-track {
    background-color: transparent;
}
#bchat_ingamechatscroll::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.ingamechatname, .ingamechatmessage {
    user-select: text;
}
.newbonklobby_chat_msg_txt_emojiscaled {
    max-width: 42px !important;
}

/* emojicreate */
#emojicreateContainer {
    width: 370px;
    height: fit-content;
    background-color: #cfd8dc;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    visibility: hidden;
    border-radius: 7px;
    pointer-events: auto;
    outline: 3000px solid rgba(0, 0, 0, 0.30);
    font-family: futurept_b1;
}
#emojicreate_canvas {
    display: none;
    position: relative;
    box-shadow: 1px 1px 5px -2px rgba(0,0,0,0.63);
    cursor: grab;
}
#emojicreate_capture {
    position: absolute;
    width: 50px;
    height: 50px;
    outline: 3000px solid rgb(0 0 0 / 0.35);
    display: none;
}
#emojicreate_nameinput {
    font-size: 13px;
    background: #fdfdfd;
    border: 1px solid #bdbdbd;
    color: #4e4e4e;
    width: 120px;
    height: 23px;
    padding-right: 4px;
    display: inline-block;
    margin-top: 1px;
    box-shadow: 1px 1px 4px -2px rgba(0,0,0,0.4);
    margin-bottom: 50px;
}
#emojicreate_buttoncontainer {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    width: 90%;
    margin: auto;
    position: absolute;
    bottom: 12px;
    left: 0;
    right: 0;
}
.emojicreate_bottombuttons {
    display: inline-block;
    height: 35px;
    line-height: 35px;
    flex-basis: 85px;
    flex-grow: 0.2;
}
#newbonklobby_chat_emojimenubutton {
    cursor: pointer;
    position: absolute;
    width: 20px;
    height: 18px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18px' height='18px' viewBox='0 0 512 512'%3E%3Cpath fill='%2365656550' d='M256 48a208 208 0 1 0 0 416a208 208 0 1 0 0-416m256 208a256 256 0 1 1-512 0a256 256 0 1 1 512 0m-367.6-48a32 32 0 1 1 64 0a32 32 0 1 1-64 0m192-32a32 32 0 1 1 0 64a32 32 0 1 1 0-64'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 1 0;
    bottom: 4px;
    right: 3px;
    /*border-left: 1px solid #a5acb0;*/
}
#newbonklobby_chat_emojimenu {
    outline: 1px solid #a5acb0;
    /* cursor: pointer; */
    position: absolute;
    width: 306px;
    height: 190px;
    bottom: 26px;
    right: 3px;
    background-color: c4cdd0;
    border-radius: 8px 8px 0;
}/*
#newbonklobby_chat_emojimenu_left {
    height: 100%;
    width: 36px;
    background-color: rgb(100 100 100 / 0.1);
}
#newbonklobby_chat_input {
    right: 21px;
    width: 91%;
}*/

`;
document.getElementsByTagName('head')[0].appendChild(CSS);
/* SKIN IMAGE GENERATOR */
avatarRenderer = PIXI.autoDetectRenderer({
    width: 36,
    height: 36,
    antialias: true,
    transparent: true
});

let generatedSkins = [];
let generatedSkins24 = [];

// stripped-down version of 'E.createImage'
const createChatSkinImage = function (avatar, element, className, size, skinId, storage, scale = 1, shadowOffset, shadowAlpha) {
    var vars = [];
    const avatarSize = size * window.devicePixelRatio * scale;
    const renderer = avatarRenderer;
    if (renderer.width != avatarSize || renderer.height != avatarSize) {
        renderer.resize(avatarSize, avatarSize);
    }

    function render() {
        renderer.render(container);
        const image = renderer.extract.image();
        image.style.width = size + 'px';
        image.style.height = size + 'px';
        while (element.getElementsByTagName('img')[0]) {
            element.removeChild(element.getElementsByTagName('img')[0]);
        }
        element.appendChild(image);
        if (className != '') {
            image.classList.add(className);
        }
        if (storage) {
            if (!storage[skinId]) {
                storage[skinId] = [];
            }
            storage[skinId] = image;
        }
        for (let i = 0; i < vars[57].length; i++) {
            vars[57][i].destroy(true);
        }
        for (let i = 0; i < vars[35].length; i++) {
            vars[35][i].destroy(true);
        }
        for (let i = 0; i < vars[19].length; i++) {
            vars[19][i].destroy(true);
            vars[19][i].dispose(true);
        }
    }
    const container = new PIXI.Container();
    const layersGraphics = new PIXI.Graphics();
    const mask = new PIXI.Graphics();
    vars[66] = 15 * (avatarSize / 36);

    layersGraphics.beginFill(avatar.bc);
    layersGraphics.drawCircle(0, 0, vars[66]);
    layersGraphics.endFill();
    mask.beginFill(0xffffff);
    mask.drawCircle(0, 0, vars[66]);
    mask.endFill();
    if (shadowAlpha > 0) {
        vars[31] = new PIXI.Graphics();
        vars[31].beginFill(0x000000);
        vars[31].alpha = shadowAlpha;
        vars[20] = vars[66] * shadowOffset;
        vars[31].drawCircle(vars[20], vars[20], vars[66]);
        vars[31].x = avatarSize / 2;
        vars[31].y = avatarSize / 2;
        container.addChild(vars[31]);
    }
    vars[64] = [];
    vars[19] = [];
    vars[35] = [];
    vars[57] = [];
    for (let layerID = avatar.layers.length - 1; layerID >= 0; layerID--) {
        const layer = avatar.layers[layerID];
        layer.scale = Math.abs(layer.scale);
        if (!(layer.id >= 0 && layer.id <= 115) || Math.abs(layer.x) > 9999 || Math.abs(layer.y) > 9999 || layer.scale > 999 || layer.angle > 9999 || isNaN(layer.color) || typeof layer.color != "number" || typeof layer.flipX != "boolean" || typeof layer.flipY != "boolean") {
            continue;
        }
        vars[24] = vars[66] / 15;
        vars[52] = layer.scale * vars[24];
        if (vars[52] < 0.08) {
            continue;
        }
        vars[64][layerID] = false;
        vars[87] = layer.id.toString();
        if (layer.id < 10) {
            vars[87] = "0" + vars[87];
        }
        if (layer.id < 100) {
            vars[87] = "0" + vars[87];
        }
        vars[13] = 16;
        vars[58] = 1.0;
        if (vars[52] > vars[13]) {
            vars[58] = vars[52] / vars[13];
            vars[52] = vars[13];
        }
        vars[68] = new PIXI.resources.SVGResource(GameResources.svgStrings[layer.id], {
            scale: vars[52],
            autoload: false
        });
        vars[19].push(vars[68]);
        vars[68].load();
        vars[65] = PIXI.Texture.from(vars[68]);
        vars[35].push(vars[65]);
        vars[65].baseTexture.layerID = layerID;
        vars[65].baseTexture.on("loaded", function() {
            vars[64][this.layerID] = true;
            if (vars[64].indexOf(false) == -1) {
                render();
            }
        });
        vars[16] = new PIXI.Sprite(vars[65]);
        vars[57].push(vars[16]);
        vars[16].tint = layer.color;
        vars[16].anchor.set(0.5);
        vars[16].angle = layer.angle;
        vars[16].x = layer.x * vars[24];
        vars[16].y = layer.y * vars[24];
        vars[15] = layer.flipX ? -1 : 1;
        vars[50] = layer.flipY ? -1 : 1;
        vars[16].scale.x = vars[16].scale.y = vars[58];
        vars[16].scale.x *= vars[15];
        vars[16].scale.y *= vars[50];
        layersGraphics.addChild(vars[16]);
    }
    vars[34] = new PIXI.Container();
    vars[34].addChild(layersGraphics);
    vars[34].addChild(mask);
    layersGraphics.mask = mask;

    vars[34].x = avatarSize / 2;
    vars[34].y = avatarSize / 2;
    container.addChild(vars[34]);
    if (vars[64].length == 0) {
        render();
    }
};

/* CURSING FILTER */
const charReplacements = [
    ["@", "a"],
    ["0", "o"],
    ["1", "i"],
    ["2", "r"],
    ["3", "e"],
    ["4", "a"],
    ["5", "s"],
    ["7", "t"],
    ["8", "b"],
    ["9", "g"],
    ["ä", "a"],
    ["ã", "a"],
    ["â", "a"],
    ["ä", "a"],
    ["á", "a"],
    ["à", "a"],
    ["å", "a"],
    ["é", "e"],
    ["è", "e"],
    ["ë", "e"],
    ["ê", "e"],
    ["§", "s"],
    ["$", "s"],
    ["£", "l"],
    ["€", "e"],
    ["ü", "u"],
    ["û", "u"],
    ["ú", "u"],
    ["ù", "u"],
    ["î", "i"],
    ["ï", "i"],
    ["í", "i"],
    ["ì", "i"],
    ["ÿ", "y"],
    ["ý", "y"],
    ["ö", "o"],
    ["ô", "o"],
    ["õ", "o"],
    ["ó", "o"],
    ["ò", "o"]
];
const wab = ["nigger", "nigga", "cunt", "coon", "fag", "faggot", "rape", "negro", "nig nog", "nignog", "asshole", "homo", "bastard", "slut", "cock", "fuck", "bitch", "pussy", "whore", "shit", "anus", "bollocks", "ballsack", "ball sack", "suck my", "tits", "clit", "dick", "fecal", "feltch", "masturbate", "wank", "pedo", "paedo", "pedofile", "pedophile", "paedophile", "paedofile", "pegging", "penis", "piss", "poof", "quim", "rectum", "scat", "jizz", "spunk", "sperm", "schlong", "shlong", "shemale", "smut", "splooge", "strapon", "strap on", "dildo", "tosser", "tushy", "twat", "vagina", "wank", "white power", "씨발", "ㅆㅂ", "개새", "십새", "섹스", "개자식", "미친새끼", "미친년", "뻑큐", "뻐큐", "쌍년", "쌍놈", "쌍넘", "쉑", "지랄", "창녀", "지럴", "좁밥", "좆", "좃까", "섹", "병신", "부랄", "ㅄ", "ㅂㅅ", "ㅈㄹ", "ㄳㄲ", "ㄱㅅㄲ", "불알", "븅", "새꺄", "새갸"];

const profanityCheck = string => {
    string = string.toLowerCase();

    for (let ind = 0; ind < charReplacements.length; ind++) {
        let char = charReplacements[ind];
        let chars = new RegExp(char[0], 'g');
        string = string.replace(chars, char[1]);
    }

    for (let ind = 0; ind < wab.length; ind++) {
        let start = string.indexOf(wab[ind]);
        if (start != -1) {
            return {
                found: true,
                findindex: start,
                continuefrom: start + wab[ind].length,
                length: wab[ind].length
            };
        }
    }
    return {
        found: false
    };
};

const sanitizeString = string => {
    let profanity = null;
    do {
        profanity = profanityCheck(string);
        if (profanity.found) {
            string = string.substring(0, profanity.findindex) + 'bonk' + string.substring(profanity.continuefrom);
        }
    } while (profanity.found);
    return string;
};

/* BYTEBUFFER */
const textDec = new window.TextDecoder();
const textEnc = new window.TextEncoder();
class Bytebuffer {
    static registerClassAlias(q7f, f7j) {
        var P$t = [arguments];
        Bytebuffer.aliases[P$t[0][0]] = P$t[0][1];
        return P$t[0][0];
    }
    constructor() {
        var X6_ = [arguments];
        this.index = 0;
        this.buffer = new ArrayBuffer(102400);
        this.view = new DataView(this.buffer);
        this.implicitClassAliasArray = [];
        this.implicitStringArray = [];
        this.bodgeCaptureZoneDataIdentifierArray = [];
    }
    reset() {
        var k40 = [arguments];
        this.index = 0;
    }
    readByte() {
        var A0m = [arguments];
        A0m[5] = this.view.getUint8(this.index);
        this.index += 1;
        return A0m[5];
    }
    writeByte(L6c) {
        var W_N = [arguments];
        this.view.setUint8(this.index, W_N[0][0]);
        this.index += 1;
    }
    readInt() {
        var t$x = [arguments];
        t$x[9] = this.view.getInt32(this.index);
        this.index += 4;
        return t$x[9];
    }
    writeInt(I9J) {
        var J6N = [arguments];
        this.view.setInt32(this.index, J6N[0][0]);
        this.index += 4;
    }
    readShort() {
        var i7d = [arguments];
        i7d[9] = this.view.getInt16(this.index);
        this.index += 2;
        return i7d[9];
    }
    writeShort(O8E) {
        var p$b = [arguments];
        this.view.setInt16(this.index, p$b[0][0]);
        this.index += 2;
    }
    readUShort() {
        var L9V = [arguments];
        L9V[2] = this.view.getUint16(this.index);
        this.index += 2;
        return L9V[2];
    }
    writeUShort(p6O) {
        var q38 = [arguments];
        this.view.setUint16(this.index, q38[0][0]);
        this.index += 2;
    }
    readUint() {
        var g1d = [arguments];
        g1d[2] = this.view.getUint32(this.index);
        this.index += 4;
        return g1d[2];
    }
    writeUint(q4y) {
        var U47 = [arguments];
        this.view.setUint32(this.index, U47[0][0]);
        this.index += 4;
    }
    rewind() {
        var L0b = [arguments];
        this.index = 0;
    }
    readInt29() {
        var E3j = [arguments];
        E3j[2] = 1;
        E3j[8] = this.readByte();
        E3j[6] = 0;
        E3j[4] = 0;
        E3j[5] = 0;
        if (E3j[8] & 0b10000000) {
            E3j[6] = this.readByte();
            E3j[2] = 2;
            if (E3j[6] & 0b10000000) {
                E3j[4] = this.readByte();
                E3j[2] = 3;
                if (E3j[4] & 0b10000000) {
                    E3j[5] = this.readByte();
                    E3j[2] = 4;
                }
            }
        }
        E3j[3] = 0;
        if (E3j[2] == 1) {
            E3j[3] += (E3j[8] & 0b00000001) << 0;
            E3j[3] += (E3j[8] & 0b00000010) << 0;
            E3j[3] += (E3j[8] & 0b00000100) << 0;
            E3j[3] += (E3j[8] & 0b00001000) << 0;
            E3j[3] += (E3j[8] & 0b00010000) << 0;
            E3j[3] += (E3j[8] & 0b00100000) << 0;
            E3j[3] += (E3j[8] & 0b01000000) << 0;
        }
        if (E3j[2] == 2) {
            E3j[3] += (E3j[8] & 0b00000001) << 7;
            E3j[3] += (E3j[8] & 0b00000010) << 7;
            E3j[3] += (E3j[8] & 0b00000100) << 7;
            E3j[3] += (E3j[8] & 0b00001000) << 7;
            E3j[3] += (E3j[8] & 0b00010000) << 7;
            E3j[3] += (E3j[8] & 0b00100000) << 7;
            E3j[3] += (E3j[8] & 0b01000000) << 7;
            E3j[3] += (E3j[6] & 0b00000001) << 0;
            E3j[3] += (E3j[6] & 0b00000010) << 0;
            E3j[3] += (E3j[6] & 0b00000100) << 0;
            E3j[3] += (E3j[6] & 0b00001000) << 0;
            E3j[3] += (E3j[6] & 0b00010000) << 0;
            E3j[3] += (E3j[6] & 0b00100000) << 0;
            E3j[3] += (E3j[6] & 0b01000000) << 0;
        }
        if (E3j[2] == 3) {
            E3j[3] += (E3j[8] & 0b00000001) << 14;
            E3j[3] += (E3j[8] & 0b00000010) << 14;
            E3j[3] += (E3j[8] & 0b00000100) << 14;
            E3j[3] += (E3j[8] & 0b00001000) << 14;
            E3j[3] += (E3j[8] & 0b00010000) << 14;
            E3j[3] += (E3j[8] & 0b00100000) << 14;
            E3j[3] += (E3j[8] & 0b01000000) << 14;
            E3j[3] += (E3j[6] & 0b00000001) << 7;
            E3j[3] += (E3j[6] & 0b00000010) << 7;
            E3j[3] += (E3j[6] & 0b00000100) << 7;
            E3j[3] += (E3j[6] & 0b00001000) << 7;
            E3j[3] += (E3j[6] & 0b00010000) << 7;
            E3j[3] += (E3j[6] & 0b00100000) << 7;
            E3j[3] += (E3j[6] & 0b01000000) << 7;
            E3j[3] += (E3j[4] & 0b00000001) << 0;
            E3j[3] += (E3j[4] & 0b00000010) << 0;
            E3j[3] += (E3j[4] & 0b00000100) << 0;
            E3j[3] += (E3j[4] & 0b00001000) << 0;
            E3j[3] += (E3j[4] & 0b00010000) << 0;
            E3j[3] += (E3j[4] & 0b00100000) << 0;
            E3j[3] += (E3j[4] & 0b01000000) << 0;
        }
        if (E3j[2] == 4) {
            E3j[3] += (E3j[8] & 0b00000001) << 22;
            E3j[3] += (E3j[8] & 0b00000010) << 22;
            E3j[3] += (E3j[8] & 0b00000100) << 22;
            E3j[3] += (E3j[8] & 0b00001000) << 22;
            E3j[3] += (E3j[8] & 0b00010000) << 22;
            E3j[3] += (E3j[8] & 0b00100000) << 22;
            E3j[3] -= (E3j[8] & 0b01000000) << 22;
            E3j[3] += (E3j[6] & 0b00000001) << 15;
            E3j[3] += (E3j[6] & 0b00000010) << 15;
            E3j[3] += (E3j[6] & 0b00000100) << 15;
            E3j[3] += (E3j[6] & 0b00001000) << 15;
            E3j[3] += (E3j[6] & 0b00010000) << 15;
            E3j[3] += (E3j[6] & 0b00100000) << 15;
            E3j[3] += (E3j[6] & 0b01000000) << 15;
            E3j[3] += (E3j[4] & 0b00000001) << 8;
            E3j[3] += (E3j[4] & 0b00000010) << 8;
            E3j[3] += (E3j[4] & 0b00000100) << 8;
            E3j[3] += (E3j[4] & 0b00001000) << 8;
            E3j[3] += (E3j[4] & 0b00010000) << 8;
            E3j[3] += (E3j[4] & 0b00100000) << 8;
            E3j[3] += (E3j[4] & 0b01000000) << 8;
            E3j[3] += (E3j[5] & 0b00000001) << 0;
            E3j[3] += (E3j[5] & 0b00000010) << 0;
            E3j[3] += (E3j[5] & 0b00000100) << 0;
            E3j[3] += (E3j[5] & 0b00001000) << 0;
            E3j[3] += (E3j[5] & 0b00010000) << 0;
            E3j[3] += (E3j[5] & 0b00100000) << 0;
            E3j[3] += (E3j[5] & 0b01000000) << 0;
            E3j[3] += (E3j[5] & 0b10000000) << 0;
        }
        return E3j[3];
    }
    readBoolean() {
        var H29 = [arguments];
        H29[5] = this.readByte();
        return H29[5] == 1;
    }
    writeBoolean(M3r) {
        var w28 = [arguments];
        if (w28[0][0]) {
            this.writeByte(1);
        } else {
            this.writeByte(0);
        }
    }
    readDouble() {
        var r_4 = [arguments];
        r_4[8] = this.view.getFloat64(this.index);
        this.index += 8;
        return r_4[8];
    }
    writeDouble(s8H) {
        var b6a = [arguments];
        this.view.setFloat64(this.index, b6a[0][0]);
        this.index += 8;
    }
    readFloat() {
        var F9g = [arguments];
        F9g[6] = this.view.getFloat32(this.index);
        this.index += 4;
        return F9g[6];
    }
    writeFloat(W2A) {
        var f4P = [arguments];
        this.view.setFloat32(this.index, f4P[0][0]);
        this.index += 4;
    }
    readUTF() {
        var s_d = [arguments];
        s_d[3] = this.readByte();
        s_d[6] = this.readByte();
        s_d[2] = s_d[3] * 256 + s_d[6];
        s_d[9] = new Uint8Array(s_d[2]);
        for (s_d[4] = 0; s_d[4] < s_d[2]; s_d[4]++) {
            s_d[9][s_d[4]] = this.readByte();
        }
        return textDec.decode(s_d[9]);
    }
    writeUTF(E_0) {
        var P0R = [arguments];
        P0R[5] = textEnc.encode(P0R[0][0]);
        P0R[9] = P0R[5].length;
        P0R[4] = Math.floor(P0R[9] / 256);
        P0R[6] = P0R[9] % 256;
        this.writeByte(P0R[4]);
        this.writeByte(P0R[6]);
        P0R[3] = this;
        P0R[5].forEach(T2O);

        function T2O(h5c, W2N, o1S) {
            var d93 = [arguments];
            P0R[3].writeByte(d93[0][0]);
        }
    }
    readObject() {
        var r9_ = [arguments];
        r9_[8] = () => {
            var  l0W, g0A, r9h, B7G, U7D, B04, y7Z, w9I, v2u;
            l0W = this.readByte();
            if (l0W == 0x07) {
                g0A = this.readByte();
                r9h = (g0A - 1) / 2;
                B7G = new Uint8Array(r9h);
                for (var Q5Y = 0; Q5Y < r9h; Q5Y++) {
                    B7G[Q5Y] = this.readByte();
                }
                U7D = textDec.decode(B7G);
                if (!Bytebuffer.aliases[U7D]) {
                    throw new Error("trying to decode object with alias we don't recognise");
                }
                this.implicitClassAliasArray.push(U7D);
                B04 = new Bytebuffer.aliases[U7D]();
                B04.readExternal(this);
                return B04;
            } else {
                y7Z = (l0W - 1) / 4;
                w9I = this.implicitClassAliasArray[y7Z];
                if (!Bytebuffer.aliases[w9I]) {
                    throw new Error("trying to decode object with alias we don't recognise");
                }
                v2u = new Bytebuffer.aliases[w9I]();
                v2u.readExternal(this);
                return v2u;
            }
        };
        r9_[3] = () => {
            var  g9D, c5a, t6V, Z9I, j6T, K5w, Q8A, d_A, K4c, l1Y, E2K, V8V, I4$, P8w, I8b, T$s, o0W, W4R, E25, W7p, j9i, E0M, H62, L6K;
            g9D = 0;
            c5a = 0;
            t6V = [];
            do {
                var F7n = 1;
                var a7E = 2;
                c5a = (this.readByte() - F7n) / a7E;
                g9D += c5a;
            } while (c5a == 64);
            Z9I = this.readByte();
            for (var n7y = 0; n7y < g9D; n7y++) {
                j6T = this.readByte();
                if (j6T === Bytebuffer.T_UNDEFINED) {
                    t6V.push(undefined);
                }
                if (j6T === Bytebuffer.T_NULL) {
                    t6V.push(null);
                }
                if (j6T === Bytebuffer.T_TRUE) {
                    t6V.push(true);
                }
                if (j6T === Bytebuffer.T_FALSE) {
                    t6V.push(false);
                }
                if (j6T === Bytebuffer.T_OBJ) {
                    K5w = this.readByte();
                    Q8A = "";
                    if (K5w == 7) {
                        d_A = this.readByte();
                        K4c = (d_A - 1) / 2;
                        l1Y = new Uint8Array(K4c);
                        for (var k$g = 0; k$g < K4c; k$g++) {
                            l1Y[k$g] = this.readByte();
                        }
                        Q8A = textDec.decode(l1Y);
                        this.implicitClassAliasArray.push(Q8A);
                        if (!Bytebuffer.aliases[Q8A]) {
                            throw new Error("trying to decode object with alias we don't recognise");
                        }
                        E2K = new Bytebuffer.aliases[Q8A]();
                        E2K.readExternal(this);
                        t6V.push(E2K);
                    } else if (K5w > 128) {
                        V8V = this.readByte();
                        I4$ = this.readByte();
                        P8w = (I4$ - 1) / 2;
                        I8b = new Uint8Array(P8w);
                        for (var t69 = 0; t69 < P8w; t69++) {
                            I8b[t69] = this.readByte();
                        }
                        Q8A = textDec.decode(I8b);
                        this.implicitClassAliasArray.push(Q8A);
                        if (!Bytebuffer.aliases[Q8A]) {
                            throw new Error("trying to decode object with alias we don't recognise");
                        }
                        T$s = new Bytebuffer.aliases[Q8A]();
                        T$s.readAnonymous(this);
                        t6V.push(T$s);
                    } else {
                        o0W = (K5w - 1) / 4;
                        Q8A = this.implicitClassAliasArray[o0W];
                        if (!Bytebuffer.aliases[Q8A]) {
                            throw new Error("trying to decode object with alias we don't recognise");
                        }
                        W4R = new Bytebuffer.aliases[Q8A]();
                        W4R.readExternal(this);
                        t6V.push(W4R);
                    }
                }
                if (j6T === Bytebuffer.T_ARRAY) {}
                if (j6T === Bytebuffer.T_STRING) {
                    E25 = this.readByte();
                    if (E25 % 2 == 0) {
                        W7p = E25 / 2;
                        t6V.push(this.implicitStringArray[W7p]);
                    } else {
                        j9i = 0;
                        E0M = (E25 - 1) / 2;
                        j9i += E0M;
                        while (E0M == 64) {
                            var Q0G = 1;
                            var P7o = 2;
                            E0M = (this.readByte() - Q0G) / P7o;
                            j9i += E0M;
                        }
                        H62 = new Uint8Array(j9i);
                        for (var W1g = 0; W1g < j9i; W1g++) {
                            H62[W1g] = this.readByte();
                        }
                        L6K = textDec.decode(H62);
                        t6V.push(L6K);
                        this.implicitStringArray.push(L6K);
                    }
                }
            }
            return t6V;
        };
        r9_[1] = this.readByte();
        if (r9_[1] == Bytebuffer.T_NULL) {
            return null;
        }
        if (r9_[1] == Bytebuffer.T_UNDEFINED) {
            return undefined;
        }
        if (r9_[1] == Bytebuffer.T_OBJ) {
            return (1, r9_[8])();
        } else if (r9_[1] == Bytebuffer.T_ARRAY) {
            return (1, r9_[3])();
        } else {
            throw new Error("Trying to readObject on something that's not an object or array");
        }
    }
    toBase64() {
        var c9l = [arguments];
        c9l[5] = "";
        c9l[7] = new Uint8Array(this.buffer);
        c9l[9] = this.index;
        for (c9l[2] = 0; c9l[2] < c9l[9]; c9l[2]++) {
            c9l[5] += String.fromCharCode(c9l[7][c9l[2]]);
        }
        return window.btoa(c9l[5]);
    }
    fromBase64(e_4, d25) {
        var g$e = [arguments];
        g$e[4] = window.pako;
        g$e[3] = window.atob(g$e[0][0]);
        g$e[2] = g$e[3].length;
        g$e[9] = new Uint8Array(g$e[2]);
        for (g$e[6] = 0; g$e[6] < g$e[2]; g$e[6]++) {
            g$e[9][g$e[6]] = g$e[3].charCodeAt(g$e[6]);
        }
        if (g$e[0][1] === true) {
            g$e[8] = g$e[4].inflate(g$e[9]);
            g$e[9] = g$e[8];
        }
        this.buffer = g$e[9].buffer.slice(g$e[9].byteOffset, g$e[9].byteLength + g$e[9].byteOffset);
        this.view = new DataView(this.buffer);
        this.index = 0;
    }
}

/* UNSERIALIZE CONTROLS */
const unserialize = encoded => {
    let result = {};
    try {
        const buffer = new Bytebuffer();
        buffer.fromBase64(encoded, false);
        let  version = buffer.readUShort();
        if (version >= 1) {
            result.up1 = buffer.readUShort();
            result.up2 = buffer.readUShort();
            result.down1 = buffer.readUShort();
            result.down2 = buffer.readUShort();
            result.left1 = buffer.readUShort();
            result.left2 = buffer.readUShort();
            result.right1 = buffer.readUShort();
            result.right2 = buffer.readUShort();
            result.heavy1 = buffer.readUShort();
            result.heavy2 = buffer.readUShort();
            result.swing1 = buffer.readUShort();
            result.swing2 = buffer.readUShort();
        }
        if (version >= 2) {
            result.filter = buffer.readBoolean();
        }
        if (version >= 3) {
            result.stats = buffer.readBoolean();
        }
        if (version >= 3 && version <= 5) {
            let idk = buffer.readBoolean();
            if (idk) {
                result.quality = 3;
            } else {
                result.quality = 2;
            }
        }
        if (version >= 4) {
            result.help = buffer.readBoolean();
        }
        if (version >= 5) {
            result.up3 = buffer.readUShort();
            result.down3 = buffer.readUShort();
            result.left3 = buffer.readUShort();
            result.right3 = buffer.readUShort();
            result.heavy3 = buffer.readUShort();
            result.swing3 = buffer.readUShort();
        }
        if (version >= 6) {
            result.quality = buffer.readUShort();
        }
    } catch (e) {}
    return result;
}

const serialize = a => {
    var S5r = [arguments];
    S5r[5] = new Bytebuffer();
    S5r[5].writeUShort(a.version);
    S5r[5].writeUShort(a.up1);
    S5r[5].writeUShort(a.up2);
    S5r[5].writeUShort(a.down1);
    S5r[5].writeUShort(a.down2);
    S5r[5].writeUShort(a.left1);
    S5r[5].writeUShort(a.left2);
    S5r[5].writeUShort(a.right1);
    S5r[5].writeUShort(a.right2);
    S5r[5].writeUShort(a.heavy1);
    S5r[5].writeUShort(a.heavy2);
    S5r[5].writeUShort(a.swing1);
    S5r[5].writeUShort(a.swing2);
    S5r[5].writeBoolean(a.filter);
    S5r[5].writeBoolean(a.stats);
    S5r[5].writeBoolean(a.help);
    S5r[5].writeUShort(a.up3);
    S5r[5].writeUShort(a.down3);
    S5r[5].writeUShort(a.left3);
    S5r[5].writeUShort(a.right3);
    S5r[5].writeUShort(a.heavy3);
    S5r[5].writeUShort(a.swing3);
    S5r[5].writeUShort(a.quality);
    return S5r[5].toBase64();
}



/* MAP DECODER */
decodeFromDatabase = function (map) {
    b64mapdata = LZString.decompressFromEncodedURIComponent(map);
    binaryReader = new Bytebuffer();
    binaryReader.fromBase64(b64mapdata, false);
    map = {
        v: 1,
        s: { re: false, nc: false, pq: 1, gd: 25, fl: false },
        physics: { shapes: [], fixtures: [], bodies: [], bro: [], joints: [], ppm: 12 },
        spawns: [],
        capZones: [],
        m: {
            a: "noauthor",
            n: "noname",
            dbv: 2,
            dbid: -1,
            authid: -1,
            date: "",
            rxid: 0,
            rxn: "",
            rxa: "",
            rxdb: 1,
            cr: [],
            pub: false,
            mo: "",
        },
    };
    map.v = binaryReader.readShort();
    if (map.v > 15) {
        throw new Error("Future map version, please refresh page");
    }
    map.s.re = binaryReader.readBoolean();
    map.s.nc = binaryReader.readBoolean();
    if (map.v >= 3) {
        map.s.pq = binaryReader.readShort();
    }
    if (map.v >= 4 && map.v <= 12) {
        map.s.gd = binaryReader.readShort();
    } else if (map.v >= 13) {
        map.s.gd = binaryReader.readFloat();
    }
    if (map.v >= 9) {
        map.s.fl = binaryReader.readBoolean();
    }
    map.m.rxn = binaryReader.readUTF();
    map.m.rxa = binaryReader.readUTF();
    map.m.rxid = binaryReader.readUint();
    map.m.rxdb = binaryReader.readShort();
    map.m.n = binaryReader.readUTF();
    map.m.a = binaryReader.readUTF();
    if (map.v >= 10) {
        map.m.vu = binaryReader.readUint();
        map.m.vd = binaryReader.readUint();
    }
    if (map.v >= 4) {
        let crLength = binaryReader.readShort();
        for (let contributorId = 0; contributorId < crLength; contributorId++)
            map.m.cr.push(binaryReader.readUTF());
    }
    if (map.v >= 5) {
        map.m.mo = binaryReader.readUTF();
        map.m.dbid = binaryReader.readInt();
    }
    if (map.v >= 7) {
        map.m.pub = binaryReader.readBoolean();
    }
    if (map.v >= 8) {
        map.m.dbv = binaryReader.readInt();
    }
    map.physics.ppm = binaryReader.readShort();
    let broLength = binaryReader.readShort();
    for (let bodyId = 0; bodyId < broLength; bodyId++)
        map.physics.bro[bodyId] = binaryReader.readShort();

    let shapesLength = binaryReader.readShort();
    for (let shapeId = 0; shapeId < shapesLength; shapeId++) {
        let shapeType = binaryReader.readShort();
        if (shapeType == 1) {
            map.physics.shapes[shapeId] = { type: "bx", w: 10, h: 40, c: [0, 0], a: 0.0, sk: false };
            map.physics.shapes[shapeId].w = binaryReader.readDouble();
            map.physics.shapes[shapeId].h = binaryReader.readDouble();
            map.physics.shapes[shapeId].c = [binaryReader.readDouble(), binaryReader.readDouble()];
            map.physics.shapes[shapeId].a = binaryReader.readDouble();
            map.physics.shapes[shapeId].sk = binaryReader.readBoolean();
        }
        if (shapeType == 2) {
            map.physics.shapes[shapeId] = { type: "ci", r: 25, c: [0, 0], sk: false };
            map.physics.shapes[shapeId].r = binaryReader.readDouble();
            map.physics.shapes[shapeId].c = [binaryReader.readDouble(), binaryReader.readDouble()];
            map.physics.shapes[shapeId].sk = binaryReader.readBoolean();
        }
        if (shapeType == 3) {
            map.physics.shapes[shapeId] = { type: "po", v: [], s: 1, a: 0, c: [0, 0] };
            map.physics.shapes[shapeId].s = binaryReader.readDouble();
            map.physics.shapes[shapeId].a = binaryReader.readDouble();
            map.physics.shapes[shapeId].c = [binaryReader.readDouble(), binaryReader.readDouble()];
            let verticesLength = binaryReader.readShort();
            map.physics.shapes[shapeId].v = [];
            for (vertice = 0; vertice < verticesLength; vertice++) {
                map.physics.shapes[shapeId].v.push([binaryReader.readDouble(), binaryReader.readDouble()]);
            }
        }
    }
    let fixturesLength = binaryReader.readShort();
    for (let fixtureId = 0; fixtureId < fixturesLength; fixtureId++) {
        map.physics.fixtures[fixtureId] = {
            sh: 0,
            n: "Def Fix",
            fr: 0.3,
            fp: null,
            re: 0.8,
            de: 0.3,
            f: 0x4f7cac,
            d: false,
            np: false,
            ng: false,
        };
        map.physics.fixtures[fixtureId].sh = binaryReader.readShort();
        map.physics.fixtures[fixtureId].n = binaryReader.readUTF();
        map.physics.fixtures[fixtureId].fr = binaryReader.readDouble();
        if (map.physics.fixtures[fixtureId].fr == Number.MAX_VALUE) {
            map.physics.fixtures[fixtureId].fr = null;
        }
        let fricPlayers = binaryReader.readShort();
        if (fricPlayers == 0) {
            map.physics.fixtures[fixtureId].fp = null;
        }
        if (fricPlayers == 1) {
            map.physics.fixtures[fixtureId].fp = false;
        }
        if (fricPlayers == 2) {
            map.physics.fixtures[fixtureId].fp = true;
        }
        map.physics.fixtures[fixtureId].re = binaryReader.readDouble();
        if (map.physics.fixtures[fixtureId].re == Number.MAX_VALUE) {
            map.physics.fixtures[fixtureId].re = null;
        }
        map.physics.fixtures[fixtureId].de = binaryReader.readDouble();
        if (map.physics.fixtures[fixtureId].de == Number.MAX_VALUE) {
            map.physics.fixtures[fixtureId].de = null;
        }
        map.physics.fixtures[fixtureId].f = binaryReader.readUint();
        map.physics.fixtures[fixtureId].d = binaryReader.readBoolean();
        map.physics.fixtures[fixtureId].np = binaryReader.readBoolean();
        if (map.v >= 11) {
            map.physics.fixtures[fixtureId].ng = binaryReader.readBoolean();
        }
        if (map.v >= 12) {
            map.physics.fixtures[fixtureId].ig = binaryReader.readBoolean();
        }
    }
    let bodiesLength = binaryReader.readShort();
    for (let bodyId = 0; bodyId < bodiesLength; bodyId++) {
        map.physics.bodies[bodyId] = {
            p: [0, 0],
            a: 0,
            lv: [0, 0],
            av: 0,
            cf: {
                x: 0,
                y: 0,
                w: true,
                ct: 0
            },
            fx: [],
            fz: {
                on: false,
                x: 0,
                y: 0,
                d: true,
                p: true,
                a: true,
                t: 0,
                cf: 0
            },
            s: {
                type: "s",
                n: "Unnamed",
                fric: 0.3,
                fricp: false,
                re: 0.8,
                de: 0.3,
                ld: 0,
                ad: 0,
                fr: false,
                bu: false,
                f_c: 1,
                f_p: true,
                f_1: true,
                f_2: true,
                f_3: true,
                f_4: true
            }
        };
        map.physics.bodies[bodyId].s.type = binaryReader.readUTF();
        map.physics.bodies[bodyId].s.n = binaryReader.readUTF();
        map.physics.bodies[bodyId].p = [binaryReader.readDouble(), binaryReader.readDouble()];
        map.physics.bodies[bodyId].a = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.fric = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.fricp = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.re = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.de = binaryReader.readDouble();
        map.physics.bodies[bodyId].lv = [binaryReader.readDouble(), binaryReader.readDouble()];
        map.physics.bodies[bodyId].av = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.ld = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.ad = binaryReader.readDouble();
        map.physics.bodies[bodyId].s.fr = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.bu = binaryReader.readBoolean();
        map.physics.bodies[bodyId].cf.x = binaryReader.readDouble();
        map.physics.bodies[bodyId].cf.y = binaryReader.readDouble();
        map.physics.bodies[bodyId].cf.ct = binaryReader.readDouble();
        map.physics.bodies[bodyId].cf.w = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.f_c = binaryReader.readShort();
        map.physics.bodies[bodyId].s.f_1 = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.f_2 = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.f_3 = binaryReader.readBoolean();
        map.physics.bodies[bodyId].s.f_4 = binaryReader.readBoolean();
        if (map.v >= 2) {
            map.physics.bodies[bodyId].s.f_p = binaryReader.readBoolean();
        }
        if (map.v >= 14) {
            map.physics.bodies[bodyId].fz.on = binaryReader.readBoolean();
            if (map.physics.bodies[bodyId].fz.on) {
                map.physics.bodies[bodyId].fz.x = binaryReader.readDouble();
                map.physics.bodies[bodyId].fz.y = binaryReader.readDouble();
                map.physics.bodies[bodyId].fz.d = binaryReader.readBoolean();
                map.physics.bodies[bodyId].fz.p = binaryReader.readBoolean();
                map.physics.bodies[bodyId].fz.a = binaryReader.readBoolean();
                if (map.v >= 15) {
                    map.physics.bodies[bodyId].fz.t = binaryReader.readShort();
                    map.physics.bodies[bodyId].fz.cf = binaryReader.readDouble();
                }
            }
        }
        let fixturesLength = binaryReader.readShort();
        for (let fixtureId = 0; fixtureId < fixturesLength; fixtureId++) {
            map.physics.bodies[bodyId].fx.push(binaryReader.readShort());
        }
    }
    let spawnsLength = binaryReader.readShort();
    for (spawnId = 0; spawnId < spawnsLength; spawnId++) {
        map.spawns[spawnId] = {
            x: 400,
            y: 300,
            xv: 0,
            yv: 0,
            priority: 5,
            r: true,
            f: true,
            b: true,
            gr: false,
            ye: false,
            n: "Spawn",
        };
        spawn = map.spawns[spawnId];
        spawn.x = binaryReader.readDouble();
        spawn.y = binaryReader.readDouble();
        spawn.xv = binaryReader.readDouble();
        spawn.yv = binaryReader.readDouble();
        spawn.priority = binaryReader.readShort();
        spawn.r = binaryReader.readBoolean();
        spawn.f = binaryReader.readBoolean();
        spawn.b = binaryReader.readBoolean();
        spawn.gr = binaryReader.readBoolean();
        spawn.ye = binaryReader.readBoolean();
        spawn.n = binaryReader.readUTF();
    }
    let capZonesLength = binaryReader.readShort();
    for (capZoneId = 0; capZoneId < capZonesLength; capZoneId++) {
        map.capZones[capZoneId] = { n: "Cap Zone", ty: 1, l: 10, i: -1 };
        map.capZones[capZoneId].n = binaryReader.readUTF();
        map.capZones[capZoneId].l = binaryReader.readDouble();
        map.capZones[capZoneId].i = binaryReader.readShort();
        if (map.v >= 6) {
            map.capZones[capZoneId].ty = binaryReader.readShort();
        }
    }
    let jointsLength = binaryReader.readShort();
    for (jointId = 0; jointId < jointsLength; jointId++) {
        let jointType = binaryReader.readShort();
        if (jointType == 1) {
            map.physics.joints[jointId] = {
                type: "rv",
                d: { la: 0, ua: 0, mmt: 0, ms: 0, el: false, em: false, cc: false, bf: 0, dl: true },
                aa: [0, 0],
            };
            joint = map.physics.joints[jointId];
            joint.d.la = binaryReader.readDouble();
            joint.d.ua = binaryReader.readDouble();
            joint.d.mmt = binaryReader.readDouble();
            joint.d.ms = binaryReader.readDouble();
            joint.d.el = binaryReader.readBoolean();
            joint.d.em = binaryReader.readBoolean();
            joint.aa = [binaryReader.readDouble(), binaryReader.readDouble()];
        }
        if (jointType == 2) {
            map.physics.joints[jointId] = {
                type: "d",
                d: { fh: 0, dr: 0, cc: false, bf: 0, dl: true },
                aa: [0, 0],
                ab: [0, 0],
            };
            joint = map.physics.joints[jointId];
            joint.d.fh = binaryReader.readDouble();
            joint.d.dr = binaryReader.readDouble();
            joint.aa = [binaryReader.readDouble(), binaryReader.readDouble()];
            joint.ab = [binaryReader.readDouble(), binaryReader.readDouble()];
        }
        if (jointType == 3) {
            map.physics.joints[jointId] = {
                type: "lpj",
                d: { cc: false, bf: 0, dl: true },
                pax: 0,
                pay: 0,
                pa: 0,
                pf: 0,
                pl: 0,
                pu: 0,
                plen: 0,
                pms: 0,
            };
            joint = map.physics.joints[jointId];
            joint.pax = binaryReader.readDouble();
            joint.pay = binaryReader.readDouble();
            joint.pa = binaryReader.readDouble();
            joint.pf = binaryReader.readDouble();
            joint.pl = binaryReader.readDouble();
            joint.pu = binaryReader.readDouble();
            joint.plen = binaryReader.readDouble();
            joint.pms = binaryReader.readDouble();
        }
        if (jointType == 4) {
            map.physics.joints[jointId] = {
                type: "lsj",
                d: { cc: false, bf: 0, dl: true },
                sax: 0,
                say: 0,
                sf: 0,
                slen: 0,
            };
            joint = map.physics.joints[jointId];
            joint.sax = binaryReader.readDouble();
            joint.say = binaryReader.readDouble();
            joint.sf = binaryReader.readDouble();
            joint.slen = binaryReader.readDouble();
        }
        if (jointType == 5) {
            map.physics.joints[jointId] = { type: "g", n: "", ja: -1, jb: -1, r: 1 };
            joint = map.physics.joints[jointId];
            joint.n = binaryReader.readUTF();
            joint.ja = binaryReader.readShort();
            joint.jb = binaryReader.readShort();
            joint.r = binaryReader.readDouble();
        }
        if (jointType != 5) {
            map.physics.joints[jointId].ba = binaryReader.readShort();
            map.physics.joints[jointId].bb = binaryReader.readShort();
            map.physics.joints[jointId].d.cc = binaryReader.readBoolean();
            map.physics.joints[jointId].d.bf = binaryReader.readDouble();
            map.physics.joints[jointId].d.dl = binaryReader.readBoolean();
        }
    }
    return map;
};
/*
{
    name: 'test',
    description: 'just test',
    // Optional. It won't affect the result but it's for more information for users
    parameters: ['"parameter"'],
    source: 'Better Chat',
    // Optional. If getArgsString is true, the first argument in callback will be entire input without command in the begining
    getArgsString: true,
    // Optional. If hostOnly is true, the command won't show in helpContainer
    hostOnly: true,
    // Optional. The difference between `condition: () => false` is that it still can be processed
    unlisted: true,
    // Optional.
    condition: function () {
        // if false is returned, the command won't show in helpContainer
    }
    callback: function (param) {
        // your code stuff
    }
    // if callback returns true, Better Chat will let other scripts handle it too
}

// Example command:
{
    name: 'ping',
    description: 'Replies with pong',
    source: 'My Own Script',
    callback: function (param) {
        window.betterChat.showStatusMessage('Pong!');
    }
}
*/


if(!window.chatCommands) window.chatCommands = [];
window.chatCommands = window.chatCommands.concat([
    {
        name: 'help',
        description: 'Get started',
        source: 'Better Chat',
        callback: () => {
            window.chatCommands.forEach( cmd => {
                if(cmd.source == 'built-in' && (cmd.hostOnly? hostId == selfId : true)) {
                    showStatusMessage(`/${cmd.name}${cmd.parameters? ' ' + cmd.parameters.join(' ') : ''}`);
                }
            })
            showStatusMessage('* All aviable commands are listed above');
        }
    },
    {
        name: 'eval',
        description: 'Get started',
        source: 'Better Chat',
        getArgsString: true,
        unlisted: true,
        callback: (args) => {
            let val = '';
            try {
                val = String(eval(args));
                console.log(val);
            } catch (err) {
                val = err;
                console.error(err);
            }
            showStatusMessage('* ' + val);
        }
    },

    {
        name: 'clear',
        description: 'Clears chat history',
        source: 'bonk.io',
        callback: () => {
            while (chat.firstChild) {
                chat.removeChild(chat.firstChild);
            }
            while (ingameChat.firstChild) {
                ingameChat.removeChild(ingameChat.firstChild);
            }
        }
    },
    {
        name: 'balance',
        parameters: ['"user name"', '-100 to 100'],
        description: 'Balances a specific player',
        hostOnly: true,
        source: 'bonk.io',
        callback: (userName, balance) => {
            balance = parseInt(balance);
            if (balance < -100 || balance > 100) {
                showStatusMessage('* Buff/nerf amount must be between -100 and 100');
                return;
            }
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());

            if (id == -1) {
                showStatusMessage(`* Balance failed, username ${userName} not found in this room`);
                return;
            } else {
                if (balance == 0)
                    showStatusMessage(`* ${userName} has had their buff/nerf reset`);
                else
                    showStatusMessage(`* ${userName} has had their buff/nerf set to ${balance}`);
                send(29, {sid: id, bal: balance});
                receive(36, id, balance);
            }
        }
    },
    {
        name: 'roomname',
        parameters: ['"New Room Name"'],
        description: 'Sets new room name',
        hostOnly: true,
        getArgsString: true,
        source: 'bonk.io',
        callback: (name) => {
            if (name.charAt(0) == "\"") {
                name = name.substring(1);
            }
            if (name.slice(-1) == "\"") {
                name = name.substring(0, name.length - 1);
            }
            if (!name) {
                D2n("* After /roomname you must type the new desired room name", "#b53030", false);
                return;
            }
            send(52, {newName: name});
        }
    },
    {
        name: 'roompassword',
        parameters: ['"New Room Name"'],
        description: 'Sets new room password',
        hostOnly: true,
        getArgsString: true,
        source: 'bonk.io',
        callback: (password) => {
            if (password.charAt(0) == "\"") {
                password = password.substring(1);
            }
            if (password.slice(-1) == "\"") {
                password = password.substring(0, password.length - 1);
            }
            if (!password) {
                D2n("* After /roompass you must type the new desired password", "#b53030", false);
                return;
            }
            send(53, {newPass: password});
        }
    },
    {
        name: 'clearroompassword',
        description: 'Clears room password',
        hostOnly: true,
        source: 'bonk.io',
        callback: () => {
            send(53, {newPass: ''});
        }
    },
    {
        name: 'kick',
        parameters: ['"user name"'],
        description: 'Kicks a specific player, player can join the same room after being kicked',
        hostOnly: true,
        source: 'bonk.io',
        callback: (userName) => {
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());
            if (id == -1) {
                showStatusMessage(`* Kick failed, username ${userName} not found in this room`);
                return;
            } else {
                send(9, {banshortid: id, kickonly: true});
            }
        }
    },
    {
        name: 'ban',
        parameters: ['"user name"'],
        description: 'Bans a specific player',
        hostOnly: true,
        source: 'bonk.io',
        callback: () => {
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());
            if (id == -1) {
                showStatusMessage(`* Ban failed, username ${userName} not found in this room`);
                return;
            } else {
                send(9, {banshortid: id});
            }
        }
    },
    {
        name: 'mute',
        parameters: ['"user name"'],
        description: 'Mutes a specific player, wont be muted for everyone',
        source: 'bonk.io',
        callback: (userName) => {
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());
            if (id == -1) {
                showStatusMessage('* Mute failed, username ${userName} not found in this room');
                return;
            } else if (id == selfId) {
                showStatusMessage('* Can\'t mute yourself!');
            }
            playerList[id].mute = true;
            showStatusMessage('* Muted ' + userName);
        }
    },
    {
        name: 'unmute',
        parameters: ['"user name"'],
        description: 'Unmutes a specific player, wont be unmuted for everyone',
        source: 'bonk.io',
        callback: () => {
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());
            if (id == -1) {
                showStatusMessage('* Unmute failed, username ${userName} not found in this room');
                return;
            } else if (id == selfId) {
                showStatusMessage('* Can\'t mute yourself!');
            }
            playerList[id].mute = false;
            showStatusMessage('* Unmuted ' + userName);
        }
    },
    {
        name: 'move',
        parameters: ['"user name"', '[ffa|spectate|red|blue|green|yellow]'],
        description: 'Moves the player to ffa, spec, red, blue, green, yellow',
        hostOnly: true,
        source: 'bonk.io',
        callback: (userName, playerTeam) => {
            let id = playerList.findIndex(player => player.userName.toLowerCase() == userName.toLowerCase());
            if (id == -1) {
                showStatusMessage(`* Move failed, username ${userName} not found in this room`);
                return;
            } else if (!playerTeam) {
                showStatusMessage('* Move failed, usage /move "username" [team]. Team can be ffa, spectate, red, blue, green, yellow');
                return;
            } else {
                playerTeam = playerTeam.toLowerCase();
                let teamId = ['spectate', 'ffa', 'red', 'blue', 'green', 'yellow'].findIndex( team => team.startsWith(playerTeam));
                if (teamId == -1)
                    showStatusMessage('* Move failed, usage /move "username" [team]. Team can be ffa, spectate, red, blue, green, yellow');
                else {
                    send(26, {targetID: id, targetTeam: teamId});
                    receive(18, id, teamId);
                }
            }
        }
    },
    {
        name: 'lock',
        description: 'Locks teams',
        hostOnly: true,
        source: 'bonk.io',
        callback: () => {
            send(7, {teamLock: true});
        }
    },
    {
        name: 'unlock',
        description: 'Unlocks teams',
        hostOnly: true,
        source: 'bonk.io',
        callback: () => {
            send(7, {teamLock: false});
        }
    },
    {
        name: 'fav',
        description: 'Favourites current map',
        source: 'bonk.io',
        callback: () => {
            if (playerList[selfId].guest == false && map && map.m.dbv == 2 && map.m.dbid > 0) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', 'https://bonk2.io/scripts/map_fave.php', true);
                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

                xhr.onreadystatechange = () => {
                    if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                        let response = JSON.parse(xhr.responseText);
                        if (response.r == 'success') {
                            showStatusMessage('* Map added to favourites', null, true);
                        } else {
                            if (response.e == 'map_unpublished') {
                                showStatusMessage('* Couldn\'t favourite map because it isn\'t public', null, true);
                            } else if (response.e == 'already_faved') {
                                showStatusMessage('* This map is already in your favourites!', null, true);
                            } else {
                                showStatusMessage('* Couldn\'t favourite, something went wrong', null, true);
                            }
                        }
                    }
                };
                xhr.onerror = () => {
                    showStatusMessage('* Couldn\'t favourite, something went wrong', null, true);
                };

                xhr.send(`token=${token}&mapid=${map.m.dbid}&action=f`);
            } else {
                showStatusMessage('* You must be logged in and the map must be a Bonk 2 map', null, true);
            }
        }
    },
    {
        name: 'unfav',
        description: 'Unfavourites current map',
        source: 'bonk.io',
        callback: () => {
            if (playerList[selfId].guest == false && map && map.m.dbv == 2 && map.m.dbid > 0) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', 'https://bonk2.io/scripts/map_fave.php', true);
                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

                xhr.onreadystatechange = () => {
                    if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                        let response = JSON.parse(xhr.responseText);
                        if (response.r == 'success') {
                            showStatusMessage('* Map removed from favourites', null, true);
                        } else {
                            if (response.e == 'map_unpublished') {
                                showStatusMessage('* Couldn\'t unfavourite map because it isn\'t public', null, true);
                            } else if (response.e == 'already_faved') {
                                showStatusMessage('* This map isn\'t in your favourites!', null, true);
                            } else {
                                showStatusMessage('* Couldn\'t unfavourite, something went wrong', null, true);
                            }
                        }
                    }
                };
                xhr.onerror = () => {
                    showStatusMessage('* Couldn\'t unfavourite, something went wrong', null, true);
                };

                xhr.send(`token=${token}&mapid=${map.m.dbid}&action=u`);
            } else {
                showStatusMessage('* You must be logged in and the map must be a Bonk 2 map', null, true);
            }
        }
    },
    {
        name: 'curate',
        parameters: ['"comment" (optional)'],
        description: 'Curates current map',
        getArgsString: true,
        source: 'bonk.io',
        unlisted: true,
        callback: (comment = '') => {
            if (!map || map.m.dbid == -1) {
                showStatusMessage('* Failed: This map hasn\'t been saved!', '#6033cc', true);
                return;
            }
            curateMapComment = comment.replaceAll(/ +/g, ' ');
            curateMapDBID = map.m.dbid;
            curateMapDBV = map.m.dbv;
            let text = `* Curating ${map.m.n} by ${map.m.a}`;
            if (comment.length > 0) {
                text += ` with comment: "${curateMapComment}".`;
            } else {
                text += ' with no comment.';
            }
            text += ' Please double check then type /curateyes or /curateno';
            showStatusMessage(text, '#6033cc', true);
        }
    },
    {
        name: 'curateyes',
        description: 'Confirms current map curation, need curator permissions to confirm',
        source: 'bonk.io',
        unlisted: true,
        callback: () => {
            if (curateMapDBID == 0 && curateMapDBV == 0) {
                showStatusMessage('* Type /curate followed by your comment', '#6033cc', true);
            } else {
                send(51, {mapid: curateMapDBID, dbv: curateMapDBV, comment: curateMapComment});
            }
        }
    },
    {
        name: 'curateno',
        description: 'Cancels current map curation',
        source: 'bonk.io',
        unlisted: true,
        callback: () => {
            curateMapComment = "";
            curateMapDBID = 0;
            curateMapDBV = 0;
            D2n('* Cancelled.', '#6033cc', true);
        }
    },
]);

const curateMapComment = '';
const curateMapDBID = 0;
const curateMapDBV = 0;
let contextMenu = null;
let hideChatAfterClose = false;

const actions = [
    {
        name: 'copyname',
        text: 'Copy Username',
        host: false,
        onclick: function (content) {
            copyToClipboard(content.author);
            closeContextMenu();
        }
    },
    {
        name: 'copytext',
        text: 'Copy Text',
        host: false,
        onclick: function (content) {
            copyToClipboard(unemojify(content.message));
            closeContextMenu();
        }
    },
    {
        name: 'copymessage',
        text: 'Copy Message',
        host: false,
        onclick: function (content) {
            copyToClipboard(`[${content.timestamp}] ${content.author}: ${unemojify(content.message)}`);
            closeContextMenu();
        }
    },
    {
        name: 'kick',
        text: 'Kick @player',
        host: true,
        sure: false,
        onclick: function (content) {
            if (this.textContent.startsWith('Kick '))
                this.textContent = 'Sure?';
            else {
                const id = playerList.findIndex(user => user && user.userName === content.author);
                if (id == -1) showStatusMessage('* Player not found', '#b53030', isIngame());
                else if (id == selfId) showStatusMessage('* Can\'t ban yourself', '#b53030', isIngame());
                else send(9, {banshortid: id, kickonly: true});
                closeContextMenu();
            }
        }
    },
    {
        name: 'ban',
        text: 'Ban @player',
        host: true,
        sure: false,
        onclick: function (content) {
            if (this.textContent.startsWith('Ban '))
                this.textContent = 'Sure?';
            else {
                const id = playerList.findIndex(user => user && user.userName === content.author);
                if (id == -1) showStatusMessage('* Player not found ', '#b53030', isIngame());
                else if (id == selfId) showStatusMessage('* Can\'t ban yourself ', '#b53030', isIngame());
                else send(9, {banshortid: id});
                closeContextMenu();
            }
        }
    },
    {
        name: 'ping',
        text: 'Ping',
        host: false,
        onclick: function (content, chatInput) {
            focusChat();
            chatInput.value = `${chatInput.value.trimEnd()} @${content.author}`;
            closeContextMenu();
        }
    },
    {
        name: 'reply',
        text: 'Reply',
        host: false,
        onclick: function (content, chatInput) {
            let text = content.message;
            // … IS A SINGLE CHARACTER
            if (text.length > 28) text = text.substring(0, 28) + '…';
            focusChat();
            chatInput.value = unemojify(`"${unescape('\xa0')}${content.author}: ${text}${unescape('\xa0')}"${unescape('\xa0')}`) + findReply(chatInput.value).message;
            closeContextMenu();
        }
    }
];
actions.line = { line: true };
actions.forEach( action => actions[action.name] = action );

const openContextMenu = (content, actions, position) => {
    closeContextMenu();
    const addClassId = function (name) {
        return isIngame()? 'bchat_ingamecontextmenu_' + name : 'bchat_contextmenu_' + name
    }

    contextMenu = document.createElement('div');
    contextMenu.id = addClassId('menu');
    contextMenu.oncontextmenu = () => false;
    if (isIngame()) {
        ingameChatBox.appendChild(contextMenu);
        ingameChatBox.hideTimestamp = Infinity;
        ingameChatBox.style.visibility = 'inherit';
    }
    else
        document.getElementById('newbonklobby_chatbox').appendChild(contextMenu);
    let keys = Object.keys(actions);
    for (let i = 0; i < keys.length; i++) {
        const action = actions[keys[i]];
        if (action.line === true) {
            const line = document.createElement('div');
            line.classList.add(addClassId('overline'));
            contextMenu.appendChild(line);
        }
        else {
            const button = document.createElement('div');
            button.textContent = action.text.replaceAll(/(?<!\\)@player/g, content.author);

            if (isIngame()) {
                button.classList.add('bchat_ingamebutton');
                button.classList.add('bchat_contextmenu_button');
                if (action.host && !isHost())
                    button.classList.add('bchat_ingamebutton_disabled');
            }
            else {
                button.classList.add('brownButton');
                button.classList.add('brownButton_classic');
                button.classList.add('buttonShadow');
                button.classList.add('bchat_contextmenu_button');
                if (action.host && !isHost())
                    button.classList.add('brownButtonDisabled');
            }

            const chatInput = isIngame()? document.getElementById('ingamechatinputtext') : document.getElementById('newbonklobby_chat_input');
            button.onclick = () => action.onclick.call(button, content, chatInput);

            contextMenu.appendChild(button);
        }
    }
    let chatRect = document.getElementById(isIngame()? 'ingamechatbox' : 'newbonklobby_chatbox').getBoundingClientRect();
    contextMenu.style.display = 'unset';
    contextMenu.style.left = position.x - content.getBoundingClientRect().x + 10;
    contextMenu.style.top = position.y - chatRect.top - (contextMenu.clientHeight * 0.4);
    if (contextMenu.getBoundingClientRect().bottom - chatRect.bottom > -10) {
        contextMenu.style.top = chatRect.height - contextMenu.clientHeight - 10;
    }
}

const closeContextMenu = () => {
    if (contextMenu) contextMenu.parentNode.removeChild(contextMenu);
    contextMenu = null;
    ingameChatBox.hideAfter(12000);
    ingameChatBox.style.visibility = 'hidden';
}
// information about packets (Aug 25, 2023): https://github.com/UnmatchedBracket/DemystifyBonk/blob/main/Packets.md

let send = function () {};
let receive = function () {};
let playerList = null;
let hostId = -1;
let selfId = -1;
let selfAvatar = null;
let map = null;
let token = ''; // SUS, only used for map fave/unfave

const onReturnLobby = () => {
    if (chatInput.value == '')
        chatInput.value = ingameChatInput.value;
    lockKeyboard = true;
    ingameChatInput.classList.remove('ingamechatinputtextbg');
    if (ingameChatInput.value != '') {
        document.getElementById('newbonklobby_chat_lowerinstruction').style.visibility = 'hidden';
    }
    ingameChatInput.value = '';
    ingameChatBox.style.opacity = 0;
    document.getElementById('newbonklobby_chat_lowerline').style.display = 'block';
    document.getElementById('newbonklobby_chat_lowerinstruction').style.display = 'block';
    chatInput.style.display = 'block';
    // for unknown reason i can't focus chat here
}

const onStartGame = () => {
    if (chatInput.value != '')
        ingameChatInput.value = chatInput.value;
    if (chatInput.value != '') {
        lockKeyboard = false;
        ingameChatBox.hideAfter(-1, true);
        ingameChatInput.classList.add("ingamechatinputtextbg");
        // for unknown reason i can't focus chat here
    }
    else
        lockKeyboard = true;
    chatInput.value = '';
    ingameChat.style.visibility = 'visible';
    document.getElementById('newbonklobby_chat_lowerline').style.display = 'none';
    document.getElementById('newbonklobby_chat_lowerinstruction').style.display = 'none';
    chatInput.style.display = 'none';
}

let injected = false;
let _send = window.WebSocket.prototype.send;
window.WebSocket.prototype.send = function(args) {
    if (this.url.includes("socket.io/?EIO=3&transport=websocket&sid=")) {
        let packet = null;
        if (args.indexOf('[') != -1) packet = JSON.parse(args.substring(args.indexOf('[')));
        else packet = [0, args];

        if (packet[0] == 12) {
            selfAvatar = packet[1].avatar;
            token = packet[1].token;
        }
        else if (packet[0] == 13) {
            token = packet[1].token;
        }
        else if (packet[0] == 14) {
            onReturnLobby();
        }
        else if (packet[0] == 23) {
            map = decodeFromDatabase(packet[1].m);
        }

        if (!injected) {
            send = (packet, args) => {
                this.send(`42[${packet},${JSON.stringify(args)}]`);
            };
            injected = true;
            let originalReceive = this.onmessage;
            this.onmessage = function (args) {
                let packet = null;
                if (args.data.indexOf('[') != -1) packet = JSON.parse(args.data.substring(args.data.indexOf('[')));
                else packet = [0, parseInt(args)];
                if (packet[0] == 0) {
                    if (packet[1] == 41) {
                        playerList = null;
                        injected = false;
                        selfId = -1;
                    }
                }
                else if (packet[0] == 2) {
                    onLobbyJoin();
                    playerList = [{
                        userName: document.getElementById('pretty_top_name').textContent,
                        guest: document.getElementById('pretty_top_level').textContent == 'Guest',
                        level: document.getElementById('pretty_top_level').textContent == 'Guest'? 0 : parseInt(document.getElementById('pretty_top_level').textContent),
                        ready: false,
                        team: 1,
                        avatar: selfAvatar,
                        ping: 105,
                    }];
                    hostId = 0;
                    selfId = 0;
                    map = null;
                }
                else if (packet[0] == 3) {
                    onLobbyJoin();
                    playerList = packet[3];
                    hostId = packet[2];
                    selfId = packet[1];
                }
                else if (packet[0] == 4) {
                    playerList.push({
                        userName: packet[3],
                        guest: packet[4],
                        level: packet[5],
                        ready: false,
                        team: packet[6],
                        avatar: packet[7],
                        ping: 105,
                    });
                    if (hostId == selfId && customEmojis.length > 0) {
                        const max = 200000;

                        const filtered = customEmojis.filter(x => x.data.length <= max);
                        let index = 0;
                        let extra = 0;
                        let chunks = [];
                        while (index < filtered.length) {
                            let count = JSON.stringify(filtered.slice(index, index + extra + 1)).length;
                            if (index + extra + 1 == filtered.length) {
                                if (count <= max) chunks.push(filtered.slice(index, index + extra + 1));
                                else {
                                    chunks.push(filtered.slice(index, index + extra));
                                    chunks.push(filtered.slice(index + extra, index + extra + 1));
                                }
                                index = filtered.length;
                            }
                            else if (count > max) {
                                chunks.push(filtered.slice(index, index + extra));
                                index = index + extra + 1;
                                extra = 0;
                            } else extra++;
                        }

                        for (let i = 0; i < chunks.length; i++) {
                            send(4, {
                                type: 'bonkchat:emj',
                                action: 'info',
                                to: packet[1],
                                emjs: chunks[i],
                                last: i + 1 == chunks.length? true : false
                            });
                        }
                    }
                }
                else if (packet[0] == 5) {
                    playerList[packet[1]] = null;
                }
                else if (packet[0] == 6) {
                    hostId = packet[2];
                }
                else if (packet[0] == 7) {
                    if (packet[2].type && packet[2].type == 'bonkchat:emj' && packet[1] == hostId) {
                        //console.log(`bonkchat:emj [ACTION:${packet[2].action},ID:${packet[1]},HOST:${hostId}]`)
                        if (packet[2].action == 'push') {
                            for (let i in packet[2].emjs) {
                                if (customEmojis.length < settings.emojiLimit && packet[2].emjs[i].name.length < settings.emojiNameMaxLength + 4)
                                    customEmojis.push(packet[2].emjs[i]);
                                else break;
                            }
                        }
                        else if (packet[2].action == 'info') {
                            if (packet[2].to == selfId) {
                                for (let i in packet[2].emjs) {
                                    if (customEmojis.length < settings.emojiLimit && packet[2].emjs[i].name.length < settings.emojiNameMaxLength + 4)
                                        customEmojis.push(packet[2].emjs[i]);
                                    else break;
                                }
                            }
                        }
                        else if (packet[2].action == 'clear') {
                            while (typeof customEmojis[0] != 'undefined') customEmojis.pop();
                        }
                    }
                }
                else if (packet[0] == 13) {
                    onReturnLobby();
                    ingameChat.style.visibility = 'hidden';
                }
                else if (packet[0] == 15) {
                    map = decodeFromDatabase(packet[3].map);
                    onStartGame();
                }
                // status message
                else if (packet[0] == 16) {
                    if (packet[1] == 'disabled_in_quick') {
                        showStatusMessage('* Unavailable in quick play', null, true);
                    }
                }
                else if (packet[0] == 20) {
                    try {
                        onChatMessage(packet[1], packet[2]);
                    } catch (e) {
                        console.error(e);
                    }
                    // prevent from appending normal bonk message
                    return;
                }
                else if (packet[0] == 21) {
                    map = packet[1].map;
                }
                else if (packet[0] == 41) {
                    hostId = packet[1].newHost;
                }
                else if (packet[0] == 29) { // receive map switch
                    map = decodeFromDatabase(packet[1]);
                }
                else if (packet[0] == 48) { // receive in game
                    map = decodeFromDatabase(packet[1].gs.map);
                    onStartGame();
                }
                return originalReceive.call(this, args);
            };
            receive = (packet, ...args) => {
                this.onmessage({data: `42[${packet},${JSON.stringify(args).slice(1, -1)}]`});
            };
            let _onclose = this.onclose;
            this.onclose = function (args) {
                injected = false;
                return _onclose.call(this, args);
            };
        }
    }
    return _send.call(this, args);
};
let _XMLSend = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.send = function(data) {
    try {
        this.addEventListener('loadend', (event) => {
            if (event.target.responseURL.includes('/login_legacy.php')) {
                filter = unserialize(JSON.parse(event.target.response).controls).filter;
            } else if (event.target.responseURL.includes('/login_auto.php')) {
                filter = unserialize(JSON.parse(event.target.response).controls).filter;
            } else if (event.target.responseURL.includes('/account_savecontrols.php')) {
                filter = unserialize(data.match(/(?<=^|&)controls=(.*?)(?=$|&)/)[1].replaceAll('%2B', '+').replaceAll('%3D', '=')).filter;
            }
        });
    } catch (e) {}
    _XMLSend.apply(this, arguments);
};
const imgPreview = document.createElement('div');
imgPreview.id = 'bchat_imagepreview_container';
imgPreview.innerHTML = `
<div id="bchat_imagepreview_behindblocker"></div>
<div class="bchat_imagepreview_button" id="bchat_imagepreview_close"></div>
<div class="bchat_imagepreview_button" id="bchat_imagepreview_tab"></div>
<div class="bchat_imagepreview_button" id="bchat_imagepreview_link"></div>
<div id="bchat_imagepreview_infocontainer"></div>
`;
document.getElementById('newbonkgamecontainer').appendChild(imgPreview);

const imgPreviewImg = function () {
    let images = document.getElementsByClassName('bchat_imagepreview_image');
    return images.length > 0? images[0] : null;
}
const imgPreviewInfo = document.getElementById('bchat_imagepreview_infocontainer');

document.getElementById('bchat_imagepreview_behindblocker').onclick = document.getElementById('bchat_imagepreview_close').onclick = function () {
    window.anime({
        targets: imgPreview,
        opacity: 0,
        duration: 100,
        easing: "easeOutCubic",
        complete: () => {
            imgPreview.style.visibility = 'hidden';
        }
    });
    let img = imgPreviewImg();
    if (!img)
        return;
    window.anime({
        targets: img,
        scale: 0.8,
        duration: 100,
        easing: "easeOutCubic",
    });
};

let tabA = document.createElement('a');
tabA.target = '_blank';
document.getElementById('bchat_imagepreview_tab').onclick = () => tabA.click();

document.getElementById('bchat_imagepreview_link').onclick = function () {
    copyToClipboard(imgPreviewImg().src);
    this.style.backgroundColor = 'rgba(192, 192, 192, 0.33)';
    window.anime({
        targets: this,
        backgroundColor: 'rgba(64, 64, 64, 0.33)',
        duration: 800,
        easing: "easeOutCubic",
        complete: () => this.removeAttribute('style')
    });
};

const openImagePreview = image => {
    imgPreview.style.visibility = 'inherit';
    imgPreview.style.opacity = '0';
    window.anime({
        targets: imgPreview,
        opacity: 1,
        duration: 175,
        easing: "easeOutCubic",
    });
    if(imgPreviewImg())
        imgPreviewImg().remove();
    let clone = image.cloneNode();
    clone.classList.remove('newbonklobby_chat_image');
    clone.classList.add('bchat_imagepreview_image');
    imgPreview.insertBefore(clone, document.getElementById('bchat_imagepreview_close'));
    clone.style.transform = 'scale(0.8)';
    window.anime({
        targets: clone,
        scale: 1,
        duration: 175,
        easing: "easeOutCubic",
    });
    tabA.href = image.src;

    while(imgPreviewInfo.firstChild)
        imgPreviewInfo.removeChild(imgPreviewInfo.firstChild);
    let info = [
        'Size: ' + image.naturalWidth + 'x' + image.naturalHeight
    ];
    info.forEach( info => {
        let div = document.createElement('div');
        div.textContent = info;
        imgPreviewInfo.appendChild(div);
    });
}
const focusChat = function () {
    document.activeElement = null;
    let event = document.createEvent("HTMLEvents");
    event.initEvent('keydown');
    event.keyCode = 13;
    event.code = 'Enter';
    document.dispatchEvent(event);
}

let targetCommandId = -1;
const _oninput = chatInput.oninput ?? new Function();
chatInput.oninput = function (event) {
    _oninput.apply(this, arguments);
    const index = event.target.selectionEnd;
    const text = event.target.value;
    const remain = text.substring(index);
    const before = text.substring(0, index);
    const chatInput = () => this.oninput({target: this});

    let matchEmoji = before.match(/(?<=(?<!\\):)[a-z0-9_~]+$/);

    const matchPing = before.match(/(?<=@)[^@]*$/g)?.reverse()[0];
    // stringify and parse to prevent elements from referring to the same object
    let players = typeof matchPing == 'string'? JSON.parse(JSON.stringify(playerList.filter( player => player && player.userName.toLowerCase().includes(matchPing.toLowerCase()) ))) : [];

    if (text.length == 0) {
        hideCommandBar();
        if (helpContainer.showing)
            helpContainer.showing = false;
    }
    else if (matchEmoji) {
        let onlyCustom = matchEmoji[0].includes('~');
        matchEmoji = matchEmoji[0].replaceAll('~', '');
        let emojis = emojione.shortnames.split('|').filter( x => x.includes(matchEmoji) && emojione.emojioneList[x]).slice(0, 24);
        let roomEmojis = customEmojis.filter( emj => emj.name.includes(matchEmoji));
        if (onlyCustom && roomEmojis.length == 0) {
            helpContainer.showing = false;
            return;
        }
        if (emojis.length == 0 && roomEmojis.length == 0) {
            helpContainer.showing = false;
            return;
        }
        emojis = onlyCustom? [] : emojis.sort().map( emj => {
            return {
                text: `${emojione.shortnameToUnicode(emj)} ${emj}`,
                onclick: () => {
                    event.target.value = `${before.slice(0, -matchEmoji.length - 1)}${emj} ${remain}`;
                    helpContainer.showing = false;
                    chatInput();
                }
            };
        });
        roomEmojis = roomEmojis.sort().map( emj => {
            return {
                text: `<img src="${emj.data}" style="width: 20px;vertical-align: middle;"> :~${emj.name}:`,
                rightText: 'room emoji',
                onclick: () => {
                    event.target.value = `${before.slice(0, -matchEmoji.length - 1)}~${emj.name}: ${remain}`;
                    helpContainer.showing = false;
                    chatInput();
                }
            };
        });
        helpContainer.draw(roomEmojis.concat(emojis));
    }
    else if (players.length > 0) {
        players = players.map( player => {
            let name = player.userName;
            let skin = document.createElement('div');
            let id = playerList.findIndex( player => player && player.userName == name );
            if (generatedSkins24[id]) {
                skin.appendChild(generatedSkins24[id].cloneNode(true));
            } else {
                try {
                    createChatSkinImage(player.avatar, skin, 'bchat_help_skinimage', 28, id, generatedSkins24, 2, 0.1, 0.2);
                } catch (err) {}
            }
            skin.innerHTML += '@' + name;
            return {
                text: skin.innerHTML,
                name: name,
                onclick: () => {
                    event.target.value = `${before.match(/(?:.*?@)*$/)[0].slice(0, -1)}@${name} ${remain}`;
                    helpContainer.showing = false;
                    chatInput();
                }
            };
        });
        helpContainer.draw(players);
    }
    else if (text.startsWith('/')) {
        function getHelpOptions (name) {
            if (name == '')
                name = 'help';
            return window.chatCommands
            .map( (command, ind) => {
                command.ind = ind;
                return command;
            })
            .filter( command => command.name.includes(name) &&
                                !command.unlisted &&
                                (command.hostOnly? isHost() : true) &&
                                (command.condition? command.condition() : true))
            .map( command => {
                return {
                    text: `/${command.name} ${command.parameters? command.parameters.join(' ') : ''}`,
                    rightText: command.source,
                    desc: command.description ?? '',
                    name: command.name,
                    onclick: () => {
                        let args = event.target.value.match(/.*? (?<a>.*)/);
                        args = args? args.groups.a.trimStart() + ' ' : (event.target.value.startsWith('/')? remain : '');
                        event.target.value = `/${command.name} ${args}`;

                        if (before != '/' + command.name)
                            event.target.value = event.target.value.trim();

                        targetCommandId = command.ind;
                        showCommandBar(command.source, [{
                            text: '/' + command.name,
                        }].concat(command.parameters? command.parameters.map( param => {
                            return {
                                text: ' ' + param,
                            };
                        }) : []));
                        helpContainer.showing = false;
                    }
                };
            });
        };
        if (!before.includes(' ')) {
            targetCommandId = -1;
        }
        else if (before.includes(' ') || targetCommandId != -1) {
            if (targetCommandId == -1) {
                let name = before.split(' ')[0].substring(1);
                targetCommandId = window.chatCommands.findIndex( command => command.name === name);
            }
            let command = window.chatCommands[targetCommandId];

            helpContainer.showing = false;
            if (!command)
                return;
            showCommandBar(command.source, [{
                text: '/' + command.name,
            }].concat(command.parameters? command.parameters.map( param => {
                return {
                    text: ' ' + param,
                };
            }) : []));
            return;
        }
        const commands = getHelpOptions(before.substring(1));
        hideCommandBar();
        helpContainer.draw(commands);
    }
    else {
        hideCommandBar();
        targetCommandId = -1;
        if (helpContainer.showing)
            helpContainer.showing = false;
    }
}
ingameChatInput.oninput = chatInput.oninput;

// used in onLobbyJoin()
const chatKeydown = function (event) {
    if (event.code == 'Space' && helpContainer.showing && helpContainer.optionsLength == 1) {
        helpContainer.callTarget();
        event.preventDefault();
    }
    else if (event.code == 'Enter' && event.target.value.startsWith('/')) {
        if (targetCommandId == -1 && helpContainer.showing)
            helpContainer.callTarget();
        helpContainer.showing = false;
        event.target.value = processCommand(event.target.value);
        hideCommandBar();
    }
    else if (event.code == 'Enter' && helpContainer.showing) {
        helpContainer.callTarget();
        document.getElementById('newbonklobby_chat_input').blur();
        ingameChatInput.blur();
    }
    else if (event.code == 'Enter' && event.target.value.length > 0) {
        helpContainer.showing = false;
        // parse normal emojis
        event.target.value = event.target.value.replaceAll(/(?<!\\):[a-z0-9_-]{1,256}:/g, match => emojione.shortnameToUnicode(match));
        // parse room custom emojis
        event.target.value = event.target.value.replaceAll(/(?<!\\):~[a-z0-9_-]{1,256}:/g, match => name2UnicodeChar(match));
        // parse \ and emojis
        event.target.value = event.target.value.replaceAll(/\\:~?[a-z0-9_-]{1,256}:/g, match => match.slice(1));
    }

    if (event.target == ingameChatInput)
        _oningamekeydown.apply(this, arguments);
    else
        _onkeydown.apply(this, arguments);

    if (event.code == 'Enter') {
        // lobby
        if (document.activeElement == chatInput) {
            if (chatInput.value.substring(0, 1) == '/') {
                showStatusMessage(`* Command ${chatInput.value.split(' ')[0]} not recognised`, null, true);
                showStatusMessage('* Accepted commands are listed in /help', null, true);
            } else if (chatInput.value != '') {
                send(10, {message: chatInput.value});
            }
            chatInput.value = '';
        }
        // game
        else if (document.activeElement == ingameChatInput) {
            if (ingameChatInput.value.substring(0, 1) == '/') {
                showStatusMessage(`* Command ${ingameChatInput.value.split(' ')[0]} not recognised`, null, true);
                showStatusMessage('* Accepted commands are listed in /help', null, true);
            } else if (ingameChatInput.value != '') {
                send(10, {message: ingameChatInput.value});
            }
            ingameChatInput.value = '';
        }
    }
};

function processCommand (value) {
    // parse value
    const text = value.replace(/ +/, ' ');
    const splited = [];
    let index = 0;
    let ignoreSpaces = false;
    for (let i = 0; i < text.length; i++) {
        const char = text[i];
        const prevChar = text[i - 1] ?? '';
        if (char == '"' && prevChar != '\\')
            ignoreSpaces = !ignoreSpaces;
        else if (char == ' ' && !ignoreSpaces) {
            let param = text.substring(index, i);
            if(prevChar == '"')
                param = param.substring(1).substring(0, param.length - 2);
            splited.push(param.replaceAll('\\"', '"'));
            index = i + 1;
        }
        else if (i == text.length - 1) {
            let param = text.substring(index, i + 1);
            if (prevChar == '"')
                param = param.substring(1).substring(0, param.length - 2);
            splited.push(param.replaceAll('\\"', '"'));
        }
    }

    let name = splited.shift().substring(1);
    let command;
    if (targetCommandId != -1) command = window.chatCommands[targetCommandId];
    else command = window.chatCommands.filter( x => (x.condition? x.condition() : true) && (x.hostOnly? isHost() : true)).find( x => x.name == name );

    targetCommandId = -1;

    if (command) {
        let pass = false;
        try {
            if (command.getArgsString)
                splited.unshift(value.substring(command.name.length + 2).trim());
            pass = command.callback(...splited);
        }
        catch (error) {
            showStatusMessage(`* Something went wrong... [Processing /${command.name}]`);
            console.error(error);
        }
        if (!pass)
            return '';
    }
    return value;
}

let lockKeyboard = true;
const DocumentKeydown = function (event) {
    if (helpContainer.showing)
        helpContainer.arrowNavigate(event);

    if (event.code == 'Enter' && isIngame()) {
        if (document.activeElement != ingameChatInput) {
            lockKeyboard = false;
            ingameChatBox.hideAfter(-1);
            ingameChatInput.classList.add("ingamechatinputtextbg");
            ingameChatInput.focus();
        }
        else {
            lockKeyboard = true;
            ingameChatBox.hideAfter(10000);
            ingameChatInput.classList.remove("ingamechatinputtextbg");
            ingameChatInput.blur();
        }

        event.stopImmediatePropagation();
    }
    else if (isIngame() && !lockKeyboard)
        event.stopImmediatePropagation();
    else if (!isIngame() && document.getElementById('gamerenderer').style.visibility == 'inherit')
        event.stopImmediatePropagation();

    _documentKeydown.apply(this, arguments);
};


const chatSound = new Howl({
    src: GameResources.soundStrings.popNote,
    volume: 1
});

const findReply = message => {
    let match = message.match(/"\xa0(.*?): (.*?)\xa0"\xa0(.*)/);
    if (match) {
        return {
            found: true,
            reply: {
                author: match[1],
                message: match[2],
            },
            message: match[3]
        }
    }
    return {
        found: false,
        message: message
    }
}

const onChatMessage = (id, message) => {
    if (playerList[id].mute) {
        return;
    }

    if (filter) {
        message = sanitizeString(message);
    }

    notified = false;
    if (document.getElementById('newbonklobby_chat_content')) {
        let player = playerList[id];
        notified = appendlobbyChatMessage(id, player, message, notified);
    }

    if (document.getElementById('ingamechatcontent')) {
        let player = playerList[id];
        notified = appendIngameChatMessage(id, player, message, notified);
    }

    if (localStorage.getItem('mute') == 'false') {
        chatSound.stop();
        chatSound.play();
    }
}

const handleMessageTextContent = (element, text, imageOnload, isIngame) => {
    const addClass = function (name) {
        return isIngame? 'bchat_msg_ingame' + name : 'bchat_msg_' + name
    }
    let link = false;
    let start = 0;
    let imageLoaded = false;
    do {
        link = text.substring(start).match(linkRegex);
        if (link) {
            element.innerHTML += text.substring(start, start + link.index);
            element.innerHTML += `<a class="${addClass('href')}" href="${link[0]}" target="_blank">${link[0]}</a>`;
            start += link.index + link[0].length;

            if (!imageLoaded && settings.whiteList.some( x => link[0].startsWith(x) )) {
                let testImage = new Image();
                let imageSrc = link[0].replaceAll('&', '&amp;');
                testImage.onload = function () {
                    element.innerHTML = element.innerHTML.replaceAll(`href="${imageSrc}" target="_blank">${imageSrc}<`, `onclick="openImagePreview(this.parentNode.parentNode.parentNode.find('bchat_msg_imagecontainer').children[0])">${imageSrc.split('/').reverse()[0]}<`)
                    if (!imageLoaded) {
                        imageLoaded = true;
                        imageOnload(testImage);
                    }
                };
                testImage.src = imageSrc;
            }
        } else {
            element.innerHTML += text.substring(start);
        }
    } while (link)
        return element.textContent.replaceAll(/:~?[a-z0-9_]{1,256}:/g, match => '\\' + match);
}

// create emoji image elements in element's innerHTML
const emojisToImages = (element, scale = 1) => {
    let matches = element.innerHTML.match(new RegExp(`[\\ue100-\\ue1${customEmojis.length.toString().padStart(2, '0')}]`, 'g'));
    let width = Math.round(element.offsetHeight * scale);
    if (matches) {
        for (let i = 0; i < matches.length; i++) {
            let emoji = customEmojis[matches[i].charCodeAt() - 57600];
            element.innerHTML = element.innerHTML.replaceAll(matches[i][0],
        `<span style="display: inline-block;">
                <span style="display: none;">:${emoji.name}:</span>
                <img style="max-height: 100%;max-width: ${width}px;vertical-align: text-bottom;" alt=":${emoji.name}:" src="${emoji.data}">
            </span>`);
        }
    }
}

const appendlobbyChatMessage = (id, player, messageText, notified) => {
    let prevMessage = chat.lastChild;
    let showSkin = false;
    if (prevMessage && prevMessage.author == player.userName)
        showSkin = true;

    const message = document.createElement('div');
    message.innerHTML = `
    <div class="bchat_msg_reply" style="display: none;">
        <a class="bchat_msg_replyhref">
            <div class="bchat_msg_replyarrow"></div>
            <span class="bchat_msg_replyauthor"></span>
            <span class="bchat_msg_replytext"></span>
        </a>
    </div>
    <div class="bchat_msg_skinbox"></div>
    <span class="bchat_msg_name"></span>
    <div class="bchat_msg_txtcontainer">
        <span class="bchat_msg_txt"></span>
        <span class="bchat_msg_time"></span>
    </div>
    <div class="bchat_msg_imagearea" style="display: none;"></div>
    `;

    message.find = className => {
        return message.getElementsByClassName(className)[0] || null;
    };

    let skinbox = message.find('bchat_msg_skinbox');
    if (generatedSkins[id]) {
        skinbox.appendChild(generatedSkins[id].cloneNode(true));
    } else {
        try {
            createChatSkinImage(player.avatar, skinbox, '', 34, id, generatedSkins, 2, 0.1, 0.2);
        } catch (err) {}
    }

    let name = message.find('bchat_msg_name');
    name.textContent = message.author = player.userName;

    let reply = findReply(messageText);

    let text = message.find('bchat_msg_txt');
    let imageArea = message.find('bchat_msg_imagearea');
    const imageOnload = function (image) {
        let container = document.createElement('div');
        container.className = 'bchat_msg_imagecontainer';

        image.className = 'bchat_msg_image';
        image.onclick = function () {
            openImagePreview(this);
        }

        let toggleButton = document.createElement('div');
        toggleButton.className = settings.autoShowImages? 'bchat_msg_imagehide' : 'bchat_msg_imageshow brownButton brownButton_classic buttonShadow';
        toggleButton.onclick = function () {
            if (this.classList.contains('bchat_msg_imagehide')) {
                image.style.display = 'none';
                this.classList.remove('bchat_msg_imagehide');
                this.classList.add('brownButton');
                this.classList.add('brownButton_classic');
                this.classList.add('buttonShadow');
                this.classList.add('bchat_msg_imageshow');
            } else if (this.classList.contains('bchat_msg_imageshow')) {
                image.style.display = 'block';
                this.classList.remove('bchat_msg_imageshow');
                this.classList.remove('brownButton');
                this.classList.remove('brownButton_classic');
                this.classList.remove('buttonShadow');
                this.classList.add('bchat_msg_imagehide');
            }
        }
        container.appendChild(image);
        container.appendChild(toggleButton);

        imageArea.appendChild(container);
        imageArea.style.display = 'block';
    }
    message.message = handleMessageTextContent(text, reply.message, imageOnload);

    let time = message.find('bchat_msg_time');
    time.textContent = message.timestamp = new Date().toLocaleTimeString();

    let canBeNotified = settings.notify && Notification.permission != 'denied' && document.visibilityState != 'visible' && id !== selfId;
    if (!notified && canBeNotified && reply.message.includes('@' + playerList[selfId].userName)) {
        new Notification(reply.message);
        notified = true;
    }

    let replyAuthor = message.find('bchat_msg_replyauthor');
    let replyMessage = message.find('bchat_msg_replytext');
    if (reply.found && replyAuthor && replyMessage) {
        message.find('bchat_msg_reply').style.display = 'block';
        replyAuthor.textContent = message.replyAuthor = reply.reply.author;
        replyMessage.textContent = message.replyMessage = reply.reply.message;

        let elem = Array.from(chat.children).findLast( element => element.find('bchat_msg_txt')?.textContent.startsWith(reply.reply.message.slice(0, -1)));
        if (elem) {
            let id = 'chatmsgid' + (performance.now() * 1e+12).toString(16);
            elem.id = id;
            message.find('bchat_msg_replyhref').onclick = () => elem.highlight();
            message.find('bchat_msg_replyhref').href = '#' + id;
        }

        if (!notified && canBeNotified && reply.reply.author == playerList[selfId].userName) {
            new Notification(reply.message);
            notified = true;
        }
    }
    else if (showSkin) {
        skinbox.style.display = 'none';
        name.style.display = 'none';
    }

    message.isMessage = true;
    chat.appendChild(message);

    let scale = message.message.match(new RegExp(`[\\ue100-\\ue1${customEmojis.length.toString().padStart(2, '0')}]\\s{0,3}?`, 'g'))?.length <= 6 &&
                message.message.match(new RegExp(`[\\s\\ue100-\\ue1${customEmojis.length.toString().padStart(2, '0')}]`, 'g'))?.length == message.message.length;
    emojisToImages(text, scale? 2.5 : 1);
    emojisToImages(replyMessage);

    return notified;
}

const appendIngameChatMessage = (id, player, messageText, notified) => {
    const message = document.createElement('div');
    message.innerHTML = `
    <div class="bchat_msg_ingamereply" style="display: none;">
        <a class="bchat_msg_ingamereplyhref">
            <div class="bchat_msg_ingamereplyarrow"></div>
            <span class="bchat_msg_ingamereplyauthor"></span>
            <span class="bchat_msg_ingamereplytext"></span>
        </a>
    </div>
    <span class="bchat_msg_ingamename"></span>
    <div class="bchat_msg_ingametxtcontainer">
        <span class="bchat_msg_ingametxt"></span>
        <span class="bchat_msg_ingametime"></span>
    </div>
    <div class="bchat_msg_ingameimagearea" style="display: none;"></div>
    `;

    message.find = className => {
        return message.getElementsByClassName(className)[0] || null;
    };

    let name = message.find('bchat_msg_ingamename');
    name.textContent = message.author = player.userName;
    name.textContent += ':';

    let reply = findReply(messageText);

    let text = message.find('bchat_msg_ingametxt');
    let imageArea = message.find('bchat_msg_ingameimagearea');
    const imageOnload = function (image) {
        let container = document.createElement('div');
        container.className = 'bchat_msg_imagecontainer';

        image.className = 'bchat_msg_ingameimage';
        image.onclick = function () {
            openImagePreview(this);
        }

        let toggleButton = document.createElement('div');
        toggleButton.className = settings.autoShowImages? 'bchat_msg_ingameimagehide' : 'bchat_msg_ingameimageshow brownButton brownButton_classic buttonShadow';
        toggleButton.onclick = function () {
            if (this.classList.contains('bchat_msg_ingameimagehide')) {
                image.style.display = 'none';
                this.classList.remove('bchat_msg_ingameimagehide');
                this.classList.add('bchat_msg_ingamechatbutton');
                this.classList.add('bchat_msg_ingameimageshow');
            } else if (this.classList.contains('bchat_msg_ingameimageshow')) {
                image.style.display = 'block';
                this.classList.remove('bchat_msg_ingamechatbutton');
                this.classList.remove('bchat_msg_ingameimageshow');
                this.classList.add('bchat_msg_ingameimagehide');
            }
        }
        container.appendChild(image);
        container.appendChild(toggleButton);

        imageArea.appendChild(container);
        imageArea.style.display = 'block';
    }
    message.message = handleMessageTextContent(text, reply.message, imageOnload, true);

    let time = message.find('bchat_msg_ingametime');
    time.textContent = message.timestamp = new Date().toLocaleTimeString();

    let canBeNotified = settings.notify && Notification.permission != 'denied' && document.visibilityState != 'visible' && id !== selfId;
    if (!notified && canBeNotified && reply.message.includes('@' + playerList[selfId].userName)) {
        new Notification(reply.message);
        notified = true;
    }

    let replyAuthor = message.find('bchat_msg_ingamereplyauthor');
    let replyMessage = message.find('bchat_msg_ingamereplytext');
    if (reply.found && replyAuthor && replyMessage) {
        message.find('bchat_msg_ingamereply').style.display = 'block';
        replyAuthor.textContent = message.replyAuthor = reply.reply.author;
        replyMessage.textContent = message.replyMessage = reply.reply.message;

        let elem = Array.from(ingameChat.children).findLast( element => element.find('bchat_msg_ingametxt')?.textContent.startsWith(reply.reply.message.slice(0, -1)));
        if (elem) {
            let id = 'chatgamemsgid' + (performance.now() * 1e+12).toString(16);
            elem.id = id;
            message.find('bchat_msg_ingamereplyhref').onclick = () => elem.highlight();
            message.find('bchat_msg_ingamereplyhref').href = '#' + id;
        }

        if (!notified && canBeNotified && reply.reply.author == playerList[selfId].userName) {
            new Notification(reply.message);
            notified = true;
        }
    }

    message.isMessage = true;
    ingameChat.appendChild(message);

    emojisToImages(text);
    emojisToImages(replyMessage);

    return notified;
}

window.betterChat.showStatusMessage = function (text, color = '#b53030', ingame) {
    if (color == null)
        color = '#b53030';
    let status = document.createElement("div");
    let scroll = chat.scrollTop + chat.clientHeight >= chat.scrollHeight - 5;
    let message = document.createElement("span");
    message.style.color = color;
    message.classList.add("newbonklobby_chat_status");
    message.appendChild(document.createTextNode(text));
    status.appendChild(message);
    chat.appendChild(status);
    if (chat.childElementCount > 250) {
        chat.removeChild(chat.firstChild);
    }
    if (scroll) {
        chat.scrollTop = chat.scrollHeight;
    }
    if (ingame) {
        let ingameStatus = document.createElement("div");
        let ingameMessage = document.createElement("span");
        ingameMessage.classList.add("ingamechatstatus");
        ingameMessage.appendChild(document.createTextNode(text));
        ingameStatus.appendChild(ingameMessage);
        ingameChat.appendChild(ingameStatus);
        if (ingameChat.childElementCount > 100) {
            ingameChat.removeChild(ingameChat.firstChild);
        }
        ingameChat.scrollTop = ingameChat.scrollHeight;
    }
}

const showStatusMessage = window.betterChat.showStatusMessage;

const doLimit = function () {
    if(this.children.length < settings.maxMessages) return 0;
    return 1000;
};
document.getElementById("newbonklobby_chat_content").__defineGetter__("childElementCount", doLimit);
document.getElementById("ingamechatcontent").__defineGetter__("childElementCount", doLimit);

let originalAppendChild = chat.appendChild;
chat.appendChild = function () {
    let scrollDown = chat.scrollTop + chat.clientHeight >= chat.scrollHeight - 30;
    originalAppendChild.apply(this, arguments);
    const message = this.lastChild;
    message.classList.add('bchat_content');

    message.message = message.message ?? message.textContent;
    message.author = message.author ?? '';
    message.playerId = message.playerId ?? -1;
    message.timestamp = message.timestamp ?? new Date().toLocaleTimeString();

    message.hide = function () {
        message.style.display = 'none';
    };
    message.show = function () {
        message.style.display = 'block';
    };
    message.find = function (className) {
        return message.getElementsByClassName(className)[0] || null;
    };
    message.highlight = function () {
        this.style.backgroundColor = 'rgba(145, 154, 157, 0.5)';
        window.anime({
            targets: this,
            backgroundColor: 'rgba(145, 154, 157, 0)',
            delay: 250,
            duration: 500,
            easing: "easeOutCubic",
            complete: () => {
                this.style.backgroundColor = '';
            }
        });
    };

    let time;
    if (!message.find('bchat_msg_time')) {
        time = document.createElement('span');
        time.className = 'bchat_msg_time';
        time.textContent = message.timestamp;
        message.lastChild.innerHTML += ' ';
        message.appendChild(time);
    } else
        time = message.find('bchat_msg_time');
    message.addEventListener('mouseover', () => time.style.visibility = 'inherit');
    message.addEventListener('mouseout', () => time.style.visibility = 'hidden');

    message.actions = [
        actions.copytext
    ];

    message.oncontextmenu = function (event) {
        this.classList.add('bchat_contentselected');
        try {
            openContextMenu(this, message.actions, event);
        } catch (e) {
            console.error(e)
        }
        let documentMouseEvent = (event) => {
            message.classList.remove('bchat_contentselected');
            document.removeEventListener('mousedown', documentMouseEvent);
            if(contextMenu && contextMenu.contains(event.target)) return;
            closeContextMenu();
        }
        document.addEventListener('mousedown', documentMouseEvent);
        return false;
    };

    if (message.children[0] && message.children[0].classList.contains('newbonklobby_chat_status')) {
        let text = message.textContent;
        if (text == '* You\'re doing that too much!') {
            //informRatelimited();
        }
        else if (text.startsWith('* ') && text.endsWith(' has joined the game ')) {
            message.actions = [
                actions.kick,
                actions.ban,
                actions.line, // line
                actions.copyname,
                actions.ping
            ];
        }
    }

    if (message.isMessage) {
        message.actions = [
            actions.copyname,
            actions.copytext,
            actions.line,  // line
            actions.copymessage,
            actions.line,  // line
            actions.kick,
            actions.ban,
            actions.line,  // line
            actions.ping,
            actions.reply
        ];
    }


    if (scrollDown)
        chat.scrollTop = chat.scrollHeight;
};

originalAppendChild = ingameChat.appendChild;
ingameChat.appendChild = function () {
    let scrollDown = ingameChatScroll.scrollTop + ingameChatScroll.clientHeight >= ingameChatScroll.scrollHeight - 30;
    originalAppendChild.apply(this, arguments);
    const message = this.lastChild;
    message.classList.add('bchat_ingamecontent');

    message.message = message.message ?? message.textContent;
    message.name = message.name ?? '';
    message.playerId = message.playerId ?? -1;
    message.timestamp = message.timestamp ?? new Date().toLocaleTimeString();

    message.hide = function () {
        message.style.display = 'none';
    };
    message.show = function () {
        message.style.display = 'block';
    };
    message.find = (name) => {
        return message.getElementsByClassName(name)[0] || null;
    };
    message.highlight = function (name) {
        this.style.backgroundColor = 'rgba(145, 154, 157, 0.5)';
        window.anime({
            targets: this,
            backgroundColor: 'rgba(145, 154, 157, 0)',
            delay: 250,
            duration: 500,
            easing: "easeOutCubic",
            complete: () => {
                this.style.backgroundColor = '';
            }
        });
    };

    let time;
    if (!message.find('bchat_msg_ingametime')) {
        time = document.createElement('span');
        time.className = 'bchat_msg_ingametime';
        time.textContent = message.timestamp;
        message.lastChild.innerHTML += ' ';
        message.appendChild(time);
    } else
        time = message.find('bchat_msg_ingametime');
    message.addEventListener('mouseover', () => time.style.visibility = 'inherit');
    message.addEventListener('mouseout', () => time.style.visibility = 'hidden');

    message.actions = [
        actions.copytext
    ];

    message.oncontextmenu = function (event) {
        this.classList.add('bchat_ingamecontentselected');
        try {
            openContextMenu(this, message.actions, event);
        } catch (e) {
            console.error(e)
        }
        let documentMouseEvent = (event) => {
            message.classList.remove('bchat_ingamecontentselected');
            document.removeEventListener('mousedown', documentMouseEvent);
            if(contextMenu && contextMenu.contains(event.target)) return;
            closeContextMenu();
        }
        document.addEventListener('mousedown', documentMouseEvent);
        return false;
    };

    if (message.children[0] && message.children[0].classList.contains('ingamechatstatus')) {
        let text = message.textContent;
        if(text.startsWith('* ') && text.endsWith(' has joined the game.')) {
            message.actions = [
                actions.kick,
                actions.ban,
                actions.line,  // line
                actions.copyname,
                actions.ping
            ];
        }
    }

    if (message.isMessage) {
        message.actions = [
            actions.copyname,
            actions.copytext,
            actions.line,  // line
            actions.copymessage,
            actions.line,  // line
            actions.kick,
            actions.ban,
            actions.line,  // line
            actions.ping,
            actions.reply
        ];
    }

    if (scrollDown)
        ingameChatScroll.scrollTop = ingameChatScroll.scrollHeight;
    ingameChatBox.hideAfter(12000);
}

ingameChatBox.hideTimestamp = -1;
let chatHideTimeout = null;
ingameChatBox.hideAfter = function (time, forced = false) {
    if (chatHideTimeout != null) {
        clearTimeout(chatHideTimeout);
        chatHideTimeout = null;
    }
    if (!isIngame() && !forced)
        return;
    if (time != -1) {
        chatHideTimeout = setTimeout(function () {
            ingameChatBox.style.opacity = 0;
        }, time);
    }
    else
        time = 1e+50;
    ingameChatBox.hideTimestamp = Date.now() + time;
    ingameChatBox.style.opacity = 1;
};

let ingameChatObserver = new MutationObserver(() => {
    if (isIngame()) {
        if (ingameChatBox.hideTimestamp > Date.now())
            ingameChatBox.style.opacity = 1;
        else
            ingameChatBox.style.opacity = 0;
        ingameChatBox.style.visibility = 'inherit';
    }
});
ingameChatObserver.observe(ingameChatBox, {attributes: true});

let ingameChatScroll = document.createElement('div');
ingameChatScroll.id = 'bchat_ingamechatscroll';
ingameChat.parentElement.insertBefore(ingameChatScroll, ingameChat);
ingameChatScroll.appendChild(ingameChat);
class HelpContainer {
    #container = null;
    #showing = false;
    constructor () {
        this.#container = document.createElement('div');
        this.#container.innerHTML = `
        <table>
            <tbody>
                <tr>
                    <td class="newbonklobby_chat_helpoption">
                        /move "user name" ffa or spec
                        <span class="newbonklobby_chat_helpoptiondesc">Moves the player to ffa or spec</span>
                    </td>
                    <td class="newbonklobby_chat_helpoptionright">built-in</td>
                </tr>
            </tbody>
        </table>
        `;
        const self = this;
        this.arrowNavigate = function (e) {
            const rows = self.#container.getElementsByTagName('TBODY')[0].rows;
            const last = rows.length - 1;
            if (self.focus != -1)
                rows[self.focus].removeAttribute('style');

            self.focus = Math.min(last, Math.max(-1, self.focus));

            if (e.key == 'ArrowUp') {
                e.preventDefault();
                if (self.focus == 0 || self.focus == -1)
                    self.focus = last;
                else self.focus--;

                if (isIngame()) rows[self.focus].style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
                else rows[self.focus].style.backgroundColor = 'rgba(100,100,100,0.10)';
                self.callTarget = () => rows[self.focus].click();
            }
            else if (e.key == 'ArrowDown') {
                e.preventDefault();
                if (self.focus == last) self.focus = 0;
                else self.focus++;

                if (isIngame()) rows[self.focus].style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
                else rows[self.focus].style.backgroundColor = 'rgba(100,100,100,0.10)';
                self.callTarget = () => rows[self.focus].click();
            }
        };
        this.optionsLength = 0;
        this.focus = -1;
        this.callTarget = null;
    }

    set showing (value) {
        this.#showing = !!value;
        if (this.#showing === true) {}
        else {
            while (this.#container.getElementsByTagName('TBODY')[0].rows.length > 0)
                this.#container.getElementsByTagName('TBODY')[0].deleteRow(0);
        }
        this.optionsLength = 0;
        this.callTarget = null;
    }
    get showing () {
        return this.#showing;
    }

    draw (options) {
        while (this.#container.getElementsByTagName('TBODY')[0].rows.length > 0)
                this.#container.getElementsByTagName('TBODY')[0].deleteRow(0);
        this.callTarget = null;
        if (Array.isArray(options) && options.length > 0) {
            this.#showing = true;
            const self = this;
            options = options.filter( option => !(typeof option.text !== 'string' ||
                    (typeof option.rightText !== 'undefined' && typeof option.rightText !== 'string') ||
                    (typeof option.desc !== 'undefined' && typeof option.desc !== 'string') ||
                    (typeof option.color !== 'undefined' && typeof option.color !== 'string')))
            this.optionsLength = options.length;
            options.forEach( option => {
                let optionElement = document.createElement('tr');
                optionElement.style.color = option.color;
                optionElement.onclick = (e) => {
                    option.onclick(e);

                    if(document.activeElement != document.getElementById('newbonklobby_chat_input') && document.activeElement != document.getElementById('ingamechatinputtext'))
                        focusChat();
                    const input = isIngame()? ingameChatInput : chatInput;
                    input.selectionStart = input.value.length;
                    input.selectionEnd = input.value.length;
                };
                if (!self.callTarget)
                    self.callTarget = () => optionElement.click();

                let optionLeftText = document.createElement('td');
                optionLeftText.innerHTML = option.text;
                let optionRightText = document.createElement('td');
                optionRightText.textContent = option.rightText;
                let optionDesc = document.createElement('span');
                optionDesc.textContent = option.desc;

                optionLeftText.insertBefore(optionDesc, optionLeftText.firstElementChild);
                optionElement.appendChild(optionLeftText);
                optionElement.appendChild(optionRightText);

                self.#container.getElementsByTagName('TBODY')[0].appendChild(optionElement);
            });

        }
        else {
            this.#showing = false;
            this.optionsLength = 0;
            this.callTarget = null;
        }

        if (isIngame()) this.toIngame();
        else this.toLobby();

    }
    toIngame () {
        this.focus = -1;
        this.#container.classList.remove('bchat_helpcontainer');
        this.#container.classList.add('bchat_ingamehelpcontainer');

        // table
        this.#container.firstElementChild.classList.remove('bchat_helptable');
        this.#container.firstElementChild.classList.add('bchat_ingamehelptable');

        Array.from(this.#container.getElementsByTagName('TBODY')[0].rows).forEach( row => {
            row.cells[0].classList.remove('bchat_helpcontainer_optionleft');
            row.cells[0].classList.add('bchat_ingamehelpcontainer_optionleft');

            row.cells[1].classList.remove('bchat_helpcontainer_optionright');
            row.cells[1].classList.add('bchat_ingamehelpcontainer_optionright');

            row.cells[0].children[0].classList.remove('bchat_helpcontainer_optiondesc');
            row.cells[0].children[0].classList.add('bchat_ingamehelpcontainer_optiondesc');
        });
        ingameChatBox.appendChild(this.#container);
    }
    toLobby () {
        this.focus = -1;
        this.#container.classList.remove('bchat_ingamehelpcontainer');
        this.#container.classList.add('bchat_helpcontainer');

        // table
        this.#container.firstElementChild.classList.remove('bchat_ingamehelptable');
        this.#container.firstElementChild.classList.add('bchat_helptable');

        Array.from(this.#container.getElementsByTagName('TBODY')[0].rows).forEach( row => {
            row.cells[0].classList.remove('bchat_ingamehelpcontainer_optionleft');
            row.cells[0].classList.add('bchat_helpcontainer_optionleft');

            row.cells[1].classList.remove('bchat_ingamehelpcontainer_optionright');
            row.cells[1].classList.add('bchat_helpcontainer_optionright');

            let element = Array.from(row.cells[0].children).find( elem => elem.tagName == 'SPAN');
            element.classList.remove('bchat_ingamehelpcontainer_optiondesc');
            element.classList.add('bchat_helpcontainer_optiondesc');
        });
        chatBox.appendChild(this.#container);
    }
}
let helpContainer = new HelpContainer();

let commandBar = null;
const showCommandBar = (source, arrayOfText) => {
    hideCommandBar();
    if (arrayOfText.length == 0)
        return;

    let ingame = isIngame();
    commandBar = document.createElement('div');
    commandBar.id = ingame? 'bchat_ingamecommandbar' : 'bchat_commandbar';
    let textColor = ingame? 'white' : 'black';

    while (commandBar.firstChild)
        commandBar.removeChild(commandBar.firstChild);

    let sourceLabel = document.createElement('div');
    sourceLabel.id = 'bchat_commandbar_top';
    sourceLabel.textContent = source;
    sourceLabel.style.color = textColor;
    commandBar.appendChild(sourceLabel);

    arrayOfText.forEach( arrayElement => {
        let element = document.createElement('div');
        element.className = 'bchat_commandbar_text';
        element.textContent = arrayElement.text;
        element.style.color = arrayElement.color ?? textColor;
        element.style.opacity = arrayElement.opacity ?? 1;
        commandBar.appendChild(element);
    });
    if (ingame) ingameChatBox.appendChild(commandBar);
    else chatBox.appendChild(commandBar);
};
const hideCommandBar = () => {
    if (!commandBar)
        return;
    commandBar.remove();
    commandBar = null;
};
// emojify messages (emoji_name => emojiChar | unicodeChar)
const emojify = (text) => {
    let matches = text.match(new RegExp(`[\\ue100-\\ue1${customEmojis.length.toString().padStart(2, '0')}]`, 'g'));
    if (matches) {
        for (let i = 0; i < matches.length; i++) {
            text = text.replaceAll(matches[i][0], `:${customEmojis[matches[i].charCodeAt() - 57600].name}:`);
        }
    }
    return text;
}
// unemojify messages for context menu (unicodeChar => emoji_name)
const unemojify = (text) => {
    let matches = text.match(new RegExp(`[\\ue100-\\ue1${customEmojis.length.toString().padStart(2, '0')}]`, 'g'));
    if (matches) {
        for (let i = 0; i < matches.length; i++) {
            text = text.replaceAll(matches[i][0], `:~${customEmojis[matches[i].charCodeAt() - 57600].name}:`);
        }
    }
    return text;
}
// get unicode character of custom emoji by its ID
const name2UnicodeChar = name => {
    name = name.replaceAll(':', '').replaceAll('~', '');
    let ind = customEmojis.findIndex( emj => emj.name === name);
    if (ind == -1)
        return name;
    return unescape(`%u${(57600 + ind).toString(16)}`);
}


let bonklobbyObserver = new MutationObserver((mutationList) => {
    if (!isIngame() && document.getElementById('newbonklobby').style.display == 'block')
        onReturnLobby();
});
bonklobbyObserver.observe(document.getElementById('gamerenderer'), {attributes: true});

console.log('Better Chat run');