// ==UserScript==
// @name IdlePixel+
// @namespace com.anwinity.idlepixel
// @version 0.0.2
// @description Idle-Pixel plugin framework
// @author Anwinity
// @match https://idle-pixel.com/play.php*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
const CONFIG_TYPES_INTEGER = ["integer", "int"];
const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
const CONFIG_TYPES_STRING = ["string", "text"];
const CONFIG_TYPES_SELECT = ["select"];
if(window.IdlePixelPlus) {
if(window.IdlePixelPlus.version == GM_info.script.version) {
// same version, just ignore this one
return;
}
console.error(`Failed to load IdlePixelPlus (v${GM_info.script.version}) because another version of IdlePixelPlus (v${window.IdlePixelPlus.version}) is already loaded.`);
return;
}
class IdlePixelPlusPlugin {
constructor(id, opts) {
if(typeof id !== "string") {
throw new TypeError("IdlePixelPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
}
this.id = id;
this.opts = opts || {};
this.config = {};
}
getConfig(name) {
if(!this.config) {
IdlePixelPlus.loadPluginConfigs(this.id);
}
if(this.config) {
return this.config[name];
}
}
onConfigsChanged() { }
onLogin() { }
onMessageReceived(data) { }
onVariableSet(key, valueBefore, valueAfter) { }
onChat(data) { }
onPanelChanged(panelBefore, panelAfter) { }
}
const internal = {
init() {
const self = this;
// hook into websocket messages
const original_open_websocket = window.open_websocket;
window.open_websocket = function() {
original_open_websocket.apply(this, arguments);
const original_onmessage = window.websocket.websocket.onmessage;
window.websocket.websocket.onmessage = function(event) {
original_onmessage.apply(window.websocket.websocket, arguments);
self.onMessageReceived(event.data);
}
}
// hook into Items.set, which is where var_ values are set
const original_items_set = Items.set;
Items.set = function(key, value) {
let valueBefore = window["var_"+key];
original_items_set.apply(this, arguments);
let valueAfter = window["var_"+key];
self.onVariableSet(key, valueBefore, valueAfter);
}
// hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
const original_switch_panels = window.switch_panels;
window.switch_panels = function(id) {
let panelBefore = Globals.currentPanel;
if(panelBefore && panelBefore.startsWith("panel-")) {
panelBefore = panelBefore.substring("panel-".length);
}
self.hideCustomPanels();
original_switch_panels.apply(this, arguments);
let panelAfter = Globals.currentPanel;
if(panelAfter && panelAfter.startsWith("panel-")) {
panelAfter = panelAfter.substring("panel-".length);
}
self.onPanelChanged(panelBefore, panelAfter);
}
// create plugin menu item and panel
const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
lastMenuItem.after(`
<div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
<img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
</div>
`);
self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
let content = `
<style>
.idlepixelplus-plugin-box {
display: block;
position: relative;
padding: 0.25em;
color: white;
background-color: rgb(107, 107, 107);
border: 1px solid black;
border-radius: 6px;
margin-bottom: 0.5em;
}
.idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
position: absolute;
right: 2px;
top: 2px;
cursor: pointer;
}
.idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
display: grid;
grid-template-columns: minmax(100px, min-content) 1fr;
row-gap: 0.5em;
column-gap: 0.5em;
white-space: nowrap;
}
</style>
`;
self.forEachPlugin(plugin => {
let id = plugin.id;
let name = "An IdlePixel+ Plugin!";
let description = "";
let author = "unknown";
if(plugin.opts.about) {
let about = plugin.opts.about;
name = about.name || name;
description = about.description || description;
author = about.author || author;
}
content += `
<div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
<strong><u>${name||id}</u></strong> (by ${author})<br />
<span>${description}</span><br />
<div class="idlepixelplus-plugin-config-section" style="display: none">
<hr style="grid-column: span 2">
`;
if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
plugin.opts.config.forEach(cfg => {
if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
content += `
<div>
<label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
</div>
<div>
<input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" />
</div>
`;
}
else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
content += `
<div>
<label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
</div>
<div>
<input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" />
</div>
`;
}
else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
content += `
<div>
<label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
</div>
<div>
<input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" />
</div>
`;
}
else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
content += `
<div>
<label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
</div>
<div>
<input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" />
</div>
`;
}
else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
content += `
<div>
<label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
</div>
<div>
<select id="idlepixelplus-config-${plugin.id}-${cfg.id}">
`;
if(cfg.options && Array.isArray(cfg.options)) {
cfg.options.forEach(option => {
content += `<option value="${option.value}">${option.label || option.value}</option>`;
});
}
content += `
</select>
</div>
`;
}
});
content += `
<div style="grid-column: span 2">
<button onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
<button onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
</div>
`;
}
content += "</div>";
if(plugin.opts.config) {
content += `
<div class="idlepixelplus-plugin-settings-button">
<button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
</div>`;
}
content += "</div>";
});
return content;
});
console.log(`IdlePixelPlus (v${self.version}) initialized.`);
}
};
class IdlePixelPlus {
constructor() {
this.version = GM_info.script.version;
this.plugins = {};
this.panels = {};
this.debug = false;
}
getVar(name, type) {
let s = window[`var_${name}`];
if(type) {
switch(type) {
case "int":
case "integer":
return parseInt(s);
case "number":
case "float":
return parseFloat(s);
case "boolean":
case "bool":
if(s=="true") return true;
if(s=="false") return false;
return undefined;
}
}
return s;
}
getVarOrDefault(name, defaultValue, type) {
let s = window[`var_${name}`];
if(s==null || typeof s === "undefined") {
return defaultValue;
}
if(type) {
let value;
switch(type) {
case "int":
case "integer":
value = parseInt(s);
return isNaN(value) ? defaultValue : value;
case "number":
case "float":
value = parseFloat(s);
return isNaN(value) ? defaultValue : value;
case "boolean":
case "bool":
if(s=="true") return true;
if(s=="false") return false;
return defaultValue;
}
}
return s;
}
loadPluginConfigs(id) {
if(typeof id !== "string") {
throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
}
const plugin = this.plugins[id];
const config = {};
let stored;
try {
stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
console.log(id, localStorage.getItem(`idlepixelplus.${id}.config`));
console.log(id, stored);
}
catch(err) {
console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
stored = {};
}
if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
plugin.opts.config.forEach(cfg => {
const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
let value = stored[cfg.id];
if(value==null || typeof value === "undefined") {
value = cfg.default;
}
config[cfg.id] = value;
if(el) {
if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
el.prop("checked", value);
}
else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
el.val(value);
}
else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
el.val(value);
}
else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
el.val(value);
}
else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
el.val(value);
}
}
});
}
plugin.config = config;
if(typeof plugin.onConfigsChanged === "function") {
plugin.onConfigsChanged();
}
}
savePluginConfigs(id) {
if(typeof id !== "string") {
throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
}
const plugin = this.plugins[id];
const config = {};
if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
plugin.opts.config.forEach(cfg => {
const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
let value;
if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
config[cfg.id] = el.is(":checked");
}
else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
config[cfg.id] = parseInt(el.val());
}
else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
config[cfg.id] = parseFloat(el.val());
}
else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
config[cfg.id] = el.val();
}
else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
config[cfg.id] = el.val();
}
});
}
plugin.config = config;
localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
if(typeof plugin.onConfigsChanged === "function") {
plugin.onConfigsChanged();
}
}
addPanel(id, title, content) {
if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
}
const panels = $("#panels");
panels.append(`
<div id="panel-${id}" style="display: none">
<h1>${title}</h1>
<hr>
<div class="idlepixelplus-panel-content"></div>
</div>
`);
this.panels[id] = {
id: id,
title: title,
content: content
};
this.refreshPanel(id);
}
refreshPanel(id) {
if(typeof id !== "string") {
throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
}
const panel = this.panels[id];
if(!panel) {
throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
}
let content = panel.content;
if(!["string", "function"].includes(typeof content)) {
throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
}
if(typeof content === "function") {
content = content();
if(typeof content !== "string") {
throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
}
}
const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
panelContent.html(content);
if(id === "idlepixelplus") {
this.forEachPlugin(plugin => {
this.loadPluginConfigs(plugin.id);
});
}
}
registerPlugin(plugin) {
if(!(plugin instanceof IdlePixelPlusPlugin)) {
throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
}
if(plugin.id in this.plugins) {
throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
}
// TODO: easy config system
// TODO: custom panels
this.plugins[plugin.id] = plugin;
this.loadPluginConfigs(plugin.id);
console.log(`IdlePixelPlus registered plugin "${plugin.id}"`);
}
forEachPlugin(f) {
if(typeof f !== "function") {
throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
}
Object.values(this.plugins).forEach(plugin => {
try {
f(plugin);
}
catch(err) {
console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
console.error(err);
}
});
}
setPanel(panel) {
if(typeof panel !== "string") {
throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
}
window.switch_panels(`panel-${panel}`);
}
sendMessage(message) {
if(typeof message !== "string") {
throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
}
if(window.websocket && window.websocket.websocket && window.websocket.websocket.readyState==1) {
window.websocket.websocket.send(message);
}
}
hideCustomPanels() {
Object.values(this.panels).forEach((panel) => {
const el = $(`#panel-${panel.id}`);
if(el) {
el.css("display", "none");
}
});
}
onMessageReceived(data) {
if(this.debug) {
console.log(`IP+ onMessageReceived: ${data}`);
}
if(data) {
this.forEachPlugin((plugin) => {
if(typeof plugin.onMessageReceived === "function") {
plugin.onMessageReceived(data);
}
});
if(data.startsWith("VALID_LOGIN")) {
this.onLogin();
}
else if(data.startsWith("CHAT=")) {
const split = data.substring("CHAT=".length).split("~");
const chatData = {
username: split[0],
tag: null,
sigil: null,
level: split[3],
message: split[4]
};
// CHAT=anwinity~none~none~1565~test
// TODO: none and none, probably for tag and sigil
}
}
}
onLogin() {
if(this.debug) {
console.log(`IP+ onLogin`);
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onLogin === "function") {
plugin.onLogin();
}
});
}
onVariableSet(key, valueBefore, valueAfter) {
if(this.debug) {
console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onVariableSet === "function") {
plugin.onVariableSet(key, valueBefore, valueAfter);
}
});
}
onChat(data) {
if(this.debug) {
console.log(`IP+ onChat`, data);
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onChat === "function") {
plugin.onChat(data);
}
});
}
onPanelChanged(panelBefore, panelAfter) {
if(this.debug) {
console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
}
if(panelAfter === "idlepixelplus") {
this.refreshPanel("idlepixelplus");
}
this.forEachPlugin((plugin) => {
if(typeof plugin.onPanelChanged === "function") {
plugin.onPanelChanged(panelBefore, panelAfter);
}
});
}
}
// Add to window and init
window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
window.IdlePixelPlus = new IdlePixelPlus();
internal.init.call(window.IdlePixelPlus);
})();