Definable ModMenu

Definable is a modular ModMenu for Drawaria.Online

目前為 2024-12-22 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Definable ModMenu
// @namespace    Definable
// @version      0.1.2
// @description  Definable is a modular ModMenu for Drawaria.Online
// @homepage     https://drawaria.online/profile/?uid=63196790-c7da-11ec-8266-c399f90709b7
// @author       ≺ᴄᴜʙᴇ³≻
// @match        https://drawaria.online/
// @match        https://drawaria.online/test
// @match        https://drawaria.online/room/*
// @icon         https://i.ibb.co/8Xb1gXg/Andrew-T-Austin-brightly-lit-tesseract-hypercube.png
// @grant        none
// @license      GNU GPLv3
// @run-at       document-end
// ==/UserScript==

(() => {
  String.prototype.toFunction = function () {
    return new Function("(" + this + ")()");
  };
  String.prototype.__invokeAsFunction = function (...args) {
    new Function("(" + this + ")()")();
  };

  Function.prototype.callAsWorker = function (...args) {
    return new Promise((resolve, reject) => {
      const code = `self.onmessage = e => self.postMessage((${this.toString()}).call(null, ...e.data));`,
        blob = new Blob([code], { type: "text/javascript" }),
        worker = new Worker((window.URL ?? window.webkitURL).createObjectURL(blob));
      worker.onmessage = (e) => (resolve(e.data), worker.terminate());
      worker.onerror = (e) => (reject(e.message), worker.terminate());
      worker.postMessage(args);
    });
  };

  const originalElementPrototypeAppend = Element.prototype.append;
  Element.prototype.append = function (...elements) {
    elements = elements.filter((appendingElement) => {
      if (appendingElement instanceof HTMLElement) return true;
      return false;
    });
    originalElementPrototypeAppend.apply(this, elements);
    return this;
  };
  // Function.toString().replace(/^\s*\/\/.+$/gm, "").split("\n").filter((s) => s).map((s) => s.trim()).join("");
})();

