Flatmmo+

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/538103/1603307/Flatmmo%2B.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Flatmmo+ TESTING
// @namespace    com.Zlef.flatmmo
// @version      0.0.6
// @description  FlatMMO plugin framework (Heavily inspired & copied from Anwinity's IdlePixel+)
// @author       Zlef
// @match        *://flatmmo.com/play.php*
// @grant        none
// ==/UserScript==

// LIVE VERSION = 0.0.6

(function () {
    'use strict';

    const VERSION = "0.0.6";

    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


            try {
                if (opts.settings) {
                    console.log(`[FlatmmoPlusPlugin] Found settings for ${id}`);

                    if (typeof window.zlef?.SettingsManager !== "function") {
                        throw new Error("zlef.SettingsManager is not defined yet.");
                    }

                    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);
                }
            } catch (err) {
                console.error(`[FlatmmoPlusPlugin] Failed to initialise settings for "${id}":`, err);
            }


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

        }

        getSetting(key, type = null) {
            return window.FlatmmoPlus?.getSetting(`${this.id}.${key}`, type);
        }

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

        getSetting(key, type = null) {
            if (!this.settingsManagers) return undefined;

            const [pluginId, ...path] = key.split(".");
            const manager = this.settingsManagers[pluginId];
            if (!manager) {
                console.warn(`[FlatmmoPlus] No settings manager for plugin "${pluginId}"`);
                return undefined;
            }

            let current = manager.getSettings();

            for (let i = 0; i < path.length; i++) {
                const part = path[i];

                if (!current[part]) return undefined;

                const setting = current[part];

                // If it's a section, go deeper
                if (setting.type === "section") {
                    current = setting.settings;
                } else {
                    // If we're at the last part, return the value with optional coercion
                    if (i === path.length - 1) {
                        let value = setting.value;

                        if (type) {
                            switch (type.toLowerCase()) {
                                case "int":
                                case "integer":
                                    return parseInt(value);
                                case "bool":
                                case "boolean":
                                    return value === true || value === "true";
                                case "string":
                                    return String(value);
                                default:
                                    console.warn(`[FlatmmoPlus] Unknown type coercion '${type}' for setting '${key}'`);
                                    return value;
                            }
                        }

                        return value;
                    } else {
                        // Trying to go deeper into a non-section setting
                        return undefined;
                    }
                }
            }

            return undefined;
        }


        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;
                        const label = setting.label || key;

                        if (setting.type === 'section') {
                            const nested = modal.addSection(parent, label);
                            build(setting.settings, nested, fullKey);
                        } else if (setting.type === 'multicheckbox') {
                            const nested = modal.addSection(parent, label);
                            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);
                            }, label);
                        } else if (setting.type === 'numinput') {
                            modal.addInput(parent, 'number', setting.value, '', setting.minValue, setting.maxValue, value => {
                                manager.settingsChanged(fullKey, value);
                            }, label);
                        } else if (setting.type === 'text') {
                            modal.addInput(parent, 'text', setting.value, setting.placeholder, undefined, undefined, value => {
                                manager.settingsChanged(fullKey, value);
                            }, label);
                        } else if (setting.type === 'radio') {
                            modal.addRadioButtons(parent, label, setting.options, setting.value, value => {
                                manager.settingsChanged(fullKey, value);
                            }, label);
                        } else if (setting.type === 'combobox') {
                            modal.addCombobox(parent, setting.options, setting.value, value => {
                                manager.settingsChanged(fullKey, value);
                            }, label);
                        } else if (setting.type === 'button') {
                            modal.addButton(parent, label, 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();


})();