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.0beta3
// @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
// @require      https://cdnjs.cloudflare.com/ajax/libs/emojione/4.5.0/lib/js/emojione.min.js
// @run-at       document-end
// @grant        none
// ==/UserScript==


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 ingameChat = document.getElementById('ingamechatcontent');
const ingameChatBox = document.getElementById('ingamechatbox');
const ownEmojis = [];
const customEmojis = [];

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

let filter = true;

const addClass = (name, isIngame) => isIngame? 'bchat_msg_ingame' + name : 'bchat_msg_' + name;

const isIngame = () => document.getElementById('newbonklobby').style.display !== 'block';
const isHost = () => !document.getElementById('newbonklobby_startbutton').classList.contains('brownButtonDisabled');

function copyToClipboard (text) {
    navigator.clipboard.writeText(text).catch(err => {
        console.error('Failed to copy text: ' + text, err);
    });
}
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;
}
.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;
}
.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;
}
.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;
}
.bchat_msg_ingametime {
    font-size: 12px;
    color: #ffffff8d;
    user-select: none;
}
.bchat_msg_ingamereply {
    font-family: "futurept_b1";
    margin-bottom: 1px;
}
.bchat_msg_ingamereplyhref {
    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 {
    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);
}

/* CONTEXT MENU */



#pretty_bottom {
    display: none;
}
.newbonklobby_chat_image_container {
    padding: 4px 0;
    margin-left: 28px;
    display: block;
}
.newbonklobby_chat_image {
    max-height: 100px;
    max-width: calc(100% - 40px);
    border-radius: 10px;
    cursor: pointer;
}
.newbonklobby_chat_image_showbutton {
    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;
    bottom: 5px;
}
.newbonklobby_chat_image_showbutton:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
.newbonklobby_chat_image_hidebutton {
    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='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: 3px;
    right: 17px;
    border-radius: 5px;
}
.newbonklobby_chat_image_hidebutton:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
#imagepreviewcontainer {
    width: 100%;
    height: 100%;
    position: absolute;
    visibility: hidden;
    z-index: 2;
}
#imagepreviewbehindblocker {
    width: 100%;
    height: 100%;
    position: absolute;
    background-color: rgba(0, 0, 0, 0.70);
}
#imagepreview {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    max-width: 80%;
    max-height: 80%;
}
.imagepreviewbutton {
    cursor: pointer;
    position: absolute;
    top: 50px;
    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;
}
.imagepreviewbutton:hover {
    background-color: rgba(90, 90, 90, 0.33);
}
#imagepreview_close {
    right: 40px;
    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");
}
#imagepreview_opennewtab {
    right: 100px;
    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");
}
#imagepreview_link {
    right: 150px;
    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");
}
#imagepreview_infocontainer {
    position: absolute;
    max-width: 50%;
    top: 50px;
    left: 40px;
    color: #dbdbdb;
    font-family: "futurept_b1";
    text-shadow: 1px 1px 3px black;
}
#newbonklobby_chat_actionmenu {
    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;
}
.newbonklobby_chat_actionmenu_button {
    margin-top: 5px;
    width: 145px;
}
.newbonklobby_chat_actionmenu_overline {
    margin-top: 5px;
    width: 145px;
    height: 1px;
    background-color: #a5acb0;
}
.newbonklobby_chat_msgselected {
    background-color: rgba(13, 125, 120, 0.12) !important;
}
#ingamechatcontent {
    pointer-events: all;
    overflow-y: scroll;
    max-height: 125px;
    margin-right: 2px;
}
#ingamechatbox {
    height: 152px;
    overflow: visible;
}
#ingamechatcontent::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
#ingamechatcontent::-webkit-scrollbar-track {
    background-color: transparent;
}
#ingamechatcontent::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.ingamechatname, .ingamechatmessage {
    user-select: text;
}
.ingamechatreplytext {
    display: block;
    user-select: text;
    margin: 5px 0 -3px 15px;
    text-decoration: none;
}
.ingamechatreplyhref {
    font-size: 12px;
    color: #abababba;
    text-decoration: none;
}
.ingamechatreplyhref:hover {
    text-decoration: underline;
}
.ingamechattime {
    font-size: 12px;
    color: #ffffff8d;
    margin-left: 3px;
}
.ingamechatselected {
    background-color: rgba(13, 125, 120, 0.12) !important;
}
#ingamechatactionmenu {
    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);
}
.ingamechatactionmenuoverline {
    margin-top: 5px;
    width: 145px;
    height: 1px;
    background-color: white;
}
.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;
}
.ingamechatbutton:hover {
    background-color: rgba(0, 0, 0, 0.4);
}
.ingamechatbuttondisabled {
    background-color: rgba(209, 82, 82, 0.3);
    pointer-events: none !important;
}
.ingamechatimagecontainer {
    padding: 4px 0;
    display: block;
}
.ingamechatimage {
    max-height: 70px;
    max-width: calc(70% - 40px);
    border-radius: 6px;
    cursor: pointer;
}
.ingamechatimageshowbutton {
    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;
}
.ingamechatimageshowbutton:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
.ingamechatimagehidebutton {
    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='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: 3px;
    right: 17px;
    border-radius: 5px;
}
.ingamechatimagehidebutton:hover {
    background-color: rgba(100, 100, 100, 0.2);
}
.newbonklobby_chat_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);
}
.newbonklobby_chat_helpcontainer::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
.newbonklobby_chat_helpcontainer::-webkit-scrollbar-track {
    background-color: transparent;
    margin-top: 5px;
}
.newbonklobby_chat_helpcontainer::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.newbonklobby_chat_helptable, .ingamechathelptable {
    border-collapse: collapse;
    font-family: "futurept_b1";
    width: 97%;
    margin-left: 2.5%;
}
.newbonklobby_chat_helptable tr:nth-last-child(n + 2) {
	border-bottom: 1px solid #c2ccd1;
}
.ingamechathelptable tr:nth-last-child(n + 2) {
	border-bottom: 1px solid white;
}
.newbonklobby_chat_helptable tr, .ingamechathelptable tr {
    cursor: pointer;
    font-size: 14px;
    height: 30px;
}
.newbonklobby_chat_helptable tr:hover {
    background-color: rgba(100,100,100,0.10);
}
.ingamechathelptable tr:hover {
    background-color: rgba(0, 0, 0, 0.2);
}
.newbonklobby_chat_helpoption {
    padding: 5px;
    padding-left: 10px;
}
.newbonklobby_chat_helpoptiondesc {
    display: block;
    color: #4e4e4e;
    font-size: 12px;
    pointer-events: none;
}
.newbonklobby_chat_helpoptionright {
    padding-right: 10px;
    text-align: right;
    color: #4e4e4e;
    width: 35%;
}
.ingamechathelpcontainer {
    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;
}
.ingamechathelpcontainer::-webkit-scrollbar {
    background-color: transparent;
    width: 8px;
}
.ingamechathelpcontainer::-webkit-scrollbar-track {
    background-color: transparent;
    margin-top: 5px;
}
.ingamechathelpcontainer::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.18);
    border-radius: 4px;
}
.ingamechathelpoption {
    color: white;
    padding: 5px;
    padding-left: 10px;
}
.ingamechathelpoptiondesc {
    display: block;
    color: #e3e3e3a0;
    font-size: 12px;
    pointer-events: none;
}
.ingamechathelpoptionright {
    padding-right: 10px;
    text-align: right;
    color: #e3e3e3a0;
    width: 35%;
}
.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'
window.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.firstChild) {
            element.removeChild(element.firstChild);
        }
        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", "씨발", "ㅆㅂ", "개새", "십새", "섹스", "개자식", "미친새끼", "미친년", "뻑큐", "뻐큐", "쌍년", "쌍놈", "쌍넘", "쉑", "지랄", "창녀", "지럴", "좁밥", "좆", "좃까", "섹", "병신", "부랄", "ㅄ", "ㅂㅅ", "ㅈㄹ", "ㄳㄲ", "ㄱㅅㄲ", "불알", "븅", "새꺄", "새갸"];

