Flatmmo+

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

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/538103/1603307/Flatmmo%2B.js

  1. // ==UserScript==
  2. // @name Flatmmo+ TESTING
  3. // @namespace com.Zlef.flatmmo
  4. // @version 0.0.6
  5. // @description FlatMMO plugin framework (Heavily inspired & copied from Anwinity's IdlePixel+)
  6. // @author Zlef
  7. // @match *://flatmmo.com/play.php*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // LIVE VERSION = 0.0.6
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const VERSION = "0.0.6";
  17.  
  18. if(window.FlatmmoPlus) {
  19. // already loaded
  20. return;
  21. }
  22.  
  23. function logFancy(s, color="#00f7ff") {
  24. console.log("%cFlatmmoPlus: %c"+s, `color: ${color}; font-weight: bold; font-size: 12pt;`, "color: white; font-weight: normal; font-size: 10pt;");
  25. }
  26.  
  27.  
  28. class FlatmmoPlusPlugin {
  29. constructor(id, opts = {}) {
  30. if (typeof id !== "string") {
  31. throw new TypeError("FlatmmoPlusPlugin constructor requires (id: string, opts?: object)");
  32. }
  33.  
  34. this.id = id;
  35. this.opts = opts;
  36.  
  37. console.log(`[FlatmmoPlusPlugin] Initialising plugin: ${id}`);
  38.  
  39. this.config = {}; // placeholder for future config use
  40.  
  41.  
  42. try {
  43. if (opts.settings) {
  44. console.log(`[FlatmmoPlusPlugin] Found settings for ${id}`);
  45.  
  46. if (typeof window.zlef?.SettingsManager !== "function") {
  47. throw new Error("zlef.SettingsManager is not defined yet.");
  48. }
  49.  
  50. this.settingsManager = new zlef.SettingsManager(id, opts.settings);
  51. this.settings = this.settingsManager.getSettings();
  52.  
  53. console.log(`[FlatmmoPlusPlugin] Created SettingsManager for ${id}`);
  54. console.log(`[FlatmmoPlusPlugin] Loaded settings for ${id}:`, this.settings);
  55. }
  56. } catch (err) {
  57. console.error(`[FlatmmoPlusPlugin] Failed to initialise settings for "${id}":`, err);
  58. }
  59.  
  60.  
  61. /*
  62. if (opts.settings) {
  63. console.log(`[FlatmmoPlusPlugin] Found settings for ${id}`);
  64.  
  65. const sectionTitle = opts.about?.name || id.replace(/([A-Z])/g, ' $1').trim();
  66. this.settingsManager = new zlef.SettingsManager(id, opts.settings);
  67. this.settings = this.settingsManager.getSettings();
  68.  
  69. console.log(`[FlatmmoPlusPlugin] Created SettingsManager for ${id}`);
  70. console.log(`[FlatmmoPlusPlugin] Loaded settings for ${id}:`, this.settings);
  71. }
  72. */
  73.  
  74. }
  75.  
  76. getSetting(key, type = null) {
  77. return window.FlatmmoPlus?.getSetting(`${this.id}.${key}`, type);
  78. }
  79.  
  80. onLogin() {}
  81. onMessageReceived(data) {}
  82. onMessageSent(data) {}
  83. onVariableSet(key, valueBefore, valueAfter) {}
  84. onSettingsChanged() {}
  85.  
  86. }
  87.  
  88.  
  89.  
  90. class FlatmmoPlus {
  91. constructor() {
  92. console.log('[Framework] Initialising');
  93. logFancy(`VERSION ${VERSION}`);
  94. this.messageQueue = [];
  95. this.hookedSockets = new WeakSet();
  96. this.plugins = {};
  97. this.login_on_ws = true;
  98. this._hasLoggedIn = false;
  99.  
  100. // Custom panel support
  101. this.customPanels = []; // { id, pluginId }
  102. this.customPanelPage = 0;
  103. this.activePanel = "inventory";
  104. this.bottomButtons = []; // last 5 interface buttons
  105. this.customPanelButtons = [];
  106.  
  107. this.init();
  108. }
  109.  
  110. getSetting(key, type = null) {
  111. if (!this.settingsManagers) return undefined;
  112.  
  113. const [pluginId, ...path] = key.split(".");
  114. const manager = this.settingsManagers[pluginId];
  115. if (!manager) {
  116. console.warn(`[FlatmmoPlus] No settings manager for plugin "${pluginId}"`);
  117. return undefined;
  118. }
  119.  
  120. let current = manager.getSettings();
  121.  
  122. for (let i = 0; i < path.length; i++) {
  123. const part = path[i];
  124.  
  125. if (!current[part]) return undefined;
  126.  
  127. const setting = current[part];
  128.  
  129. // If it's a section, go deeper
  130. if (setting.type === "section") {
  131. current = setting.settings;
  132. } else {
  133. // If we're at the last part, return the value with optional coercion
  134. if (i === path.length - 1) {
  135. let value = setting.value;
  136.  
  137. if (type) {
  138. switch (type.toLowerCase()) {
  139. case "int":
  140. case "integer":
  141. return parseInt(value);
  142. case "bool":
  143. case "boolean":
  144. return value === true || value === "true";
  145. case "string":
  146. return String(value);
  147. default:
  148. console.warn(`[FlatmmoPlus] Unknown type coercion '${type}' for setting '${key}'`);
  149. return value;
  150. }
  151. }
  152.  
  153. return value;
  154. } else {
  155. // Trying to go deeper into a non-section setting
  156. return undefined;
  157. }
  158. }
  159. }
  160.  
  161. return undefined;
  162. }
  163.  
  164.  
  165. registerPlugin(plugin) {
  166. if (!(plugin instanceof FlatmmoPlusPlugin)) {
  167. console.warn(`[Framework] Invalid plugin:`, plugin);
  168. return;
  169. }
  170.  
  171. const id = plugin.id;
  172. if (this.plugins[id]) {
  173. console.warn(`[Framework] Plugin "${id}" already registered.`);
  174. return;
  175. }
  176.  
  177. this.plugins[id] = plugin;
  178.  
  179. if (plugin.settingsManager) {
  180. if (!this.settingsManagers) this.settingsManagers = {};
  181. this.settingsManagers[plugin.id] = plugin.settingsManager;
  182. }
  183.  
  184.  
  185. const version = plugin.opts?.about?.version || "?";
  186. logFancy(`registered plugin "${id}" (v${version})`);
  187. }
  188.  
  189. broadcast(methodName, ...args) {
  190. for (const plugin of Object.values(this.plugins)) {
  191. const fn = plugin[methodName];
  192. if (typeof fn === "function") {
  193. try {
  194. fn.apply(plugin, args);
  195. } catch (err) {
  196. console.error(`[Framework] Error in plugin "${plugin.id}" method "${methodName}":`, err);
  197. }
  198. }
  199. }
  200. }
  201.  
  202. init() {
  203. window.FlatmmoPlusSettingsManagers = {};
  204.  
  205. this.initCustomCSS();
  206. this.overrideGlobalWebSocket();
  207. if (!this.login_on_ws) {
  208. this.waitForGameVisibility();
  209. }
  210. }
  211.  
  212. initCustomCSS() {
  213. const css = `
  214. .fmp-button-container {
  215. display: flex;
  216. flex-wrap: wrap;
  217. gap: 4px;
  218. margin-top: 0px;
  219. justify-content: flex-start;
  220. align-items: center;
  221. }
  222.  
  223. .fmp-back-btn {
  224. display: none;
  225. width: 15px;
  226. height: 50px;
  227. line-height: 50px;
  228. font-size: 18px;
  229. padding: 0;
  230. text-align: center;
  231. background: white;
  232. color: black;
  233. }
  234.  
  235. .fmp-next-btn {
  236. display: none;
  237. width: 15px;
  238. height: 50px;
  239. line-height: 50px;
  240. font-size: 18px;
  241. padding: 0;
  242. text-align: center;
  243. background: white;
  244. color: black;
  245. }
  246.  
  247. .fmp-panel-btn {
  248. width: 50px;
  249. height: 50px;
  250. background-size: cover;
  251. background-position: center;
  252. }
  253.  
  254. .fmp-custom-panel {
  255. display: none;
  256. max-height: 500px;
  257. overflow-y: auto;
  258. }
  259. `;
  260. const style = document.createElement('style');
  261. style.type = 'text/css';
  262. style.appendChild(document.createTextNode(css));
  263. document.head.appendChild(style);
  264. }
  265.  
  266. overrideGlobalWebSocket() {
  267. const NativeWebSocket = window.WebSocket;
  268. const self = this;
  269.  
  270. console.log('[Framework] Overriding global WebSocket constructor');
  271.  
  272. window.WebSocket = function (...args) {
  273. const ws = new NativeWebSocket(...args);
  274. setTimeout(() => self.hookSocket(ws), 1000);
  275. return ws;
  276. };
  277.  
  278. window.WebSocket.prototype = NativeWebSocket.prototype;
  279. Object.assign(window.WebSocket, NativeWebSocket);
  280. }
  281.  
  282. hookSocket(ws) {
  283. if (!ws || this.hookedSockets.has(ws)) return;
  284.  
  285. this.hookedSockets.add(ws);
  286.  
  287. const origSend = ws.send;
  288. ws.send = (...args) => {
  289. const data = args[0];
  290. this.onMessageSent(data);
  291. return origSend.apply(ws, args);
  292. };
  293.  
  294. const origOnMessage = ws.onmessage;
  295. ws.onmessage = (event) => {
  296. if (this.login_on_ws && event.data.startsWith("LOGGED_IN=") && !this._hasLoggedIn) {
  297. this._hasLoggedIn = true;
  298. this._onLogin();
  299. this.onLogin();
  300. }
  301. this.onMessageReceived(event.data);
  302. if (typeof origOnMessage === 'function') {
  303. origOnMessage.call(ws, event);
  304. }
  305. };
  306. }
  307.  
  308. waitForGameVisibility() {
  309. const gameDiv = document.getElementById('game');
  310. if (!gameDiv) return console.warn('[Framework] #game not found');
  311.  
  312. const obs = new MutationObserver(() => {
  313. const visible = window.getComputedStyle(gameDiv).display !== 'none';
  314. if (visible && !this._hasLoggedIn) {
  315. obs.disconnect();
  316. this._hasLoggedIn = true;
  317. this._onLogin();
  318. this.onLogin();
  319. }
  320. });
  321.  
  322. obs.observe(gameDiv, { attributes: true, attributeFilter: ['style'] });
  323. }
  324.  
  325. injectPluginSettingsUI() {
  326. const settingsPanel = document.getElementById('ui-panel-settings');
  327. if (!settingsPanel) return;
  328.  
  329. const settingsTable = settingsPanel.querySelector('.settings-ui');
  330. if (!settingsTable) return;
  331.  
  332. const container = document.createElement('div');
  333. container.style.marginTop = "10px";
  334.  
  335. const hr = document.createElement('hr');
  336. container.appendChild(hr);
  337.  
  338. const title = document.createElement('div');
  339. title.className = "ui-panel-title";
  340. title.innerText = "PLUGIN SETTINGS";
  341. container.appendChild(title);
  342.  
  343. const btn = document.createElement('button');
  344. btn.innerText = "Open Plugin Settings";
  345. btn.className = "btn";
  346. btn.style.marginTop = "6px";
  347. btn.onclick = () => this.createMultiSettingsModal();
  348.  
  349. container.appendChild(btn);
  350.  
  351. settingsPanel.appendChild(container);
  352. }
  353.  
  354. createMultiSettingsModal() {
  355. const modal = new window.zlef.Modal("Plugin Settings");
  356. const content = document.createElement('div');
  357.  
  358. modal.addTitle(content, "Plugin Settings", 2, 'center');
  359.  
  360. for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
  361. const sectionTitle = pluginId;
  362. const section = modal.addSection(content, sectionTitle);
  363.  
  364. const build = (settings, parent, parentKey = '') => {
  365. for (const key in settings) {
  366. const setting = settings[key];
  367. const fullKey = parentKey ? `${parentKey}.${key}` : key;
  368. const label = setting.label || key;
  369.  
  370. if (setting.type === 'section') {
  371. const nested = modal.addSection(parent, label);
  372. build(setting.settings, nested, fullKey);
  373. } else if (setting.type === 'multicheckbox') {
  374. const nested = modal.addSection(parent, label);
  375. for (const subKey in setting.values) {
  376. modal.addCheckbox(nested, setting.values[subKey], value => {
  377. manager.settingsChanged(fullKey, value, subKey);
  378. }, subKey);
  379. }
  380. } else if (setting.type === 'checkbox') {
  381. modal.addCheckbox(parent, setting.value, value => {
  382. manager.settingsChanged(fullKey, value);
  383. }, label);
  384. } else if (setting.type === 'numinput') {
  385. modal.addInput(parent, 'number', setting.value, '', setting.minValue, setting.maxValue, value => {
  386. manager.settingsChanged(fullKey, value);
  387. }, label);
  388. } else if (setting.type === 'text') {
  389. modal.addInput(parent, 'text', setting.value, setting.placeholder, undefined, undefined, value => {
  390. manager.settingsChanged(fullKey, value);
  391. }, label);
  392. } else if (setting.type === 'radio') {
  393. modal.addRadioButtons(parent, label, setting.options, setting.value, value => {
  394. manager.settingsChanged(fullKey, value);
  395. }, label);
  396. } else if (setting.type === 'combobox') {
  397. modal.addCombobox(parent, setting.options, setting.value, value => {
  398. manager.settingsChanged(fullKey, value);
  399. }, label);
  400. } else if (setting.type === 'button') {
  401. modal.addButton(parent, label, setting.function, 'btn');
  402. }
  403. }
  404. };
  405.  
  406. build(manager.getSettings(), section);
  407. }
  408.  
  409. modal.addModal(content, "Plugin Settings", 600, "auto", () => {
  410. for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
  411. manager.saveSettings();
  412.  
  413. if (manager.isDirty()) {
  414. const plugin = this.plugins[pluginId];
  415. if (plugin && typeof plugin.onSettingsChanged === "function") {
  416. plugin.onSettingsChanged();
  417. }
  418. manager.clearDirty(); // reset after broadcast
  419. }
  420. }
  421. });
  422. }
  423.  
  424. showCombinedPluginSettings() {
  425. const modal = new zlef.Modal("AllPluginSettings");
  426. const content = document.createElement("div");
  427.  
  428. modal.addTitle(content, "Plugin Settings", 2, "center");
  429.  
  430. for (const [pluginId, manager] of Object.entries(this.settingsManagers)) {
  431. const sectionName = manager.defaultSettings[pluginId]?.name || pluginId;
  432. const section = modal.addSection(content, sectionName);
  433.  
  434. // Build settings UI directly into section
  435. manager._buildSettings(manager.settings, section, modal); // Or whatever internal builder logic you extract
  436. }
  437.  
  438. modal.addModal(content, "Plugin Settings", 600, "auto", () => {
  439. for (const manager of Object.values(this.settingsManagers)) {
  440. manager.saveSettings();
  441. }
  442. });
  443. }
  444.  
  445. _onLogin(){
  446. this.captureBottomButtons();
  447. this.injectPaginationButtons();
  448. this.hijackSwitchPanels();
  449. this.injectPluginSettingsUI();
  450. }
  451.  
  452. // Event relays to plugins
  453. onMessageReceived(data) {
  454. this.broadcast("onMessageReceived", data);
  455. }
  456.  
  457. onMessageSent(data) {
  458. this.broadcast("onMessageSent", data);
  459. }
  460.  
  461. onLogin() {
  462. this.broadcast("onLogin");
  463. }
  464.  
  465. onSettingsChanged() {
  466. this.broadcast('onSettingsChanged');
  467. }
  468.  
  469. captureBottomButtons() {
  470. const tdUI = document.querySelector('.td-ui');
  471. if (!tdUI) return;
  472.  
  473. const allButtons = Array.from(tdUI.querySelectorAll('.interface-btn')).filter(btn => btn.id.startsWith('ui-button-'));
  474. this.bottomButtons = allButtons.slice(-5);
  475. }
  476.  
  477.  
  478. hijackSwitchPanels() {
  479. const original = window.switch_panels;
  480. const self = this;
  481.  
  482. window.switch_panels = function(id) {
  483. const custom = self.customPanels.find(p => p.id === `ui-panel-${id}`);
  484. if (custom) {
  485. document.querySelectorAll('.ui-panel').forEach(p => p.style.display = 'none');
  486. const el = document.getElementById(`ui-panel-${id}`);
  487. if (el) el.style.display = 'block';
  488. self.activePanel = id;
  489. return;
  490. }
  491.  
  492. original(id);
  493. self.activePanel = id;
  494.  
  495. self.customPanels.forEach(p => {
  496. const el = document.getElementById(p.id);
  497. if (el) el.style.display = 'none';
  498. });
  499. };
  500. }
  501.  
  502. addPanel(plugin, title = "Untitled", icon = 'https://i.imgur.com/kAp2VY9.png') {
  503. console.log(plugin);
  504. const pluginCount = this.customPanels.filter(p => p.pluginId === plugin.id).length;
  505. const id = `${plugin.id.replace(/\s/g, '')}${pluginCount + 1}`;
  506. const fullId = `ui-panel-${id}`;
  507.  
  508. const div = document.createElement('div');
  509. div.id = fullId;
  510. div.className = 'ui-panel fmp-custom-panel';
  511. div.innerHTML = `
  512. <div class="ui-panel-title">${title}</div>
  513. `;
  514.  
  515. const after = document.getElementById('ui-panel-worship');
  516. if (after?.parentNode) after.parentNode.insertBefore(div, after.nextSibling);
  517.  
  518. this.customPanels.push({ id: fullId, pluginId: plugin.id, icon });
  519. this.updatePanelButtons();
  520. return div;
  521. }
  522.  
  523.  
  524. injectPaginationButtons() {
  525. const style = document.createElement('style');
  526. style.innerHTML = `
  527. #ui-button-monster_log,
  528. #ui-button-worship,
  529. #ui-button-donor-shop,
  530. #ui-button-settings,
  531. #ui-button-database {
  532. margin-right: 4px;
  533. }
  534. `;
  535. document.head.appendChild(style);
  536.  
  537. const tdUI = document.querySelector('.td-ui');
  538. if (!tdUI || this.bottomButtons.length < 5) return;
  539.  
  540. const btnContainer = document.createElement('div');
  541. btnContainer.id = 'fmp-button-container';
  542. btnContainer.className = 'fmp-button-container';
  543.  
  544. // Move existing buttons
  545. this.bottomButtons.forEach(btn => btnContainer.appendChild(btn));
  546.  
  547. // Create back button
  548. const backBtn = document.createElement('div');
  549. backBtn.id = 'fmp-back-btn';
  550. backBtn.className = 'interface-btn hover fmp-back-btn';
  551. backBtn.innerText = '<';
  552. backBtn.title = 'Back';
  553. backBtn.onclick = () => {
  554. this.customPanelPage--;
  555. this.updatePanelButtons();
  556. };
  557. this.backBtn = backBtn;
  558. btnContainer.appendChild(backBtn);
  559.  
  560. // Create next button
  561. const nextBtn = document.createElement('div');
  562. nextBtn.id = 'fmp-next-btn';
  563. nextBtn.className = 'interface-btn hover fmp-next-btn';
  564. nextBtn.innerText = '>';
  565. nextBtn.title = 'Next';
  566. nextBtn.onclick = () => {
  567. this.customPanelPage++;
  568. this.updatePanelButtons();
  569. };
  570. this.nextBtn = nextBtn;
  571. btnContainer.appendChild(nextBtn);
  572.  
  573. tdUI.appendChild(btnContainer);
  574. }
  575.  
  576. updatePanelButtons() {
  577. const total = this.customPanels.length;
  578. const pages = Math.ceil(total / 5) + 1;
  579.  
  580. const showingBaseUI = this.customPanelPage === 0;
  581. const container = document.getElementById('fmp-button-container');
  582. if (!container) return;
  583.  
  584. this.customPanelButtons.forEach(btn => btn.remove());
  585. this.customPanelButtons = [];
  586.  
  587. this.bottomButtons.forEach(btn => btn.style.display = showingBaseUI ? 'inline-block' : 'none');
  588.  
  589. if (this.customPanelPage > 0) {
  590. const startIndex = (this.customPanelPage - 1) * 5;
  591. const panelsToShow = this.customPanels.slice(startIndex, startIndex + 5);
  592.  
  593. panelsToShow.forEach((panel, i) => {
  594. const btn = document.createElement('div');
  595. btn.className = 'interface-btn hover fmp-panel-btn';
  596. btn.title = `${panel.pluginId} (${i + 1})`;
  597. btn.id = `fmp-panel-btn-${i}`;
  598. btn.onclick = () => window.switch_panels(panel.id.replace("ui-panel-", ""));
  599.  
  600. btn.style.backgroundImage = `url('${panel.icon}')`;
  601. btn.style.backgroundSize = "cover";
  602. btn.style.backgroundPosition = "center";
  603.  
  604. if (this.nextBtn && container.contains(this.nextBtn)) {
  605. container.insertBefore(btn, this.nextBtn);
  606. } else {
  607. container.appendChild(btn);
  608. }
  609.  
  610. this.customPanelButtons.push(btn);
  611. });
  612. }
  613.  
  614. if (this.backBtn && this.nextBtn) {
  615. this.backBtn.style.display = this.customPanelPage > 0 ? 'inline-block' : 'none';
  616. this.nextBtn.style.display = this.customPanelPage < pages - 1 ? 'inline-block' : 'none';
  617. }
  618. }
  619.  
  620.  
  621.  
  622. }
  623.  
  624. // Add to window and init
  625. window.FlatmmoPlusPlugin = FlatmmoPlusPlugin;
  626. window.FlatmmoPlus = new FlatmmoPlus();
  627.  
  628.  
  629. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址