(async function ($name) {
  "use strict";
  const cache = {
    storage: await caches.open($name),
    /**
     * @param {string} url
     * @param {string} scriptContent
     */
    saveScript: function (url, scriptContent) {
      this.storage.put(url, new Response(scriptContent, { headers: { "Content-Type": "application/javascript" } }));
    },
    /**
     * @param {string} url
     * @param {object} json
     */
    saveObject: function (url, json) {
      this.storage.put(url, new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }));
    },
    /**
     * @param {string} url
     * @returns {Promise<string>}
     */
    loadScript: async function (url) {
      const response = (await this.storage.match(url)) || (await fetch(url));
      return await response.text();
    },
    /**
     * @param {string} url
     * @returns {Promise<object>}
     */
    loadObject: async function (url) {
      const response = (await this.storage.match(url)) || (await fetch(url));
      return await response.json();
    },
  };
  const UI = {
    /**
     * Returns the first element that is a descendant of node that matches selectors.
     * @param {string} selectors
     * @param {ParentNode} parentElement
     * @returns {Element|null}
     */
    querySelect: (selectors, parentElement = document) => parentElement.querySelector(selectors),
    /**
     * Returns all element descendants of node that match selectors.
     * @param {string} selectors
     * @param {ParentNode} parentElement
     * @returns {NodeListOf<Element>}
     */
    querySelectAll: (selectors, parentElement = document) => parentElement.querySelectorAll(selectors),
    /**
     * Create Element and assign properties to it.
     * @param {string} tagName
     * @param {object} properties
     * @returns {Element}
     */
    createElement: (tagName, properties = {}) => {
      /** @type {Element} */
      const element = Object.assign(document.createElement(tagName), properties);
      if (properties.title) {
        try {
          element.setAttribute("data-toggle", "tooltip");
          element.setAttribute("data-html", "true");
          jQuery(element).tooltip({ boundary: "window" });
        } catch (error) {}
      }
      return element;
    },
    /**
     * Assign attributes to element.
     * @param {Element} element
     * @param {object} attributes
     */
    setAttributes: (element, attributes) => Object.entries(attributes).forEach(([k, v]) => element.setAttribute(k, v)),
    /**
     * Assign styles to element.
     * @param {Element} element
     * @param {object} styles
     */
    setStyles: (element, styles) => Object.assign(element.style, styles),
    /**
     * @param {string} name
     * @returns {HTMLDivElement}
     */
    createContainer: function (name = "div") {
      return this.createElement(name, { className: "container" });
    },
    /**
     * @returns {HTMLDivElement}
     */
    createRow: function () {
      return this.createElement("div", { className: "row" });
    },
    /**
     * @param {string} name
     * @returns {HTMLElement}
     */
    createIcon: function (name) {
      return this.createElement("i", { className: `fas fa-${name}` });
    },
    /**
     * @param {string} type
     * @param {object} properties
     * @returns {HTMLInputElement}
     */
    createInput: function (type, properties = {}) {
      const input = this.createElement("input", { ...{ className: "form-control" }, ...properties, ...{ type: type } });
      if (!input.id) input.id = uid(8);
      return input;
    },
    /**
     * @param {HTMLInputElement} input
     * @param {object} properties
     * @returns {HTMLLabelElement}
     */
    createLabelFor: function (input, properties = {}) {
      const label = this.createElement("label", { ...{ className: "btn btn-sm btn-outline-secondary" }, ...properties, ...{ htmlFor: input.id } });
      label.appendChild(input);
      input.hidden = true;
      if (input.type === "checkbox" || input.type === "radio") {
        input.addEventListener("input", function () {
          label.classList[input.checked ? "add" : "remove"]("active");
        });
      }
      return label;
    },
    /**
     * @param {string} className
     * @returns {HTMLElement & { show: Function, hide: Function }}
     */
    createSpinner: function (className = "text-secondary") {
      const spinner = this.createElement("div", { className: [className, "spinner-border spinner-border-sm"].join(" ") });
      spinner.show = function () {
        spinner.classList.remove("d-none");
      };
      spinner.hide = function () {
        spinner.classList.add("d-none");
      };
      spinner.hide();
      return spinner;
    },
    /**
     * @param {HTMLInputElement} input
     * @param {HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement} addon
     * @returns {HTMLDivElement}
     */
    createGroup: function (input, addon) {
      const group = this.createElement("div", { className: "input-group input-group-sm" });
      const groupAppend = this.createElement("div", { className: "input-group-append" });

      if (!(addon instanceof HTMLInputElement || addon instanceof HTMLButtonElement)) {
        addon.classList.add("input-group-text");
      }
      groupAppend.appendChild(addon);

      group.appendChild(input);
      group.appendChild(groupAppend);

      return group;
    },
    /**
     * @param {Array<HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement>} inputs
     * @returns {HTMLDivElement}
     */
    createInputGroup: function (...inputs) {
      const group = this.createElement("div", { className: "input-group input-group-sm" });

      let previousElementSibling = inputs[0];
      let appendGroup = this.createElement("div", { className: "input-group-append" });
      for (let index = 0; index < inputs.length; index++) {
        const input = inputs[index];

        if (input instanceof HTMLInputElement) {
          if (!(previousElementSibling instanceof HTMLInputElement)) {
            group.appendChild(appendGroup);
            appendGroup = this.createElement("div", { className: "input-group-append" });
          }
          input.hidden = false;
          group.appendChild(input);
        } else {
          input.classList.add("input-group-text");
          appendGroup.appendChild(input);
          if (index + 1 === inputs.length) {
            group.appendChild(appendGroup);
          }
        }
        previousElementSibling = input;
      }

      return group;
    },
  };
  const DrawariaOnlineMessageTypes = {
    /**
     * @param {string} message
     * @returns {string}
     */
    chatmsg: function (message) {
      // 42["chatmsg","a"]
      const data = ["chatmsg", message];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @returns {string}
     */
    passturn: function () {
      // 42["passturn"]
      const data = ["passturn"];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @param {number|string} playerid
     * @returns {string}
     */
    pgdrawvote: function (playerid) {
      // 42["pgdrawvote",2,0]
      const data = ["pgdrawvote", playerid, 0];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @returns {string}
     */
    pgswtichroom: function () {
      // 42["pgswtichroom"]
      const data = ["pgswtichroom"];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @returns {string}
     */
    playerafk: function () {
      // 42["playerafk"]
      const data = ["playerafk"];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @returns {string}
     */
    playerrated: function () {
      // 42["playerrated"]
      const data = ["playerrated"];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @param {number|string} gestureid
     * @returns {string}
     */
    sendgesture: function (gestureid) {
      // 42["sendgesture",16]
      const data = ["sendgesture", gestureid];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @returns {string}
     */
    sendvote: function () {
      // 42["sendvote"]
      const data = ["sendvote"];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @param {number|string} playerid
     * @returns {string}
     */
    sendvotekick: function (playerid) {
      // 42["sendvotekick",93]
      const data = ["sendvotekick", playerid];
      return `${42}${JSON.stringify(data)}`;
    },
    /**
     * @param {number|string} wordid
     * @returns {string}
     */
    wordselected: function (wordid) {
      // 42["wordselected",0]
      const data = ["sendvotekick", wordid];
      return `${42}${JSON.stringify(data)}`;
    },
    clientcmd: {
      /**
       * @param {number|string} itemid
       * @param {boolean} isactive
       * @returns {string}
       */
      activateitem: function (itemid, isactive) {
        const data = ["clientcmd", 12, [itemid, isactive]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} itemid
       * @returns {string}
       */
      buyitem: function (itemid) {
        const data = ["clientcmd", 11, [itemid]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} itemid
       * @param {"zindex"|"shared"} target
       * @param {any} value
       * @returns {string}
       */
      canvasobj_changeattr: function (itemid, target, value) {
        const data = ["clientcmd", 234, [itemid, target, value]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      canvasobj_getobjects: function () {
        const data = ["clientcmd", 233];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} itemid
       * @returns {string}
       */
      canvasobj_remove: function (itemid) {
        let data = ["clientcmd", 232, [itemid]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} itemid
       * @param {number|string} positionX
       * @param {number|string} positionY
       * @param {number|string} speed
       * @returns {string}
       */
      canvasobj_setposition: function (itemid, positionX, positionY, speed) {
        const data = ["clientcmd", 230, [itemid, 100 / positionX, 100 / positionY, { movespeed: speed }]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} itemid
       * @param {number|string} rotation
       * @returns {string}
       */
      canvasobj_setrotation: function (itemid, rotation) {
        const data = ["clientcmd", 231, [itemid, rotation]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {any} value
       * @returns {string}
       */
      customvoting_setvote: function (value) {
        const data = ["clientcmd", 301, [value]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {any} value
       * @returns {string}
       */
      getfpid: function (value) {
        const data = ["clientcmd", 901, [value]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      getinventory: function () {
        const data = ["clientcmd", 10, [true]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      getspawnsstate: function () {
        const data = ["clientcmd", 102];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} positionX
       * @param {number|string} positionY
       * @returns {string}
       */
      moveavatar: function (positionX, positionY) {
        const data = ["clientcmd", 103, [1e4 * Math.floor(positionX * 0.01 * 1e4) + Math.floor(positionY * 0.01 * 1e4), false]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      setavatarprop: function () {
        const data = ["clientcmd", 115];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} flagid
       * @param {boolean} isactive
       * @returns {string}
       */
      setstatusflag: function (flagid, isactive) {
        const data = ["clientcmd", 3, [flagid, isactive]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {number|string} tokenid
       * @returns {string}
       */
      settoken: function (playerid, tokenid) {
        const data = ["clientcmd", 2, [playerid, tokenid]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {any} value
       * @returns {string}
       */
      snapchatmessage: function (playerid, value) {
        const data = ["clientcmd", 330, [playerid, value]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      spawnavatar: function () {
        const data = ["clientcmd", 101];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      startrollbackvoting: function () {
        const data = ["clientcmd", 320];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      trackforwardvoting: function () {
        const data = ["clientcmd", 321];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} trackid
       * @returns {string}
       */
      votetrack: function (trackid) {
        const data = ["clientcmd", 1, [trackid]];
        return `${42}${JSON.stringify(data)}`;
      },
    },
    /**
     * @param {string} roomID
     * @param {string} name
     * @param {string} uid
     * @param {string} wt
     * @returns {string}
     */
    startplay: function (roomID, name = undefined, uid = undefined, wt = undefined) {
      const data = `${420}${JSON.stringify(["startplay", name, 2, "en", roomID, null, [null, "https://drawaria.online/", 1000, 1000, [null, uid, wt], null]])}`;
      return data;
    },
    clientnotify: {
      /**
       * @param {number|string} playerid
       * @returns {string}
       */
      requestcanvas: function (playerid) {
        const data = ["clientnotify", playerid, 10001];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {string} base64
       * @returns {string}
       */
      respondcanvas: function (playerid, base64) {
        const data = ["clientnotify", playerid, 10002, [base64]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {number|string} imageid
       * @returns {string}
       */
      galleryupload: function (playerid, imageid) {
        const data = ["clientnotify", playerid, 11, [imageid]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {any} type
       * @returns {string}
       */
      warning: function (playerid, type) {
        const data = ["clientnotify", playerid, 100, [type]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {string} targetname
       * @param {boolean} mute
       * @returns {string}
       */
      mute: function (playerid, targetname, mute = false) {
        const data = ["clientnotify", playerid, 1, [mute, targetname]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {string} targetname
       * @param {boolean} hide
       * @returns {string}
       */
      hide: function (playerid, targetname, hide = false) {
        const data = ["clientnotify", playerid, 3, [hide, targetname]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {string} reason
       * @param {string} targetname
       * @returns {string}
       */
      report: function (playerid, reason, targetname) {
        const data = ["clientnotify", playerid, 2, [targetname, reason]];
        return `${42}${JSON.stringify(data)}`;
      },
    },
    drawcmd: {
      /**
       * @param {number|string} playerid
       * @param {number|string} x1
       * @param {number|string} y1
       * @param {number|string} x2
       * @param {number|string} y2
       * @param {number|string} size
       * @param {number|string} color
       * @param {boolean} ispixel
       * @returns {string}
       */
      line: function (x1, y1, x2, y2, color, size = 4, ispixel = true, playerid = 0) {
        const data = ["drawcmd", 0, [x1 * 0.001, y1 * 0.001, x2 * 0.001, y2 * 0.001, false, -size, color, playerid, ispixel]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @param {number|string} x1
       * @param {number|string} y1
       * @param {number|string} x2
       * @param {number|string} y2
       * @param {number|string} size
       * @param {number|string} color
       * @returns {string}
       */
      erase: function (x1, y1, x2, y2, color, size, ispixel = true, playerid = 0) {
        const data = ["drawcmd", 1, [x1 * 0.01, y1 * 0.01, x2 * 0.01, y2 * 0.01, false, -size, color, playerid, ispixel]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} x
       * @param {number|string} y
       * @param {number|string} color
       * @param {number|string} tolerance
       * @param {number|string} r
       * @param {number|string} g
       * @param {number|string} b
       * @param {number|string} a
       * @returns {string}
       */
      flood: function (x, y, color, tolerance, r, g, b, a) {
        // 42["drawcmd",2,[x, y,color,{"0":r,"1":g,"2":b,"3":a},size]]
        const data = ["drawcmd", 2, [x * 0.01, y * 0.01, color, { 0: r, 1: g, 2: b, 3: a }, tolerance]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @param {number|string} playerid
       * @returns {string}
       */
      undo: function (playerid) {
        // 42["drawcmd",3,[playerid]]
        const data = ["drawcmd", 3, [playerid]];
        return `${42}${JSON.stringify(data)}`;
      },
      /**
       * @returns {string}
       */
      clear: function () {
        // 42["drawcmd",4,[]]
        const data = ["drawcmd", 4, []];
        return `${42}${JSON.stringify(data)}`;
      },
    },
  };

  /**
   * @param {string} message
   * @param {string} styles
   */
  const log = (message, styles = "color:#5090C1", application = undefined) => console.log("%c" + [`[${application ?? $name}]`, message].join(" "), styles);

  /**
   * Generate radnow UID
   * @param {number} size
   * @returns {string}
   */
  const uid = (size = 8) => {
    const MASK = 0x3d;
    const LETTERS = "abcdefghijklmnopqrstuvwxyz";
    const NUMBERS = "1234567890";
    const SPECIALS = "-_";
    const charset = `${NUMBERS}${LETTERS}${LETTERS.toUpperCase()}${SPECIALS}`.split("");

    const bytes = new Uint8Array(size);
    crypto.getRandomValues(bytes);

    return bytes.reduce((acc, byte) => `${acc}${charset[byte & MASK]}`, "");
  };

  /**
   * @param {number[]} byteArray
   * @returns {string}
   */
  const toHexString = (byteArray) => {
    return byteArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
  };

  /**
   * @param {string} key
   * @param {string} value
   */
  const setCookie = (key, value) => {
    if (window.Cookie) Cookies.set(key, value);
    document.cookie = `${key}=${value}; Secure; Path=/; SameSite=None; Partitioned;`;
  };

  /**
   * @param {(index:number,value:*)=>{}} callbackOnDelete
   * @param {(index:number,value:*)=>{}} callbackOnSet
   * @returns {Array<*>}
   */
  const makeObservableArray = (callbackOnDelete, callbackOnSet) => {
    if (!("Proxy" in window)) {
      console.warn("Your browser doesn't support Proxies.");
      return;
    }

    // a proxy for our array
    const proxy = new Proxy([], {
      deleteProperty: function (target, property) {
        (callbackOnDelete ?? ___)(property, target[property]);
        delete target[property];
        return true;
      },
      set: function (target, property, value, receiver) {
        target[property] = value;
        (callbackOnSet ?? ___)(property, value);
        return true;
      },
    });

    Object.defineProperty(proxy, "isProxy", {
      value: true,
      writable: false,
    });

    return proxy;
  };

  /**
   * @param {string} message
   * @returns {Array<any>|object}
   */
  const tryParseJSON = (message) => {
    try {
      return JSON.parse(message);
    } catch (error) {
      return [];
    }
  };

  const clearScripts = (remove = true) => {
    try {
      let array = document.querySelectorAll('script[src]:not([data-codemaid="ignore"])');
      array.forEach((script) => {
        if (script.src != "") document.head.appendChild(script);
      });
    } catch (error) {
      console.error(error);
    }

    try {
      let unifiedScript = DefinableCore.UI.createElement("script");

      let scripts = document.querySelectorAll('script:not([src]):not([data-codemaid="ignore"])');
      let unifiedScriptContent = "";
      scripts.forEach((script) => {
        let content = script.textContent; //.replaceAll(/\s/g, '');

        unifiedScriptContent += `try{${content}}catch(e){console.warn(e);}`;
        script.remove();
      });

      unifiedScript.textContent = unifiedScriptContent;

      if (!remove) document.head.appendChild(unifiedScript);
    } catch (error) {
      console.error(error);
    }
  };

  const clearStyles = (remove = false) => {
    try {
      let unifiedStyles = DefinableCore.UI.createElement("style");
      unifiedStyles.textContet = "";

      let styles = document.querySelectorAll('style:not([data-codemaid="ignore"])');
      styles.forEach((style) => {
        unifiedStyles.textContent += style.textContent;
        style.remove();
      });
      if (!remove) document.head.appendChild(unifiedStyles);
    } catch (error) {
      console.error(error);
    }
  };

  const clearEmbeds = () => {
    try {
      let array = document.querySelectorAll("iframe");
      array.forEach((iframe) => {
        iframe.remove();
      });
    } catch (error) {
      console.error(error);
    }
  };

  class Player {
    /** @type {Array<Player>} */
    static instances = [];
    static get noConflict() {
      return Player.instances[0] ?? new Player(uid(12));
    }
    /**
     * @param {string|undefined} inviteLink
     * @returns {URL}
     */
    static getSocketServerURL(inviteLink) {
      if (typeof inviteLink === "undefined") return `wss://sv3.drawaria.online/socket.io/?sid1=undefined&hostname=drawaria.online&EIO=3&transport=websocket`;
      const roomID = this.getRoomID(inviteLink);
      const [_voidable, serverPrefix] = roomID.split(".");
      return new URL(`wss://${typeof serverPrefix === "undefined" ? "" : "sv".concat(serverPrefix, ".")}drawaria.online/socket.io/?sid1=undefined&hostname=drawaria.online&EIO=3&transport=websocket`);
    }
    /**
     * @param {string|undefined} inviteLink
     * @returns {string}
     */
    static getRoomID(inviteLink) {
      const inviteURL = new URL(inviteLink);
      return inviteURL.pathname.slice(6);
    }
    static parseMessage = DrawariaOnlineMessageTypes;

    /** @type {string} */
    name;
    /** @type {string} */
    uid;
    /** @type {string} */
    wt;
    /** @type {string} */
    roomID;

    /** @type {WebSocket} */
    socket;

    /** @type {Map<string, Function[]>} */
    events;

    /**
     * @param {string} name
     */
    constructor(name = undefined) {
      this.name = name;
      this.uid = "_";
      this.wt = undefined;
      this.roomID = undefined;

      this.events = new Map();

      Player.instances.push(this);
    }

    /**
     * @param {string} inviteLink
     */
    connect = (inviteLink) => {
      if (this.isConnected) return;
      this.#createNewConnection(inviteLink);
    };

    disconnect = () => {
      this.send("41");
      if (this.isConnected) this.socket.close();
    };

    reconnect = () => {
      if (!this.isConnected) {
        this.connect(`https://drawaria.online/room/${this.roomID}`);
        return;
      }
      this.send("41");
      this.send("40");
    };

    /**
     * @param {string} inviteLink
     */
    enterRoom = (inviteLink = undefined) => {
      this.roomID = Player.getRoomID(inviteLink ?? document.querySelector("#invurl").value);
      this.reconnect();
    };

    nextRoom = () => {
      this.send(Player.parseMessage.pgswtichroom());
    };

    leaveRoom = () => {
      this.send("41");
    };

    /**
     * @param {string} payload
     */
    send = (payload) => {
      if (this.isConnected) this.socket.send(payload);
      else debug(`send failed! Connection is closed`);
    };

    /**
     * @returns {boolean}
     */
    get isConnected() {
      return typeof this.socket !== "undefined" && this.socket instanceof WebSocket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING);
    }

    #sendHeartbeat = () => {
      if (this.isConnected) this.socket.send(2);
    };

    /**
     * @param {string} inviteLink
     */
    #createNewConnection = (inviteLink) => {
      this.socket = new WebSocket(Player.getSocketServerURL(inviteLink));
      this.roomID = Player.getRoomID(inviteLink);
      this.socket.addEventListener("open", this.#startHeartbeat);
      this.socket.addEventListener("message", this.#handleMessage);
    };

    #startHeartbeat = () => {
      this.heartbeatInterval = setInterval(this.#sendHeartbeat, 25000);
    };

    #handleMessage = (rawMessage) => {
      const [eventType, eventName, eventData] = this.#parseMessageEvent(rawMessage);

      switch (eventType) {
        case "40":
          this.send(Player.parseMessage.startplay(this.roomID, this.name, this.uid, this.wt));
          break;
        case "430":
          break;
        case "42":
          this.__invokeEvent(eventName, eventData);
          break;
        default:
      }
    };

    /**
     * @param {MessageEvent} messageEvent
     * @returns {[string, string, Array<any>|object]}
     */
    #parseMessageEvent = (messageEvent) => {
      const rawData = String(messageEvent.data);
      const messageType = (rawData.match(/^\d+/i) ?? [""])[0];
      const messageData = messageType.length === rawData.length ? [] : tryParseJSON(rawData.slice(messageType.length));
      return [messageType, Array.isArray(messageData) ? messageData.shift() : "", messageData];
    };

    /**
     * @param {DrawariaOnlineEvents} event
     * @param {Function} callback
     */
    addEventListener = (event, callback, id = undefined) => {
      if (!this.hasEventListener(event)) {
        this.events.set(event, []);
      }
      callback.id = id ?? this.addEventListener.caller.name ?? uid(8);
      try {
        this.events.get(event).push(callback);
      } catch (error) {
        debug(`addEventListener returned error \"${error}\"`, "color:firebrick");
      }
    };

    /**
     * @param {DrawariaOnlineEvents} event
     * @param {*} data
     */
    __invokeEvent = (event, data) => {
      if (this.hasEventListener(event)) {
        const listeners = this.events.get(event);
        listeners.forEach((listener) => listener(data));
      }
    };

    /**
     * @param {DrawariaOnlineEvents} name
     * @returns {boolean}
     */
    hasEventListener = (name) => {
      return this.events.has(name);
    };
  }

  class DefinableCore {
    static UI = UI;
    static Player = Player;

    constructor(moduleName, moduleIcon = "code") {
      this.name = moduleName;
      this.__initializeHead(moduleIcon);
      this.__initializeBody();
    }

    __initializeHead(moduleIcon) {
      const input = this.UI.createInput("checkbox");
      this.label = this.UI.createLabelFor(input, { title: this.name });
      const icon = this.UI.createIcon(moduleIcon);
      this.label.appendChild(icon);
    }

    __initializeBody() {
      const submoduleSelectionContainer = this.UI.createElement("nav", { className: "row" });
      const wrapper = this.UI.createElement("div");
      this.wrapper = wrapper;
      this.__contaier = this.UI.createElement("div");
      this.__contaier.appendChild(this.UI.createElement("b", { textContent: this.name }));
      // this.wrapper.appendChild(submoduleSelectionContainer);
      this.__contaier.appendChild(submoduleSelectionContainer);
      this.wrapper.appendChild(this.__contaier);
      const input = this.UI.querySelect("&>input", this.label);
      input.addEventListener("input", function () {
        wrapper.classList[input.checked ? "remove" : "add"]("d-none");
      });
      wrapper.classList.add("d-none");
    }

    createRow() {
      const row = this.UI.createRow();
      this.__contaier.appendChild(row);
      return row;
    }

    /**
     * @param {DefinableCore} submodule
     */
    registerModule(submodule) {
      // const submoduleSelectionContainer = this.UI.querySelect("&>nav", this.wrapper);
      const submoduleSelectionContainer = this.UI.querySelect("&>nav", this.__contaier);
      submoduleSelectionContainer.appendChild(submodule.label);
      this.wrapper.appendChild(submodule.wrapper);
    }

    get UI() {
      return DefinableCore.UI;
    }
    get Player() {
      return DefinableCore.Player;
    }
  }

  window.addEventListener("definable:core:init", function () {
    const ui = DefinableCore.UI;
    const definable = new DefinableCore("Definable", "code");

    const chatbox = ui.querySelect("#chatbox_messages");
    /** @type {HTMLElement} */
    const devider = chatbox.previousElementSibling;
    devider.before(definable.wrapper);
    definable.wrapper.before(devider.cloneNode(false));

    setTimeout(() => {
      window.dispatchEvent(new CustomEvent("definable:init", { detail: { main: definable, core: DefinableCore } }));
      definable.wrapper.classList.remove("d-none");
      definable.wrapper.classList.add("container");
      definable.wrapper.id = "definable";
    }, 500);
  });

  setTimeout(() => {
    window.dispatchEvent(new CustomEvent("definable:core:init"));
    clearEmbeds();
    clearScripts();
    clearStyles();
    document.head.appendChild(
      UI.createElement("style", { id: "definableStyles", textContent: ["#definable .row { gap: 0.125rem; margin-bottom: 0.125rem; }", "#definable label { margin: 0; }"].join("\n") })
    );
    // console.clear();
  }, 500);

  function launchScriptManager() {
    const ui = DefinableCore.UI;
    const container = ui.createContainer();

    {
      const row = ui.createRow();

      const scriptSourceLinkInput = ui.createInput("text", { className: "form-control" });
      const requestSourceInput = ui.createInput("button");
      const requestSourceLabel = ui.createLabelFor(requestSourceInput);
      const group = ui.createGroup(scriptSourceLinkInput, requestSourceLabel);
      const spinnerIcon = ui.createSpinner("");
      const defaultIcon = ui.createIcon("external-link-alt");

      requestSourceLabel.appendChild(defaultIcon);
      requestSourceLabel.appendChild(spinnerIcon);

      requestSourceInput.addEventListener("click", function () {
        defaultIcon.classList.add("d-none");
        spinnerIcon.show();

        if (!scriptSourceLinkInput.value.startsWith("http")) {
          log("Not a valid Link: ".concat(scriptSourceLinkInput.value), "color: firebrick;");
          spinnerIcon.hide();
          defaultIcon.classList.remove("d-none");
          return;
        }

        const requestSourceLink = new URL(scriptSourceLinkInput.value);
        log("Requesting Module: ".concat(requestSourceLink), "color: rebeccapurple;");

        fetch(requestSourceLink).then(async (response) => {
          const copy = response.clone();
          const content = await copy.text();

          if (copy.status === 200 && content) {
            cache.saveScript(requestSourceLink, content);
            log("Module installed!", "color: forestgreen;");
            spinnerIcon.hide();
            defaultIcon.classList.remove("d-none");
            scriptSourceLinkInput.value = "";
          }
        });
      });

      row.appendChild(group);
      container.appendChild(row);
    }

    DefinableCore.UI.querySelect("#login-leftcol").appendChild(container);
  }
})("definable");