function 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
    };
};

function 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 */
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 Bytebuffer.textDec.decode(s_d[9]);
    }
    writeUTF(E_0) {
        var P0R = [arguments];
        P0R[5] = Bytebuffer.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 = Bytebuffer.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 = Bytebuffer.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 = Bytebuffer.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 = Bytebuffer.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 */
function 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;
}

function 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();
}
/*
{
    name: 'test',
    description: 'prints test',
    parameters: ['"parameter"'],
    source: 'Bonk Chat',
    callback: function(param) {
        showStatusMessage(`* test [${param}]`);
    }
}
*/

if(!window.chatCommands) window.chatCommands = [];
window.chatCommands = window.chatCommands.concat([
    {
        name: 'help',
        description: 'Get started',
        source: 'Bonk Chat',
        callback: function() {
            window.chatCommands.forEach( cmd => {
                if(cmd.builtin && (cmd.hostOnly? hostId == selfId : true)) {
                    showStatusMessage(`/${cmd.name}${cmd.parameters? ' ' + cmd.parameters.join(' ') : ''}`);
                }
            })
            showStatusMessage('All aviable commands are listed above');
        }
    },

    {
        name: 'clear',
        description: 'Clears chat history',
        builtin: true
    },
    {
        name: 'balance',
        parameters: ['"user name"', '-100 to 100'],
        description: 'Balances a specific player',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'roomname',
        parameters: ['"New Room Name"'],
        description: 'Sets new room name',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'roompass',
        parameters: ['"New Room Name"'],
        description: 'Sets new room password',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'clearroompass',
        description: 'Clears room password',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'kick',
        parameters: ['"user name"'],
        description: 'Kicks a specific player, player can join the same room after being kicked',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'ban',
        parameters: ['"user name"'],
        description: 'Bans a specific player',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'mute',
        parameters: ['"user name"'],
        description: 'Mutes a specific player, wont be muted for everyone',
        builtin: true
    },
    {
        name: 'unmute',
        parameters: ['"user name"'],
        description: 'Unmutes a specific player, wont be unmuted for everyone',
        builtin: true
    },
    {
        name: 'move',
        parameters: ['"user name"', 'ffa or spec'],
        description: 'Moves the player to ffa or spec',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'lock',
        description: 'Locks teams',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'unlock',
        description: 'Unlocks teams',
        hostOnly: true,
        builtin: true
    },
    {
        name: 'fav',
        description: 'Favourites current map',
        builtin: true
    },
    {
        name: 'unfav',
        description: 'Unfavouritess current map',
        builtin: true
    },
    {
        name: 'curate',
        parameters: ['"comment" (optional)'],
        description: 'Curates current map, ignore this if you are not curator',
        builtin: true
    },
    {
        name: 'curateyes',
        description: 'Confirms current map curation, ignore this if you are not curator',
        builtin: true
    },
    {
        name: 'curateno',
        description: 'Cancels current map curation, ignore this if you are not curator',
        builtin: true
    },
]);

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 @user',
        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(`42[9,{"banshortid":${id},"kickonly":true}]`);
                closeContextMenu();
            }
        }
    },
    {
        name: 'ban',
        text: 'Ban @user',
        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(`42[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 );

function openContextMenu (content, actions, position) {
    const addClass = function (name) {
        return isIngame()? 'bchat_msg_ingame' + name : 'bchat_msg_' + name
    }
    closeContextMenu();
    /*legacy*/if (isIngame()) content.classList.add('ingamechatselected');
    contextMenu = document.createElement('div');
    contextMenu.id = isIngame()? 'ingamechatactionmenu' : 'newbonklobby_chat_actionmenu';
    contextMenu.oncontextmenu = () => false;
    if (isIngame()) {
        document.getElementById('ingamechatbox').appendChild(contextMenu);
        // Prevent ingame chat from hiding
        let callback = (mutation) => {
            if (mutation[0].target.style.visibility == 'hidden') {
                hideChatAfterClose = true;
                mutation[0].target.style.visibility = 'inherit';
            }
            observer.disconnect();
        }
        let observer = new MutationObserver(callback);
        observer.observe(document.getElementById('ingamechatbox'), {attributes: true});
    }
    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');

            if (isIngame()) line.classList.add('ingamechatactionmenuoverline');
            else line.classList.add('newbonklobby_chat_actionmenu_overline');

            contextMenu.appendChild(line);
        }
        else {
            const button = document.createElement('div');
            button.textContent = action.text.replaceAll(/(?<!\\)@user/g, content.author);

            if (isIngame()) {
                button.classList.add('ingamechatbutton');
                if (action.host && !isHost())
                    button.classList.add('ingamechatbuttondisabled');
            }
            else {
                button.classList.add('brownButton');
                button.classList.add('brownButton_classic');
                button.classList.add('buttonShadow');
                button.classList.add('newbonklobby_chat_actionmenu_button');
                button.classList.add('newbonklobby_chat_actionmenu_buttonoverlined');
                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;
    }
}

function closeContextMenu () {
    if (contextMenu) contextMenu.parentNode.removeChild(contextMenu);
    contextMenu = null;
    if (hideChatAfterClose) {
        hideChatAfterClose = false;
        document.getElementById('ingamechatbox').style.visibility = 'hidden';
    }
    [...ingameChat.children].forEach( element => element.classList.remove('ingamechatselected') );
}
// information about packets (Aug 25, 2023): https://github.com/UnmatchedBracket/DemystifyBonk/blob/main/Packets.md

let send = function() {};
let playerList = null;
let hostId = -1;
let selfId = -1;
let selfAvatar = null;

let injected = false;
let originalSend = 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;
        }
        if (!injected) {
            send = (args) => {
                this.send(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) {
                    generatedSkins = [];
                    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;
                }
                else if (packet[0] == 3) {
                    generatedSkins = [];
                    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++) {
                            const sendPacket = {
                                type: 'bonkchat:emj',
                                action: 'info',
                                to: packet[1],
                                emjs: chunks[i],
                                last: i + 1 == chunks.length? true : false
                            };
                            send(`42[4,${JSON.stringify(sendPacket)}]`);
                        }
                    }
                }
                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] == 20) {
                    try {
                        onChatMessage(packet[1], packet[2]);
                    } catch (e) {
                        console.error(e)
                    }
                    // prevent from appending normal bonk message
                    return;
                }
                else if (packet[0] == 41) {
                    hostId = packet[1].newHost;
                }
                return originalReceive.call(this, args);
            };
            let originalClose = this.onclose;
            this.onclose = function(args){
                injected = false;
                while(typeof customEmojis[0] != 'undefined') customEmojis.pop();
                return originalClose.call(this, args);
            };
        }
    }
    return originalSend.call(this, args);
};
/*let originalXMLOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(_, url) {
    this.betterChat = {};
    if (url.includes("login_legacy.php") || url.includes("login_auto.php")) {
        this.betterChat.login = true;
    }
    originalXMLOpen.apply(this, arguments);
};
*/
let originalXMLSend = 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) {}
    originalXMLSend.apply(this, arguments);
};
let imagePreviewContainer = document.createElement('div');
imagePreviewContainer.id = 'imagepreviewcontainer';
let imagePreviewBehindBlocker = document.createElement('div');
imagePreviewBehindBlocker.id = 'imagepreviewbehindblocker';
// image preview close button
let imagePreviewClose = document.createElement('div');
imagePreviewClose.classList.add('imagepreviewbutton');
imagePreviewClose.id = 'imagepreview_close';
imagePreviewBehindBlocker.onclick = imagePreviewClose.onclick = function () {
    window.anime({
        targets: imagePreviewContainer,
        opacity: 0,
        duration: 100,
        easing: "easeOutCubic",
        complete: () => {
            imagePreviewContainer.style.visibility = 'hidden';
        }
    });
    let imagePreview = document.getElementById('imagepreview');
    if(!imagePreview) return;
    window.anime({
        targets: imagePreview,
        scale: 0.8,
        duration: 100,
        easing: "easeOutCubic",
    });
};
// open in a new tab button
let imagePreviewTab = document.createElement('div');
imagePreviewTab.classList.add('imagepreviewbutton');
imagePreviewTab.id = 'imagepreview_opennewtab';
// '<a>' element just to have 'target' attribute
let tabA = document.createElement('a');
tabA.target = '_blank';
imagePreviewTab.onclick = () => tabA.click();

let imagePreviewLink = document.createElement('div');
imagePreviewLink.classList.add('imagepreviewbutton');
imagePreviewLink.id = 'imagepreview_link';
imagePreviewLink.onclick = () => {
    const text = document.getElementById('imagepreview').src;

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

    imagePreviewLink.style.backgroundColor = 'rgba(192, 192, 192, 0.33)';
    window.anime({
        targets: imagePreviewLink,
        backgroundColor: 'rgba(64, 64, 64, 0.33)',
        duration: 800,
        easing: "easeOutCubic",
        complete: () => imagePreviewLink.removeAttribute('style')
    });
};
let imageInfo = document.createElement('div');
imageInfo.id = 'imagepreview_infocontainer';

imagePreviewContainer.appendChild(imagePreviewBehindBlocker);
imagePreviewContainer.appendChild(imagePreviewClose);
imagePreviewContainer.appendChild(imagePreviewTab);
imagePreviewContainer.appendChild(imagePreviewLink);
imagePreviewContainer.appendChild(imageInfo);

document.getElementById('newbonkgamecontainer').appendChild(imagePreviewContainer);

function openImagePreview (image) {
    imagePreviewContainer.style.visibility = 'inherit';
    imagePreviewContainer.style.opacity = '0';
    window.anime({
        targets: imagePreviewContainer,
        opacity: 1,
        duration: 175,
        easing: "easeOutCubic",
    });
    if(document.getElementById('imagepreview')) document.getElementById('imagepreview').remove();
    let imagePreview = image.cloneNode();
    imagePreview.classList.remove('newbonklobby_chat_image');
    imagePreview.id = 'imagepreview';
    imagePreviewContainer.insertBefore(imagePreview, imagePreviewClose);
    imagePreview.style.transform = 'scale(0.8)';
    window.anime({
        targets: imagePreview,
        scale: 1,
        duration: 175,
        easing: "easeOutCubic",
    });
    tabA.href = image.src;

    while(imageInfo.firstChild) imageInfo.removeChild(imageInfo.firstChild);
    let info = [
        'Link: ' + image.src,
        'Name: ' + image.src.match(/\/[a-zA-Z0-9_%-]{1,}(?:\.[a-zA-Z0-9_%-]{1,})?(?=[^.a-zA-z0-9_%\-]|$)/g).reverse()[0].substring(1),
        'Size: ' + image.naturalWidth + 'x' + image.naturalHeight
    ];
    info.forEach( info => {
        let div = document.createElement('div');
        div.textContent = info;
        imageInfo.appendChild(div);
    });
}
function focusChat() {
    document.activeElement = null;
    let event = document.createEvent("HTMLEvents");
    event.initEvent('keydown');
    event.keyCode = 13;
    document.dispatchEvent(event);
}

document.getElementById('newbonklobby_chat_input').oninput = function (event) {
    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});

    const matchEmoji = before.match(/(?<=:)[a-z0-9_~]+/g)?.reverse()[0];

    const matchPing = before.match(/(?<=@)[^@]*/g)?.reverse()[0];
    let players = typeof matchPing == 'string'? JSON.parse(JSON.stringify(playerList.filter( x => x.userName.includes(matchPing) ))) : [];

    if(matchEmoji && before.lastIndexOf(matchEmoji) + matchEmoji.length == index) {
        const emojis = emojione.shortnames.split('|').filter( x => x.includes(matchEmoji) && emojione.emojioneList[x]).slice(0, 24);
        const roomEmojis = customEmojis.map( (x, id) => {
            return {
                text: `<img src="${x.data}" style="width: 20px;vertical-align: middle;"> :${x.name}:`,
                rightText: 'custom',
                name: x.name,
                onclick: () => {
                    event.target.value = `${before.slice(0, - matchEmoji.length)}${x.name}: ${remain}`;
                    hideHelpContainer();
                    chatInput();
                }
            };
        }).filter( x => x.name.includes(matchEmoji) );
        if(emojis.length == 0 && roomEmojis.length == 0) {
            hideHelpContainer();
            return;
        }
        let normalEmojis = emojis.map( x => {
            return {
                text: `${emojione.shortnameToUnicode(x)} ${x}`,
                onclick: () => {
                    event.target.value = `${before.slice(0, - matchEmoji.length - 1)}${emojione.shortnameToUnicode(x)} ${remain}`;
                    hideHelpContainer();
                    chatInput();
                }
            };
        });
        showHelpContainer(roomEmojis.concat(normalEmojis), isIngame());
    }
    else if(players.length > 0) {
        players = players.map( player => {
            let name = player.userName;
            let skin = document.createElement('div');
            let id = playerList.findIndex( player => player.userName == name );
            if (generatedSkins24[id]) {
                skin.appendChild(generatedSkins24[id].cloneNode(true));
            } else {
                try {
                    window.createChatSkinImage(player.avatar, skin, '', 28, id, generatedSkins24, 2, 0.1, 0.2);
                    skin.children[0].style.verticalAlign = 'middle';
                    skin.children[0].style.margin = '0 5px 0 -5px';
                } catch (err) {}
            }
            skin.innerHTML += '@' + name;
            return {
                text: skin.innerHTML,
                name: name,
                onclick: () => {
                    event.target.value = `${before.match(/(?:.*?@)*/)[0].slice(0, -1)}@${name} ${remain}`;
                    hideHelpContainer();
                    chatInput();
                }
            };
        });
        showHelpContainer(players, isIngame());
    }
    else if(text.startsWith('/')) {
        function getHelpOptions(name) {
            return Array.from(window.chatCommands.map(function (x, id) {
                const command = {
                    text: `/${x.name} ${x.parameters? x.parameters.join(' ') : ''}`,
                    rightText: x.builtin? 'built-in' : x.source,
                    desc: x.description ?? '',
                    name: x.name,
                    hostOnly: x.hostOnly,
                    onclick: () => {
                        let match = event.target.value.match(/.*? (?<a>.*)/);
                        let args = match? match.groups.a : (event.target.value.startsWith('/')? remain : '');
                        event.target.value = `/${x.name}${x.parameters && x.parameters.length > 0? ' ' + args : '' + args}`;
                        targetCommandId = id;
                        showHelpContainer([command], isIngame());
                    }
                };
                return command;
            }).filter( x => x.name.includes(name) && (x.hostOnly?  isHost() : true))).reverse();
        };
        if(text.includes(' ')) {
            if(helpContainer && helpContainer.children.length == 1) return;
            const command = [getHelpOptions(before.split(' ')[0].substring(1))[0]];
            showHelpContainer(command, isIngame());
            return;
        }
        else if(text.length <= 1) {
            hideHelpContainer();
            return;
        }
        const commands = getHelpOptions(before.substring(1));
        showHelpContainer(commands, isIngame());
    }
    else if(helpContainer) hideHelpContainer();
}
document.getElementById('ingamechatinputtext').oninput = document.getElementById('newbonklobby_chat_input').oninput;

const originalKeyDown = document.getElementById('newbonklobby_chat_input').onkeydown ?? new Function();
document.getElementById('newbonklobby_chat_input').onkeydown = function(event) {
    if (event.code == 'Space' && helpContainer) {
        if (helpFocus == -1) {
            helpFocus = 0;
        }
        helpContainer.children[0].children[0].children[helpFocus].click();
        return;
    }
    else if (event.code == 'Enter' && event.target.value.startsWith('/')) {
        if (helpContainer)
            helpContainer.children[0].children[0].children[Math.max(0, helpFocus)].click();
        hideHelpContainer();
        event.target.value = processCommand(event.target.value);
    }
    else if (event.code == 'Enter' && helpContainer && helpFocus != -1) {
        helpContainer.children[0].children[0].children[helpFocus].click();
        document.getElementById('newbonklobby_chat_input').blur();
        document.getElementById('ingamechatinputtext').blur();
    }
    else if (event.code == 'Enter' && event.target.value.length > 0) {
        hideHelpContainer();
        // replace :name: with emoji
        // unfinished //~
        let matches = event.target.value.match(/(?<!\\):[a-z0-9_~]{1,256}:/g);
        if (matches) {
            matches = matches.map( x => {
                return {
                    name: x.slice(1, -1),
                    forceCustom: false,
                    // ce - Custom Emoji
                    ceId: customEmojis.findIndex( emj => emj.name == x )
                }
            } );
            for (let id = 0; id < ids.length; id++) {
                //console.log(matches.ceId[id])
                if (matches.ceId[id] != -1) {
                    event.target.value = event.target.value.replace(`:${matches[id]}:`, getUnicodeChar(matches.ceId[id])).trim();
                } else {}
            }
        }
    }
    originalKeyDown.call(this, event);
};
document.getElementById('ingamechatinputtext').onkeydown = document.getElementById('newbonklobby_chat_input').onkeydown;

function processCommand (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);
            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);
        }
    }

    let name = splited.shift().substring(1);
    let command;
    if(targetCommandId > -1) command = window.chatCommands[targetCommandId];
    else command = window.chatCommands.find( x => x.name == name );

    targetCommandId = -1;

    if (command && !command.builtin) {
        try {
            command.callback(...splited);
        }
        catch (error) {
            showStatusMessage(`* Something went wrong... [Processing /${command.name}]`);
            console.error(error);
        }
        return '';
    }
    return value;
}

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

function 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
    }
}

function 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();
    }
}

function 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.lastChild.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;
}

// create emoji image elements in element's innerHTML
function 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>`);
        }
    }
}

