IdlePixel+

Idle-Pixel plugin framework

当前为 2022-03-10 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/441206/1026687/IdlePixel%2B.js

  1. // ==UserScript==
  2. // @name IdlePixel+
  3. // @namespace com.anwinity.idlepixel
  4. // @version 0.0.2
  5. // @description Idle-Pixel plugin framework
  6. // @author Anwinity
  7. // @match https://idle-pixel.com/play.php*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
  15. const CONFIG_TYPES_INTEGER = ["integer", "int"];
  16. const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
  17. const CONFIG_TYPES_STRING = ["string", "text"];
  18. const CONFIG_TYPES_SELECT = ["select"];
  19.  
  20. if(window.IdlePixelPlus) {
  21. if(window.IdlePixelPlus.version == GM_info.script.version) {
  22. // same version, just ignore this one
  23. return;
  24. }
  25. console.error(`Failed to load IdlePixelPlus (v${GM_info.script.version}) because another version of IdlePixelPlus (v${window.IdlePixelPlus.version}) is already loaded.`);
  26. return;
  27. }
  28.  
  29. class IdlePixelPlusPlugin {
  30.  
  31. constructor(id, opts) {
  32. if(typeof id !== "string") {
  33. throw new TypeError("IdlePixelPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
  34. }
  35. this.id = id;
  36. this.opts = opts || {};
  37. this.config = {};
  38. }
  39.  
  40. getConfig(name) {
  41. if(!this.config) {
  42. IdlePixelPlus.loadPluginConfigs(this.id);
  43. }
  44. if(this.config) {
  45. return this.config[name];
  46. }
  47. }
  48.  
  49. onConfigsChanged() { }
  50. onLogin() { }
  51. onMessageReceived(data) { }
  52. onVariableSet(key, valueBefore, valueAfter) { }
  53. onChat(data) { }
  54. onPanelChanged(panelBefore, panelAfter) { }
  55.  
  56. }
  57.  
  58. const internal = {
  59. init() {
  60. const self = this;
  61.  
  62. // hook into websocket messages
  63. const original_open_websocket = window.open_websocket;
  64. window.open_websocket = function() {
  65. original_open_websocket.apply(this, arguments);
  66. const original_onmessage = window.websocket.websocket.onmessage;
  67. window.websocket.websocket.onmessage = function(event) {
  68. original_onmessage.apply(window.websocket.websocket, arguments);
  69. self.onMessageReceived(event.data);
  70. }
  71. }
  72.  
  73. // hook into Items.set, which is where var_ values are set
  74. const original_items_set = Items.set;
  75. Items.set = function(key, value) {
  76. let valueBefore = window["var_"+key];
  77. original_items_set.apply(this, arguments);
  78. let valueAfter = window["var_"+key];
  79. self.onVariableSet(key, valueBefore, valueAfter);
  80. }
  81.  
  82. // hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
  83. const original_switch_panels = window.switch_panels;
  84. window.switch_panels = function(id) {
  85. let panelBefore = Globals.currentPanel;
  86. if(panelBefore && panelBefore.startsWith("panel-")) {
  87. panelBefore = panelBefore.substring("panel-".length);
  88. }
  89. self.hideCustomPanels();
  90. original_switch_panels.apply(this, arguments);
  91. let panelAfter = Globals.currentPanel;
  92. if(panelAfter && panelAfter.startsWith("panel-")) {
  93. panelAfter = panelAfter.substring("panel-".length);
  94. }
  95. self.onPanelChanged(panelBefore, panelAfter);
  96. }
  97.  
  98. // create plugin menu item and panel
  99. const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
  100. lastMenuItem.after(`
  101. <div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
  102. <img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
  103. </div>
  104. `);
  105. self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
  106. let content = `
  107. <style>
  108. .idlepixelplus-plugin-box {
  109. display: block;
  110. position: relative;
  111. padding: 0.25em;
  112. color: white;
  113. background-color: rgb(107, 107, 107);
  114. border: 1px solid black;
  115. border-radius: 6px;
  116. margin-bottom: 0.5em;
  117. }
  118. .idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
  119. position: absolute;
  120. right: 2px;
  121. top: 2px;
  122. cursor: pointer;
  123. }
  124. .idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
  125. display: grid;
  126. grid-template-columns: minmax(100px, min-content) 1fr;
  127. row-gap: 0.5em;
  128. column-gap: 0.5em;
  129. white-space: nowrap;
  130. }
  131. </style>
  132. `;
  133. self.forEachPlugin(plugin => {
  134. let id = plugin.id;
  135. let name = "An IdlePixel+ Plugin!";
  136. let description = "";
  137. let author = "unknown";
  138. if(plugin.opts.about) {
  139. let about = plugin.opts.about;
  140. name = about.name || name;
  141. description = about.description || description;
  142. author = about.author || author;
  143. }
  144. content += `
  145. <div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
  146. <strong><u>${name||id}</u></strong> (by ${author})<br />
  147. <span>${description}</span><br />
  148. <div class="idlepixelplus-plugin-config-section" style="display: none">
  149. <hr style="grid-column: span 2">
  150. `;
  151. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  152. plugin.opts.config.forEach(cfg => {
  153. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  154. content += `
  155. <div>
  156. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  157. </div>
  158. <div>
  159. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" />
  160. </div>
  161. `;
  162. }
  163. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  164. content += `
  165. <div>
  166. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  167. </div>
  168. <div>
  169. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" />
  170. </div>
  171. `;
  172. }
  173. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  174. content += `
  175. <div>
  176. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  177. </div>
  178. <div>
  179. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" />
  180. </div>
  181. `;
  182. }
  183. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  184. content += `
  185. <div>
  186. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  187. </div>
  188. <div>
  189. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" />
  190. </div>
  191. `;
  192. }
  193. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  194. content += `
  195. <div>
  196. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  197. </div>
  198. <div>
  199. <select id="idlepixelplus-config-${plugin.id}-${cfg.id}">
  200. `;
  201. if(cfg.options && Array.isArray(cfg.options)) {
  202. cfg.options.forEach(option => {
  203. content += `<option value="${option.value}">${option.label || option.value}</option>`;
  204. });
  205. }
  206. content += `
  207. </select>
  208. </div>
  209. `;
  210. }
  211. });
  212. content += `
  213. <div style="grid-column: span 2">
  214. <button onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
  215. <button onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
  216. </div>
  217. `;
  218. }
  219. content += "</div>";
  220. if(plugin.opts.config) {
  221. content += `
  222. <div class="idlepixelplus-plugin-settings-button">
  223. <button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
  224. </div>`;
  225. }
  226. content += "</div>";
  227. });
  228.  
  229. return content;
  230. });
  231.  
  232. console.log(`IdlePixelPlus (v${self.version}) initialized.`);
  233. }
  234. };
  235.  
  236. class IdlePixelPlus {
  237.  
  238. constructor() {
  239. this.version = GM_info.script.version;
  240. this.plugins = {};
  241. this.panels = {};
  242. this.debug = false;
  243. }
  244.  
  245. getVar(name, type) {
  246. let s = window[`var_${name}`];
  247. if(type) {
  248. switch(type) {
  249. case "int":
  250. case "integer":
  251. return parseInt(s);
  252. case "number":
  253. case "float":
  254. return parseFloat(s);
  255. case "boolean":
  256. case "bool":
  257. if(s=="true") return true;
  258. if(s=="false") return false;
  259. return undefined;
  260. }
  261. }
  262. return s;
  263. }
  264.  
  265. getVarOrDefault(name, defaultValue, type) {
  266. let s = window[`var_${name}`];
  267. if(s==null || typeof s === "undefined") {
  268. return defaultValue;
  269. }
  270. if(type) {
  271. let value;
  272. switch(type) {
  273. case "int":
  274. case "integer":
  275. value = parseInt(s);
  276. return isNaN(value) ? defaultValue : value;
  277. case "number":
  278. case "float":
  279. value = parseFloat(s);
  280. return isNaN(value) ? defaultValue : value;
  281. case "boolean":
  282. case "bool":
  283. if(s=="true") return true;
  284. if(s=="false") return false;
  285. return defaultValue;
  286. }
  287. }
  288. return s;
  289. }
  290.  
  291. loadPluginConfigs(id) {
  292. if(typeof id !== "string") {
  293. throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
  294. }
  295. const plugin = this.plugins[id];
  296. const config = {};
  297. let stored;
  298. try {
  299. stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
  300. console.log(id, localStorage.getItem(`idlepixelplus.${id}.config`));
  301. console.log(id, stored);
  302. }
  303. catch(err) {
  304. console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
  305. stored = {};
  306. }
  307. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  308. plugin.opts.config.forEach(cfg => {
  309. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  310. let value = stored[cfg.id];
  311. if(value==null || typeof value === "undefined") {
  312. value = cfg.default;
  313. }
  314. config[cfg.id] = value;
  315.  
  316. if(el) {
  317. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
  318. el.prop("checked", value);
  319. }
  320. else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
  321. el.val(value);
  322. }
  323. else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
  324. el.val(value);
  325. }
  326. else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
  327. el.val(value);
  328. }
  329. else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
  330. el.val(value);
  331. }
  332. }
  333. });
  334. }
  335. plugin.config = config;
  336. if(typeof plugin.onConfigsChanged === "function") {
  337. plugin.onConfigsChanged();
  338. }
  339. }
  340.  
  341. savePluginConfigs(id) {
  342. if(typeof id !== "string") {
  343. throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
  344. }
  345. const plugin = this.plugins[id];
  346. const config = {};
  347. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  348. plugin.opts.config.forEach(cfg => {
  349. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  350. let value;
  351. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  352. config[cfg.id] = el.is(":checked");
  353. }
  354. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  355. config[cfg.id] = parseInt(el.val());
  356. }
  357. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  358. config[cfg.id] = parseFloat(el.val());
  359. }
  360. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  361. config[cfg.id] = el.val();
  362. }
  363. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  364. config[cfg.id] = el.val();
  365. }
  366. });
  367. }
  368. plugin.config = config;
  369. localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
  370. if(typeof plugin.onConfigsChanged === "function") {
  371. plugin.onConfigsChanged();
  372. }
  373. }
  374.  
  375. addPanel(id, title, content) {
  376. if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
  377. throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
  378. }
  379. const panels = $("#panels");
  380. panels.append(`
  381. <div id="panel-${id}" style="display: none">
  382. <h1>${title}</h1>
  383. <hr>
  384. <div class="idlepixelplus-panel-content"></div>
  385. </div>
  386. `);
  387. this.panels[id] = {
  388. id: id,
  389. title: title,
  390. content: content
  391. };
  392. this.refreshPanel(id);
  393. }
  394.  
  395. refreshPanel(id) {
  396. if(typeof id !== "string") {
  397. throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
  398. }
  399. const panel = this.panels[id];
  400. if(!panel) {
  401. throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
  402. }
  403. let content = panel.content;
  404. if(!["string", "function"].includes(typeof content)) {
  405. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  406. }
  407. if(typeof content === "function") {
  408. content = content();
  409. if(typeof content !== "string") {
  410. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  411. }
  412. }
  413. const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
  414. panelContent.html(content);
  415. if(id === "idlepixelplus") {
  416. this.forEachPlugin(plugin => {
  417. this.loadPluginConfigs(plugin.id);
  418. });
  419. }
  420. }
  421.  
  422. registerPlugin(plugin) {
  423. if(!(plugin instanceof IdlePixelPlusPlugin)) {
  424. throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
  425. }
  426. if(plugin.id in this.plugins) {
  427. throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
  428. }
  429.  
  430. // TODO: easy config system
  431. // TODO: custom panels
  432.  
  433. this.plugins[plugin.id] = plugin;
  434. this.loadPluginConfigs(plugin.id);
  435. console.log(`IdlePixelPlus registered plugin "${plugin.id}"`);
  436. }
  437.  
  438. forEachPlugin(f) {
  439. if(typeof f !== "function") {
  440. throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
  441. }
  442. Object.values(this.plugins).forEach(plugin => {
  443. try {
  444. f(plugin);
  445. }
  446. catch(err) {
  447. console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
  448. console.error(err);
  449. }
  450. });
  451. }
  452.  
  453. setPanel(panel) {
  454. if(typeof panel !== "string") {
  455. throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
  456. }
  457. window.switch_panels(`panel-${panel}`);
  458. }
  459.  
  460. sendMessage(message) {
  461. if(typeof message !== "string") {
  462. throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
  463. }
  464. if(window.websocket && window.websocket.websocket && window.websocket.websocket.readyState==1) {
  465. window.websocket.websocket.send(message);
  466. }
  467. }
  468.  
  469. hideCustomPanels() {
  470. Object.values(this.panels).forEach((panel) => {
  471. const el = $(`#panel-${panel.id}`);
  472. if(el) {
  473. el.css("display", "none");
  474. }
  475. });
  476. }
  477.  
  478. onMessageReceived(data) {
  479. if(this.debug) {
  480. console.log(`IP+ onMessageReceived: ${data}`);
  481. }
  482. if(data) {
  483. this.forEachPlugin((plugin) => {
  484. if(typeof plugin.onMessageReceived === "function") {
  485. plugin.onMessageReceived(data);
  486. }
  487. });
  488. if(data.startsWith("VALID_LOGIN")) {
  489. this.onLogin();
  490. }
  491. else if(data.startsWith("CHAT=")) {
  492. const split = data.substring("CHAT=".length).split("~");
  493. const chatData = {
  494. username: split[0],
  495. tag: null,
  496. sigil: null,
  497. level: split[3],
  498. message: split[4]
  499. };
  500. // CHAT=anwinity~none~none~1565~test
  501. // TODO: none and none, probably for tag and sigil
  502. }
  503. }
  504. }
  505.  
  506. onLogin() {
  507. if(this.debug) {
  508. console.log(`IP+ onLogin`);
  509. }
  510. this.forEachPlugin((plugin) => {
  511. if(typeof plugin.onLogin === "function") {
  512. plugin.onLogin();
  513. }
  514. });
  515. }
  516.  
  517. onVariableSet(key, valueBefore, valueAfter) {
  518. if(this.debug) {
  519. console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
  520. }
  521. this.forEachPlugin((plugin) => {
  522. if(typeof plugin.onVariableSet === "function") {
  523. plugin.onVariableSet(key, valueBefore, valueAfter);
  524. }
  525. });
  526. }
  527.  
  528. onChat(data) {
  529. if(this.debug) {
  530. console.log(`IP+ onChat`, data);
  531. }
  532. this.forEachPlugin((plugin) => {
  533. if(typeof plugin.onChat === "function") {
  534. plugin.onChat(data);
  535. }
  536. });
  537. }
  538.  
  539. onPanelChanged(panelBefore, panelAfter) {
  540. if(this.debug) {
  541. console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
  542. }
  543. if(panelAfter === "idlepixelplus") {
  544. this.refreshPanel("idlepixelplus");
  545. }
  546. this.forEachPlugin((plugin) => {
  547. if(typeof plugin.onPanelChanged === "function") {
  548. plugin.onPanelChanged(panelBefore, panelAfter);
  549. }
  550. });
  551. }
  552.  
  553. }
  554.  
  555. // Add to window and init
  556. window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
  557. window.IdlePixelPlus = new IdlePixelPlus();
  558. internal.init.call(window.IdlePixelPlus);
  559.  
  560. })();