Flatmmo+ TESTING

FlatMMO plugin framework (Heavily inspired & copied from Anwinity's IdlePixel+)

Version vom 05.06.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.ip-ddns.com/scripts/538103/1602094/Flatmmo%2B%20TESTING.js

// ==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.ip-ddns.com/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();


})();