function appendlobbyChatMessage (id, player, messageText, notified) {
    let prevMessage = chat.lastChild;
    let prevAuthor = false;
    if (prevMessage && prevMessage.author == player.userName)
        prevAuthor = 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 {
            window.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';
    if (!notified && canBeNotified && reply.message.includes('@' + player.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;

        if (!notified && canBeNotified && reply.reply.author == player.userName) {
            new Notification(reply.message);
            notified = true;
        }
    }
    else if (prevAuthor) {
        skinbox.style.display = 'none';
        name.style.display = 'none';
    }

    message.isMessage = true;
    let scrollDown = chat.scrollTop + chat.clientHeight >= chat.scrollHeight - 5;
    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);

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

    return notified;
}

function 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';
    if (!notified && canBeNotified && reply.message.includes('@' + player.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;

        if (!notified && canBeNotified && reply.reply.author == player.userName) {
            new Notification(reply.message);
            notified = true;
        }
    }

    message.isMessage = true;
    let scrollDown = ingameChat.scrollTop + ingameChat.clientHeight >= ingameChat.scrollHeight - 5;
    ingameChat.appendChild(message);

    emojisToImages(text);
    emojisToImages(replyMessage);

    if (scrollDown) {
        ingameChat.scrollTop = ingameChat.scrollHeight;
    }

    return notified;
}

window.showStatusMessage = function (text, color = '#b53030', ingame) {
    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 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 () {
    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.visibility = 'hidden';
    };
    message.show = function () {
        message.style.visibility = 'inherit';
    };
    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 = '';
            }
        });
    };
    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,
                /* line */ actions.line,
                actions.copyname,
                actions.ping
            ];
        }
        return;
    }

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

