scratch extension loader

none

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         scratch extension loader
// @version      6
// @description  none
// @run-at       document-start
// @tag          lib loader
// @author       rssaromeo
// @license      GPLv3
// @match        *://*/*
// @include      *
// @sandbox dom
// @icon         data:image/png;base64,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
// @require https://update.greasyfork.org/scripts/491829/1356221/tampermonkey%20storage%20proxy.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant unsafeWindow
// @namespace https://greasyfork.org/users/1184528
// ==/UserScript==

// add get turbomode state
// add way to toggle on and off extensions
// how to change the properties of a menu biased on the value of another menu
//
// add set var
// add get var
// add return project id
;(async () => {
  const MenuManager = (() => {
    const items = [] // { key, label, callback, id }

    function register(label, callback, key) {
      // Prevent duplicate keys
      if (items.find((i) => i.label === label)) {
        throw new Error(`Menu key "${label}" already exists`)
      }
      callback = callback.bind(label)
      const obj = { key, label }
      const cb = () => {
        var val = callback()
        if (val) {
          MenuManager.update(obj.id, val)
        }
      }
      obj.cb = cb
      obj.id = GM_registerMenuCommand(label, cb, key)
      items.push(obj)
    }

    function update(id, newLabel) {
      const index = items.findIndex((i) => i.id === id)
      if (index === -1) return

      // Remove clicked and everything after it
      const toRebuild = items.splice(index)

      // Unregister them
      for (const item of toRebuild) {
        GM_unregisterMenuCommand(item.id)
      }

      // Update target item
      toRebuild[0].label = newLabel ?? toRebuild[0].label

      // Re-register in same order
      for (const item of toRebuild) {
        item.id = GM_registerMenuCommand(item.label, item.cb, item.key)
        items.push(item)
      }
    }

    return { register, update }
  })()

  const { savelib, waitforlib } = loadlib("libloader")
  var getvm = function () {
    const oldBind = Function.prototype.bind
    Function.prototype.bind = function (...args) {
      if (
        args[0] &&
        args[0]?.hasOwnProperty?.("editingTarget") &&
        args[0].hasOwnProperty("runtime")
      ) {
        Function.prototype.bind = oldBind
        savelib("scratch", { vm: args[0] })
      }
      return oldBind.apply(this, args)
    }
  }
  getvm()
  var menus = {}
  var extensionerrors = []
  unsafeWindow.ee = extensionerrors
  var projectid =
    location.href.match(/(?<=\/)[0-9]+(?=\/)/)?.[0] || "local"
  // debugger
  var sp = new storageproxy("extensionoptions")
  // debugger
  var bt = {
    cmd: "command",
    ret: "reporter",
    hat: "hat",
    bool: "Boolean",
  }
  var inp = {
    int: "number",
    num: "number",
    str: "string",
  }
  var menufuncs = class {
    menu_varnames(targetid) {
      try {
        var name = vm.runtime.targets.find((e) => e.id == targetid)
          .sprite.name
        if (!gettarget(name).runtime.ioDevices.cloud.stage)
          return [" "]
        var globalvars = Object.values(
          gettarget(name).runtime.ioDevices.cloud.stage.variables,
        )
        var localvars = Object.values(gettarget(name).variables)
        globalvars = globalvars.filter((e) => e.type == "")
        localvars = localvars.filter((e) => e.type == "")
        // log(globalvars, localvars)
        var arr = [
          ...localvars.map((e) => ({
            text: "__local " + e.name,
            value: JSON.stringify([name, e.name]),
          })),
          ...globalvars.map((e) => ({
            text: "__global " + e.name,
            value: JSON.stringify([undefined, e.name]),
          })),
        ]
        return arr.length ? arr : [" "]
      } catch (e) {
        error("error from menu_varnames", e)
        return [" "]
      }
    }
    menu_listnames(targetid) {
      try {
        // if (!gettarget(name)) return [" "]
        var name = vm.runtime.targets.find((e) => e.id == targetid)
          .sprite.name
        if (!gettarget(name).runtime.ioDevices.cloud.stage)
          return [" "]
        var globalvars = Object.values(
          gettarget(name).runtime.ioDevices.cloud.stage.variables,
        )
        var localvars = Object.values(gettarget(name).variables)
        globalvars = globalvars.filter((e) => e.type == "list")
        localvars = localvars.filter((e) => e.type == "list")
        // log(globalvars, localvars)
        var arr = [
          ...localvars.map((e) => ({
            text: "__local " + e.name,
            value: JSON.stringify([name, e.name]),
          })),
          ...globalvars.map((e) => ({
            text: "__global " + e.name,
            value: JSON.stringify([undefined, e.name]),
          })),
        ]

        return arr.length ? arr : [" "]
      } catch (e) {
        error("error from menu_listnames", e)
        return [" "]
      }
    }
    menu_spritelistwithglobal(targetid) {
      try {
        var spritenames = Object.values(vm.runtime.targets).map(
          (e) => e.sprite.name,
        )
        var name = Object.values(vm.runtime.targets).find(
          (e) => e.id == targetid,
        ).sprite.name
        return [
          name,
          { text: "--- global list ---", value: "" },
          ...spritenames.filter((e) => e !== name && e !== "Stage"),
        ]
      } catch (e) {
        error("error from menu_spritelistwithglobal", e)
        return [" "]
      }
    }
    menu_spritelistwithoutglobal(targetid) {
      try {
        var spritenames = Object.values(vm.runtime.targets).map(
          (e) => e.sprite.name,
        )
        var name = Object.values(vm.runtime.targets).find(
          (e) => e.id == targetid,
        ).sprite.name
        return [
          name,
          ...spritenames.filter((e) => e !== name && e !== "Stage"),
        ]
      } catch (e) {
        error("error from menu_spritelistwithoutglobal", e)
        return [" "]
      }
    }
    menu_fullkeylist() {
      return [
        "escape",
        "enter",
        "up arrow",
        "down arrow",
        "left arrow",
        "right arrow",
        "tab",
        "control",
        "alt",
        "shift",
        "win",
        "delete",
        "insert",
        "home",
        "end",
        "page up",
        "page down",
        "caps lock",
        "scroll lock",
        ...Array.from({ length: 24 }, (_, i) => i + 1).map((e) => ({
          text: "F" + e,
          value: "f" + e,
        })),
        { text: "space", value: " " },
        ..."~`abcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()-_=+[]\\|;:'\",<.>/?{}",
        "contextmenu",
        "mediaplaypause",
        "audiovolumemute",
        "audiovolumedown",
        "audiovolumeup",
        "launchapplication2",
        "launchmediaplayer",
        "mediatracknext",
        "mediatrackprevious",
        "Meta",
      ]
    }
    menu_fullkeylistandany() {
      return ["any", ...this.menu_fullkeylist()]
    }
  }
  var a = loadlib("allfuncs")
  var enabledextensions = sp.get() ?? {
    extensionmanagercreatedbyrssaromeo: true,
  }
  var extensionclasses = []
  // unsafeWindow.extensionclasses = extensionclasses
  newext(
    "extension manager",
    "rssaromeo",
    class {
      getlasterror() {
        return String(
          extensionerrors[extensionerrors.length - 1]?.message ??
            false,
        )
      }
      getlasterrorextensionid() {
        return String(
          extensionerrors[extensionerrors.length - 1]?.extensionid ??
            false,
        )
      }
      getlasterrorblockid() {
        return String(
          extensionerrors[extensionerrors.length - 1]?.blockid ??
            false,
        )
      }
      puterrorsintolist({ listname }) {
        var [sprite, listname] = JSON.parse(listname)
        scratchlist(
          listname,
          extensionerrors.map((e) => JSON.stringify(e)),
          sprite,
        )
      }
    },
    [
      newblock(
        bt.ret,
        "getlasterrorextensionid",
        "lasterror: extension id",
      ),
      newblock(bt.ret, "getlasterror", "lasterror: message"),
      newblock(bt.ret, "getlasterrorblockid", "lasterror: block id"),
      newblock(
        bt.cmd,
        "puterrorsintolist",
        "put errors into list [listname]",
        [newmenu("listnames", { defaultValue: "" })],
      ),
    ],
  )
  savelib("scratchextesnsionmanager", {
    newmenu,
    newext,
    newblock,
    bt,
    inp,
    gettarget,
    totype,
    scratch_math,
    projectid,
    canvas,
    scratchvar,
    scratchlist,
  })
  // unsafeWindow.sp = sp
  await waitforlib("scratch")
  await a.waituntil(canvas)

  var vm = loadlib("scratch").vm
  loadallextensions()
  function loadallextensions() {
    // debugger
    for (var __class of extensionclasses) {
      var extensionInstance = new __class(
        vm.extensionManager.runtime,
        __class.thisExtensionIsEnabled,
      )
      vm.extensionManager._loadedExtensions.set(
        extensionInstance.getInfo().id,
        vm.extensionManager._registerInternalExtension(
          extensionInstance,
        ),
      )
    }
  }

  function newblock(blockType, opcode, text, args) {
    var arguments = Object.fromEntries(
      [...(text.match(/(?<=\[)\w+(?=\])/g) || [])].map((_, i) => [
        _,
        typeof args?.[i] == "object" ?
          {
            type: args?.[i]?.type || args?.[i] || inp.str,
            disableMonitor: false,
            // defaultValue: false,
            // filter: [Scratch.TargetType.SPRITE],
            // filter: [Scratch.TargetType.STAGE],
            // isTerminal: false,
            // shouldRestartExistingThreads: true,
            // isEdgeActivated: false,
            ...(0 in args[i] && 1 in args[i] ?
              {
                type: args[i][0] ?? inp.str,
                defaultValue: args[i][1],
              }
            : args[i]),
          }
        : {
            type: args?.[i]?.type || args?.[i] || inp.str,
            // defaultValue: false,
            disableMonitor: false,
          },
      ]),
    )
    // log(arguments)
    return {
      hideFromPalette: false,
      blockType,
      opcode,
      text,
      arguments,
    }
  }

  function newmenu(name, opts = {}) {
    var data = {
      acceptReporters: true,
      items: "menu_" + name,
      ...opts,
    }
    menus[name] = data
    return { menu: name, ...opts }
  }

  function newext(
    name,
    username,
    _class,
    blockinfo,
    blockcolor = "#777777",
    menuicon = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDY0IDY0IiB3aWR0aD0iNjQiIGhlaWdodD0iNjQiPgoJPHRpdGxlPk5ldyBQcm9qZWN0PC90aXRsZT4KCTxkZWZzPgoJCTxpbWFnZSAgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBpZD0iaW1nMSIgaHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFFQUFBQUJBQkFNQUFBQllSMnp0QUFBQUFYTlNSMElCMmNrc2Z3QUFBQ1JRVEZSRkFBQUFBT05sLy8vL0FPTmwvLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vL0I2Y0NhQUFBQUF4MFVrNVRBUDhBQUFJRUR4TUJBd1VIM0Q5UFB3QUFBRVZKUkVGVWVKeGpaRUFHakFKQTR2OEhGS0ZSQmFNS1JoWFFRb0VnQTM0d3FvQllCVXJJUEhBb2c4T2JCQVhHWU9yOXFJSkJyMEJaQ2ErQ3RMMmhCd2RGN2g0WkNnRFBCSCtKaSsyWXRnQUFBQUJKUlU1RXJrSmdnZz09Ii8+Cgk8L2RlZnM+Cgk8c3R5bGU+Cgk8L3N0eWxlPgoJPHVzZSBpZD0iQmFja2dyb3VuZCIgaHJlZj0iI2ltZzEiIHg9IjAiIHk9IjAiLz4KPC9zdmc+",
    blockimg = "",
  ) {
    var bg = "#282828"
    if (blockcolor[0] != "#") blockcolor = "#" + blockcolor
    blockinfo = {
      color1:
        /https?:\/\/turbowarp\.org/.test(location.href) ? blockcolor
        : bg,
      color2: bg,
      color3: blockcolor,

      menuIconURI: menuicon,
      blockIconURI: blockimg,

      blocks: blockinfo,
    }
    blockinfo.id =
      name.replaceAll(/[^a-z]+/gi, "") + "createdby" + username
    blockinfo.name = name.replaceAll(/ /gi, " ") //+ " - by " + username
    blockinfo.menus = menus
    // warn(enabledextensions)
    MenuManager.register(
      (enabledextensions[blockinfo.id] ? "✅enabled" : "❌disabled") +
        (": " + blockinfo.name),
      () => {
        // warn(enabledextensions[blockinfo.id])
        enabledextensions[blockinfo.id] =
          !enabledextensions[blockinfo.id]

        // warn(enabledextensions[blockinfo.id])
        return (
          (enabledextensions[blockinfo.id] ? "✅enabled" : "❌disabled") +
          (": " + blockinfo.name)
        )
      },
      { autoClose: false },
    )
    // log("menus", menus.spritelistwithoutglobal.items)
    blockinfo.blocks.unshift(
      newblock(
        bt.bool,
        "thisextensionexists",
        "the extension {" + blockinfo.name + "} is enabled",
      ),
    )
    function Classes(bases) {
      class Bases {
        constructor() {
          bases.forEach((base) => Object.assign(this, new base()))
        }
      }
      bases.forEach((base) => {
        Object.getOwnPropertyNames(base.prototype)
          .filter((prop) => prop != "constructor")
          .forEach(
            (prop) => (Bases.prototype[prop] = base.prototype[prop]),
          )
      })
      return Bases
    }
    const properties = Object.getOwnPropertyNames(_class.prototype)
    for (const property of properties) {
      if (typeof _class.prototype[property] === "function") {
        const lastFunc = _class.prototype[property].bind(
          _class.prototype,
        )
        _class.prototype[property] = function (...a) {
          if (!enabledextensions[blockinfo.id]) {
            return false
          }
          return lastFunc(...a)
        }
      }
    }
    _class.prototype.thisextensionexists = () => {
      return enabledextensions[blockinfo.id]
    }
    function tryCatchDecorator(constructor) {
      const prototype = constructor.prototype
      const originalPrototype = Object.getPrototypeOf(prototype)

      for (let key of [
        ...Object.getOwnPropertyNames(prototype),
        ...Object.getOwnPropertyNames(originalPrototype),
      ]) {
        if (typeof prototype[key] === "function") {
          let originalMethod = prototype[key]
          prototype[key] = function (...args) {
            try {
              return originalMethod.apply(this, args)
            } catch (e) {
              extensionerrors.push({
                extensionid: blockinfo.id,
                blockid: key,
                message: e.message,
              })
              console.error("error from " + key, e)
              return false
            }
          }
        }
      }

      return constructor
    }
    // log("blockinfo.blocks", blockinfo.blocks)
    var __class = tryCatchDecorator(
      class extends Classes([_class, menufuncs]) {
        constructor(runtime, enabled) {
          super(runtime)
          if (enabled !== undefined)
            this.thisExtensionIsEnabled = enabled
          this.runtime = runtime
        }
        getInfo() {
          // log("getInfo", blockinfo)
          return blockinfo
        }
      },
    )
    extensionclasses.push(__class)
    sp.enabledextensions = enabledextensions
    return { isEnabled: () => enabledextensions[blockinfo.id] }
  }

  function scratchvar(varname, value, spritename) {
    if (value !== undefined) {
      if (gettarget(spritename)?.getvar(varname))
        gettarget(spritename).getvar(varname).value = String(value)
      else {
        console.warn(`var "${varname}" does not exist`)
      }
    }
  }

  function totype(inp, type, forced) {
    //number, string, list, object, json, bool
    inp = String(inp)
    try {
      switch (type) {
        // case "regex":
        //   try {
        //     return new RegExp(inp)
        //   } catch (e) {
        //     return fail(inp, type, forced)
        //   }
        case "string":
          return String(inp)
        case "number":
          if (inp == "true") inp = 1
          if (inp == "false") inp = 0
          if (/^-?[0-9]*\.?[0-9]+$/.test(inp)) return Number(inp)
          if (inp === "NaN" || inp == "nan") return NaN
          return fail(inp, type, forced)
        case "list":
          if (scratchlist(inp)) return scratchlist(inp)
          inp = JSON.parse(inp)
          if (inp.reverse) return inp
          return fail(inp, type, forced)
        case "object":
          inp = JSON.parse(inp)
          // if (/^[\-0-9]+$/.test(inp) || inp === true || inp === false)
          //   return undefined
          if (
            Object.keys(inp).length !== undefined &&
            inp.length === undefined &&
            !Array.isArray(inp)
          )
            return inp
          return fail(inp, type, forced)
        case "bool":
          if (inp === "1" || inp === "true") return true
          if (inp === "0" || inp === "false") return false
          return fail(inp, type)
        // case "json":
        //   if (
        //     totype(inp, "object") !== undefined ||
        //     totype(inp, "list") !== undefined
        //   )
        //     return totype(inp, "object") || totype(inp, "list")
        //   else {
        //     fail(inp, type, forced)
        //   }
      }
    } catch (s) {
      return fail(inp, type, forced)
    }

    function fail(inp, type, forced) {
      if (forced) {
        throw new Error(`"${inp}" must be of type "${type}"`)
      } else return undefined
    }
  }

  // listen(window, "keydown", (e) => {
  //   var index = vm.runtime.ioDevices.keyboard._keysPressed.indexOf(
  //     e.key.toUpperCase(),
  //   )
  //   if (index !== -1) {
  //     vm.runtime.ioDevices.keyboard._keysPressed.splice(index, 1)
  //   }
  //   vm.runtime.ioDevices.keyboard._keysPressed.push(e.key.toUpperCase())
  // })
  // listen(window, "keyup", (e) => {
  //   var index = vm.runtime.ioDevices.keyboard._keysPressed.indexOf(
  //     e.key.toUpperCase(),
  //   )
  //   if (index !== -1) {
  //     vm.runtime.ioDevices.keyboard._keysPressed.splice(index, 1)
  //   }
  // })

  function gettarget(sprite) {
    if (sprite)
      var x =
        vm.runtime.getSpriteTargetByName(sprite) ||
        vm.runtime.getTargetForStage()
    else var x = vm.runtime.getTargetForStage()
    x.getvar = x?.lookupVariableByNameAndType
    return x
  }

  function scratchlist(listname, value, spritename) {
    //fix regex?
    if (value === undefined && /^\[\]$/.test(listname))
      return JSON.parse(listname)
    if (value !== undefined) {
      if (gettarget(spritename)?.getvar(listname, "list"))
        gettarget(spritename).getvar(listname, "list").value = [
          ...value,
        ]
      else console.warn(`list "${listname}" does not exist`)
    } else {
      return gettarget(spritename)?.getvar(listname, "list")?.value
    }
  }

  function scratch_math(operator, n) {
    switch (operator) {
      case "sin":
        return Math.round(Math.sin((Math.PI * n) / 180) * 1e10) / 1e10
      case "cos":
        return Math.round(Math.cos((Math.PI * n) / 180) * 1e10) / 1e10
      case "asin":
        return (Math.asin(n) * 180) / Math.PI
      case "acos":
        return (Math.acos(n) * 180) / Math.PI
      case "atan":
        return (Math.atan(n) * 180) / Math.PI
      case "log":
        return Math.log(n) / Math.LN10
    }
    return 0
  }

  function canvas() {
    return (
      window?.vm?.runtime?.renderer?.canvas ||
      document.querySelector(
        "#app > div > div.gui_body-wrapper_-N0sA.box_box_2jjDp > div > div.gui_stage-and-target-wrapper_69KBf.box_box_2jjDp > div.stage-wrapper_stage-wrapper_2bejr.box_box_2jjDp > div.stage-wrapper_stage-canvas-wrapper_3ewmd.box_box_2jjDp > div > div.stage_stage_1fD7k.box_box_2jjDp > div:nth-child(1) > canvas",
      ) ||
      document.querySelector(
        "#view > div > div.inner > div:nth-child(2) > div.guiPlayer > div.stage-wrapper_stage-wrapper_2bejr.box_box_2jjDp > div.stage-wrapper_stage-canvas-wrapper_3ewmd.box_box_2jjDp > div > div.stage_stage_1fD7k.box_box_2jjDp > div:nth-child(1) > canvas",
      ) ||
      document.querySelector(
        "#app > div > div > div > div.gui_body-wrapper_-N0sA.box_box_2jjDp > div > div.gui_stage-and-target-wrapper_69KBf.box_box_2jjDp > div.stage-wrapper_stage-wrapper_2bejr.box_box_2jjDp > div.stage-wrapper_stage-canvas-wrapper_3ewmd.box_box_2jjDp > div > div.stage_stage_1fD7k.box_box_2jjDp > div:nth-child(1) > canvas",
      ) ||
      document.querySelector(
        ".stage_stage_yEvd4 > div:nth-child(1) > canvas:nth-child(1)",
      )
    )
  }
})()