Greasy Fork

Greasy Fork is available in English.

FlatMMOPlus

FlatMMO plugin framework

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/544062/1632723/FlatMMOPlus.js

// ==UserScript==
// @name         FlatMMOPlus
// @namespace    com.dounford.flatmmo
// @version      0.0.3
// @description  FlatMMO plugin framework
// @author       Anwinity ported by Dounford
// @match        *://flatmmo.com/play.php*
// @grant        none
// ==/UserScript==

(function() {
	'use strict';

	const VERSION = "0.0.3"

	if (window.FlatMMOPlus) {
		//already loaded
		return;
	}

	const LOCAL_STORAGE_KEY_DEBUG = "FlatMMOPlus:debug";
 
    const CONFIG_TYPES_LABEL = ["label"];
    const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
    const CONFIG_TYPES_INTEGER = ["integer", "int"];
    const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
    const CONFIG_TYPES_STRING = ["string", "text"];
    const CONFIG_TYPES_SELECT = ["select"];
    const CONFIG_TYPES_COLOR = ["color"];

	const CHAT_COMMAND_NO_OVERRIDE = ["help", "mute", "ban", "players"];

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

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

	const internal = {
		init() {
			const self = this;

			document.head.insertAdjacentHTML("beforeend", `<style>
				.displaynone {
					display: none !important;
				}
			</style>`)

			//Tries to hook into the websocket messages
			const hookIntoOnMessage = () => {
				try {
					const original_onmessage = Globals.websocket.onmessage;
					if (typeof original_onmessage === "function") {
						Globals.websocket.onmessage = function(event) {
							if(event.data.startsWith("SET_MAP")) {
								const mapBefore = current_map;
								original_onmessage(event);
								const mapAfter = current_map;
								self.onMapChanged(mapBefore, mapAfter);
								return;
							} else if (event.data.startsWith("SET_INVENTORY_ITEMS")) {
								const inventoryBefore = items;
								original_onmessage(event);
								const inventoryAfter = items;
								self.onInventoryChanged(inventoryBefore, inventoryAfter);
								return;
							}
							original_onmessage(event);
							self.onMessageReceived(event.data);
						}
					}
					return true;
				} catch (err) {
					console.error("Had trouble hooking into websocket...");
                    return false;
				}
			}
			//This will call itself
			(function(){
				if(!hookIntoOnMessage()) {
                    // try once more
                    setTimeout(hookIntoOnMessage, 40);
                }
			})()


			//For some reason I was unable to change the original function, so I just deleted and added a new one
			window.removeEventListener("keypress", keypress_listener, false);
			function fmpKeyPress(e) {
				//flatChat handles messages in another way, but checks for custom commands inside itself
				if (self.plugins.flatChat) {
					return;
				}
				//If typing on panels it should not type on the chat
				if(document.querySelector(".td-ui").contains(document.activeElement)) {
					return;
				}
				if(Globals.local_username == null) return;
				if(has_modal_open()) return;

				let keyCode = e.keyCode;
				let char = String.fromCharCode(keyCode);


				//firefox fix
				if(keyCode == "47" || keyCode == "39") {
					e.preventDefault(); 
				}
				//13 is Enter
				if(keyCode == "13") {
					const message = local_chat_message.trim()
					if(message.length == 0) return; //if empty do nothing

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

						if (window.FlatMMOPlus.handleCustomChatCommand(command, data)) {
							local_chat_message = "";
							return
						} 
					}
					Globals.websocket.send('CHAT=' + message);
					local_chat_message = "";
					return;
				}

				//if any normal key then checks if the message is too big
				if(LOCAL_CHAT_MAX_LENGTH <= local_chat_message.length) {
					return;
				}
				//if not then add the key pressed to the message
				local_chat_message += char;
			}

			window.addEventListener("keypress", fmpKeyPress, false);

			// hook into switch_panels, which is called when the main panel is changed. This is used for custom panels.
            const original_switch_panels = window.switch_panels;
            window.switch_panels = function(id) {
				let panelBefore = self.currentPanel;
                self.hideCustomPanels();
                original_switch_panels(id);
				self.currentPanel = id;
				let panelAfter = id;
				self.onPanelChanged(panelBefore, panelAfter);
            }

            // create plugin menu item and panel
			const settingsBody = document.querySelector(".settings-ui tbody")
			settingsBody.insertAdjacentHTML("beforeend",`<tr onclick="FlatMMOPlus.setPanel('flatmmoplus')">
				<td colspan="2" style="cursor: pointer;text-align: center;font-size: 2rem;font-weight: bold;">PLUGINS</td>
			</tr>`)

			self.addPanel("flatmmoplus", "FlatMMO+ Plugins", function() {
                let content = `
                <style>
                    .flatmmoplus-plugin-box {
                        display: block;
                        position: relative;
                        padding: 0.25em;
                        color: white;
                        background-color: rgb(107, 107, 107);
                        border: 1px solid black;
                        border-radius: 6px;
                        margin-bottom: 0.5em;
                    }
                    .flatmmoplus-plugin-box .flatmmoplus-plugin-settings-button {
                        position: absolute;
                        right: 2px;
                        top: 2px;
                        cursor: pointer;
						button {
							font-size: small;
						}
                    }
                    .flatmmoplus-plugin-box .flatmmoplus-plugin-config-section {
                        display: grid;
                        grid-template-columns: minmax(100px, min-content) 1fr;
                        row-gap: 0.5em;
                        column-gap: 0.5em;
                        white-space: nowrap;
                    }
                </style>
                `;
                self.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">
                        <strong><u>${name||id}</u></strong><br>(by ${author})<br />
                        <span>${description}</span><br />
                        <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_BOOLEAN.includes(cfg.type)) {
                                content += `
                                    <div>
                                        <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    </div>
                                    <div>
                                        <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)" />
                                    </div>
                                    `;
                            }
                            else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
                                content += `
                                    <div>
                                        <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    </div>
                                    <div>
                                        <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)" />
                                    </div>
                                    `;
                            }
                            else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
                                content += `
                                    <div>
                                        <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    </div>
                                    <div>
                                        <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)" />
                                    </div>
                                    `;
                            }
                            else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
                                content += `
                                    <div>
                                        <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    </div>
                                    <div>
                                        <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)" />
                                    </div>
                                    `;
                            }
                            else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
                                content += `
                                    <div>
                                        <label for="flatmmoplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
                                    </div>
                                    <div>
                                        <input id="flatmmoplus-config-${plugin.id}-${cfg.id}" type="color" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)" />
                                    </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>
                                    <div>
                                        <select id="flatmmoplus-config-${plugin.id}-${cfg.id}" onchange="FlatMMOPlus.setPluginConfigUIDirty('${id}', true)">
                                    `;
                                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>
                                    `;
                            }
                        });
                        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>";
                    if(plugin.opts.config) {
                        content += `
                        <div class="flatmmoplus-plugin-settings-button">
                            <button onclick="document.querySelector('#flatmmoplus-plugin-box-${id} .flatmmoplus-plugin-config-section').classList.toggle('displaynone')">Settings</button>
                        </div>`;
                    }
                    content += "</div>";
                });
 
                return content;
            });

			logFancy(`(v${self.version}) initialized.`);
		}
	}

	class FlatMMOPlus {

		constructor() {
			this.version = VERSION;
			this.plugins = {};
			this.panels = {};
			this.debug = false;
			this.nextUniqueId = 1;
			this.customChatCommands = {
				help: (command, data) => {
					console.log("help", command, data);
				}
			}
			this.customChatHelp = {};
			this.customDialogOptions = {};
			this.currentPanel = "inventory";

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

		registerCustomChatCommand(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];
            }
		}

		handleCustomChatCommand(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;
		}

		uniqueId() {
			return this.nextUniqueId++;
		}

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

		setPluginConfigUIDirty(id, dirty) {
            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];
            const button = document.getElementById(`flatmmoplus-configbutton-${plugin.id}-apply`);
            if(button) {
                button.disabled = !dirty;
            }
        }

		loadPluginConfigs(id) {
            if (typeof id !== "string") {
                throw new TypeError("FlatMMOPlus.reloadPluginConfigs takes the following arguments: (id:string)");
            }
            const plugin = this.plugins[id];
            const config = {};
            let stored;
            try {
                stored = JSON.parse(localStorage.getItem(`flatmmoplus.${id}.config`) || "{}");
            } 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_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;
                        }
                    }
                });
            }
            plugin.config = config;
            this.setPluginConfigUIDirty(id, false);
            if (typeof plugin.onConfigsChanged === "function") {
                plugin.onConfigsChanged();
            }
        }

		savePluginConfigs(id) {
            if (typeof id !== "string") {
                throw new TypeError("FlatMMOPlus.savePluginConfigs takes the following arguments: (id:string)");
            }
            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_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;
                    }
                });
            }
            plugin.config = config;
            localStorage.setItem(`flatmmoplus.${id}.config`, JSON.stringify(config));
            this.setPluginConfigUIDirty(id, false);
            if (typeof plugin.onConfigsChanged === "function") {
                plugin.onConfigsChanged();
            }
        }

		addPanel(id, title, content) {
            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)");
            }
            const lastPanel = document.querySelector("#ui-panel-worship");
            lastPanel.insertAdjacentHTML("afterend",`
            <div id="ui-panel-${id}" style="display: none" class="ui-panel">
				<div class="ui-panel-title">${title}</div>
                <hr>
                <div id="ui-panel-${id}-content"></div>
            </div>
            `);
            this.panels[id] = {
                id: id,
                title: title,
                content: content
            };
            this.refreshPanel(id);
        }

		refreshPanel(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 === "flatmmoplus") {
                this.forEachPlugin(plugin => {
                    this.loadPluginConfigs(plugin.id);
                });
            }
        }

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

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

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

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

		hideCustomPanels() {
            Object.values(this.panels).forEach((panel) => {
                const el = document.getElementById(`ui-panel-${panel.id}`);
                if(el) {
                    el.style.display = "none";
                }
            });
        }

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

		onLogin() {
			if(this.debug) {
                console.log(`FM+ onLogin`);
            }
            logFancy("login detected");
            this.forEachPlugin((plugin) => {
                if(typeof plugin.onLogin === "function") {
                    plugin.onLogin();
                }
            });
			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>`)

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

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

		onPanelChanged(panelBefore, panelAfter) {
            if(this.debug) {
                console.log(`FM+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
            }
            if(panelAfter === "flatmmoplus") {
                this.refreshPanel("flatmmoplus");
            }
            this.forEachPlugin((plugin) => {
                if(typeof plugin.onPanelChanged === "function") {
                    plugin.onPanelChanged(panelBefore, panelAfter);
                }
            });
        }

		onMapChanged(mapBefore, mapAfter) {
            if(this.debug) {
                console.log(`FMMO+ onMapChanged "${mapBefore}" -> "${mapAfter}"`);
            }
            this.forEachPlugin((plugin) => {
                if(typeof plugin.onMapChanged === "function") {
                    plugin.onMapChanged(mapBefore, mapAfter);
                }
            });
        }

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

	// Add to window and init
    window.FlatMMOPlusPlugin = FlatMMOPlusPlugin;
    window.FlatMMOPlus = new 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>`
        }
		document.getElementById("chat").insertAdjacentHTML("beforeend",help)
        //Chat auto scroll is always true for now
		chat_div_element.scrollTop = chat_div_element.scrollHeight;
    };

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

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

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

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