originalAppendChild = ingameChat.appendChild;
ingameChat.appendChild = function (args) {
    originalAppendChild.call(this, args);
    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.visibility = 'hidden';
    };
    message.show = function () {
        message.style.visibility = 'inherit';
    };
    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 = '';
            }
        });
    };

    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('ingamechatstatus')) {
        let text = message.textContent;
        if(text.startsWith('* ') && text.endsWith(' has joined the game.')) {
            message.actions = [
                actions.kick,
                actions.ban,
                /* line */ actions.line,
                actions.copyname,
                actions.ping
            ];
        }
        return;
    }

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

    ingameChatBox.hideTimestamp = Date.now() + 12000;
    setTimeout(function () {
        ingameChatBox.style.visibility = 'hidden';
    }, 12000);
}

let ingameChatObserver = new MutationObserver(function (mutationList) {
    if (mutationList.type == 'attributes' && ingameChatBox.style.visibility == 'hidden' && ingameChatBox.hideTimestamp > Date.now()) {
        ingameChatBox.style.visibility = 'inherit';
    }
});
ingameChatObserver.observe(ingameChatBox, {attributes: true});
// emojify messages (emoji_name => emojiChar | unicodeChar)
function 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)
function 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
function getUnicodeChar (id) {
    let a = 57600 + id;
    if(isNaN(a)) return '';
    return unescape(`%u${a.toString(16)}`);
}

