Greasy Fork is available in English.

FlatMMOPlus

FlatMMO plugin framework

لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.greasyfork.org/scripts/544062/1878469/FlatMMOPlus.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

Advertisement:

// ==UserScript==
// @name         FlatMMOPlus
// @namespace    com.dounford.flatmmo
// @version      1.5.1
// @description  FlatMMO plugin framework
// @author       Dounford adapted from Anwinity IPP
// @match        *://flatmmo.com/play.php*
// @grant        none
// ==/UserScript==

(function() {
	'use strict';
	const VERSION = "1.5.1";

    Set.prototype.some = function(predicate) {
        for (const item of this) {
            if (predicate(item)) {
                return true;
            }
        }
        return false;
    };

    Set.prototype.toggle = function(item) {
        if (this.has(item)) {
            this.delete(item)
        } else {
            this.add(item)
        }
    };

	const LOCAL_STORAGE_KEY_DEBUG = "FlatMMOPlus:debug";
 
    const CONFIG_TYPES_LABEL = ["label"];
    const CONFIG_TYPES_PANEL = ["panel"];
    const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
    const CONFIG_TYPES_INTEGER = ["integer", "int"];
    const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
    const CONFIG_TYPES_RANGE = ["range"];
    const CONFIG_TYPES_STRING = ["string", "text"];
    const CONFIG_TYPES_SELECT = ["select"];
    const CONFIG_TYPES_COLOR = ["color"];
    const CONFIG_TYPES_LIST = ["list", "array"];
    const CONFIG_TYPES_RELATION = ["relation", "key", "object"];
    const CONFIG_TYPES_BUTTON = ["button", "btn"];
    const CONFIG_TYPES_PRIORITY = ["sort", "priority"];


	const CHAT_COMMAND_NO_OVERRIDE = ["help"];
    const defaultHotkeys = [
        //Default F keys
        {
            key: "F1",
            name: "RUN!",
            description: "Toggle Run",
            category: "actions",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F1")
        },
        {
            key: "F2",
            name: "Eat",
            description: "Consumes a piece of food",
            category: "actions",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F2")
        },
        {
            key: "F3",
            name: "Fire",
            description: "Lights a fire",
            category: "actions",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F3")
        },
        {
            key: "F4",
            name: "Dodge",
            description: "Activates dodge ability",
            category: "actions",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F4")
        },
        //Equipment
        {
            key: "F6",
            name: "Preset 1",
            description: "Equips Preset 1",
            category: "equipments",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F6")
        },
        {
            key: "F7",
            name: "Preset 2",
            description: "Equips Preset 2",
            category: "equipments",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F7")
        },
        {
            key: "F8",
            name: "Preset 3",
            description: "Equips Preset 3",
            category: "equipments",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F8")
        },
        //Badges
        {
            key: "F9",
            name: "Badge 1",
            description: "Activates Badge 1",
            category: "badges",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F9")
        },
        {
            key: "F10",
            name: "Badge 2",
            description: "Activates Badge 2",
            category: "badges",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F10")
        },
        {
            key: "F11",
            name: "Badge 3",
            description: "Activates Badge 3",
            category: "badges",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F11")
        },
        {
            key: "F12",
            name: "Badge 4",
            description: "Activates Badge 4",
            category: "badges",
            func: () => window.FlatMMOPlus.sendMessage("SHORTCUT_KEY=F12")
        },
        //Teleports
        {
            key: "N/A",
            name: "Everbrook Teleport",
            description: "Teleports to Everbrook",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_everbrook")
        },
        {
            key: "N/A",
            name: "Mystic Vale Teleport",
            description: "Teleports to Mystic Vale",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_mysticvale")
        },
        {
            key: "N/A",
            name: "Omboko Teleport",
            description: "Teleports to Omboko",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_omboko")
        },
        {
            key: "N/A",
            name: "Dock Haven Teleport",
            description: "Teleports to Dock Haven",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_dock_haven")
        },
        {
            key: "N/A",
            name: "Jafa Teleport",
            description: "Teleports to Jafa",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_jafa_outpost")
        },
        {
            key: "N/A",
            name: "Frostvale Teleport",
            description: "Teleports to Frostvale",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=teleport_frostvale")
        },
        {
            key: "N/A",
            name: "Stuck",
            description: "Teleports to /stuck",
            category: "teleports",
            func: () => window.FlatMMOPlus.sendMessage("CHAT=/stuck")
        },
        //Worship
        {
            key: "N/A",
            name: "Remote Sell",
            description: "Opens Sell Menu",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=remote_sell")
        },
        {
            key: "N/A",
            name: "Dig",
            description: "Dig",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=dig")
        },
        {
            key: "N/A",
            name: "Timers",
            description: "Opens Timers Menu",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=timers")
        },
        {
            key: "N/A",
            name: "Hell Burrying",
            description: "Burries your bones",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=auto_hell_burying")
        },
        {
            key: "N/A",
            name: "Hunting Contact",
            description: "Opens Hunting Perks Menu",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=hunting_contact")
        },
        {
            key: "N/A",
            name: "Mass Pickup",
            description: "Picks all your items",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=mass_pickup")
        },
        {
            key: "N/A",
            name: "Focus",
            description: "Increases Damage by 5 and Accuracy by 2",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=focus")
        },
        {
            key: "N/A",
            name: "Clarity",
            description: "Increases Damage by 15 and Accuracy by 5",
            category: "worship",
            func: () => window.FlatMMOPlus.sendMessage("USE_WORSHIP=clarity")
        },
        //Gamehotkeys
        {
            key: "Enter",
            name: "Enter",
            description: "Enter",
            private: true,
            func: () => enter_pressed()
        },
        {
            key: "Backspace",
            name: "Backspace",
            description: "Backspace",
            private: true,
            repeat: true,
            func: (e) => {
                //It doesn't need to check the activeElement because it won't run on input/textarea
                if(chat_ele.value.trim().length > 0) {
                    chat_ele.value = chat_ele.value.substring(0, chat_ele.value.length - 1);
                }
                if(chat_ele.value.trim().length == 0) {
                    request_unfocus_chatbox();
                }
            }
        },
        {
            key: "Escape",
            name: "Escape",
            description: "Escape",
            private: true,
            func: (e) => {
                let closed = false;
                for (const s of opened_modals) {
                    close_modal(s);
                    closed = true;
                }
                if(!closed) {
                    close_bank();
                    close_global_market();
                }
            }
        },
        //Scrolls
        {
            key: "N/A",
            name: "Chef's House Scroll",
            description: "Uses a scroll to teleport to Chef's House",
            category: "Teleport Book",
            func: () => {
                window.FlatMMOPlus.sendMessage(`TELE_BOOK=chefs_house_teleport_scroll`);
            }
        },
        {
            key: "N/A",
            name: "Thieves Hideout Scroll",
            description: "Uses a scroll to teleport to Thieves Hideout",
            category: "Teleport Book",
            func: () => {
                window.FlatMMOPlus.sendMessage(`TELE_BOOK=thieves_hideout_teleport_scroll`);
            }
        },
        {
            key: "N/A",
            name: "Greenhouse Scroll",
            description: "Uses a scroll to teleport to Greenhouse",
            category: "Teleport Book",
            func: () => {
                window.FlatMMOPlus.sendMessage(`TELE_BOOK=greenhouse_teleport_scroll`);
            }
        },
        {
            key: "N/A",
            name: "Rogue's Grave Scroll",
            description: "Uses a scroll to teleport to Rogue's Grave",
            category: "Teleport Book",
            func: () => {
                window.FlatMMOPlus.sendMessage(`TELE_BOOK=rogue_teleport_scroll`);
            }
        },
        {
            key: "N/A",
            name: "Phantos Mansion Scroll",
            description: "Uses a scroll to teleport to Phantos Mansion",
            category: "Teleport Book",
            func: () => {
                window.FlatMMOPlus.sendMessage(`TELE_BOOK=phantos_teleport_scroll`);
            }
        },
        //Menus
        {
            key: "N/A",
            name: "View Bank",
            description: "Opens Bank Panel",
            category: "panels",
            func: () => {
                window.FlatMMOPlus.sendMessage(`VIEW_BANK`);
            }
        },
        {
            key: "N/A",
            name: "View Hunting",
            description: "Opens Hunting Panel",
            category: "panels",
            func: () => {
                switch_panels('hunting');
            }
        }
    ]
    let hotkeyOverride = {};

    //This is used for hot updates
    let plugins = {};
    let panels = {};
    let nextUniqueId = 1;
    let customChatCommands = {
        help: (command, data) => {
            console.log("help", command, data);
        }
    };
    let customChatHelp = {};
    let currentPanel = "inventory";
    let currentSettingsPanel = "sound";
    let loggedIn = false;
    let isFighting = false;
    let original_onmessage;
    let original_sendmessage;
    let original_switch_panels;
    let original_settings_modal_tab;
    let flatnotifications = {};

    const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

	if (pageWindow.FlatMMOPlus) {
        if(pageWindow.FlatMMOPlus.version >= VERSION) {
            if (window !== pageWindow) {
                window.AnimationSheetPlus = pageWindow.AnimationSheetPlus;
                window.FlatMMOPlusPlugin = pageWindow.FlatMMOPlusPlugin;
                window.FlatMMOPlus = pageWindow.FlatMMOPlus;
            }
            return;
        }
        plugins = pageWindow.FlatMMOPlus.plugins;
        panels = pageWindow.FlatMMOPlus.panels;
        nextUniqueId = pageWindow.FlatMMOPlus.nextUniqueId;
        customChatCommands = pageWindow.FlatMMOPlus.customChatCommands;
        customChatHelp = pageWindow.FlatMMOPlus.customChatHelp;
        currentPanel = pageWindow.FlatMMOPlus.currentPanel;
        currentSettingsPanel = pageWindow.FlatMMOPlus.currentSettingsPanel;
        loggedIn = pageWindow.FlatMMOPlus.loggedIn;
        isFighting = pageWindow.FlatMMOPlus.isFighting;

        original_onmessage = pageWindow.FlatMMOPlus.original_onmessage || Globals.websocket.onmessage;
        original_sendmessage = pageWindow.FlatMMOPlus.original_sendmessage || Globals.websocket.send;
        original_switch_panels = pageWindow.FlatMMOPlus.original_switch_panels || window.switch_panels;
        original_settings_modal_tab = pageWindow.FlatMMOPlus.original_settings_modal_tab || window.settings_modal_tab;
        flatnotifications = pageWindow.FlatMMOPlus.notifications || {};

        if (pageWindow.FlatMMOPlus.fmpKeyDown) {
            pageWindow.removeEventListener("keydown", pageWindow.FlatMMOPlus.fmpKeyDown, false);
        }

        document.getElementById("ui-panel-flatmmoplus")?.remove();
        document.getElementById("settings-modal-plugins-panel-btn")?.remove();
        document.getElementById("settings-modal-plugins-panel")?.remove();
        document.querySelector(".settings-ui tbody tr:last-child")?.remove()
	} else {
        document.head.insertAdjacentHTML("beforeend", `<style>
            .displaynone {
                display: none !important;
            }
            #ui-panel-hotkeys-content {
                h1 {
                    margin: 0;
                    text-transform: capitalize;
                }
                h3 {
                    margin-bottom: 0;
                }
                button {
                    cursor: pointer;
                }
            }
            .fmpHotkeysConflict {
                background-color: red;
                color: white;
            }
            .fmpHotkeysCategory {
                display: grid;
                grid-template-columns: auto auto auto;
            }
            .fmp-priority-div {
                list-style: none;
                padding: 0;
            }
            .fmp-priority-item {
                background-color: #fff;
                cursor: grab;
            }
            .fmp-priority-item.dragging {
                opacity: 0.5;
                background-color: #e0e0e0;
            }
            #ui-panel-flatmmoplus {
                height: 550px;
                overflow-y: scroll;
                scrollbar-width: thin;
                text-align: justify;

                textarea {
                    width: -webkit-fill-available;
                    width: -moz-available;
                    width: stretch;
                    resize: none;
                }

                input[type=checkbox] {
                    -webkit-appearance: none;
                    appearance: none;
                    position: relative;
                    width: 20px;
                    height: 10px;
                    border-radius: 15px;
                    background-color: #ccc;
                    outline: none;
                    cursor: pointer;
                    transition: background-color 0.3s;
                    top: 0.5rem
                }

                input[type=checkbox]::before {
                    content: '';
                    position: absolute;
                    top: 1px;
                    left: 1px;
                    width: 8px;
                    height: 8px;
                    background-color: #fff;
                    border-radius: 50%;
                    transition: transform 0.3s;
                }

                input[type=checkbox]:checked {
                    background-color: #4CAF50;
                }

                input[type=checkbox]:checked::before {
                    transform: translateX(10px);
                }

                label {
                    font-size: 1.17em;
                    font-weight: bold;
                }
            }
            .flatmmoplus-plugin-config-section >div {
                margin: 5px 0;
            }
            .fmp-list-div {
                max-height: calc(5em + 60px);
                overflow-y: auto;
                text-align: center;
            }
            .fmp-list-item {
                display: flex;
                justify-content: space-between;
                align-items: center;

                div {
                    width: 100%;
                    padding: 5px;
                }
            }
            .fmp-list-item:nth-child(odd) {
                background-color: rgba(0, 0, 0, 0.3);
            }
            .fmp-list-close {
                cursor: pointer;
                font-size: 2rem;
            }
            .fmp-objectLabel {
                display: grid;
                grid-template-columns: auto auto;
                justify-items: center;
                font-weight: bold;
            }
        </style>`);
        //The new hotkey system does everything these does
        window.removeEventListener("keypress", keypress_listener, false);
        window.removeEventListener("keyup", keyup_listener, false);
        window.removeEventListener("keydown", keydown_listener, false);
        document.getElementById("chat-text-input").onkeypress = null
        original_onmessage = Globals.websocket.onmessage;
        original_sendmessage = Globals.websocket.send;
        original_switch_panels = window.switch_panels;
        original_settings_modal_tab = window.settings_modal_tab;

        //Record hotkey remap
        window.addEventListener("click", (e) => {
            const btn = e.target.closest("[data-hotkeystring]");
            if(btn) {
                window.FlatMMOPlus.handler.hotkeyElement = btn;
                btn.innerText = "Recording";
            }
        })
    }

	function logFancy(s, color="#00f7ff") {
		console.log("%cFlatMMOPlus: %c"+s, `color: ${color}; font-weight: bold; font-size: 12pt;`, "color: silver; font-weight: normal; font-size: 10pt;");
	}

    class AnimationSheetPlus {
        constructor(filename, frames, speed, images) {
            this.filename = filename;
            this.running = false;
            this.frame_at = 0;
            this.FRAMES = frames;
            this.SPEED = speed;
            this.speed_at = 0;
            this.animation_tick_at = animation_tick;
            this.images = []
            for(var i = 1; i <= this.FRAMES; i++) {
                let image = new Image();
                image.src = images[i - 1]
                this.images.push(image);
            }
        }

        get_frame() {
            if(this.FRAMES > 0) {
                if(this.SPEED == this.speed_at) {
                    //swtich frames
                    if(this.animation_tick_at != animation_tick) {
                        this.frame_at++;
                        this.animation_tick_at = animation_tick;
                    }
                    
                    if(this.FRAMES == this.frame_at) {
                        this.frame_at = 0;
                    }
                    this.speed_at = 0;
                } else {
                    this.speed_at++;
                }
                return this.images[this.frame_at];
            } else {
                return this.images[0];
            } 
        }
    }

	class FlatMMOPlusPlugin {

		constructor(id, opts) {
			if(typeof id !== "string") {
				throw new TypeError("FlatMMOPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
			}
			this.id = id;
			this.opts = opts || {};
			this.config = null;
			this.changedConfigs = new Set();
		}

		getConfig(name) {
			if(!this.config) {
				FlatMMOPlus.loadPluginConfigs(this.id);
			}
			if(this.config) {
				return this.config[name];
			}
		}

		/*
        onConfigsChanged() { }
        onLogin() { }
        onMessageReceived(data) { }
        onChat(data) { }
		onPanelChanged(panelBefore, panelAfter)
        onMapChanged(mapBefore, mapAfter) { }
        onInventoryChanged(inventoryBefore, inventoryAfter) { }
        */
	}

    class handlerPlugin extends FlatMMOPlusPlugin {
        constructor() {
            super("handler", {
                about: {
                    name: "Plugin Handler",
                    version: VERSION,
                    author: "Liam",
                    description: "FlatMMO+ Configs"
                },
                config: [
                    {
                        id: "globalSettings",
                        label: "Share settings across all profiles",
                        type: "boolean",
                        default: false
                    },
                    {
                        id: "turnSettingsGlobal",
                        label: "Copy this profile's options to all profiles",
                        text: "Copy",
                        type: "button",
                        func: "FlatMMOPlus.handler.shareThisSettings",
                        //class: "btnClass"
                    },
                    {
                        id: "hoverNotifications",
                        label: "Plugin Notifications Orbs",
                        type: "boolean",
                        default: false
                    },
                    {
                        id: "hoverNotificationHeight",
                        label: "Notification Orbs Height",
                        type: "range",
                        min: 2,
                        max: 14,
                        step: 1,
                        default: 10,
                    },
                    {
                        id: "hoverNotificationAlpha",
                        label: "Notification Orbs Transparency",
                        type: "range",
                        min: 0,
                        max: 100,
                        default: 100
                    }
                ]
            })
            this.chatInput = document.getElementById("chat-text-input") || null;
            this.hotkeyCategories = new Set();
            this.hotkeys = {};
            this.hotkeyElement = null;

            window.addEventListener("keydown", e => {

            })
        }

        shareThisSettings() {
            let keys = {
                main: Globals.local_username,
                scripts: new Set(),
                alts: new Set(),
            }

            for (const key in localStorage) {
                const match = key.match(/flatmmoplus\.(.*?)(?:\.(.*?))?\.config/);
                if(match === null) continue;

                const id = match[1];

                keys.scripts.add(id);

                const username = match[2];
                if(username && username !== Globals.local_username) {
                    keys.alts.add(username);
                }
            }

            keys.scripts.forEach(script => {
                const value = localStorage.getItem(`flatmmoplus.${script}.${keys.main}.config`) || '{}';

                keys.alts.forEach(profile => {
                    localStorage.setItem(`flatmmoplus.${script}.${profile}.config`)
                })
            })
        }
    }

    class FlatMMOPlus {
		constructor() {
			this.version = VERSION;
			this.plugins = plugins;
			this.panels = panels;
			this.debug = false;
			this.nextUniqueId = nextUniqueId;
			this.customChatCommands = customChatCommands
			this.customChatHelp = customChatHelp;
			this.currentPanel = currentPanel;
			this.currentSettingsPanel = currentSettingsPanel;
			this.loggedIn = loggedIn;
            this.isFighting = isFighting;
            this.in_combat_ticker = 0;
            this.currentAction = this.currentAction;
            this.original_onmessage = original_onmessage;
            this.original_sendmessage = original_sendmessage;
            this.original_switch_panels = original_switch_panels;
            this.original_settings_modal_tab = original_settings_modal_tab;
            this.notifications = flatnotifications;
            this.level = [
                0,0,108,177,265,380,527,717,956,1254,1622,2068,2605,3245,4000,4885,5913,7100,8464,10022,11794,13799,16060,18599,21442,24614,28145,32063,36400,41191,46470,52277,58652,65637,73279,81625,90728,100642,111424,123135,135841,149611,164516,180634,198047,216840,237105,258938,282440,307721,334893,364077,395399,428994,465003,503577,544871,589054,636300,686794,740732,798320,859775,925326,995213,1069691,1149027,1233503,1323417,1419081,1520824,1628993,1743952,1866086,1995798,2133515,2279683,2434772,2599278,2773721,2958649,3154637,3362289,3582243,3815166,4061762,4322767,4598959,4891153,5200203,5527011,5872521,6237725,6623665,7031436,7462185,7917120,8397507,8904674,9440017,10004999,10601158,11230106,11893534,12593217,13331018,14108890,14928883,15793144,16703929,17663602,18674641,19739647,20861344,22042590,23286382,24595860,25974317,27425202,28952134,30558903,32249481,34028033,35898920,37866713,39936202,42112405,44400579,46806231,49335129,51993317,54787124,57723179,60808425,64050133,67455918,71033752,74791986,78739361,82885031,87238578,91810037,96609909,101649191,106939392,112492559,118321304,124438823,130858933,137596089,144665421,152082764,159864685,168028523,176592418,185575354,194997190,204878707,215241643,226108743,237503800,249451703,261978489,275111393,288878903,303310817,318438300,334293948,350911852,368327667,386578678,405703877,425744040,446741808,468741765,491790534,515936862,541231719,567728396,595482609,624552610,654999299,686886341,720280294,755250734,791870395,830215305,870364937,912402361,956414404,1002491823,1050729473,1101226494,1154086502,1209417785,1267333518,1327951974,1391396753,1457797021,1527287754,1600009999
            ];

			if(localStorage.getItem(LOCAL_STORAGE_KEY_DEBUG) == "1") {
                this.debug = true;
            }
		}
    }

    //I'm not sure why Smitty has both keydown and the deprecated keypress, I will merge both codes here
    FlatMMOPlus.prototype.fmpKeyDown = function(e) {
        //This should make sure inputs don't break
        if(document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA" || document.activeElement.getAttribute("contenteditable") === "true") {
            if(document.activeElement.id === "chat-text-input" && e.key === "Enter") {
                enter_pressed()
            }
            return;
        };

        if(has_npc_chat_message_modal_open() && e.key === " ") {
            document.getElementById("npc-chat-message-modal-continue-btn").click();
            e.preventDefault();
            return;
        }

        //This doesn't need a switch case
        if(has_npc_chat_options_modal_open() && e.keyCode > 48 && e.keyCode < 58) {
            //1 is 49
            const value = e.keyCode - 49;
            let wrapper = document.getElementById("npc-chat-options-modal-content");
            let options = wrapper.getElementsByTagName("div");
            if(options[value] && options[value].style.display != 'none') {
                options[value].click();
            }
            return;
        }

        //Modifiers can't have hotkeys on them
        if(e.key === "Shift" || e.key === "Control" || e.key === "Alt" || e.key === "Meta") return;

        const stringKey = window.FlatMMOPlus.formatHotkey(e);

        if(window.FlatMMOPlus.handler.hotkeyElement !== null) {
            e.preventDefault();
            window.FlatMMOPlus.remapHotkey(e, stringKey);
            return;
        }

        if(window.FlatMMOPlus.handler.hotkeys.hasOwnProperty(stringKey)) {
            e.preventDefault();
            window.FlatMMOPlus.handler.hotkeys[stringKey].forEach(h =>{
                //Most hotkeys won't allow holding down to spam them
                if(h.repeat || e.repeat == false) {
                    h.func(e);
                }
            });
            return;
        }

        if(has_modal_open()) return;
        if(document.activeElement.id != "body") {return;}

        //Vanilla chat has a 100 characters limitation
        if(LOCAL_CHAT_MAX_LENGTH <= chat_ele.value.length && this.handler.chatInput?.id === "chat-text-input") {
            return;
        }

        //Special keys won't be added
        if(e.key.length > 1) return;

        //Using key instead of code should fix any browser/keyboard issues
        chat_ele.value += e.key;
    }

    FlatMMOPlus.prototype.remapHotkey = function(e, stringKey) {
        if(e.key === "Escape") {
            stringKey = "N/A"
        }
        const hotkeyName = this.handler.hotkeyElement?.getAttribute("data-hotkeyname");
        const hotkeyString = this.handler.hotkeyElement?.getAttribute("data-hotkeystring");
        const hotkeyIndex = this.handler.hotkeys[hotkeyString]?.findIndex(h => h.name === hotkeyName)

        if(hotkeyIndex !== -1) {
            const hotkey = this.handler.hotkeys[hotkeyString].splice(hotkeyIndex, 1);
            if(this.handler.hotkeys[hotkeyString].length === 0) {
                delete this.handler.hotkeys[hotkeyString];
            }

            if(!this.handler.hotkeys.hasOwnProperty(stringKey)) {
                this.handler.hotkeys[stringKey] = [];
            }

            this.handler.hotkeys[stringKey].push(...hotkey);
            this.handler.hotkeyElement.innerText = stringKey;
            this.handler.hotkeyElement.setAttribute("data-hotkeystring", stringKey);

            hotkeyOverride[hotkeyName] = stringKey;
            localStorage.setItem("FMP-hotkeys", JSON.stringify(hotkeyOverride));

            this.checkHotkeyConflicts();
        }

        this.handler.hotkeyElement = null;
    }

    FlatMMOPlus.prototype.registerHotkeyCategory = function(category) {
        const id = "fmp-hotkeys-category-" + category
        if(document.getElementById(id)) return;

        const container = document.createElement("div");

        container.innerHTML = `<h1>${category}</h1>
        <div class="fmpHotkeysCategory" id="${id}"></div>`

        document.getElementById("ui-panel-hotkeys-content").append(container);

        this.handler.hotkeyCategories.add(category);
    }

    //Hotkeys can be overwriten
    FlatMMOPlus.prototype.registerHotkey = function(hotkey) {
        if(typeof hotkey.func !== "function") {
            console.error("You forgot the the hotkey.func or it is not a function")
        }
        //Modifiers are optional
        hotkey.ctrlKey = hotkey.ctrlKey || false;
        hotkey.altKey = hotkey.altKey || false;
        hotkey.shiftKey = hotkey.shiftKey || false;
        hotkey.metaKey = hotkey.metaKey || false;

        let stringKey = this.formatHotkey(hotkey);

        //Users can modify the key
        if(hotkeyOverride.hasOwnProperty(hotkey.name)) {
            stringKey = hotkeyOverride[hotkey.name];
        }

        //Plugins can override or redeclare keyboards
        const oldEl = document.getElementById("fmp-hotkeysContainer-" + hotkey.name);
        if(oldEl) {
            oldEl.remove()
        }

        //Some hotkeys should not show on hotkey menu
        if(!hotkey.private) {
            //Hotkey Panel has categories, if not specified it will be misc, if it doesn't exist it will add
            hotkey.category = hotkey.category || "misc";
            if(!this.handler.hotkeyCategories.has(hotkey.category)) {
                this.registerHotkeyCategory(hotkey.category)
            }

            const categoryId = "fmp-hotkeys-category-" + hotkey.category;
            document.getElementById(categoryId).insertAdjacentHTML("beforeend", `<div id="fmp-hotkeysContainer-${hotkey.name}">
                <h3>${hotkey.name}</h3>
                <p>${hotkey.description}</p>
                <button class="fmp-hotkeys-buttons" id="fmp-hotkeys-${hotkey.name}" data-hotkeyname="${hotkey.name}" data-hotkeystring="${stringKey}">${stringKey}</button>
            </div>`)
        }

        //Conflicts are fine
        if(!this.handler.hotkeys.hasOwnProperty(stringKey)) {
            this.handler.hotkeys[stringKey] = [];
        }
        this.handler.hotkeys[stringKey].push(hotkey);

        this.checkHotkeyConflicts();
    }

    //Some plugins may remove default hotkeys for some reason
    FlatMMOPlus.prototype.deleteHotkey = function(hotkeyName) {
        const el = document.getElementById("fmp-hotkeysContainer-" + hotkeyName);
        if(el) {
            el.remove()
        }

        //Remove it from hotkey map
        this.handler.hotkeys = Object.fromEntries(
            Object.entries(this.handler.hotkeys).map(([key, array]) => [
                key, 
                array.filter(h => h.name !== hotkeyName)
            ])
        )

        this.checkHotkeyConflicts();
    }

    FlatMMOPlus.prototype.formatHotkey = function(key) {
        const parts = [];
        if (key.ctrlKey) parts.push("Ctrl");
        if (key.altKey) parts.push("Alt");
        if (key.shiftKey) parts.push("Shift");
        if (key.metaKey) parts.push("Meta");
        parts.push(key.key.toUpperCase());
        return parts.join(" + ");
    };

    FlatMMOPlus.prototype.checkHotkeyConflicts = function() {
        for (const list of Object.values(this.handler.hotkeys)) {
            const hasConflict = list.length > 1;

            list.forEach(h => {
                const el = document.getElementById(`fmp-hotkeys-${h.name}`);
                if (el) {
                    el.classList.toggle("fmpHotkeysConflict", hasConflict);
                }
            });
        }
    }
    
	FlatMMOPlus.prototype.registerCustomChatCommand = function(command, f, help) {
        if (Array.isArray(command)) {
            command.forEach(cmd => this.registerCustomChatCommand(cmd, f, help))
            return;
        }
        if(typeof command !== "string" || typeof f !== "function") {
            throw new TypeError("FlatMMOPlus.registerCustomChatCommand takes the following arguments: (command:string, f:function)");
        }
        if(CHAT_COMMAND_NO_OVERRIDE.includes(command)) {
            throw new Error(`Cannot override the following chat commands: ${CHAT_COMMAND_NO_OVERRIDE.join(", ")}`);
        }
        if(command in this.customChatCommands) {
            console.warn(`FlatMMOPlus: re-registering custom chat command "${command}" which already exists.`);
        }
        this.customChatCommands[command] = f;
        if(help && typeof help === "string") {
            this.customChatHelp[command] = help.replace(/%COMMAND%/g, command);
        } else {
            delete this.customChatHelp[command];
        }
    }

    FlatMMOPlus.prototype.handleCustomChatCommand = function(command, message) {
        // return true if command handler exists, false otherwise
        const f = this.customChatCommands[command];
        if(typeof f === "function") {
            try {
                f(command, message);
            }
            catch(err) {
                console.error(`Error executing custom command "${command}"`, err);
            }
            return true;
        }
        return false;
    }

    FlatMMOPlus.prototype.uniqueId = function() {
        return this.nextUniqueId++;
    }

    FlatMMOPlus.prototype.setDebug = function(debug) {
        if(debug) {
            this.debug = true;
            localStorage.setItem(LOCAL_STORAGE_KEY_DEBUG, "1");
        }
        else {
            this.debug = false;
            localStorage.removeItem(LOCAL_STORAGE_KEY_DEBUG);
        }
    }

    FlatMMOPlus.prototype.setPluginConfigUIDirty = function(id, dirty, configId) {
        if(typeof id !== "string" || typeof dirty !== "boolean") {
            throw new TypeError("FlatMMOPlus.setPluginConfigUIDirty takes the following arguments: (id:string, dirty:boolean)");
        }
        const plugin = this.plugins[id];
        if(configId) {
            plugin.changedConfigs.add(configId);
        }
        const button = document.getElementById(`flatmmoplus-configbutton-${plugin.id}-apply`);
        if(button) {
            button.disabled = !dirty;
        }
    }

    FlatMMOPlus.prototype.newListField = function(pluginId, configId, text = "New Field") {
        if(typeof pluginId !== "string" || typeof configId !== "string" || typeof text !== "string") {
            throw new TypeError("FlatMMOPlus.newListField takes the following arguments: (id:string, text:string)");
        }
        const plugin = this.plugins[pluginId];
        const parentDiv = document.getElementById(`flatmmoplus-config-${pluginId}-${configId}`);

        const item = document.createElement("div");
        const textDiv = document.createElement("div");
        const closeSpan = document.createElement("span");

        item.className = "fmp-list-item";
        item.addEventListener("dblclick", function(e){
            e.preventDefault();
            textDiv.contentEditable = true;
            textDiv.focus();
        })
        item.addEventListener("contextmenu", function(e) {
            e.preventDefault();
            textDiv.contentEditable = true;
            textDiv.focus();
        });


        textDiv.innerText = text;
        textDiv.spellcheck = false;
        textDiv.autocorrect = false;
        textDiv.addEventListener("focusout", function(e) {
            this.contentEditable = false;
            plugin.changedConfigs.add(configId);
            window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        });

        
        closeSpan.innerText = "×";
        closeSpan.className = "fmp-list-close";
        closeSpan.addEventListener("click", function(){
            item.remove();
            window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        })

        item.appendChild(textDiv);
        item.appendChild(closeSpan);
        parentDiv.appendChild(item);

        parentDiv.scrollTop = parentDiv.scrollHeight;

        plugin.changedConfigs.add(configId);
        window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        if(text = "New Field") {
            textDiv.contentEditable = true;
            textDiv.focus();
        }
    }

    FlatMMOPlus.prototype.newObjectField = function(pluginId, configId, key = "Key", value = "Value") {
        if(typeof pluginId !== "string" || typeof configId !== "string" || typeof key !== "string") {
            throw new TypeError("FlatMMOPlus.newObjectField takes the following arguments: (id:string, key:string, value:string)");
        }
        const plugin = this.plugins[pluginId];
        const parentDiv = document.getElementById(`flatmmoplus-config-${pluginId}-${configId}`);

        const item = document.createElement("div");
        const keyDiv = document.createElement("div");
        const valueDiv = document.createElement("div");
        const closeSpan = document.createElement("span");

        item.className = "fmp-list-item";
        function editField(e) {
            e.preventDefault();
            const el = e.target;
            el.contentEditable = true;
            el.focus();
        }
        keyDiv.ondblclick = (e) => editField(e);
        keyDiv.oncontextmenu = (e) => editField(e);
        valueDiv.ondblclick = (e) => editField(e);
        valueDiv.oncontextmenu = (e) => editField(e);

        keyDiv.innerText = key;
        keyDiv.spellcheck = false;
        keyDiv.autocorrect = false;

        valueDiv.innerText = value;
        valueDiv.spellcheck = false;
        valueDiv.autocorrect = false;

        function focusOut(e) {
            e.preventDefault();
            const el = e.target;
            el.contentEditable = false;
            plugin.changedConfigs.add(configId);
            window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        }
        keyDiv.onblur = (e) => focusOut(e);
        valueDiv.onblur = (e) => focusOut(e);

        closeSpan.innerText = "×";
        closeSpan.className = "fmp-list-close";
        closeSpan.addEventListener("click", function(){
            item.remove();
            window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        })

        item.appendChild(keyDiv);
        item.appendChild(valueDiv);
        item.appendChild(closeSpan);
        parentDiv.appendChild(item);

        parentDiv.scrollTop = parentDiv.scrollHeight;

        plugin.changedConfigs.add(configId);
        window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
        if(key = "Key") {
            keyDiv.contentEditable = true;
            keyDiv.focus();
        }
    }

    FlatMMOPlus.prototype.newPriorityField = function(pluginId, configId, value = "New Item") {
        if(typeof pluginId !== "string" || typeof configId !== "string" || typeof value !== "string") {
            throw new TypeError("FlatMMOPlus.newPriorityField takes the following arguments: (pluginId: string, configId:string, value: string)");
        }

        const plugin = this.plugins[pluginId];
        const config = plugin.opts.config.find(c => c.id == configId);
        const parentDiv = document.getElementById(`flatmmoplus-config-${pluginId}-${configId}`);

        const item = document.createElement("div");
        const textDiv = document.createElement("div");

        item.className = "fmp-priority-item fmp-list-item";
        item.setAttribute("draggable", true);


        textDiv.innerText = value;
        textDiv.spellcheck = false;
        textDiv.autocorrect = false;


        item.appendChild(textDiv);
        parentDiv.appendChild(item);

        //Some lists may let users add/remove/edit items
        if(config.canAddNew) {
            const closeSpan = document.createElement("span");

            closeSpan.innerText = "×";
            closeSpan.className = "fmp-list-close";
            closeSpan.addEventListener("click", function(){
                item.remove();
                window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
            })
            item.appendChild(closeSpan);

            item.addEventListener("contextmenu", function(e) {
                e.preventDefault();
                textDiv.contentEditable = true;
                textDiv.focus();
            });

            textDiv.addEventListener("focusout", function(e) {
                this.contentEditable = false;
                plugin.changedConfigs.add(configId);
                window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
            });

            if(value = "New Item") {
                textDiv.contentEditable = true;
                textDiv.focus();
            }
        }

        parentDiv.scrollTop = parentDiv.scrollHeight;

        plugin.changedConfigs.add(configId);
        window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId)
    }

    FlatMMOPlus.prototype.loadPluginConfigs = function(id) {
        if (typeof id !== "string") {
            throw new TypeError("FlatMMOPlus.reloadPluginConfigs takes the following arguments: (id:string)");
        }
        let value;
        const globalValue = localStorage.getItem(`flatmmoplus.${id}.config`) || "{}";
        const accountValue = localStorage.getItem(`flatmmoplus.${id}.${Globals.local_username}.config`);
        if(id !== "handler") {
            const usesGlobal = this.handler.config.globalSettings;
            
            if(usesGlobal) {
                value = globalValue;
            } else if (accountValue == null) {
                value = globalValue;
                //Make sure it exists next time
                localStorage.setItem(`flatmmoplus.${id}.${Globals.local_username}.config`, globalValue);
            } else {
                value = accountValue;
            }
        } else {
            value = globalValue;
        }

        const plugin = this.plugins[id];
        const config = {};
        let stored;
        try {
            stored = JSON.parse(value || "{}");
        } catch(err) {
            console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
            stored = {};
        }

        if (plugin.opts.config && Array.isArray(plugin.opts.config)) {
            plugin.opts.config.forEach(cfg => {
                const el = document.getElementById(`flatmmoplus-config-${plugin.id}-${cfg.id}`);
                let value = stored[cfg.id];
                if (value == null || typeof value === "undefined") {
                    value = cfg.default;
                }
                config[cfg.id] = value;

                if (el) {
                    if (CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
                        el.checked = value;
                    } else if (CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
                        el.value = value;
                    } else if (CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
                        el.value = value;
                    } else if (CONFIG_TYPES_RANGE.includes(cfg.type) && typeof value === "number") {
                        el.value = value;
                        document.getElementById(`flatmmoplus-config-${plugin.id}-${cfg.id}-value`).innerText = value
                    } else if (CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
                        el.value = value;
                    } else if (CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
                        el.value = value;
                    } else if (CONFIG_TYPES_COLOR.includes(cfg.type) && typeof value === "string") {
                        el.value = value;
                    } else if (CONFIG_TYPES_LIST.includes(cfg.type)) {
                        el.innerHTML = "";
                        if(typeof value === "string") {
                            value = value.split(",")
                            .map(item => item.trim())
                            .filter(item=> item !== "");
                            if(cfg.unique) {
                                const valuesSet = new Set(value);
                                value = Array.from(valuesSet);
                            }
                            config[cfg.id] = value;
                        }
                        value.forEach(item => {
                            window.FlatMMOPlus.newListField(id, cfg.id, item);
                        })
                    } else if (CONFIG_TYPES_PRIORITY.includes(cfg.type)) {
                        el.innerHTML = "";
                        if (cfg.unique) {
                            const valuesSet = new Set(value);
                            value = Array.from(valuesSet);
                        }

                        console.log(value)

                        value.forEach(item => {
                            window.FlatMMOPlus.newPriorityField(id, cfg.id, item);
                        })
                    } else if (CONFIG_TYPES_RELATION.includes(cfg.type)) {
                        el.innerHTML = "";
                        for(let item in value) {
                            window.FlatMMOPlus.newObjectField(id, cfg.id, item, value[item]);
                        }
                    } 
                    if(cfg.onChange) {
                        el.onchange = cfg.onChange;
                    }
                }
            });
        }
        plugin.config = config;
        plugin.changedConfigs.clear();
        this.setPluginConfigUIDirty(id, false);
        if (typeof plugin.onConfigsChanged === "function") {
            plugin.onConfigsChanged();
        }
    }

    FlatMMOPlus.prototype.savePluginConfigs = function(id) {
        if (typeof id !== "string") {
            throw new TypeError("FlatMMOPlus.savePluginConfigs takes the following arguments: (id:string)");
        }
        let key;
        if(id !== "handler") {
            key = this.handler.config.globalSettings ? `flatmmoplus.${id}.config` : `flatmmoplus.${id}.${Globals.local_username}.config`;
        } else {
            key = `flatmmoplus.${id}.config`;
        }
        const plugin = this.plugins[id];
        const config = {};
        if (plugin.opts.config && Array.isArray(plugin.opts.config)) {
            plugin.opts.config.forEach(cfg => {
                const el = document.getElementById(`flatmmoplus-config-${plugin.id}-${cfg.id}`);
                if (CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
                    config[cfg.id] = el.checked;
                } else if (CONFIG_TYPES_INTEGER.includes(cfg.type)) {
                    config[cfg.id] = parseInt(el.value);
                } else if (CONFIG_TYPES_FLOAT.includes(cfg.type)) {
                    config[cfg.id] = parseFloat(el.value);
                } else if (CONFIG_TYPES_RANGE.includes(cfg.type)) {
                    config[cfg.id] = parseInt(el.value);
                } else if (CONFIG_TYPES_STRING.includes(cfg.type)) {
                    config[cfg.id] = el.value;
                } else if (CONFIG_TYPES_SELECT.includes(cfg.type)) {
                    config[cfg.id] = el.value;
                } else if (CONFIG_TYPES_COLOR.includes(cfg.type)) {
                    config[cfg.id] = el.value;
                } else if (CONFIG_TYPES_LIST.includes(cfg.type)) {
                    let values = [];
                    document.querySelectorAll(`#flatmmoplus-config-${plugin.id}-${cfg.id} div div`).forEach(el => {
                        values.push(el.innerText)
                    })

                    //This will remove duplicates
                    if(cfg.unique) {
                        const valuesSet = new Set(values);
                        values = Array.from(valuesSet);
                    }
                    config[cfg.id] = values;
                } else if (CONFIG_TYPES_PRIORITY.includes(cfg.type)) {
                    let values = [];
                    document.querySelectorAll(`#flatmmoplus-config-${plugin.id}-${cfg.id} div div`).forEach(el => {
                        values.push(el.innerText)
                    })

                    //This will remove duplicates
                    if(cfg.unique) {
                        const valuesSet = new Set(values);
                        values = Array.from(valuesSet);
                    }
                    config[cfg.id] = values;
                } else if (CONFIG_TYPES_RELATION.includes(cfg.type)) {
                    const values = {};
                    document.querySelectorAll(`#flatmmoplus-config-${plugin.id}-${cfg.id}>div`).forEach(el => {
                        values[el.children[0].innerText] = el.children[1].innerText;
                    })

                    config[cfg.id] = values;
                }
            });
        }
        plugin.config = config;
        localStorage.setItem(key, JSON.stringify(config));
        this.setPluginConfigUIDirty(id, false);
        if (typeof plugin.onConfigsChanged === "function") {
            plugin.onConfigsChanged();
        }
        plugin.changedConfigs.clear();
    }
    
    FlatMMOPlus.prototype.showWarning = function(message, color = "orange") {
        add_to_chat("none", "none", "none", color, message);
    }

    FlatMMOPlus.prototype.addPanel = function(id, title, content, hasTabButton) {
        if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
            throw new TypeError("FlatMMOPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
        }
        if(this.panels[id]) {
            console.error("There already is a panel with this id");
            return;
        }
        if(hasTabButton) {
            const firstPanel = document.querySelector("#settings-modal-sound-panel");
            firstPanel.insertAdjacentHTML("beforebegin", `<div class="settings-modal-panel-btn hover" onclick="settings_modal_tab('${id}')" id="settings-modal-${id}-panel-btn" style="margin-right: 10px;">${title}</div>`)
        }
        const lastPanel = document.querySelector("#settings-modal-mutes-panel");
        lastPanel.insertAdjacentHTML("afterend",`
        <div class="settings-modal-panel" style="display: none" id="settings-modal-${id}-panel">
            <div id="ui-panel-${id}-content"></div>
        </div>
        `);
        this.panels[id] = {
            id: id,
            title: title,
            content: content
        };
        this.refreshPanel(id);
    }

    FlatMMOPlus.prototype.refreshPanel = function(id) {
        if(typeof id !== "string") {
            throw new TypeError("FlatMMOPlus.refreshPanel takes the following arguments: (id:string)");
        }
        const panel = this.panels[id];
        if(!panel) {
            throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
        }
        let content = panel.content;
        if(!["string", "function"].includes(typeof content)) {
            throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
        }
        if(typeof content === "function") {
            content = content();
            if(typeof content !== "string") {
                throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
            }
        }
        const panelContent = document.getElementById(`ui-panel-${id}-content`);
        panelContent.innerHTML = content;
        if(id === "plugins") {
            this.forEachPlugin(plugin => {
                this.loadPluginConfigs(plugin.id);
            });
        }
    }

    FlatMMOPlus.prototype.registerPlugin = function(plugin) {
        if(!(plugin instanceof FlatMMOPlusPlugin)) {
            throw new TypeError("FlatMMOPlus.registerPlugin takes the following arguments: (plugin:FlatMMOPlusPlugin)");
        }
        if(plugin.id in this.plugins) {
            throw new Error(`FlatMMOPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
        }

        this.plugins[plugin.id] = plugin;
        this.loadPluginConfigs(plugin.id);
        let versionString = plugin.opts&&plugin.opts.about&&plugin.opts.about.version ? ` (v${plugin.opts.about.version})` : "";
        logFancy(`registered plugin "${plugin.id}"${versionString}`);

        //Calls onlogin when the plugin is loaded with delay
        if(this.loggedIn) {
            plugin.onLogin();
        }
    }

    FlatMMOPlus.prototype.forEachPlugin = function(f) {
        if(typeof f !== "function") {
            throw new TypeError("FlatMMOPlus.forEachPlugin takes the following arguments: (f:function)");
        }
        Object.values(this.plugins).forEach(plugin => {
            try {
                f(plugin);
            }
            catch(err) {
                console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
                console.error(err);
            }
        });
    }

    FlatMMOPlus.prototype.setPanel = function(panel) {
        if(typeof panel !== "string") {
            throw new TypeError("FlatMMOPlus.setPanel takes the following arguments: (panel:string)");
        }
        window.settings_modal_tab(panel);
    }

    FlatMMOPlus.prototype.sendMessage = function(message) {
        if(typeof message !== "string") {
            throw new TypeError("FlatMMOPlus.sendMessage takes the following arguments: (message:string)");
        }
        if(Globals.websocket && Globals.websocket.readyState == 1) {
            Globals.websocket.send(message);
        }
    }

    FlatMMOPlus.prototype.hideCustomPanels = function() {
        Object.values(this.panels).forEach((panel) => {
            const el = document.getElementById(`settings-modal-${panel.id}-panel`);
            if(el) {
                el.style.display = "none";
            }
            const btn = document.getElementById(`settings-modal-${panel.id}-panel-btn`);
            if(btn) {
                btn.style.backgroundColor = "";
            }
        });
    }

    FlatMMOPlus.prototype.onMessageSent = function(message) {
        if(this.debug) {
            console.log(`FM+ onMessageSent: ${message}`);
        }
        let interrupt = false;
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onMessageSent === "function") {
                if(plugin.onMessageSent(message)) {
                    interrupt = true;
                };
            }
        });

        return interrupt;
    }

    FlatMMOPlus.prototype.onMessageReceived = function(data) {
        if(this.debug) {
            console.log(`FM+ onMessageReceived: ${data}`);
        }
        if(data) {
            this.forEachPlugin((plugin) => {
                if(typeof plugin.onMessageReceived === "function") {
                    plugin.onMessageReceived(data);
                }
            });
            if(data.startsWith("LOGGED_IN")) {
                this.onLogin();

            } else if (data.startsWith("YELL")) {
                const split = data.substring("YELL=".length).split("~");

                const chatData = {
                    username: split[0].slice(0, -7).trim(),
                    tag: split[1],
                    sigil: split[2],
                    color: split[3],
                    message: split[4],
                    yell: true
                }
                this.onChat(chatData);
            } else if (data.startsWith("CHAT_LOCAL_MESSAGE=")) {
                const split = data.substring("CHAT_LOCAL_MESSAGE=".length).split("~");
                let [sender, message] = split[1].split(" yelled: ");
                //Server messages don't have the "yelled"
                if (!message) {
                    message = sender;
                    sender = "";
                }
                const chatData = {
                    username: sender,
                    tag: "none",
                    sigil: "none",
                    color: split[0],
                    message: message,
                    yell: true
                }
                this.onChat(chatData);

            } else if (data.startsWith("CHAT=")) {
                const split = data.substring("CHAT=".length).split("~");
                
                const chatData = {
                    username: split[0],
                    tag: split[1],
                    sigil: split[2],
                    color: split[3],
                    message: split[4],
                    yell: false
                }
                this.onChat(chatData);
            } else if (data.startsWith("REFRESH_PLAYER_HP_BAR=")) {
                const split = data.substring("REFRESH_PLAYER_HP_BAR=".length).split("~");
                if(split[0] === Globals.local_username) {
                    if(this.isFighting && split[3] === "false") {
                        this.onFightEnded();
                    } else if (!this.isFighting && split[3] === "true") {
                        this.onFightStarted();
                    }
                }
            } else if (data.startsWith(`SET_PLAYER_ANIMATION=${Globals.local_username}`)) {
                const split = data.substring("SET_PLAYER_ANIMATION=".length).split("~");
                this.currentAction = split[1];
                this.onActionChanged();
            } else if (data.startsWith("CUSTOM")) {
                const [sender, content] = data.substring("CUSTOM=".length).split("~");
                let plugin = "unknown";
                let command = "unknown";
                let payload = content;

                const splitContent = content.split(":");
                if (splitContent.length >= 3) {
                    plugin = splitContent[0];
                    command = splitContent[1];
                    payload = splitContent.slice(2).join(":");
                }
                this.onCustomReceived(sender, plugin, command, payload);
            }
        }
    }

    FlatMMOPlus.prototype.onCustomReceived = function(sender, script, command, payload) {
        if(this.debug) {
            console.log(`FMMO+ onCustomReceived: ${sender} ${message}`);
        }

        this.forEachPlugin((plugin) => {
            if (typeof plugin.onCustomReceived === "function") {
                plugin.onCustomReceived(sender, script, command, payload);
            }
        })
    }

    FlatMMOPlusPlugin.prototype.sendCustom = function(user, command, payload) {
        let message = `CUSTOM=${user}~${this.id}`;
        message += command ? `:${command}` : ":ping";
        message += payload ? `:${payload}` : ":";
        window.FlatMMOPlus.sendMessage(message);
    }

    FlatMMOPlus.prototype.sendCustomMessage = function(toPlayer, content) {
        if(this.debug) {
            console.log(`FMMO+ sendCustomMessage`, toPlayer, opts);
        }

        const message = `CUSTOM=${toPlayer}~${content}`;
        this.sendMessage(message);
    }

    FlatMMOPlus.prototype.onLogin = function() {
        if(this.debug) {
            console.log(`FM+ onLogin`);
        }
        logFancy("login detected");
        
        document.getElementById("chat").insertAdjacentHTML("beforeend",`<div style="color: white;">
            <span><strong style="color:cyan">FYI: </strong> Use the /help command to see information on available chat commands.</span>
            <br>
        </div>`)

        this.forEachPlugin((plugin) => {
            if(typeof plugin.onLogin === "function") {
                plugin.onLogin();
            }
        });
        this.loggedIn = true;

        setTimeout(() => {
            Object.defineProperty(players[Globals.local_username], 'in_combat_ticker', {
                get: function() {
                    return FlatMMOPlus.in_combat_ticker;
                },
                set: function(newValue) {
                    FlatMMOPlus.in_combat_ticker = newValue;
                    if(newValue === 0) {
                        window.FlatMMOPlus.onFightEnded();
                    }
                }
            });
        }, 3000);

        //Chat auto scroll is always true for now
        chat_div_element.scrollTop = chat_div_element.scrollHeight;

        const original_paint = window.paint_effects;

        window.paint_effects = function() {
            original_paint();

            try {
                window.FlatMMOPlus.onPaint();
            } catch (error) {
                console.warn("Error on FlatMMO+ onPaint", error.message)
            }
        };

        const originalLayer1 = window.paint_layer_1;
        window.paint_layer_1 = function() {
            originalLayer1();

            try {
                window.FlatMMOPlus.onPaintObjects();
            } catch (error) {
                console.warn("Error on FlatMMO+ paint_layer_1", error.message)
            }
        };

        const originalGroundItems = window.paint_ground_items;
        window.paint_ground_items = function() {
            originalGroundItems();

            try {
                window.FlatMMOPlus.onPaintNpcs();
            } catch (error) {
                console.warn("Error on FlatMMO+ paint_ground_items", error.message)
            }
        };
    }

    FlatMMOPlus.prototype.onChat = function(data) {
        if(this.debug) {
            console.log(`FM+ onChat`, data);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onChat === "function") {
                plugin.onChat(data);
            }
        });
    }

    FlatMMOPlus.prototype.addNotification = function(name, imageSrc, title = "", text = "", ticks = 900, color = "white", bgColor = "black", titleColor, progressColor = "red") {
        const img = new Image();
        img.src = imageSrc;
        img.onload = () => {
            this.notifications[name] = {
                image: img,
                title,
                text,
                color,
                titleColor: titleColor || color,
                bgColor,
                progressColor,
                ticksFull: ticks,
                ticks
            }
        }
    }

    FlatMMOPlus.prototype.paintNotifications = function() {
        const nots = Object.keys(this.notifications);
        if(nots.length === 0) return;
        if(this.handler.config.hoverNotifications) {
             this.paintHoverNotifications(nots);
        } else {
            this.paintBlockNotifications();
        }
    }

    FlatMMOPlus.prototype.paintHoverNotifications = function(notifications) {
        const CIRCLE_R   = 22;
        const ARC_R      = 30;
        const ARC_W      = 5;
        const SPACING    = 12;
        const WIDGET_W   = ARC_R * 2 + SPACING;
        const FADE_TICKS = 60;

        // Center the row of widgets horizontally and vertically
        const totalW = notifications.length * WIDGET_W - SPACING;
        let cx = canvasWidth / 2 - totalW / 2 + ARC_R;
        const CY = this.handler.config.hoverNotificationHeight * TILE_SIZE - (ARC_R + ARC_W + 20);

        const mx = mouse_over_now.x;
        const my = mouse_over_now.y;
        let tooltip = null; // {cx, text} — drawn last so it's always on top

        for (let notification in this.notifications) {
            const not = this.notifications[notification];
            not.ticks--;

            if(not.ticks <= 0) {
                delete this.notifications[notification];
                return;
            }

            const alpha = this.handler.config.hoverNotificationAlpha === 100 ? not.ticks < FADE_TICKS ? Math.max(0, not.ticks / FADE_TICKS) : 1.0 : this.handler.config.hoverNotificationAlpha;

            ctx.save();
            ctx.globalAlpha = alpha;

            // Dark backing disc
            ctx.beginPath();
            ctx.arc(cx, CY, ARC_R + ARC_W, 0, Math.PI * 2);
            ctx.fillStyle = "rgba(0,0,0,0.6)";
            ctx.fill();

            // Clip icon to inner circle
            ctx.save();
            ctx.beginPath();
            ctx.arc(cx, CY, CIRCLE_R, 0, Math.PI * 2);
            ctx.clip();
            const imgWidth = CIRCLE_R * 2;
            ctx.drawImage(not.image, cx - CIRCLE_R, CY - CIRCLE_R, imgWidth, imgWidth * (not.image.naturalHeight / not.image.naturalWidth));
            ctx.restore();

            // Empty arc track
            ctx.beginPath();
            ctx.arc(cx, CY, ARC_R, -Math.PI / 2, Math.PI * 1.5);
            ctx.strokeStyle = "rgba(255,255,255,0.15)";
            ctx.lineWidth   = ARC_W;
            ctx.lineCap     = "butt";
            ctx.stroke();

            // Filled progress arc (clockwise from top)
            const endAngle = -Math.PI / 2 + (not.ticks / not.ticksFull) * Math.PI * 2;
            ctx.beginPath();
            ctx.arc(cx, CY, ARC_R, -Math.PI / 2, endAngle);
            ctx.strokeStyle = not.progressColor;
            ctx.lineWidth   = ARC_W;
            ctx.lineCap     = "round";
            ctx.stroke();

            ctx.restore();

            // Queue tooltip if mouse is hovering this orb
            const dx = mx - cx, dy = my - CY;
            if (dx * dx + dy * dy <= (ARC_R + ARC_W) * (ARC_R + ARC_W)) {
                tooltip = { cx, not };
            }

            cx += WIDGET_W;
        }

        // Draw tooltip on top of all orbs
        if (tooltip) {
            const tx   = tooltip.cx;
            const ty   = CY - (ARC_R + ARC_W + 14);
            ctx.save();
            const PAD = 16;
            ctx.textAlign   = "center";
            const tw = ctx.measureText(tooltip.not.title).width;
            const txw = ctx.measureText(tooltip.not.text).width;
            const rectW = tw > txw ? tw : txw;
            const th = 16;
            const txh = 13;
            ctx.font        = "16px sans-serif";
            ctx.fillStyle   = tooltip.not.bgColor;
            ctx.fillRect(tx - rectW / 2 - PAD, ty - th - PAD / 2, rectW + PAD * 2, th + txh + PAD);
            ctx.fillStyle   = tooltip.not.titleColor;
            ctx.fillText(tooltip.not.title, tx, ty);
            ctx.fillStyle   = tooltip.not.color;
            ctx.font        = "13px sans-serif";
            ctx.fillText(tooltip.not.text, tx, ty + th);
            ctx.restore();
        }
    }

    FlatMMOPlus.prototype.paintBlockNotifications = function() {
        const PADDING = 10;
        let yOffset = 0;
        let y = 13 * TILE_SIZE;
        for (let notification in this.notifications) {
            const not = this.notifications[notification];
            if(not.ticks <= 0) {
                delete this.notifications[notification];
                return;
            }
            not.ticks--;
            ctx.font = "20px serif";
            ctx.fillStyle = "white";
            ctx.globalAlpha = 0.2;
            ctx.fillRect(21 * TILE_SIZE, y - yOffset, TILE_SIZE * 3 - PADDING, TILE_SIZE - PADDING);
            ctx.globalAlpha = 1.0;
            ctx.drawImage(not.image, 21 * TILE_SIZE + PADDING, y + 10 - yOffset, 32, 32); 
            ctx.fillStyle = not.color;
            ctx.fillText(not.title, 21 * TILE_SIZE + 50, y + 20 - yOffset);
            ctx.font = "16px serif";
            ctx.fillText(not.text, 21 * TILE_SIZE + 50, y + 40 - yOffset);
            yOffset += TILE_SIZE;
        }
    }

    //After vanilla paint
    FlatMMOPlus.prototype.onPaint = function() {
        if(this.debug) {
            console.log("FM+ onPaint");
        }

        this.forEachPlugin((plugin) => {
            if(typeof plugin.onPaint === "function") {
                plugin.onPaint();
            }
        });

        this.paintNotifications();
    }

    //Between paint_layer_1 and paint_map_objects_lower_shadows
    FlatMMOPlus.prototype.onPaintObjects = function() {
        if(this.debug) {
            console.log("FM+ onPaintObjects");
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onPaintObjects === "function") {
                plugin.onPaintObjects();
            }
        });
    }

    //Between paint_ground_items and paint_npcs
    FlatMMOPlus.prototype.onPaintNpcs = function() {
        if(this.debug) {
            console.log("FM+ onPaintNpcs");
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onPaintNpcs === "function") {
                plugin.onPaintNpcs();
            }
        });
    }

    FlatMMOPlus.prototype.onPanelChanged = function(panelBefore, panelAfter) {
        if(this.debug) {
            console.log(`FM+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onPanelChanged === "function") {
                plugin.onPanelChanged(panelBefore, panelAfter);
            }
        });
    }
    
    FlatMMOPlus.prototype.onSettingsPanelChanged = function(panelBefore, panelAfter) {
        if(this.debug) {
            console.log(`FM+ onSettingsPanelChanged "${panelBefore}" -> "${panelAfter}"`);
        }
        if(panelAfter === "plugins") {
            this.refreshPanel("plugins");
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onSettingsPanelChanged === "function") {
                plugin.onSettingsPanelChanged(panelBefore, panelAfter);
            }
        });
    }
    
    FlatMMOPlus.prototype.onMapChanged = function(mapBefore, mapAfter) {
        if(this.debug) {
            console.log(`FMMO+ onMapChanged "${mapBefore}" -> "${mapAfter}"`);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onMapChanged === "function") {
                plugin.onMapChanged(mapBefore, mapAfter);
            }
        });
    }

    FlatMMOPlus.prototype.onInventoryChanged = function(inventoryBefore, inventoryAfter) {
        if(this.debug) {
            console.log(`FMMO+ onInventoryChanged "${inventoryBefore}" -> "${inventoryAfter}"`);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onInventoryChanged === "function") {
                plugin.onInventoryChanged(inventoryBefore, inventoryAfter);
            }
        });
    }

    FlatMMOPlus.prototype.onDamageTaken = function(hpBefore, hpAfter) {
        if(this.debug) {
            console.log(`FMMO+ onDamageTaken "${hpBefore}" -> "${hpAfter}"`);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onDamageTaken === "function") {
                plugin.onDamageTaken(hpBefore, hpAfter);
            }
        });
    }

    FlatMMOPlus.prototype.onFightStarted = function() {
        if(this.debug) {
            console.log(`FMMO+ onFightStarted`);
        }
        this.isFighting = true;
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onFightStarted === "function") {
                plugin.onFightStarted();
            }
        });
    }
    
    FlatMMOPlus.prototype.onFightEnded = function() {
        if(this.debug) {
            console.log(`FMMO+ onFightEnded`);
        }
        this.isFighting = false;
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onFightEnded === "function") {
                plugin.onFightEnded();
            }
        });
    }

    FlatMMOPlus.prototype.onActionChanged = function() {
        if(this.debug) {
            console.log(`FMMO+ onActionChanged`);
        }
        this.forEachPlugin((plugin) => {
            if(typeof plugin.onActionChanged === "function") {
                plugin.onActionChanged();
            }
        });
    }

    FlatMMOPlus.prototype.init = function(){
        //Tries to hook into the websocket messages
        const hookIntoOnMessage = () => {
            try {
                if (typeof this.original_onmessage === "function") {
                    Globals.websocket.onmessage = (event) => {
                        if(event.data.startsWith("SET_MAP")) {
                            const mapBefore = current_map;
                            this.original_onmessage(event);
                            const mapAfter = current_map;
                            this.onMapChanged(mapBefore, mapAfter);
                            return;
                        } else if (event.data.startsWith("SET_INVENTORY_ITEMS")) {
                            const inventoryBefore = items;
                            this.original_onmessage(event);
                            const inventoryAfter = items;
                            this.onInventoryChanged(inventoryBefore, inventoryAfter);
                            return;
                        } else if (event.data.startsWith(`REFRESH_PLAYER_HP_BAR=${Globals.local_username}`)) {
                            const hpBefore = players[Globals.local_username].hp;
                            this.original_onmessage(event);
                            const hpAfter = players[Globals.local_username].hp;
                            if(hpAfter < hpBefore) {
                                this.onDamageTaken(hpBefore, hpAfter);
                            }
                        }
                        this.original_onmessage(event);
                        this.onMessageReceived(event.data);
                    }
                }
                if(typeof this.original_sendmessage === "function") {
                    Globals.websocket.send = (message) => {
                        let canSend = !this.onMessageSent(message);
                        if(canSend) {
                            this.original_sendmessage.call(Globals.websocket, message);
                        }
                    }
                }
                return true;
            } catch (err) {
                console.error("Had trouble hooking into websocket...");
                return false;
            }
        }
        //This will call itthis
        (function(){
            if(!hookIntoOnMessage()) {
                // try once more
                setTimeout(hookIntoOnMessage, 40);
            }
        })()

        window.addEventListener("keydown", this.fmpKeyDown, false);

        document.getElementById("canvas").insertAdjacentHTML("beforeend",'<div id="FMPNotifications"></div>')

        // hook into switch_panels, which is called when the main panel is changed. This is used for custom panels.
        window.switch_panels = (id) => {
            let panelBefore = this.currentPanel;
            this.original_switch_panels(id);
            this.currentPanel = id;
            let panelAfter = id;
            this.onPanelChanged(panelBefore, panelAfter);
        }
        
        window.settings_modal_tab = (id) => {
            let panelBefore = this.currentSettingsPanel;
            this.hideCustomPanels();
            //I'm using links because some panels don't have a button
            if(document.getElementById(`settings-modal-${id}-panel-btn`)) {
                this.original_settings_modal_tab(id);
            } else {
                this.original_settings_modal_tab("links");
                document.getElementById('settings-modal-links-panel-btn').style.backgroundColor = '';
                document.getElementById('settings-modal-links-panel').style.display = 'none'
                document.getElementById(`settings-modal-${id}-panel`).style.display = '';
            }

            this.currentSettingsPanel = id;
            let panelAfter = id;
            this.onSettingsPanelChanged(panelBefore, panelAfter);
        }

        window.enter_pressed = function() {
            const message = chat_ele.value.trim();
            if (message === "") return;

            chat_ele.value = "";
            request_unfocus_chatbox();

            if(message.startsWith("/")) {
                const space = message.indexOf(" ");
                let command;
                let data;
                if (space <= 0) {
                    command = message.substring(1);
                    data = "";
                } else {
                    command = message.substring(1, space);
                    data = message.substring(space + 1);
                }

                //FMP only excutes if this is a valid command, so there is no reason to check if it exists
                if (window.FlatMMOPlus.handleCustomChatCommand(command, data)) {
                    return;
                }
            }
            //If it is not a valid command or isn't a command at all it will send the message
            Globals.websocket.send('CHAT=' + message);
        }

        //Remove vanilla keyboard menu
        document.getElementById("settings-modal-keyboard-panel-btn").style.display = "none";

        this.addPanel("hotkeys", "Hotkeys", "", true);

        this.addPanel("plugins", "Plugins", () => {
            let content = "";
            this.forEachPlugin(plugin => {
                let id = plugin.id;
                let name = "An FlatMMO+ Plugin!";
                let description = "";
                let author = "unknown";
                if(plugin.opts.about) {
                    let about = plugin.opts.about;
                    name = about.name || name;
                    description = about.description || description;
                    author = about.author || author;
                }
                content += `
                <div id="flatmmoplus-plugin-box-${id}" class="flatmmoplus-plugin-box">
                    <div style="display:flex;justify-content: space-between; align-items:center;">
                        <div>
                            <strong style="font-size: large;"><u>${name||id}</u></strong>(by ${author})<br />
                            <span>${description}</span><br />
                        </div>
                        ${plugin.opts.config ? `<div class="flatmmoplus-plugin-settings-button">
                        <button onclick="document.querySelector('#flatmmoplus-plugin-box-${id} .flatmmoplus-plugin-config-section').classList.toggle('displaynone')">Settings</button>
                    </div>` : ""}
                    </div>
                    <div class="flatmmoplus-plugin-config-section displaynone">
                        <hr style="grid-column: span 2">
                `;
                if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
                    plugin.opts.config.forEach(cfg => {
                        if(CONFIG_TYPES_LABEL.includes(cfg.type)) {
                            content += `<h5 style="grid-column: span 2; margin-bottom: 0; font-weight: 600">${cfg.label}</h5>`;
                        } else if (CONFIG_TYPES_PANEL.includes(cfg.type)) {
                            content += `<div onclick="FlatMMOPlus.setPanel('${cfg.panel}')" style="cursor: pointer;">
                                <h2 style="text-align: center;">${cfg.label}</h2>
                            </div>`
                        } else if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}')" />
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}')" />
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}');" />
                                </div>
                                `;
                        } else if (CONFIG_TYPES_RANGE.includes(cfg.type)) {
                            content += `<div>
                                <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                <div>
                                    <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="range" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}');" oninput="document.getElementById('flatmmoplus-config-${plugin.id}-${cfg.id}-value').innerText = this.value" />
                                    <span id="flatmmoplus-config-${plugin.id}-${cfg.id}-value"></span>
                                </div>
                            </div>`;
                        }
                        else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <textarea id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}')" rows="5" autocomplete="off" spellcheck="false"></textarea>
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="color" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true, '${cfg.id}')" />
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_LIST.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label>${cfg.label || cfg.id}</label>
                                    <div class="fmp-list-div" id="flatmmoplus-config-${plugin.id}-${cfg.id}"></div>
                                    <button style="cursor:pointer;font-size: 1rem;" onclick="FlatMMOPlus.newListField('${plugin.id}', '${cfg.id}')">Add new field</button>
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_RELATION.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label>${cfg.label || cfg.id}</label>
                                    <div class="fmp-objectLabel">
                                        <label>${cfg.key || "Key"}</label>
                                        <label>${cfg.value || "Value"}</label>
                                    </div>
                                    <div class="fmp-list-div" id="flatmmoplus-config-${plugin.id}-${cfg.id}"></div>
                                    <button style="cursor:pointer;font-size: 1rem;" onclick="FlatMMOPlus.newObjectField('${plugin.id}', '${cfg.id}')">Add new field</button>
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_PRIORITY.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label>${cfg.label || cfg.id}</label>
                                    <div class="fmp-priority-div" id="flatmmoplus-config-${plugin.id}-${cfg.id}" data-pluginid="${plugin.id}" data-configid="${cfg.id}"></div>
                                    ${cfg.canAddNew ? `<button style="cursor:pointer;font-size: 1rem;" onclick="FlatMMOPlus.newPriorityField('${plugin.id}', '${cfg.id}')">Add new field</button>` : ""}
                                </div>
                                `;
                        }
                        else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    <div>
                                        <select id="flatmmoplus-config-${plugin.id}-${cfg.id}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true,'${cfg.id}')">
                                `;
                            if(cfg.options && Array.isArray(cfg.options)) {
                                cfg.options.forEach(option => {
                                    if(typeof option === "string") {
                                        content += `<option value="${option}">${option}</option>`;
                                    }
                                    else {
                                        content += `<option value="${option.value}">${option.label || option.value}</option>`;
                                    }

                                });
                            }
                            content += `
                                        </select>
                                    </div>
                                </div>`;
                        }
                        else if (CONFIG_TYPES_BUTTON.includes(cfg.type)) {
                            content += `
                                <div>
                                    <label>${cfg.label || cfg.id}</label>
                                    <button onclick="${cfg.func}()" style="cursor: pointer;" class="${cfg.class || "fmpConfigBtn"}" style="text-align: center;">${cfg.text}</button>
                                </div>
                            `;
                        }
                    });
                    content += `
                    <div style="grid-column: span 2">
                        <button id="flatmmoplus-configbutton-${plugin.id}-reload" onclick="FlatMMOPlus.loadPluginConfigs('${id}')">Reload</button>
                        <button id="flatmmoplus-configbutton-${plugin.id}-apply" onclick="FlatMMOPlus.savePluginConfigs('${id}')">Apply</button>
                    </div>
                    `;
                }
                content += "</div>";
                content += "</div>";
            });

            return content;
        }, true);

        this.handler = new handlerPlugin()
    
        this.registerPlugin(this.handler);
        
        
        /** Priority Config Type */
        document.addEventListener('dragstart', (e) => {
            if (e.target.classList.contains('fmp-priority-item')) {
                e.target.classList.add('fmp-priority-dragging');
            }
        });
        document.addEventListener('dragend', (e) => {
            if (e.target.classList.contains('fmp-priority-item')) {
                e.target.classList.remove('fmp-priority-dragging');
            }
        });
        document.addEventListener("dragover", (e) => {
            const currentItem = e.target.closest(".fmp-priority-item");
            if (!currentItem) return;

            e.preventDefault()

            const draggingItem = document.querySelector(".fmp-priority-dragging")

            const list = currentItem.closest(".fmp-priority-div");
            
            if(!draggingItem || !list || list !== draggingItem.parentElement) {
                return;
            };


            const siblings = [...list.querySelectorAll('.fmp-priority-item:not(.fmp-priority-dragging)')];

            const nextSibling = siblings.find(sibling => {
                const box = sibling.getBoundingClientRect();
                return e.clientY <= box.top + box.height / 2;
            });

            if (nextSibling) {
                list.insertBefore(draggingItem, nextSibling);
            } else {
                list.appendChild(draggingItem);
            }

            const pluginId = list.getAttribute("data-pluginid");
            const configId = list.getAttribute("data-configid");
            window.FlatMMOPlus.plugins[pluginId].changedConfigs.add(configId);
            window.FlatMMOPlus.setPluginConfigUIDirty(pluginId, true, configId);
        });

        /*Hotkeys Setup*/
        hotkeyOverride = JSON.parse(localStorage.getItem("FMP-hotkeys") || '{}');
        defaultHotkeys.forEach(hotkey => window.FlatMMOPlus.registerHotkey(hotkey));

        document.getElementById("fmp-hotkeysContainer-Preset 1").insertAdjacentHTML("beforeend", `<button onclick="Globals.websocket.send('SAVE_PRESET=F6')"><img src="images/icons/save.png"> Save Preset</button>`);
        document.getElementById("fmp-hotkeysContainer-Preset 2").insertAdjacentHTML("beforeend", `<button onclick="Globals.websocket.send('SAVE_PRESET=F7')"><img src="images/icons/save.png"> Save Preset</button>`);
        document.getElementById("fmp-hotkeysContainer-Preset 3").insertAdjacentHTML("beforeend", `<button onclick="Globals.websocket.send('SAVE_PRESET=F8')"><img src="images/icons/save.png"> Save Preset</button>`)

        
        logFancy(`(v${this.version}) initialized.`);
        if(this.loggedIn === false && Object.keys(item_sell_prices).length !== 0) {
            this.onLogin();
        }
    }

	// Add to window and init
    window.AnimationSheetPlus = AnimationSheetPlus;
    window.FlatMMOPlusPlugin = FlatMMOPlusPlugin;
    window.FlatMMOPlus = new FlatMMOPlus();

    if (typeof unsafeWindow !== 'undefined' && window !== unsafeWindow) {
        unsafeWindow.AnimationSheetPlus = window.AnimationSheetPlus;
        unsafeWindow.FlatMMOPlusPlugin = window.FlatMMOPlusPlugin;
        unsafeWindow.FlatMMOPlus = window.FlatMMOPlus;
    }

	window.FlatMMOPlus.customChatCommands["help"] = (command, data='') => {
        let help;
        if(data && data!="help") {
            let helpContent = window.FlatMMOPlus.customChatHelp[data.trim()] || "No help content was found for this command.";
			help = `<div style="color: white;">
              	<strong><u>Command Help:</u></strong><br />
				<span><strong style="color:cyan">/${data}:</strong> <span>${helpContent}</span>
				<br>`
        }
        else {
			help = `<div style="color: white;">
              	<strong><u>Command Help:</u></strong><br />
              	<strong>Available Commands:</strong> <span>${Object.keys(window.FlatMMOPlus.customChatCommands).sort().map(s => "/"+s).join(" ")}</span><br />
              	<span>Use the /help command for more information about a specific command: /help &lt;command&gt;</span>
			</div>`
        }
        window.FlatMMOPlus.showWarning(help, "white");
        //Chat auto scroll is always true for now
		chat_div_element.scrollTop = chat_div_element.scrollHeight;
    };

	//flatChat overrides this
	window.FlatMMOPlus.registerCustomChatCommand(["clear", "clean"], (command, data='') => {
        document.getElementById("chat").innerHTML = "";
    }, "Clears all messages in chat.");

	window.FlatMMOPlus.registerCustomChatCommand(["y", "yell"], (command, data='') => {
        Globals.websocket.send('CHAT=/yell ' + data);
    }, "Chat to everyone on server.<br><strong>Usage:</strong> /yell [message]");

	window.FlatMMOPlus.registerCustomChatCommand("pm", (command, data='') => {
        Globals.websocket.send('CHAT=/pm ' + data);
    }, "Send a private message to someone.<br><strong>Usage:</strong> /pm [username] [message]");

	window.FlatMMOPlus.registerCustomChatCommand("stuck", (command, data='') => {
        Globals.websocket.send('CHAT=/stuck');
    }, "`Use if your character is stuck and cannot move.");

    window.FlatMMOPlus.registerCustomChatCommand(["players","who"], (command, data='') => {
        Globals.websocket.send('CHAT=/players');
    }, "Show all players online.");

    window.FlatMMOPlus.registerCustomChatCommand("ohelp", (command, data='') => {
        Globals.websocket.send(`CHAT=/help`);
    }, `Shows the original vanilla /help`);

    window.FlatMMOPlus.registerCustomChatCommand("collections", () => {
        Globals.websocket.send(`CHAT=/collections`);
    }, `Access to your collection log`);

    window.FlatMMOPlus.registerCustomChatCommand("detach", () => {
        document.getElementById("chat").style.display = "none";
        document.querySelector('[data-settings="hide_chat"]').checked = true;
        openChatPopup();
    }, `Detach the chat from the window`);

    window.FlatMMOPlus.registerCustomChatCommand("timers", () => {
        openTimersPopup();
    }, `Opens timers menu`);

    window.FlatMMOPlus.registerCustomChatCommand("autoyell", (command, data="on") => {
        Globals.websocket.send("CHAT=/autoyell " + data);
    }, `Turns auto yell on`);

    window.FlatMMOPlus.registerCustomChatCommand("tick", () => {
        this.showWarning(`The current action takes ${progress_bar_target + 1} ticks (${(progress_bar_target + 1) / 2} seconds)`);
    }, `Shows the time needed to complete the current action`);

    window.FlatMMOPlus.registerCustomChatCommand("trade", (command, data='') => {
        if (data === "") {
            FlatMMOPlus.showWarning("You need to specify the username", "red");
            return;
        }
        Globals.websocket.send("SEND_TRADE_REQUEST=" + data.replaceAll("_", " "));
    }, `Send a trade request if the player is in the same map.<br><b>Usage:</b>/trade [username]`);

	window.FlatMMOPlus.init();
})();