// ==UserScript==
// @name FlatMMOPlus
// @namespace com.dounford.flatmmo
// @version 0.0.2
// @description FlatMMO plugin framework
// @author Dounford
// @match *://flatmmo.com/play.php*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const VERSION = "0.0.1"
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("%FlatMMOPlus: %c"+s, `color: ${color}; font-weight: bold; font-size: 12pt;`, "color: black; 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];
}
}
}
const internal = {
init() {
const self = this;
//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.onMapChange(mapBefore, mapAfter);
return;
} else if (event.data.startsWith("SET_INVENTORY_ITEMS")) {
const inventoryBefore = items;
original_onmessage(event);
const inventoryAfter = items;
self.onInventoryChange(inventoryBefore, inventoryAfter);
return;
}
original_onmessage(event);
self.onMessageReceived(event.data);
}
}
} catch (err) {
console.error("Had trouble hooking into websocket...");
return false;
}
}
//This will call itself
(function(){
if(!hookIntoOnMessage()) {
// try once more
setTimeout(hookIntoOnMessage, 40);
}
})()
//Change the original send chat function
keypress_listener = function(e) {
//flatChat handles messages in another way, but checks for custom commands inside itself
if (FlatMMOPlus.plugins.flatChat) {
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 = "";
} else {
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;
}
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 = {};
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 = $(`#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.insertAdjacentElement("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(`ui-panel-${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("~");
const [sender, message] = split[1].split(" yelled: ");
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);
}
});
}
onMapChange(mapBefore, mapAfter) {
if(this.debug) {
console.log(`FMMO+ onMapChange "${mapBefore}" -> "${mapAfter}"`);
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onMapChange === "function") {
plugin.onMapChange(mapBefore, mapAfter);
}
});
}
onInventoryChange(inventoryBefore, inventoryAfter) {
if(this.debug) {
console.log(`FMMO+ onInventoryChange "${inventoryBefore}" -> "${inventoryAfter}"`);
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onInventoryChange === "function") {
plugin.onInventoryChange(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 <command></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);
})();