let helpContainer = null;
let helpFocus = -1;

function helpTableNavigate (e) {
    const children = [...helpContainer.children[0].children[0].children];
    const ingame = helpContainer.className == 'ingamechathelpcontainer';
    const last = children.length - 1;
    helpFocus = Math.min(last, Math.max(-1, helpFocus));
    if(helpFocus != -1) children[helpFocus].removeAttribute('style');
    if(e.key == 'ArrowUp') {
        e.preventDefault();
        if(helpFocus == 0 || helpFocus == -1) helpFocus = last;
        else helpFocus--;

        if(ingame) children[helpFocus].style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
        else children[helpFocus].style.backgroundColor = 'rgba(100,100,100,0.10)';
    }
    else if(e.key == 'ArrowDown') {
        e.preventDefault();
        if(helpFocus == last) helpFocus = 0;
        else helpFocus++;

        if(ingame) children[helpFocus].style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
        else children[helpFocus].style.backgroundColor = 'rgba(100,100,100,0.10)';
    }
}

let targetCommandId = -1;

function showHelpContainer (optionsList, ingame) {
    hideHelpContainer();
    helpFocus = -1;
    helpContainer = document.createElement('div');
    let helpTable = document.createElement('table');
    let tbody = document.createElement('tbody');
    if(ingame) {
        helpContainer.className = 'ingamechathelpcontainer';
        helpTable.className = 'ingamechathelptable';
        document.getElementById('ingamechatbox').appendChild(helpContainer);
    }
    else {
        helpContainer.className = 'newbonklobby_chat_helpcontainer';
        helpTable.className = 'newbonklobby_chat_helptable';
        document.getElementById('newbonklobby_chatbox').appendChild(helpContainer);
    }

    helpContainer.appendChild(helpTable);
    helpTable.appendChild(tbody);

    for(let i in optionsList) {
        let option = optionsList[i];
        let optionElement = document.createElement('tr');
        if(option.color) 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 chatInput = document.getElementById('newbonklobby_chat_input');
            chatInput.selectionStart = chatInput.value.length;
            chatInput.selectionEnd = chatInput.value.length;
        };
        let leftText = document.createElement('td');
        leftText.className = ingame? 'ingamechathelpoption' : 'newbonklobby_chat_helpoption';
        leftText.innerHTML = option.text;
        optionElement.appendChild(leftText);
        if(option.rightText) {
            let rightText = document.createElement('td');
            rightText.className = ingame? 'ingamechathelpoptionright' : 'newbonklobby_chat_helpoptionright';
            rightText.textContent = option.rightText;
            optionElement.appendChild(rightText);
        }
        if(option.desc && typeof option.desc === 'string' && option.desc.length > 0) {
            let desc = document.createElement('span');
            desc.className = ingame? 'ingamechathelpoptiondesc' : 'newbonklobby_chat_helpoptiondesc';
            desc.textContent = option.desc;
            leftText.appendChild(desc);
        }
        helpContainer.getElementsByTagName('TBODY')[0].appendChild(optionElement);
    }

    document.addEventListener('keydown', helpTableNavigate);
}

function hideHelpContainer () {
    if(!helpContainer) return;
    let tbody = helpContainer.getElementsByTagName('TBODY')[0];
    while(tbody.firstChild) tbody.removeChild(tbody.firstChild);
    if(helpContainer) {
        helpContainer.parentNode.removeChild(helpContainer);
        helpContainer = null;
    }
    document.removeEventListener('keydown', helpTableNavigate);
}

/*
let bonklobbyObserver = new MutationObserver((mutationList) => {
    [...chat.children].forEach( element => element.classList.remove('newbonklobby_chat_msgselected') );
    [...ingameChat.children].forEach( element => element.classList.remove('ingamechatselected') );
    closeContextMenu();
});
bonklobbyObserver.observe(document.getElementById('newbonklobby'), {attributes: true});*/

console.log('Better Chat run');