// ==UserScript==
// @name Flatmmo+ TESTING
// @namespace com.Zlef.flatmmo
// @version 0.0.5
// @description FlatMMO plugin framework (Heavily inspired & copied from Anwinity's IdlePixel+)
// @author Zlef
// @match *://flatmmo.com/play.php*
// @grant none
// @require https://update.greasyfork.org/scripts/538225/1602013/Zlef%27s%20dynamic%20modal%20and%20settings%20manager.js
// ==/UserScript==
// LIVE VERSION = 0.0.5
(function () {
'use strict';
const VERSION = "0.0.5";
if(window.FlatmmoPlus) {
// already loaded
return;
}
function logFancy(s, color="#00f7ff") {
console.log("%cFlatmmoPlus: %c"+s, `color: ${color}; font-weight: bold; font-size: 12pt;`, "color: white; font-weight: normal; font-size: 10pt;");
}
class FlatmmoPlusPlugin {
constructor(id, opts = {}) {
if (typeof id !== "string") {
throw new TypeError("FlatmmoPlusPlugin constructor requires (id: string, opts?: object)");
}
this.id = id;
this.opts = opts;
console.log(`[FlatmmoPlusPlugin] Initialising plugin: ${id}`);
this.config = {}; // placeholder for future config use
if (opts.settings) {
console.log(`[FlatmmoPlusPlugin] Found settings for ${id}`);
const sectionTitle = opts.about?.name || id.replace(/([A-Z])/g, ' $1').trim();
this.settingsManager = new zlef.SettingsManager(id, opts.settings);
this.settings = this.settingsManager.getSettings();
console.log(`[FlatmmoPlusPlugin] Created SettingsManager for ${id}`);
console.log(`[FlatmmoPlusPlugin] Loaded settings for ${id}:`, this.settings);
}
}
onLogin() {}
onMessageReceived(data) {}
onMessageSent(data) {}
onVariableSet(key, valueBefore, valueAfter) {}
onSettingsChanged() {}
}
class FlatmmoPlus {
constructor() {
console.log('[Framework] Initialising');
logFancy(`VERSION ${VERSION}`);
this.messageQueue = [];
this.hookedSockets = new WeakSet();
this.plugins = {};
this.login_on_ws = true;
this._hasLoggedIn = false;
// Custom panel support
this.customPanels = []; // { id, pluginId }
this.customPanelPage = 0;
this.activePanel = "inventory";
this.bottomButtons = []; // last 5 interface buttons
this.customPanelButtons = [];
this.init();
}
registerPlugin(plugin) {
if (!(plugin instanceof FlatmmoPlusPlugin)) {
console.warn(`[Framework] Invalid plugin:`, plugin);
return;
}
const id = plugin.id;
if (this.plugins[id]) {
console.warn(`[Framework] Plugin "${id}" already registered.`);
return;
}
this.plugins[id] = plugin;
if (plugin.settingsManager) {
if (!this.settingsManagers) this.settingsManagers = {};
this.settingsManagers[plugin.id] = plugin.settingsManager;
}
const version = plugin.opts?.about?.version || "?";
logFancy(`registered plugin "${id}" (v${version})`);
}
broadcast(methodName, ...args) {
for (const plugin of Object.values(this.plugins)) {
const fn = plugin[methodName];
if (typeof fn === "function") {
try {
fn.apply(plugin, args);
} catch (err) {
console.error(`[Framework] Error in plugin "${plugin.id}" method "${methodName}":`, err);
}
}
}
}
init() {
window.FlatmmoPlusSettingsManagers = {};
this.initCustomCSS();
this.overrideGlobalWebSocket();
if (!this.login_on_ws) {
this.waitForGameVisibility();
}
}
initCustomCSS() {
const css = `
.fmp-button-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 0px;
justify-content: flex-start;
align-items: center;
}
.fmp-back-btn {
display: none;
width: 15px;
height: 50px;
line-height: 50px;
font-size: 18px;
padding: 0;
text-align: center;
background: white;
color: black;
}
.fmp-next-btn {
display: none;
width: 15px;
height: 50px;
line-height: 50px;
font-size: 18px;
padding: 0;
text-align: center;
background: white;
color: black;
}
.fmp-panel-btn {
width: 50px;
height: 50px;
background-size: cover;
background-position: center;
}
.fmp-custom-panel {
display: none;
max-height: 500px;
overflow-y: auto;
}
`;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
overrideGlobalWebSocket() {
const NativeWebSocket = window.WebSocket;
const self = this;
console.log('[Framework] Overriding global WebSocket constructor');
window.WebSocket = function (...args) {
const ws = new NativeWebSocket(...args);
setTimeout(() => self.hookSocket(ws), 1000);
return ws;
};
window.WebSocket.prototype = NativeWebSocket.prototype;
Object.assign(window.WebSocket, NativeWebSocket);
}
hookSocket(ws) {
if (!ws || this.hookedSockets.has(ws)) return;
this.hookedSockets.add(ws);
const origSend = ws.send;
ws.send = (...args) => {
const data = args[0];
this.onMessageSent(data);
return origSend.apply(ws, args);
};
const origOnMessage = ws.onmessage;
ws.onmessage = (event) => {
if (this.login_on_ws && event.data.startsWith("LOGGED_IN=") && !this._hasLoggedIn) {
this._hasLoggedIn = true;
this._onLogin();
this.onLogin();
}
this.onMessageReceived(event.data);
if (typeof origOnMessage === 'function') {
origOnMessage.call(ws, event);
}
};
}
waitForGameVisibility() {
const gameDiv = document.getElementById('game');
if (!gameDiv) return console.warn('[Framework] #game not found');
const obs = new MutationObserver(() => {
const visible = window.getComputedStyle(gameDiv).display !== 'none';
if (visible && !this._hasLoggedIn) {
obs.disconnect();
this._hasLoggedIn = true;
this._onLogin();
this.onLogin();
}
});
obs.observe(gameDiv, { attributes: true, attributeFilter: ['style'] });
}
injectPluginSettingsUI() {
const settingsPanel = document.getElementById('ui-panel-settings');
if (!settingsPanel) return;
const settingsTable = settingsPanel.querySelector('.settings-ui');
if (!settingsTable) return;
const container = document.createElement('div');
container.style.marginTop = "10px";
const hr = document.createElement('hr');
container.appendChild(hr);
const title = document.createElement('div');
title.className = "ui-panel-title";
title.innerText = "PLUGIN SETTINGS";
container.appendChild(title);
const btn = document.createElement('button');
btn.innerText = "Open Plugin Settings";
btn.className = "btn";
btn.style.marginTop = "6px";
btn.onclick = () => this.createMultiSettingsModal();
container.appendChild(btn);
settingsPanel.appendChild(container);
}
createMultiSettingsModal() {
const modal = new window.zlef.Modal("Plugin Settings");
const content = document.createElement('div');
modal.addTitle(content, "Plugin Settings", 2, 'center');
for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
const sectionTitle = pluginId;
const section = modal.addSection(content, sectionTitle);
const build = (settings, parent, parentKey = '') => {
for (const key in settings) {
const setting = settings[key];
const fullKey = parentKey ? `${parentKey}.${key}` : key;
if (setting.type === 'section') {
const nested = modal.addSection(parent, setting.name);
build(setting.settings, nested, fullKey);
} else if (setting.type === 'multicheckbox') {
const nested = modal.addSection(parent, setting.name);
for (const subKey in setting.values) {
modal.addCheckbox(nested, setting.values[subKey], value => {
manager.settingsChanged(fullKey, value, subKey);
}, subKey);
}
} else if (setting.type === 'checkbox') {
modal.addCheckbox(parent, setting.value, value => {
manager.settingsChanged(fullKey, value);
}, key);
} else if (setting.type === 'numinput') {
modal.addInput(parent, 'number', setting.value, '', setting.minValue, setting.maxValue, value => {
manager.settingsChanged(fullKey, value);
}, key);
} else if (setting.type === 'text') {
modal.addInput(parent, 'text', setting.value, setting.placeholder, undefined, undefined, value => {
manager.settingsChanged(fullKey, value);
}, key);
} else if (setting.type === 'radio') {
modal.addRadioButtons(parent, key, setting.options, setting.value, value => {
manager.settingsChanged(fullKey, value);
}, key);
} else if (setting.type === 'combobox') {
modal.addCombobox(parent, setting.options, setting.value, value => {
manager.settingsChanged(fullKey, value);
}, key);
} else if (setting.type === 'button') {
modal.addButton(parent, setting.name, setting.function, 'btn');
}
}
};
build(manager.getSettings(), section);
}
modal.addModal(content, "Plugin Settings", 600, "auto", () => {
for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
manager.saveSettings();
if (manager.isDirty()) {
const plugin = this.plugins[pluginId];
if (plugin && typeof plugin.onSettingsChanged === "function") {
plugin.onSettingsChanged();
}
manager.clearDirty(); // reset after broadcast
}
}
});
}
showCombinedPluginSettings() {
const modal = new zlef.Modal("AllPluginSettings");
const content = document.createElement("div");
modal.addTitle(content, "Plugin Settings", 2, "center");
for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
const sectionName = manager.defaultSettings[pluginId]?.name || pluginId;
const section = modal.addSection(content, sectionName);
// Build settings UI directly into section
manager._buildSettings(manager.settings, section, modal); // Or whatever internal builder logic you extract
}
modal.addModal(content, "Plugin Settings", 600, "auto", () => {
for (const manager of Object.values(this.settingsManagers)) {
manager.saveSettings();
}
});
}
_onLogin(){
this.captureBottomButtons();
this.injectPaginationButtons();
this.hijackSwitchPanels();
this.injectPluginSettingsUI();
}
// Event relays to plugins
onMessageReceived(data) {
this.broadcast("onMessageReceived", data);
}
onMessageSent(data) {
this.broadcast("onMessageSent", data);
}
onLogin() {
this.broadcast("onLogin");
}
onSettingsChanged() {
this.broadcast('onSettingsChanged');
}
captureBottomButtons() {
const tdUI = document.querySelector('.td-ui');
if (!tdUI) return;
const allButtons = Array.from(tdUI.querySelectorAll('.interface-btn')).filter(btn => btn.id.startsWith('ui-button-'));
this.bottomButtons = allButtons.slice(-5);
}
hijackSwitchPanels() {
const original = window.switch_panels;
const self = this;
window.switch_panels = function(id) {
const custom = self.customPanels.find(p => p.id === `ui-panel-${id}`);
if (custom) {
document.querySelectorAll('.ui-panel').forEach(p => p.style.display = 'none');
const el = document.getElementById(`ui-panel-${id}`);
if (el) el.style.display = 'block';
self.activePanel = id;
return;
}
original(id);
self.activePanel = id;
self.customPanels.forEach(p => {
const el = document.getElementById(p.id);
if (el) el.style.display = 'none';
});
};
}
addPanel(plugin, title = "Untitled", icon = 'https://i.imgur.com/kAp2VY9.png') {
console.log(plugin);
const pluginCount = this.customPanels.filter(p => p.pluginId === plugin.id).length;
const id = `${plugin.id.replace(/\s/g, '')}${pluginCount + 1}`;
const fullId = `ui-panel-${id}`;
const div = document.createElement('div');
div.id = fullId;
div.className = 'ui-panel fmp-custom-panel';
div.innerHTML = `
<div class="ui-panel-title">${title}</div>
`;
const after = document.getElementById('ui-panel-worship');
if (after?.parentNode) after.parentNode.insertBefore(div, after.nextSibling);
this.customPanels.push({ id: fullId, pluginId: plugin.id, icon });
this.updatePanelButtons();
return div;
}
injectPaginationButtons() {
const style = document.createElement('style');
style.innerHTML = `
#ui-button-monster_log,
#ui-button-worship,
#ui-button-donor-shop,
#ui-button-settings,
#ui-button-database {
margin-right: 4px;
}
`;
document.head.appendChild(style);
const tdUI = document.querySelector('.td-ui');
if (!tdUI || this.bottomButtons.length < 5) return;
const btnContainer = document.createElement('div');
btnContainer.id = 'fmp-button-container';
btnContainer.className = 'fmp-button-container';
// Move existing buttons
this.bottomButtons.forEach(btn => btnContainer.appendChild(btn));
// Create back button
const backBtn = document.createElement('div');
backBtn.id = 'fmp-back-btn';
backBtn.className = 'interface-btn hover fmp-back-btn';
backBtn.innerText = '<';
backBtn.title = 'Back';
backBtn.onclick = () => {
this.customPanelPage--;
this.updatePanelButtons();
};
this.backBtn = backBtn;
btnContainer.appendChild(backBtn);
// Create next button
const nextBtn = document.createElement('div');
nextBtn.id = 'fmp-next-btn';
nextBtn.className = 'interface-btn hover fmp-next-btn';
nextBtn.innerText = '>';
nextBtn.title = 'Next';
nextBtn.onclick = () => {
this.customPanelPage++;
this.updatePanelButtons();
};
this.nextBtn = nextBtn;
btnContainer.appendChild(nextBtn);
tdUI.appendChild(btnContainer);
}
updatePanelButtons() {
const total = this.customPanels.length;
const pages = Math.ceil(total / 5) + 1;
const showingBaseUI = this.customPanelPage === 0;
const container = document.getElementById('fmp-button-container');
if (!container) return;
this.customPanelButtons.forEach(btn => btn.remove());
this.customPanelButtons = [];
this.bottomButtons.forEach(btn => btn.style.display = showingBaseUI ? 'inline-block' : 'none');
if (this.customPanelPage > 0) {
const startIndex = (this.customPanelPage - 1) * 5;
const panelsToShow = this.customPanels.slice(startIndex, startIndex + 5);
panelsToShow.forEach((panel, i) => {
const btn = document.createElement('div');
btn.className = 'interface-btn hover fmp-panel-btn';
btn.title = `${panel.pluginId} (${i + 1})`;
btn.id = `fmp-panel-btn-${i}`;
btn.onclick = () => window.switch_panels(panel.id.replace("ui-panel-", ""));
btn.style.backgroundImage = `url('${panel.icon}')`;
btn.style.backgroundSize = "cover";
btn.style.backgroundPosition = "center";
if (this.nextBtn && container.contains(this.nextBtn)) {
container.insertBefore(btn, this.nextBtn);
} else {
container.appendChild(btn);
}
this.customPanelButtons.push(btn);
});
}
if (this.backBtn && this.nextBtn) {
this.backBtn.style.display = this.customPanelPage > 0 ? 'inline-block' : 'none';
this.nextBtn.style.display = this.customPanelPage < pages - 1 ? 'inline-block' : 'none';
}
}
}
// Add to window and init
window.FlatmmoPlusPlugin = FlatmmoPlusPlugin;
window.FlatmmoPlus = new FlatmmoPlus();
})();