From aa5af4d14bdde843c096c18259aa5ef81803769c Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 13:38:53 -0400 Subject: [PATCH 01/15] new runpy --- static/extensions/ScrTwPm/runpython.js | 3307 ++++++++++++++++++++++++ 1 file changed, 3307 insertions(+) create mode 100644 static/extensions/ScrTwPm/runpython.js diff --git a/static/extensions/ScrTwPm/runpython.js b/static/extensions/ScrTwPm/runpython.js new file mode 100644 index 000000000..66fd73a80 --- /dev/null +++ b/static/extensions/ScrTwPm/runpython.js @@ -0,0 +1,3307 @@ + +(function(Scratch) { + 'use strict'; + + + const { vm, Cast } = Scratch; + const runtime = vm.runtime; + const isPM = Scratch.extensions.isPenguinMod; + + const keysMenu = [ + { text: "space", value: "space" }, { text: "up arrow", value: "up arrow" }, { text: "down arrow", value: "down arrow" }, + { text: "right arrow", value: "right arrow" }, { text: "left arrow", value: "left arrow" }, + { text: "a", value: "a" }, { text: "b", value: "b" }, { text: "c", value: "c" }, + { text: "d", value: "d" }, { text: "e", value: "e" }, { text: "f", value: "f" }, + { text: "g", value: "g" }, { text: "h", value: "h" }, { text: "i", value: "i" }, + { text: "j", value: "j" }, { text: "k", value: "k" }, { text: "l", value: "l" }, + { text: "m", value: "m" }, { text: "n", value: "n" }, { text: "o", value: "o" }, + { text: "p", value: "p" }, { text: "q", value: "q" }, { text: "r", value: "r" }, + { text: "s", value: "s" }, { text: "t", value: "t" }, { text: "u", value: "u" }, + { text: "v", value: "v" }, { text: "w", value: "w" }, { text: "x", value: "x" }, + { text: "y", value: "y" }, { text: "z", value: "z" }, { text: "0", value: "0" }, + { text: "1", value: "1" }, { text: "2", value: "2" }, { text: "3", value: "3" }, + { text: "4", value: "4" }, { text: "5", value: "5" }, { text: "6", value: "6" }, + { text: "7", value: "7" }, { text: "8", value: "8" }, { text: "9", value: "9" } + ]; + + let Thread; // defined by exports + let conditionStorage = Object.create(null), keybinds = Object.create(null); + let hats = { ...runtime._hats }, overrideCalls = {}; + window.scrtwpmrunpyIssueTimes = []; + + const regenReporters = ["scrtwpmrunpy_getarg"]; + if (Scratch.gui) Scratch.gui.getBlockly().then(SB => { + const originalCheck = SB.scratchBlocksUtils.isShadowArgumentReporter; + SB.scratchBlocksUtils.isShadowArgumentReporter = function (block) { + if (originalCheck(block)) return true; + return block.isShadow() && regenReporters.includes(block.type); + }; + }); + + const resetStorage = () => { + conditionStorage = Object.create(null); + window.scrtwpmrunpyIssueTimes = []; + }; + runtime.on("PROJECT_STOP_ALL", resetStorage); + runtime.on("PROJECT_START", resetStorage); + + const postData = (key, down) => { + if (key === "space") key = " "; + if (key.includes("arrow")) key = key.charAt(0).toUpperCase() + key.slice(1).replace(" arrow", ""); + runtime.ioDevices.keyboard.postData({ key, isDown: down }); + }; + runtime.on("KEY_PRESSED", (key) => { + key = key.toLowerCase(); + // Use this for compatibility with other extensions + if (keybinds[key] !== undefined) keybinds[key].keyV.forEach(item => postData(item, true)); + }); + runtime.on("AFTER_EXECUTE", () => { + const keys = runtime.ioDevices.keyboard._keysPressed; + for (let i = 0; i < Object.keys(keybinds).length; i++) { + const keyName = Object.keys(keybinds)[i]; + const key = keybinds[keyName]; + if (keys.indexOf(keyName.toLowerCase()) === -1) key.keyV.forEach(i => postData(i, false)); + } + }); + + // capture all types of blockable Errors + const ogError = Error; + window.Error = function(message) { + window.scrtwpmrunpyIssueTimes.push(Math.floor(Date.now() / 200) * 200); + const err = new ogError(message); + Object.setPrototypeOf(err, window.Error.prototype); + return err; + }; + Object.setPrototypeOf(window.Error, ogError); + window.Error.prototype = Object.create(ogError.prototype); + window.Error.prototype.constructor = window.Error; + + const ogConsoleEr = console.error; + console.error = (...args) => { + window.scrtwpmrunpyIssueTimes.push(Math.floor(Date.now() / 200) * 200); + return ogConsoleEr.apply(this, args); + } + + // Override needed for "ifRunBlock" + const ogRestartThread = runtime._restartThread; + runtime._restartThread = function (thread) { + const forceStop = (t, c) => { + if (c && t.procedures !== null && Object.keys(t.procedures).length > 0) try { t.generator.return() } catch {} + t.status = 4; + } + // Check if we exist in the thread, then stop the script + if (thread.isCompiled) { + const e = thread.compatibilityStackFrame; + if (e !== undefined && e !== null && e.SPifThread !== undefined) forceStop(e.SPifThread, true); + } else { + for (let i = 0; i < thread.stackFrames.length; i++) { + const e = thread.stackFrames[i].executionContext; + if (e !== null && e.SPifThread !== undefined) { + forceStop(e.SPifThread, false); + break; + } + } + } + return ogRestartThread.call(this, thread); + }; + + // override needed for "get from sprite" blocks + const ogVisReport = runtime.visualReport; + if (isPM) { + runtime.visualReport = function (blockId, value) { + if (overrideCalls[blockId]) { + overrideCalls[blockId].pushReportedValue(value); + delete overrideCalls[blockId]; + return; + } + return ogVisReport.call(this, blockId, value); + } + } else { + runtime.visualReport = function (target, blockId, value) { + if (overrideCalls[blockId]) { + overrideCalls[blockId].pushReportedValue(value); + delete overrideCalls[blockId]; + return; + } + return ogVisReport.call(this, target, blockId, value); + } + } + + // thread patcher for special threads + const expRenderedTarget = new vm.exports.RenderedTarget({ blocks: null }, runtime); + const Blocks = expRenderedTarget.blocks.constructor; + const ogGetNext = Blocks.prototype.getNextBlock; + Blocks.prototype.getNextBlock = function(name) { + const thisBlock = ogGetNext.call(this, name); + if (thisBlock) return thisBlock; + for (const target of this.runtime.targets) { + if (!target.isOriginal || target.blocks === this) continue; + const targetBlock = ogGetNext.call(target.blocks, name); + if (targetBlock) return targetBlock; + } + return undefined; + } + const ogGetBranch = Blocks.prototype.getBranch; + Blocks.prototype.getBranch = function(id, branchNum) { + const thisBlock = ogGetBranch.call(this, id, branchNum); + if (thisBlock) return thisBlock; + for (const target of this.runtime.targets) { + if (!target.isOriginal || target.blocks === this) continue; + const targetBlock = ogGetBranch.call(target.blocks, id, branchNum); + if (targetBlock) return targetBlock; + } + return undefined; + } + + // Thank you to @FurryR for the help + function getUnsafeExports() { + if (vm.exports.i_will_not_ask_for_help_when_these_break) return vm.exports.i_will_not_ask_for_help_when_these_break(); + else if (vm.exports.JSGenerator && vm.exports.IRGenerator?.exports) return { + ...vm.exports, ScriptTreeGenerator: vm.exports.IRGenerator.exports.ScriptTreeGenerator + }; + } + const exports = getUnsafeExports(); + if (exports) { + Thread = exports.Thread; + const { JSGenerator, ScriptTreeGenerator } = exports; + const _ogIRdescendStack = ScriptTreeGenerator.prototype.descendStackedBlock; + ScriptTreeGenerator.prototype.descendStackedBlock = function (block) { + switch (block.opcode) { + case "scrtwpmrunpy_breakLoop": return { kind: "scrtwpmrunpy.break", id: block.id }; + case "scrtwpmrunpy_continueLoop": return { kind: "scrtwpmrunpy.continue", id: block.id }; + default: return _ogIRdescendStack.call(this, block); + } + }; + const _ogJSdescendStack = JSGenerator.prototype.descendStackedBlock; + JSGenerator.prototype.descendStackedBlock = function (node) { + switch (node.kind) { + case "scrtwpmrunpy.break": { + // execute in compatibility layer in case we are in a non-compiled loop block + if (this.frames.find(frame => frame.isLoop)?.isLoop) this.source += "break;\n"; + else this.source += `yield* executeInCompatibilityLayer({}, runtime.getOpcodeFunction("scrtwpmrunpy_breakLoop"), false, false, "${node.id}", null);\n`; + break; + } + case "scrtwpmrunpy.continue": { + // execute in compatibility layer in case we are in a non-compiled loop block + if (this.frames.find(frame => frame.isLoop)?.isLoop) this.source += "continue;\n"; + else this.source += `yield* executeInCompatibilityLayer({}, runtime.getOpcodeFunction("scrtwpmrunpy_continueLoop"), false, false, "${node.id}", null);\n`; + break; + } + default: return _ogJSdescendStack.call(this, node); + } + }; + } + + + class RunPython { + constructor(runtime) { + // Initialize an array holding your default dropdown menu options + this.text = "" + this.pytext = "" + this.class = "" + this.variables = ["var"]; + this.lists = ["list"]; + this.varvar = '0' + this.lstlist = '["item"]' + this.runtime = runtime + this.strictEditing = true + + } + getInfo() { + return { + id: "scrtwpmscrtwpmrunpy", + name: "Python", + menuIconURI : `data:image/webp;base64,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`, + color1: "#4584b6", + color3: "#ffdd55", + color2: "#1e415e", + blocks: [ + + { + opcode: 'openPyDocs', + blockType: Scratch.BlockType.BUTTON, + text: 'Open Documentation', + }, + + { blockType: Scratch.BlockType.LABEL, text: "Run Python" }, + + { + opcode: 'whenSessionStarts', + text: 'when python code starts', + blockType: Scratch.BlockType.HAT, + isEdgeActivated: false, + // arguments: { + + // } + }, + + { + opcode: 'startSession', + text: 'run python code', + blockType: Scratch.BlockType.COMMAND, + // arguments: { + + // } + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { blockType: Scratch.BlockType.LABEL, text: "Strict Editing" }, + + { + opcode: 'aboutstrict', + blockType: Scratch.BlockType.BUTTON, + text: 'What is Strict Editing?', + }, + { + opcode: 'setstrict', + text: 'set strict editing to [OO]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + OO: {type: Scratch.ArgumentType.STRING, menu:"oo"} + + }, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'isstrict', + text: 'is strict editing on?', + blockType: Scratch.BlockType.BOOLEAN, + disableMonitor: true, + // arguments: { + + // } + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + + + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Display Output" }, + { + opcode: 'displayBlock', + text: 'show python output', + blockType: Scratch.BlockType.COMMAND, + // arguments: { + + // } + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'deleteBlock', + text: 'hide python output', + blockType: Scratch.BlockType.COMMAND, + // arguments: { + + // } + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'clear', + text: 'clear python output', + blockType: Scratch.BlockType.COMMAND, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'getpytext', + text: 'python output', + blockType: Scratch.BlockType.REPORTER, + // arguments: { + + // } + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + allowDropAnywhere: true, + }, + + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Print" }, + + { + opcode: 'print', + text: 'print [TEXT]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: 'Python is fun!'}, + + } + }, + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Inputs" }, + + { + opcode: 'input', + text: 'input [INPUT]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + INPUT: {type: Scratch.ArgumentType.STRING, defaultValue: 'Is Python fun? '}, + + } + }, + { + opcode: 'ans', + text: 'entered answer', + blockType: Scratch.BlockType.REPORTER, + allowDropAnywhere: true, + + }, + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Variables" }, + + { + opcode: 'createvar', + text: 'create variable named [NAME]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: 'var2'}, + + }, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'deletevar', + text: 'delete variable [NAME]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: 'vars'}, + + }, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'getvar', + text: 'get variable [NAME]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: "vars"}, + }, + allowDropAnywhere: true, + + }, + { + opcode: 'setvar', + text: '[NAME] = [VALUE]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: "vars"}, + VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '0'}, + + } + }, + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Lists" }, + + { + opcode: 'createlist', + text: 'create list named [NAME]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: 'list2'}, + + }, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'deletelist', + text: 'delete list [NAME]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: 'lists'}, + + }, + color2: "#4584b6", + color1: "#ffd015", + color3: "#1e415e", + }, + { + opcode: 'getlist', + text: 'get list [NAME]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + }, + allowDropAnywhere: true, + }, + + { + opcode: "setlist", + blockType: Scratch.BlockType.COMMAND, + text: '[NAME] = \[', + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '0'}, + + }, + + mutator: "scrtwpmrunpyextender", + extensions: ["scrtwpmrunpyextender_string"], + disableMonitor: true, + }, + { + opcode: "setlisttwo", + blockType: Scratch.BlockType.COMMAND, + text: '[NAME] = [VALUE]', + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '["Python", "3.14"]'}, + + }, + }, + { + opcode: 'reverselist', + text: '[LIST].reverse()', + blockType: Scratch.BlockType.COMMAND, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + } + }, + { + opcode: 'operatelist', + text: '[LIST].[OPERATION] [TEXT]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + OPERATION: {type: Scratch.ArgumentType.STRING, menu: "listsops"}, + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "item2"}, + } + }, + { + opcode: 'extendlist', + text: '[LIST].extend[L2]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + L2: {type: Scratch.ArgumentType.STRING, defaultValue: '["item4","item5"]'}, + } + }, + { + opcode: 'listinsert', + text: '[LIST].insert [TEXT], [NUM]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "item3"}, + NUM: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, + } + }, + { + opcode: 'listpop', + text: '[LIST].pop [TEXT]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "item"}, + }, + allowDropAnywhere: true, + }, + + { + opcode: 'itrlist', + text: '[LIST]([INDEX])', + blockType: Scratch.BlockType.REPORTER, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + INDEX: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'indexlist', + text: '[LIST].index [TEXT]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + LIST: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "item"}, + }, + allowDropAnywhere: true, + }, + + + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Controls" }, + + { + opcode: 'ifs', + text: 'if [BOOL]:', + blockType: Scratch.BlockType.LOOP, + arguments: { + BOOL: {type: Scratch.ArgumentType.BOOLEAN}, + } + }, + { + opcode: 'elifs', + text: 'elif [BOOL]:', + blockType: Scratch.BlockType.LOOP, + arguments: { + BOOL: {type: Scratch.ArgumentType.BOOLEAN}, + } + }, + { + opcode: 'elses', + text: 'else:', + blockType: Scratch.BlockType.LOOP, + // arguments: { + // BOOL: {type: Scratch.ArgumentType.BOOLEAN}, + // } + }, + { + opcode: 'whileloop', + text: 'while [BOOL]:', + blockType: Scratch.BlockType.LOOP, + arguments: { + BOOL: {type: Scratch.ArgumentType.BOOLEAN}, + } + }, + { + opcode: 'foriinlist', + text: 'for [I] in [LIST]:', + blockType: Scratch.BlockType.LOOP, + arguments: { + I: {type: Scratch.ArgumentType.STRING, menu: "vars"}, + LIST: {type: Scratch.ArgumentType.STRING, defaultValue: '["item1","item2","item3"]'}, + } + }, + { + opcode: 'rangesone', + text: 'range [N2]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + N2: {type: Scratch.ArgumentType.NUMBER, defaultValue: 5}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'rangestwo', + text: 'range [N1], [N2]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + N1: {type: Scratch.ArgumentType.NUMBER, defaultValue: 2}, + N2: {type: Scratch.ArgumentType.NUMBER, defaultValue: 7}, + }, + allowDropAnywhere: true, + }, + // break block still working onn... + // { + // opcode: 'breaknow', + // text: 'break', + // blockType: Scratch.BlockType.COMMAND, // COMMAND allows it to sit inside loops safely + // isTerminal: true // Visually turns the block into a cap block (flat bottom) + // }, + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Functions" }, + + { + opcode: 'deffunc', + text: 'def [NAME] ([ARG]):', + blockType: Scratch.BlockType.LOOP, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "my_func"}, + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "hello, goodbye"}, + } + }, + { + opcode: "returnfunc", + blockType: Scratch.BlockType.COMMAND, + text: "return [VALUE]", + isTerminal: true, + arguments: { + VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: "1"} + } + }, + { + opcode: 'callfunc', + text: '[NAME] ([ARG])', + blockType: Scratch.BlockType.COMMAND, + arguments: { + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "one, two"}, + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "my_func"}, + } + }, + { + opcode: 'getarg', + text: 'parameter [ARG]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "hello"}, + // NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "my_func"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'callfuncrr', + text: '[NAME] ([ARG])', + blockType: Scratch.BlockType.REPORTER, + arguments: { + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "one, two"}, + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "my_func"}, + }, + allowDropAnywhere: true, + }, + { + opcode: "argsreporter", + blockType: Scratch.BlockType.REPORTER, + text: ' ', + // arguments: { + // NAME: {type: Scratch.ArgumentType.STRING, menu: "lists"}, + // VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '0'}, + + + // }, + + mutator: "scrtwpmrunpyextender", + extensions: ["scrtwpmrunpyextender_argsreporter"], + disableMonitor: true, + allowDropAnywhere: true, + }, + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Classes" }, + + { + opcode: 'setclass', + text: 'class [CLASS]:', + blockType: Scratch.BlockType.LOOP, + arguments: { + CLASS: {type: Scratch.ArgumentType.STRING, defaultValue: 'My_class'}, + } + }, + // { + // opcode: 'instantiate', + // text: 'class [CLASS] ([ARGS])', + // blockType: Scratch.BlockType.REPORTER, + // arguments: { + // CLASS: {type: Scratch.ArgumentType.STRING, defaultValue: 'My_class'}, + // ARGS: {type: Scratch.ArgumentType.STRING, defaultValue: 'one, two'}, + // } + // }, + { + opcode: 'deffuncclass', + text: 'def [NAME] (self, [ARG]) in class:', + blockType: Scratch.BlockType.LOOP, + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "class_func"}, + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "hello, goodbye"}, + } + }, + { + opcode: 'setclassvar', + text: 'self.[VAR] = [VALUE] in class', + blockType: Scratch.BlockType.COMMAND, + arguments: { + VAR: {type: Scratch.ArgumentType.STRING, defaultValue: 'class_var'}, + VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '0'}, + } + }, + { + opcode: "setclasslist", + blockType: Scratch.BlockType.COMMAND, + text: 'self.[NAME] = \[', + arguments: { + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: 'class_list'}, + VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: '0'}, + + }, + + mutator: "scrtwpmrunpyextender", + extensions: ["scrtwpmrunpyextender_class"], + disableMonitor: true, + }, + + { + opcode: 'callfuncclass', + text: 'from class [CLASS].[NAME] ([ARG])', + blockType: Scratch.BlockType.COMMAND, + arguments: { + CLASS: {type: Scratch.ArgumentType.STRING, defaultValue: "My_class"}, + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "one, two"}, + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "class_func"}, + } + }, + { + opcode: 'callfuncclassrr', + text: 'from class [CLASS].[NAME] ([ARG])', + blockType: Scratch.BlockType.REPORTER, + arguments: { + CLASS: {type: Scratch.ArgumentType.STRING, defaultValue: "My_class"}, + ARG: {type: Scratch.ArgumentType.STRING, defaultValue: "one, two"}, + NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "class_func"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'getfromclass', + text: 'from class [CLASS].[VAR]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + CLASS: {type: Scratch.ArgumentType.STRING, defaultValue: 'My_class'}, + VAR: {type: Scratch.ArgumentType.STRING, defaultValue: 'class_var'}, + }, + allowDropAnywhere: true, + }, + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Strings" }, + + { + opcode: 'joiner', + text: 'f[JOINED]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + JOINED: {type: Scratch.ArgumentType.STRING, defaultValue: "Python"}, + }, + + mutator: "scrtwpmrunpyextender", + extensions: ["scrtwpmrunpyextender_fstring"], + disableMonitor: true, + allowDropAnywhere: true, + }, + { + opcode: 'toul', + text: '[TEXT].[UL]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "PyThOn"}, + UL: {type: Scratch.ArgumentType.STRING, menu: "ul"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'strreplace', + text: '[STRING].replace [T1], [T2]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: "Python is fun"}, + T1: {type: Scratch.ArgumentType.STRING, defaultValue: "fun"}, + T2: {type: Scratch.ArgumentType.STRING, defaultValue: "awesome"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'strstrip', + text: '[STRING].[STRIP]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: " whitespace? "}, + STRIP: {type: Scratch.ArgumentType.STRING, menu: "strip"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'itrstr', + text: '[STRING]([INDEX])', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: "Python is fun"}, + INDEX: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'strcount', + text: '[STRING].count [LETTER]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: "Python is fun"}, + LETTER: {type: Scratch.ArgumentType.STRING, defaultValue: "n"}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'strcounttwo', + text: '[STRING].count [LETTER], [N1], [N2]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: "Python is fun"}, + LETTER: {type: Scratch.ArgumentType.STRING, defaultValue: "n"}, + N1: {type: Scratch.ArgumentType.NUMBER, defaultValue: 3}, + N2: {type: Scratch.ArgumentType.NUMBER, defaultValue: 9}, + }, + allowDropAnywhere: true, + }, + { + opcode: 'strfind', + text: '[STRING].[FIND] [LETTER]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + STRING: {type: Scratch.ArgumentType.STRING, defaultValue: "Python is fun"}, + FIND: {type: Scratch.ArgumentType.STRING, menu: "find"}, + LETTER: {type: Scratch.ArgumentType.STRING, defaultValue: "n"}, + }, + allowDropAnywhere: true, + }, + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Math" }, + { + opcode: 'add', + blockType: Scratch.BlockType.REPORTER, + text: '[ONE][ADD][TWO]', + arguments: { + ONE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 16 }, + ADD: { type: Scratch.ArgumentType.STRING, menu: "add" }, + TWO: { type: Scratch.ArgumentType.NUMBER, defaultValue: 16 } + }, + allowDropAnywhere: true, + }, + { + opcode: 'executePyMath', + blockType: Scratch.BlockType.REPORTER, + text: 'math.[OPERATION] ( [X] )', + arguments: { + OPERATION: { + type: Scratch.ArgumentType.STRING, + menu: 'singleArgOps', + defaultValue: 'sqrt' + }, + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 16 } + }, + allowDropAnywhere: true, + }, + { + opcode: 'getPyConstant', + blockType: Scratch.BlockType.REPORTER, + text: 'math.[CONSTANT]', + arguments: { + CONSTANT: { + type: Scratch.ArgumentType.STRING, + menu: 'mathConstants', + defaultValue: 'pi' + } + }, + allowDropAnywhere: true, + }, + { + opcode: 'executePyMath2Arg', + blockType: Scratch.BlockType.REPORTER, + text: 'math.[OPERATION] ( [X] , [Y] )', + arguments: { + OPERATION: { + type: Scratch.ArgumentType.STRING, + menu: 'twoArgOps', + defaultValue: 'pow' + }, + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 4 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 2 } + }, + allowDropAnywhere: true, + }, + + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Operators" }, + + { + opcode: 'oneopstwo', + text: '[ONE][OPERATION][TWO]', + blockType: Scratch.BlockType.BOOLEAN, + arguments: { + ONE: {type: Scratch.ArgumentType.STRING, defaultValue: 21}, + OPERATION: {type: Scratch.ArgumentType.STRING, menu:"ops"}, + TWO: {type: Scratch.ArgumentType.STRING, defaultValue: 36}, + } + }, + { + opcode: 'oneandortwo', + text: '[ONE][OPERATION][TWO]', + blockType: Scratch.BlockType.BOOLEAN, + arguments: { + ONE: {type: Scratch.ArgumentType.BOOLEAN}, + OPERATION: {type: Scratch.ArgumentType.STRING, menu:"andorpython"}, + TWO: {type: Scratch.ArgumentType.BOOLEAN}, + } + }, + { + opcode: 'notone', + text: 'not [ONE]', + blockType: Scratch.BlockType.BOOLEAN, + arguments: { + ONE: {type: Scratch.ArgumentType.BOOLEAN}, + } + }, + + + + + + { blockType: Scratch.BlockType.LABEL, text: "Time" }, + + { + opcode: 'sleep', + text: 'time.sleep [NUM]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + NUM: {type: Scratch.ArgumentType.NUMBER, defaultValue: 2}, + + } + }, + + { + opcode: 'runTimeOp', + blockType: Scratch.BlockType.REPORTER, + text: 'time.[OPERATION] [ARG]', + arguments: { + OPERATION: { + type: Scratch.ArgumentType.STRING, + menu: 'timeOps' + }, + ARG: { + type: Scratch.ArgumentType.STRING, + + }, + }, + allowDropAnywhere: true, + }, + + + + + { blockType: Scratch.BlockType.LABEL, text: "Random" }, + + { + opcode: 'randchoice', + text: 'random.choice [ARR]', + blockType: Scratch.BlockType.REPORTER, + arguments: { + ARR: {type: Scratch.ArgumentType.STRING, defaultValue: '["yes","no"]'}, + }, + allowDropAnywhere: true, + }, + + + ], + menus: { + vars: { + acceptReporters: true, + items: 'getThoseVars' + }, + lists: { + acceptReporters: true, + items: 'getThoseLists' + }, + ul:{ + acceptReporters:false, + items:["upper()", "lower()", "title()"] + }, + strip:{ + acceptReporters:false, + items:["strip()", "lstrip()", "rstrip()"], + }, + find:{ + acceptReporters:false, + items:["find", "rfind"] + }, + ops:{ + acceptReporters:false, + items:["==", "!=", "<", "<=", ">", ">="] + }, + listsops:{ + acceptReporters:false, + items:["append", "remove"] + }, + andorpython:{ + acceptReporters:false, + items:["and", "or"] + }, + add:{ + acceptReporters:false, + items:["+", "-", "*", "/"] + }, + singleArgOps: { + acceptReporters: false, + items: [ + { text: 'sqrt', value: 'sqrt' }, + { text: 'cbrt', value: 'cbrt' }, + { text: 'floor', value: 'floor' }, + { text: 'ceil', value: 'ceil' }, + { text: 'trunc', value: 'trunc' }, + { text: 'fabs', value: 'fabs' }, + { text: 'factorial', value: 'factorial' }, + { text: 'exp', value: 'exp' }, + { text: 'exp2', value: 'exp2' }, + { text: 'log', value: 'log' }, + { text: 'log2', value: 'log2' }, + { text: 'log10', value: 'log10' }, + { text: 'sin', value: 'sin' }, + { text: 'cos', value: 'cos' }, + { text: 'tan', value: 'tan' }, + { text: 'asin', value: 'asin' }, + { text: 'acos', value: 'acos' }, + { text: 'atan', value: 'atan' }, + { text: 'sinh', value: 'sinh' }, + { text: 'cosh', value: 'cosh' }, + { text: 'tanh', value: 'tanh' }, + { text: 'asinh', value: 'asinh' }, + { text: 'acosh', value: 'acosh' }, + { text: 'atanh', value: 'atanh' }, + { text: 'degrees', value: 'degrees' }, + { text: 'radians', value: 'radians' } + ] + }, + mathConstants: { + acceptReporters: false, + items: [ + { text: 'pi', value: 'pi' }, + { text: 'e', value: 'e' }, + { text: 'tau', value: 'tau' }, + { text: 'inf', value: 'inf' }, + { text: 'nan', value: 'nan' } + ] + }, + twoArgOps: { + acceptReporters: false, + items: [ + { text: 'pow', value: 'pow' }, + { text: 'gcd', value: 'gcd' }, + { text: 'lcm', value: 'lcm' }, + { text: 'fmod', value: 'fmod' }, + { text: 'remainder', value: 'remainder' }, + { text: 'atan2', value: 'atan2' }, + { text: 'hypot', value: 'hypot' } + ] + }, + timeOps: { + acceptReporters: false, + items: [ + { text: 'time()', value: 'time' }, + { text: 'ctime()', value: 'ctime' }, + { text: 'sleep()', value: 'sleep' }, + { text: 'gmtime()', value: 'gmtime' }, + { text: 'localtime()', value: 'localtime' }, + { text: 'asctime()', value: 'asctime' } + ] + }, + oo: { + acceptReporters: false, + items: ["on","off"] + } + } + }; + } + + +openPyDocs(){ +// window.open('') +} + +setstrict(args, util){ + if(args.OO === "on"){ + this.strictEditing = true + } else { + this.strictEditing = false + } +} + +isstrict(args, utii){ + return(this.strictEditing) +} +aboutstrict(){ + window.alert("Strict editing makes the blue python blocks only work when they are under the When Python Code Starts hat block. This is similar to how the editor works in Edublocks. If you have it off, it will allow python blocks to be used outside of python scripts (not recommended). However, it can be turned off for certain debugging purposes, but by default it is on to mirror coding in Python as much as possible. Feel free to turn it off if it annoys you.") +} + + + + range = (start, end) => JSON.stringify(Array.from({ length: end - start }, (_, i) => start + i)) + + reportParentLoop(args, util) { + + } + reportParentLoop(args, util) { + const loopBlock = this._getParentLoopBlock(util); + + if (!loopBlock) { + return 'Not inside any loop block!'; + } + + // Returns the opcode (e.g., "control_repeat", "control_forever", "control_repeat_until") + return `Inside a loop block of type: ${loopBlock.opcode}`; + } +_getParentLoopBlock(util) { + const target = util.target; + const blocks = target.blocks; + + // 1. Get ALL block IDs currently active or paused in this thread stack + const blockStack = util.thread.stack; + + // 2. Iterate through every active block in the running thread sequence + for (let i = 0; i < blockStack.length; i++) { + let currentBlockId = blockStack[i]; + + // Traverse upwards from each block in the active thread sequence + while (currentBlockId) { + const block = blocks.getBlock(currentBlockId); + if (!block) break; + + // Check if this block itself is a loop + if ( + block.opcode.includes('repeat') || + block.opcode.includes('forever') || + block.opcode === 'control_for_each' + ) { + return block; // Found it! + } + + // 3. Check if this block is nested directly inside a loop's mouth (SUBSTACK) + for (const id of Object.keys(blocks._blocks)) { + const potentialLoop = blocks.getBlock(id); + if (potentialLoop && potentialLoop.inputs && potentialLoop.inputs.SUBSTACK) { + if (potentialLoop.inputs.SUBSTACK.block === currentBlockId) { + return potentialLoop; // Found it via loop mouth structural link! + } + } + } + + // Check the block's physical parent connection + currentBlockId = block.parent; + } + } + + return null; // Truly not inside any loop +} + + + _getThreadStorage(util) { + // util.thread represents the precise, actively-running block sequence + if (!util.thread.temporaryVariablesStore) { + util.thread.temporaryVariablesStore = {}; + } + return util.thread.temporaryVariablesStore; + } + + // Set variable inside the thread scope + setTempVar(args, util, name, val) { + const storage = this._getThreadStorage(util); + storage[name] = val; + } + + // Read variable from the thread scope + getTempVar(args, util, name) { + const storage = this._getThreadStorage(util); + console.log(storage) + // Returns the value, or 0 if it has not been defined yet + return storage[name] !== undefined ? storage[name] : 0; + } + + + + + // Helper Functions + getTargets(myself, all) { + const spriteNames = []; + if (myself) spriteNames.push({ text: "myself", value: "_myself_" }); + else spriteNames.push({ text: "Stage", value: "_stage_" }); + if (all) { + spriteNames.push( + { text: "All Sprites", value: "_all_" }, { text: "All Main Sprites", value: "_all_2" }, { text: "All Clones", value: "_all_3" } + ); + } + const targets = runtime.targets; + for (let i = 1; i < targets.length; i++) { + const target = targets[i]; + const name = target.getName(); + if (target.isOriginal) spriteNames.push({ text: name, value: name }); + } + return spriteNames.length > 0 ? spriteNames : [""]; + } + + organizeHats() { + const allHats = runtime._hats; + const vanillaHats = [ + {text: "when I start as clone", value: "control_start_as_clone"}, + {text: "when green flag clicked", value: "event_whenflagclicked"}, + {text: "when key pressed", value: "event_whenkeypressed"}, + {text: "when sprite clicked", value: "event_whenthisspriteclicked"}, + {text: "when stage clicked", value: "event_whenstageclicked"}, + {text: "when backdrop switches", value: "event_whenbackdropswitchesto"}, + {text: "when touching object", value: "event_whentouchingobject"}, + {text: "when value greater than", value: "event_whengreaterthan"}, + {text: "when broadcast received", value: "event_whenbroadcastreceived"} + ]; + const startIndex = Object.keys(allHats).findIndex(k => k === "event_whenbroadcastreceived"); + const filteredHats = Object.keys(allHats).filter((_, i) => i > startIndex).map(k => ({ text: k, value: k })); + return [...vanillaHats, ...filteredHats]; + } + + addMissKeys(sourceObj, checkedObj) { + Object.keys(sourceObj).forEach(key => { + if (!(key in checkedObj)) checkedObj[key] = sourceObj[key]; + }); + Object.keys(sourceObj.stackFrames[0]).forEach(key => { + if (!(key in checkedObj.stackFrames[0])) checkedObj.stackFrames[0][key] = sourceObj.stackFrames[0][key]; + }); + return checkedObj; + } + + pushThreadTarget(id, newT, oldT, stack) { + const thread = runtime._pushThread(id, oldT, { stackClick: stack }); + thread.target = newT; thread.ogTarget = oldT; + if (runtime.compilerOptions.enabled) thread.tryCompile(); + return thread; + } + + pushThread(id, util, opts) { + const ogTarget = util.thread.ogTarget; + const target = ogTarget ? ogTarget : util.target; + const thread = runtime._pushThread(id, target, opts); + if (ogTarget) { + thread.target = util.target; + thread.ogTarget = target; + } + if (runtime.compilerOptions.enabled) thread.tryCompile(); + return thread; + } + + genFakeCode(block, util, newTarget) { + if (!Thread) return "Extra Controls could not access Exports!"; + if (!block) return ""; + return new Promise((resolve) => { + const thread = util.thread; + const tempThread = new Thread(block.id); + tempThread.pushStack(block.id); + overrideCalls[block.id] = tempThread; + tempThread.stackClick = true; + tempThread.blockContainer = thread.blockContainer; + tempThread.target = newTarget; + tempThread.ogTarget = util.ogTarget ?? util.target; + tempThread.pushReportedValue = (value) => resolve(value); + runtime.threads.push(tempThread); + if (!tempThread.stackClick && !tempThread.updateMonitor) runtime.threadMap.set(tempThread.getId(), tempThread); + if (runtime.compilerOptions.enabled) tempThread.tryCompile(); + }); + } + + getThisBlock(util, branch, optBranch) { + if (branch) return util.thread.blockContainer.getBranch(util.thread.peekStack(), optBranch ? optBranch : 1); + else return util.thread.blockContainer.getBlock(util.thread.isCompiled ? util.thread.peekStack() : util.thread.peekStackFrame().op.id); + } + + getLoopBlock(thread) { + const stackFrames = thread.stackFrames, frameCount = stackFrames.length; + let loopBlock = null, stackIndex = -1; + for (let i = frameCount - 1; i >= 0; i--) { + if (i < 0) break; + if (!stackFrames[i].isLoop) continue; + loopBlock = stackFrames[i].op.id; + stackIndex = i; + break; + } + if (!loopBlock) return false; + return { block: loopBlock, index: stackIndex }; + } + + + getBlockAbove(args, util) { + // 1. Identify the active script execution thread + const thread = util.thread; + if (!thread) return 'No active thread'; + + // 2. Fetch the specific visual block currently executing this function + const currentBlockId = thread.peekStack(); + if (!currentBlockId) return 'Could not find current block ID'; + + // 3. Look up the block object inside the sprite's target runtime container + const blockContainer = thread.blockContainer; + const currentBlock = blockContainer.getBlock(currentBlockId); + if (!currentBlock) return 'Error reading current block object'; + + // 4. Extract the unique ID of the block attached to its top connection notch + const parentBlockId = currentBlock.parent; + if (!parentBlockId) { + return 'Top of stack'; // No block is physically above this one + } + + // 5. Look up the parent block to retrieve its identifier + const parentBlock = blockContainer.getBlock(parentBlockId); + if (!parentBlock) return 'Unknown block connection'; + + // 6. Return the internal Scratch/PenguinMod execution opcode name + return parentBlock.opcode; + } + + + + // 1. Ensure the thread context exists + check(args, util){ + if(this.strictEditing){ + // return true + if (!util || !util.thread) { + return 'No Thread Found'; + } + + + const thread = util.thread; + const blockContainer = thread.target.blocks; + + // 2. Get the block that is currently executing right now (this reporter) + let currentBlockId = thread.peekStack(); + if (!currentBlockId) return false; + + let currentBlock = blockContainer.getBlock(currentBlockId); + if (!currentBlock) return true; + + // 3. Trace upward through parents until hitting the top-level Hat block + while (currentBlock && currentBlock.parent) { + currentBlockId = currentBlock.parent; + currentBlock = blockContainer.getBlock(currentBlockId); + } + + // 4. Return the top block's opcode + if (currentBlock) { + return (currentBlock.opcode === "scrtwpmrunpy_whenSessionStarts"); + } + + return false; +} else {return true} + } + + getThoseVars() { + // Always ensure there is at least one fallback option so the dropdown doesn't crash + if (this.variables.length === 0) { + return ['Create a variable!']; + } + return this.variables; + } + + getThoseLists() { + // Always ensure there is at least one fallback option so the dropdown doesn't crash + if (this.lists.length === 0) { + return ['Create a list!']; + } + return this.lists; + } + + startSession(args, util){ + util.startHats('scrtwpmrunpy_whenSessionStarts') + } + + whenSessionStarts(args){ + return true + } + + + + displayBlock(args, util) { + + try{ + Array.from(document.querySelectorAll("#pythonblock")).forEach((item, index) => { + item.remove() + }) + + }catch(error){ + + } + const el = document.createElement("div"); + el.id = "pythonblock" + el.style.width = "100%" + el.style.height = "100%" + el.style.backgroundColor = "black"; + el.style.fontFamily = "consolas" + el.style.fontSize = "15px" + el.style.color = "white" + el.style.padding = "15px" + el.style.overflowY = "scroll" + + // 2. Style it to float over the canvas + el.style.position = 'absolute'; + el.style.pointerEvents = 'auto'; // Set to 'none' if it shouldn't block clicks + el.style.zIndex = '10'; + + // 3. Position (Note: Stage center is 0,0; HTML top-left is 0,0) + el.style.left = 0; + el.style.top = 0; + + // 4. Append to the stage parent container + const container = Scratch.renderer.canvas.parentElement; + container.appendChild(el); + document.getElementById("pythonblock").innerHTML = this.pytext + + } + + deleteBlock(args, util){ + + try{ + Array.from(document.querySelectorAll("#pythonblock")).forEach((item, index) => { + item.remove() + }) + + }catch(error){ + + } + } + + clear(args, util){ + try{ + this.pytext = "" + document.getElementById("pythonblock").innerHTML = this.pytext + document.getElementById("theelemtnforpython").remove() + + + }catch(error){ + + } + } + print(args, util){if(this.check(args, util)){ + try{ + this.pytext = `${this.pytext}
${args.TEXT}` + document.getElementById("pythonblock").innerHTML = this.pytext + + }catch(error){ + + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + +block(){ + return(this.text) +} + + getpytext(args, util){if(this.check(args, util)){ + return(this.pytext.replaceAll("
", "\n")) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + getvar(args, util) {if(this.check(args, util)){ + try{ + if(args.NAME !== "Create a variable!"){ + return(this[`var${args.NAME}`]) + } + + }catch(error){ + + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + deletevar(args, util) { + const newItem = String(args.NAME); + + let vars = this.variables; + let index = vars.indexOf(newItem); + + if (index > -1) { + vars.splice(index, 1); + } + } + createvar (args, util){ + const newItem = String(args.NAME); + if (!this.variables.includes(newItem)) { + this.variables.push(newItem); + } + this[`var${newItem}`] = "0" + + } + + setvar(args, util){if(this.check(args, util)){ + if(this.variables.includes(args.NAME)){ + this[`var${args.NAME}`] = args.VALUE + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + getlist(args, util) {if(this.check(args, util)){ + try{ + if(args.NAME !== "Create a list!"){ + return(this[`lst${args.NAME}`]) + } + + }catch(error){ + + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + deletelist(args, util) { + const newItem = String(args.NAME); + + let lsts = this.lists; + let index = lsts.indexOf(newItem); + + if (index > -1) { + lsts.splice(index, 1); + } + } + createlist (args, util){ + const newItem = String(args.NAME); + if (!this.lists.includes(newItem)) { + this.lists.push(newItem); + } + this[`lst${newItem}`] = '["item"]' + + } + + setlist(args, util){if(this.check(args, util)){ + + const prefix = "ARG"; + let string = "["; + for (let i = 0; prefix + i in args; i++) { + string += '"' + string += Scratch.Cast.toString(args[prefix + i]); + string += '"' + string += "," + } + string = string.slice(0, -1) + string += "]" + + + + if(this.lists.includes(args.NAME)){ + this[`lst${args.NAME}`] = string + } + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + setlisttwo(args, util){if(this.check(args, util)){ + + if(this.lists.includes(args.NAME)){ + this[`lst${args.NAME}`] = args.VALUE + } + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + joiner(args, util){if(this.check(args, util)){ + + const prefix = "ARG"; + let string = args.JOINED; + for (let i = 0; prefix + i in args; i++) { + string += Scratch.Cast.toString(args[prefix + i]); + } + return(string) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + sleep(args, util){if(this.check(args, util)){ + return new Promise((resolve) => { + const delayMs = Number(args.NUM) * 1000; + + setTimeout(() => { + resolve(); + }, delayMs); + }); + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + toul(args, util){if(this.check(args, util)){ + const inputStr = String(args.TEXT); + // Check the selected dropdown menu choice + if (args.UL === "upper()") { + return inputStr.toUpperCase(); + } else if (args.UL === "lower()") { + return inputStr.toLowerCase(); + } else { + return inputStr + .toLowerCase() + .replace(/\b\w/g, char => char.toUpperCase()); + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + strreplace(args, util){if(this.check(args, util)){ + return(args.STRING.replaceAll(args.T1, args.T2)) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + input(args, util){if(this.check(args, util)){ +try{ + +this.pytext = `${this.pytext}
${args.INPUT}`; +document.getElementById("pythonblock").innerHTML = this.pytext; +this.text = ""; + +try { + const oldEl = document.getElementById("theelemtnforpython"); + if (oldEl) oldEl.remove(); +} catch (error) { +} + +const els = document.createElement('span'); +els.id = "theelemtnforpython"; +els.setAttribute("tabindex", -1); +els.style.outline = "none"; // Hide focus ring + +const container = Scratch.renderer.canvas.parentElement; +container.appendChild(els); +els.focus(); + +return new Promise((resolve) => { + const handleKeyDown = (event) => { + const key = event.key; + + if (key === "Enter") { + this.pytext = `${this.pytext}${this.text}`; + document.getElementById("pythonblock").innerHTML = this.pytext; + + els.removeEventListener('keydown', handleKeyDown); + els.remove(); + + resolve(this.text); + return; + } + + if (key === "Backspace") { + this.text = this.text.slice(0, -1); + document.getElementById("pythonblock").innerHTML = `${this.pytext}${this.text}`; + } else if (key.length === 1) { + this.text = this.text + key; + document.getElementById("pythonblock").innerHTML = `${this.pytext}${this.text}`; + } + }; + + els.addEventListener('keydown', handleKeyDown); +}); +}catch(error){ + +} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + ans(args, util){if(this.check(args, util)){ + return(this.text) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + strstrip(args, util){if(this.check(args, util)){ + if(args.STRIP === "strip()"){ + return(args.STRING.trim()) + } else if(args.STRIP === "lstrip()"){ + return(args.STRING.trimStart()) + } else { + return(args.STRING.trimEnd()) + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + itrstr(args, util){if(this.check(args, util)){ + try{ + return(args.STRING[Number(args.INDEX)]) + } catch (error){} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + itrlist(args, util){if(this.check(args, util)){ + try{ + return(JSON.parse(this[`lst${args.LIST}`] )[Number(args.INDEX)]) + } catch (error){} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + strcount(args, util){if(this.check(args, util)){ + return(args.STRING.split(args.LETTER).length - 1) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + strcounttwo(args, util){if(this.check(args, util)){ + return(args.STRING.slice(args.N1, args.N2).split(args.LETTER).length - 1) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + strfind(args, util){if(this.check(args, util)){ + if(args.FIND === "find"){ + return(args.STRING.indexOf(args.LETTER)) + } else { + return(args.STRING.lastIndexOf(args.LETTER)) + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + mathematics(args, util){if(this.check(args, util)){ + return(Math[args.MATH](args.NUM)) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + executePyMath(args, util) {if(this.check(args, util)){ + const x = Number(args.X); + const op = args.OPERATION; + + switch (op) { + case 'sqrt': return x < 0 ? "ValueError: math domain error" : Math.sqrt(x); + case 'cbrt': return Math.cbrt(x); + case 'floor': return Math.floor(x); + case 'ceil': return Math.ceil(x); + case 'trunc': return Math.trunc(x); + case 'fabs': return Math.abs(x); + case 'exp': return Math.exp(x); + case 'exp2': return Math.pow(2, x); + case 'log': return x <= 0 ? "ValueError: math domain error" : Math.log(x); + case 'log2': return x <= 0 ? "ValueError: math domain error" : Math.log2(x); + case 'log10': return x <= 0 ? "ValueError: math domain error" : Math.log10(x); + + case 'factorial': + if (x < 0 || !Number.isInteger(x)) return "ValueError: factorial() only accepts non-negative integers"; + let r = 1; + for (let i = 2; i <= x; i++) r *= i; + return r; + + case 'sin': return Math.sin(x); + case 'cos': return Math.cos(x); + case 'tan': return Math.tan(x); + case 'asin': return (x < -1 || x > 1) ? "ValueError: math domain error" : Math.asin(x); + case 'acos': return (x < -1 || x > 1) ? "ValueError: math domain error" : Math.acos(x); + case 'atan': return Math.atan(x); + + case 'sinh': return Math.sinh(x); + case 'cosh': return Math.cosh(x); + case 'tanh': return Math.tanh(x); + case 'asinh': return Math.asinh(x); + case 'acosh': return x < 1 ? "ValueError: math domain error" : Math.acosh(x); + case 'atanh': return (x <= -1 || x >= 1) ? "ValueError: math domain error" : Math.atanh(x); + + case 'degrees': return x * (180 / Math.PI); + case 'radians': return x * (Math.PI / 180); + + default: return 0; + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + getPyConstant(args, util) {if(this.check(args, util)){ + switch (args.CONSTANT) { + case 'pi': return Math.PI; + case 'e': return Math.E; + case 'tau': return 2 * Math.PI; + case 'inf': return Infinity; + case 'nan': return NaN; + default: return 0; + } + }else {throw new Error("Block must be under the When Python Code Starts event")}} + + executePyMath2Arg(args, util) {if(this.check(args, util)){ + const x = Number(args.X); + const y = Number(args.Y); + const op = args.OPERATION; + + switch (op) { + case 'pow': return Math.pow(x, y); + case 'fmod': return x % y; + case 'atan2': return Math.atan2(x, y); + case 'hypot': return Math.hypot(x, y); + + case 'gcd': + const _gcd = (a, b) => { + a = Math.abs(Math.round(a)); b = Math.abs(Math.round(b)); + while (b) { let t = b; b = a % b; a = t; } + return a; + }; + return _gcd(x, y); + + case 'lcm': + if (x === 0 && y === 0) return 0; + const _lcmGcd = (a, b) => { + a = Math.abs(Math.round(a)); b = Math.abs(Math.round(b)); + while (b) { let t = b; b = a % b; a = t; } + return a; + }; + return Math.abs(Math.round(x) * Math.round(y)) / _lcmGcd(x, y); + + case 'remainder': + return x - y * Math.round(x / y); + + default: return 0; + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + ifs(args, util){if(this.check(args, util)){ + + let theval = "" +const blockContainer = util.thread.blockContainer; +const currentBlockId = util.thread.peekStack(); // +const currentBlock = blockContainer.getBlock(currentBlockId); +if (args.BOOL){ + util.startBranch(1, false) +} + +if (currentBlock) { + // 2. Add your custom hidden property (give it a unique name) + currentBlock.myHiddenPythonMeta = { + isElifChained: args.BOOL, + compiledScope: 'global', + fallbackLine: 42 + }; + + console.log("Successfully attached hidden property to the block!"); +} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + elifs(args, util){if(this.check(args, util)){ + if(this.getBlockAbove(args, util) === "scrtwpmrunpy_ifs" || this.getBlockAbove(args, util) === "scrtwpmrunpy_elifs"){ + + +let theval = false; + +const blockContainer = util.thread.blockContainer; +const currentBlockId = util.thread.peekStack(); +const currentBlock = blockContainer.getBlock(currentBlockId); + +if (currentBlock && currentBlock.parent) { + const parentBlockId = currentBlock.parent; + const parentBlock = blockContainer.getBlock(parentBlockId); + + if (parentBlock && parentBlock.myHiddenPythonMeta) { + const meta = parentBlock.myHiddenPythonMeta; + console.log("Read hidden data from parent status:", meta.isElifChained); + + // If any block above us in the chain already evaluated to true, + // then this block is blocked from running (theval becomes true to signify 'already handled') + if (meta.isElifChained === true) { + theval = true; + } + } +} else { + console.log("This block does not have a parent sitting directly above it."); +} + +// Evaluate this current block's input condition (using the 'BOOL' slot) +const currentConditionResult = Boolean(args.BOOL); + +if (currentBlock) { + // If the chain was already handled above us, pass down 'true'. + // If the chain wasn't handled, but THIS block is true, pass down 'true'. + // Otherwise, pass down 'false' so the next block down the stack gets a turn. + currentBlock.myHiddenPythonMeta = { + isElifChained: theval || currentConditionResult, + compiledScope: 'global', + fallbackLine: 42 + }; + + console.log("Successfully attached hidden property to the block!"); +} + +// ONLY execute this block's branch if the chain hasn't been handled yet +// AND this block's condition slot evaluates to true +if (!theval && currentConditionResult) { + util.startBranch(1, false); +} + + + } else { + throw new Error("elif: must be under an if: or elif: block") + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + elses(args, util){if(this.check(args, util)){ + if(this.getBlockAbove(args, util) === "scrtwpmrunpy_ifs" || this.getBlockAbove(args, util) === "scrtwpmrunpy_elifs" ){let theval = false; + +const blockContainer = util.thread.blockContainer; +const currentBlockId = util.thread.peekStack(); +const currentBlock = blockContainer.getBlock(currentBlockId); + +if (currentBlock && currentBlock.parent) { + const parentBlockId = currentBlock.parent; + const parentBlock = blockContainer.getBlock(parentBlockId); + + if (parentBlock && parentBlock.myHiddenPythonMeta) { + const meta = parentBlock.myHiddenPythonMeta; + console.log("Read hidden data from parent status:", meta.isElifChained); + + // If any block above us in the chain already evaluated to true, + // then this block is blocked from running (theval becomes true to signify 'already handled') + if (meta.isElifChained === true) { + theval = true; + } + } +} else { + console.log("This block does not have a parent sitting directly above it."); +} + +// Evaluate this current block's input condition (using the 'BOOL' slot) +// const currentConditionResult = Boolean(args.BOOL); + +if (currentBlock) { + // If the chain was already handled above us, pass down 'true'. + // If the chain wasn't handled, but THIS block is true, pass down 'true'. + // Otherwise, pass down 'false' so the next block down the stack gets a turn. + currentBlock.myHiddenPythonMeta = { + isElifChained: theval, + compiledScope: 'global', + fallbackLine: 42 + }; + + console.log("Successfully attached hidden property to the block!"); +} + +// ONLY execute this block's branch if the chain hasn't been handled yet +// AND this block's condition slot evaluates to true +if (!theval) { + util.startBranch(1, false); +} + + + } else { + throw new Error("elif: must be under an if: or elif: block") + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + oneopstwo(args, util){if(this.check(args, util)){ + switch(args.OPERATION){ + case ">" : return Number(args.ONE) > Number(args.TWO); + case "<" : return Number(args.ONE) < Number(args.TWO); + case ">=" : return Number(args.ONE) >= Number(args.TWO); + case "<=" : return Number(args.ONE) <= Number(args.TWO); + case "==" : return args.ONE == args.TWO + case "!=" : return !(args.ONE == args.TWO) + } + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + oneandortwo(args, util){if(this.check(args, util)){ + if(args.OPERATION === "and"){ + return(args.ONE && args.TWO) + } else { + return(args.ONE || args.TWO) + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + notone(args, util){if(this.check(args, util)){ + return(!args.ONE) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + reverselist(args, util){if(this.check(args, util)){ + console.log(this) + console.log(JSON.parse((this[`lst${args.LIST}`]))) + this[`lst${args.LIST}`] = JSON.stringify(JSON.parse((this[`lst${args.LIST}`])).toReversed()) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + indexlist(args, util){if(this.check(args, util)){ + return(JSON.parse(this[`lst${args.LIST}`]).indexOf(args.TEXT)) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + operatelist(args, util){if(this.check(args, util)){ +switch(args.OPERATION) { + // items:["append", "insert", "extend", "pop", "remove"] + case "append": { + console.log(this[`lst${args.LIST}`]); + let array = JSON.parse(this[`lst${args.LIST}`]); + array.push(String(args.TEXT)); + this[`lst${args.LIST}`] = JSON.stringify(array); + break; // Prevents falling through to extend + } + case "remove": { + let list = JSON.parse(this[`lst${args.LIST}`]); + let index = list.indexOf(args.TEXT); + if (index !== -1) { + list.splice(index, 1); + } + this[`lst${args.LIST}`] = JSON.stringify(list); + break; + } +} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + listpop(args, util){if(this.check(args, util)){ + let list = JSON.parse(this[`lst${args.LIST}`]) + // Find the index of the first "banana" + let index = list.indexOf(args.TEXT); + let item = list[index] + if (index !== -1) { + list.splice(index, 1); // Removes 1 item at that index + } + + this[`lst${args.LIST}`] = JSON.stringify(list); + return(item) + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + listinsert(args, util){if(this.check(args, util)){ + console.log(JSON.parse(this[`lst${args.LIST}`])) + console.log(args.NUM) + console.log(args.TEXT) + let array = JSON.parse(this[`lst${args.LIST}`]); + + // Splice modifies 'array' in-place and returns the deleted items (none) + array.splice(Number(args.NUM), 0, args.TEXT); + + // Save the mutated 'array' back to your state + this[`lst${args.LIST}`] = JSON.stringify(array); + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + extendlist(args, util){if(this.check(args, util)){ + // if(this.lists.includes(args.LISTR)) + let array = JSON.parse(this[`lst${args.LIST}`]); + let itemsToAdd = JSON.parse(args.L2); + array.push(...itemsToAdd); // Modifies the array in-place + this[`lst${args.LIST}`] = JSON.stringify(array); + // break; // Prevents falling through to remove + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + whileloop(args, util){if(this.check(args, util)){ + if (args.BOOL) { + util.startBranch(1, true) + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + foriinlist(args, util){if(this.check(args, util)){ + let arr; + try { + arr = typeof args.LIST === 'object' ? args.LIST : JSON.parse(args.LIST); + if (!Array.isArray(arr)) arr = []; // Fallback if input is JSON object but not an array + } catch (e) { + arr = []; // Fallback if text is not valid JSON + } + + // 2. Initialize tracking index on the persistent block stack frame + if (typeof util.stackFrame.index === 'undefined') { + util.stackFrame.index = 0; + } + + // 3. Check your custom evaluation condition and verify array bounds + if (this.check(args, util) && util.stackFrame.index < arr.length) { + + // Extract the element corresponding to the current step sequence + const currentItem = arr[util.stackFrame.index]; + + // Assign your context index tracking pointer (e.g. this.varIndexName) + // Supports dynamic variable naming flags passed into the arguments channel + this[`var${args.I}`] = currentItem; + + // Increment tracking pointer safely BEFORE starting the branch processing frame + util.stackFrame.index++; + + // Execute internal blocks, looping back smoothly for the next item sequence + util.startBranch(1, true); + } + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + breaknow(args, util){if(this.check(args, util)){ + if (!util || !util.thread) return; + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + rangesone(args, util){if(this.check(args, util)){ + return(this.range(0, Number(args.N2))) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + rangestwo(args, util){if(this.check(args, util)){ + return(this.range(Number(args.N1), Number(args.N2))) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + deffunc(args, util){if(this.check(args, util)){ + util.thread[`pythonfunc${args.NAME}`] = this.getThisBlock(util, true, 1); + util.thread[`pythonfunc${args.NAME}args`] = args.ARG.replace(/\s/g, "").split(",") + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + deffuncclass(args, util){if(this.check(args, util)){ + + let yes + let loopBlock + let classs + try{ + loopBlock = this._getParentLoopBlock(util); + + if (loopBlock.opcode === "scrtwpmrunpy_setclass") { + yes = true + } else { + yes = false + } + + + + console.log("yes", yes) + console.log(this.getBlockAbove(args, util)) + // Check the execution thread history array to verify the active block trail + const blockStack = util.thread.stack; + const target = util.target; + const blocks = target.blocks; + + for (let i = 0; i < blockStack.length; i++) { + let currentBlockId = blockStack[i]; + + while (currentBlockId) { + const block = blocks.getBlock(currentBlockId); + console.log("is und:" , util.thread.lastClassName) + if (!block) break; + + // If the thread actively contains our specific custom loop block opcode + // if (block.opcode === 'scrtwpmrunpy_setclass') { + // Safely fall back to the thread property we cached during the loop step execution + if (util.thread.lastClassName !== undefined) { + classs = util.thread.lastClassName; + // console.log(lastClassName) + } + + // Fallback: directly parse the static input text if the thread hasn't cached it yet + if (block.inputs && block.inputs.CLASS) { + const inputBlockId = block.inputs.CLASS.block; + const rawValue = util.runtime.getScriptRuntimePosition(target, inputBlockId); + classs = rawValue; + } + // } + + // Traverse upwards to see if it's nested inside secondary loops or conditionals + currentBlockId = block.parent; + } + } + + // return 'Not inside a custom loop!'; + + + +} catch (error) { +} + if(yes){ + + util.thread[`pythonfuncclass${classs}${args.NAME}`] = this.getThisBlock(util, true, 1); + util.thread[`pythonfuncclass${classs}${args.NAME}args`] = args.ARG.replace(/\s/g, "").split(",") + console.log("1", `pythonfuncclass${classs}${args.NAME}`) + } else {throw new Error("Class vars must be set inside of a Class: loop")} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + callfuncclass(args, util){if(this.check(args, util)){ + try{ + if (util.stackFrame.SPran) { + if (runtime.isActiveThread(util.stackFrame.SPran)) util.yield(); + } else { + console.log("2:", `pythonfuncclass${args.CLASS}${args.NAME}`) + const func = util.thread[`pythonfuncclass${args.CLASS}${args.NAME}`]; + if (func !== undefined) { + try{ + const thread = this.pushThread(func, util, { stackClick: false }); + this.addMissKeys(util.thread, thread); + let argsarg = args.ARG//.replace(/\s/g, ""); + let arguement = argsarg.split(",") + const argNames = util.thread[`pythonfuncclass${args.CLASS}${args.NAME}args`] + const argVals = arguement + const argsJSON = Object.fromEntries( + argNames.map((fruit, index) => [fruit, argVals[index]]) + ); + let last = Object.keys(argsJSON).at(-1) + console.log("l1", last) + let lengthOfObject = argNames.length + console.log("lo", lengthOfObject) + console.log("sliced", argVals.slice(lengthOfObject - 1)) + + console.log("truth:" ,last.slice(0, 2)) + if(last.slice(0, 1) === "*"){ + argsJSON[last] = argVals.slice(lengthOfObject - 1) + } + + if (thread.RunPython === undefined) { + thread.stackFrames[0].RunPython = arguement; + thread.stackFrames[0].RunPythonARGS = argsJSON; + } + + util.stackFrame.SPran = thread; + util.yield(); + } catch (error) {} + } + } + } catch (error) {} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + callfuncclassrr(args, util){if(this.check(args, util)){ + if (util.stackFrame.SPran) { + if (runtime.isActiveThread(util.stackFrame.SPran)) util.yield(); + else return util.stackFrame.SPran.justReported ?? ""; + } else { + const func = util.thread[`pythonfuncclass${args.CLASS}${args.NAME}`]; + if (func === undefined) return ""; + else { + const thread = this.pushThread(func, util, { stackClick: false }); + this.addMissKeys(util.thread, thread); + let argsarg = args.ARG//.replace(/\s/g, ""); + let arguement = argsarg.split(",") + const argNames = util.thread[`pythonfuncclass${args.CLASS}${args.NAME}args`] + const argVals = arguement + const argsJSON = Object.fromEntries( + argNames.map((fruit, index) => [fruit, argVals[index]]) + ); + let last = Object.keys(argsJSON).at(-1) + console.log("l1", last) + let lengthOfObject = argNames.length + console.log("lo", lengthOfObject) + console.log("sliced", argVals.slice(lengthOfObject - 1)) + + console.log("truth:" ,last.slice(0, 2)) + if(last.slice(0, 1) === "*"){ + argsJSON[last] = argVals.slice(lengthOfObject - 1) + } + + if (thread.RunPython === undefined) { + thread.stackFrames[0].RunPython = arguement; + thread.stackFrames[0].RunPythonARGS = argsJSON; + } + + util.stackFrame.SPran = thread; + util.yield(); + } + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + callfunc(args, util){if(this.check(args, util)){ + try{ + if (util.stackFrame.SPran) { + if (runtime.isActiveThread(util.stackFrame.SPran)) util.yield(); + } else { + const func = util.thread[`pythonfunc${args.NAME}`]; + if (func !== undefined) { + try{ + const thread = this.pushThread(func, util, { stackClick: false }); + this.addMissKeys(util.thread, thread); + let argsarg = args.ARG//.replace(/\s/g, ""); + let arguement = argsarg.split(",") + const argNames = util.thread[`pythonfunc${args.NAME}args`] + const argVals = arguement + const argsJSON = Object.fromEntries( + argNames.map((fruit, index) => [fruit, argVals[index]]) + ); + let last = Object.keys(argsJSON).at(-1) + console.log("l1", last) + let lengthOfObject = argNames.length + console.log("lo", lengthOfObject) + console.log("sliced", argVals.slice(lengthOfObject - 1)) + + console.log("truth:" ,last.slice(0, 2)) + if(last.slice(0, 1) === "*"){ + argsJSON[last] = argVals.slice(lengthOfObject - 1) + } + + if (thread.RunPython === undefined) { + thread.stackFrames[0].RunPython = arguement; + thread.stackFrames[0].RunPythonARGS = argsJSON; + } + + util.stackFrame.SPran = thread; + util.yield(); + } catch (error) {} + } + } + } catch (error) {} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + // templatefunc(args, util){if(this.check(args, util)){ + getarg(args, util) {if(this.check(args, util)){ + let returned + let array = util.thread.stackFrames[0].RunPython + // let newarr = array.split(",") + console.log ("array is: " + array, "new array is:" + array) + console.log (array, array) + // return (Object.values(array).indexOf(args.ARG)) + if (util.thread.stackFrames[0].RunPythonARGS[args.ARG] === undefined ){ + if (util.thread.stackFrames[0].RunPythonARGS[`*${args.ARG}`] !== undefined) { + returned = util.thread.stackFrames[0].RunPythonARGS[`*${args.ARG}`] + } + } else { + returned = util.thread.stackFrames[0].RunPythonARGS[args.ARG] + } + return(returned) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + callfuncrr(args, util){if(this.check(args, util)){ + if (util.stackFrame.SPran) { + if (runtime.isActiveThread(util.stackFrame.SPran)) util.yield(); + else return util.stackFrame.SPran.justReported ?? ""; + } else { + const func = util.thread[`pythonfunc${args.NAME}`]; + if (func === undefined) return ""; + else { + const thread = this.pushThread(func, util, { stackClick: false }); + this.addMissKeys(util.thread, thread); + let argsarg = args.ARG//.replace(/\s/g, ""); + let arguement = argsarg.split(",") + const argNames = util.thread[`pythonfunc${args.NAME}args`] + const argVals = arguement + const argsJSON = Object.fromEntries( + argNames.map((fruit, index) => [fruit, argVals[index]]) + ); + let last = Object.keys(argsJSON).at(-1) + console.log("l1", last) + let lengthOfObject = argNames.length + console.log("lo", lengthOfObject) + console.log("sliced", argVals.slice(lengthOfObject - 1)) + + console.log("truth:" ,last.slice(0, 2)) + if(last.slice(0, 1) === "*"){ + argsJSON[last] = argVals.slice(lengthOfObject - 1) + } + + if (thread.RunPython === undefined) { + thread.stackFrames[0].RunPython = arguement; + thread.stackFrames[0].RunPythonARGS = argsJSON; + } + + util.stackFrame.SPran = thread; + util.yield(); + } + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + returnfunc(args, util){if(this.check(args, util)){ + util.thread.justReported = args.VALUE; + //Delay the Deletion of this Thread + if (util.stackTimerNeedsInit()) { + util.startStackTimer(0); + runtime.requestRedraw(); + util.yield(); + } else if (!util.stackTimerFinished()) util.yield(); + util.thread.stopThisScript(); + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + + getarg2(args, util) {if(this.check(args, util)){ return util.thread.stackFrames[0].RunPython ?? "" } else {throw new Error("Block must be under the When Python Code Starts event")}} + // } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + argsreporter(args, util){if(this.check(args, util)){ + + const prefix = "ARG"; + let string = ""; + for (let i = 0; prefix + i in args; i++) { + string += Scratch.Cast.toString(args[prefix + i]); + string += "," + } + string = string.slice(0, -1) + + return(string) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + randchoice(args, util){if(this.check(args, util)){ + return (JSON.parse(args.ARR)[Math.floor(Math.random() * JSON.parse(args.ARR).length)]) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + add(args, util){if(this.check(args, util)){ + switch(args.ADD) { + case "+" : return Number(args.ONE) + Number(args.TWO); + case "-" : return Number(args.ONE) - Number(args.TWO); + case "*" : return Number(args.ONE) * Number(args.TWO); + case "/" : return Number(args.ONE) / Number(args.TWO); + } + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + setclass(args, util){if(this.check(args, util)){ + util.thread.lastClassName = args.CLASS; + util.startBranch(1, false) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + setclassvar(args, util){if(this.check(args, util)){ + let yes + let loopBlock + let classs + try{ + loopBlock = this._getParentLoopBlock(util); + + if (loopBlock.opcode === "scrtwpmrunpy_setclass" || loopBlock.opcode === "scrtwpmrunpy_deffuncclass") { + yes = true + } else { + yes = false + } + console.log("yes", yes) + + // Check the execution thread history array to verify the active block trail + const blockStack = util.thread.stack; + const target = util.target; + const blocks = target.blocks; + + for (let i = 0; i < blockStack.length; i++) { + let currentBlockId = blockStack[i]; + + while (currentBlockId) { + const block = blocks.getBlock(currentBlockId); + if (!block) break; + + // If the thread actively contains our specific custom loop block opcode + if (block.opcode === 'scrtwpmrunpy_setclass') { + // Safely fall back to the thread property we cached during the loop step execution + if (util.thread.lastClassName !== undefined) { + classs = util.thread.lastClassName; + } + + // Fallback: directly parse the static input text if the thread hasn't cached it yet + if (block.inputs && block.inputs.COUNT) { + const inputBlockId = block.inputs.COUNT.block; + const rawValue = util.runtime.getScriptRuntimePosition(target, inputBlockId); + classs = rawValue; + } + } + + // Traverse upwards to see if it's nested inside secondary loops or conditionals + currentBlockId = block.parent; + } + } + + // return 'Not inside a custom loop!'; + + +} catch (error) { +} + + if(yes){ + console.log("classs", classs) + this.setTempVar(args, util, `class${classs}${args.VAR}`, args.VALUE) + + } else {throw new Error("Class vars must be set inside of a Class: loop")} + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + setclasslist(args, util){if(this.check(args, util)){ + let yes + let loopBlock + let classs + try{ + loopBlock = this._getParentLoopBlock(util); + + if (loopBlock.opcode === "scrtwpmrunpy_setclass" || loopBlock.opcode === "scrtwpmrunpy_deffuncclass") { + yes = true + } else { + yes = false + } + console.log("yes", yes) + + // Check the execution thread history array to verify the active block trail + const blockStack = util.thread.stack; + const target = util.target; + const blocks = target.blocks; + + for (let i = 0; i < blockStack.length; i++) { + let currentBlockId = blockStack[i]; + + while (currentBlockId) { + const block = blocks.getBlock(currentBlockId); + if (!block) break; + + // If the thread actively contains our specific custom loop block opcode + if (block.opcode === 'scrtwpmrunpy_setclass') { + // Safely fall back to the thread property we cached during the loop step execution + if (util.thread.lastClassName !== undefined) { + classs = util.thread.lastClassName; + } + + // Fallback: directly parse the static input text if the thread hasn't cached it yet + if (block.inputs && block.inputs.COUNT) { + const inputBlockId = block.inputs.COUNT.block; + const rawValue = util.runtime.getScriptRuntimePosition(target, inputBlockId); + classs = rawValue; + } + } + + // Traverse upwards to see if it's nested inside secondary loops or conditionals + currentBlockId = block.parent; + } + } + + // return 'Not inside a custom loop!'; + + +} catch (error) { +} + + if(yes){ + + const prefix = "ARG"; + let string = "["; + for (let i = 0; prefix + i in args; i++) { + string += '"' + string += Scratch.Cast.toString(args[prefix + i]); + string += '"' + string += "," + } + string = string.slice(0, -1) + string += "]" + + console.log("classs", classs) + this.setTempVar(args, util, `class${classs}${args.NAME}`, string) + + } else {throw new Error("Class vars must be set inside of a Class: loop")} + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + instantiate(args, util){if(this.check(args, util)){ + // return(this.getTempVar(args, util, `class${args.CLASS}${args.ARGS}`)) + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + getfromclass(args, util){if(this.check(args, util)){ + return(this.getTempVar(args, util, `class${args.CLASS}${args.VAR}`)) + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + async runTimeOp(args, util) {if(this.check(args, util)){ + const op = args.OPERATION; + const argInput = args.ARG.trim(); + + // Helper to parse input into a numeric timestamp or default to current time + const getTimestamp = (input) => { + if (!input) return Date.now() / 1000; + const num = Number(input); + return isNaN(num) ? Date.now() / 1000 : num; + }; + + try { + switch (op) { + case 'time': + // Returns current Unix timestamp in seconds + return Date.now() / 1000; + + case 'ctime': { + // Converts seconds since epoch into a readable string format + const secs = getTimestamp(argInput); + return new Date(secs * 1000).toString(); + } + + case 'sleep': { + // Pauses the execution thread for X seconds + const seconds = Number(argInput) || 0; + await new Promise(resolve => setTimeout(resolve, seconds * 1000)); + return ''; + } + + case 'gmtime': { + // Returns structured UTC time components as a JSON string matching Python struct_time + const secs = getTimestamp(argInput); + const d = new Date(secs * 1000); + return JSON.stringify({ + tm_year: d.getUTCFullYear(), + tm_mon: d.getUTCMonth() + 1, + tm_mday: d.getUTCDate(), + tm_hour: d.getUTCHours(), + tm_min: d.getUTCMinutes(), + tm_sec: d.getUTCSeconds(), + tm_wday: (d.getUTCDay() + 6) % 7, // Python starts Monday (0) to Sunday (6) + tm_yday: Math.floor((d - new Date(d.getUTCFullYear(), 0, 0)) / 86400000), + tm_isdst: 0 + }); + } + + case 'localtime': { + // Returns structured local time components as a JSON string matching Python struct_time + const secs = getTimestamp(argInput); + const d = new Date(secs * 1000); + return JSON.stringify({ + tm_year: d.getFullYear(), + tm_mon: d.getMonth() + 1, + tm_mday: d.getDate(), + tm_hour: d.getHours(), + tm_min: d.getMinutes(), + tm_sec: d.getSeconds(), + tm_wday: (d.getDay() + 6) % 7, + tm_yday: Math.floor((d - new Date(d.getFullYear(), 0, 0)) / 86400000), + tm_isdst: -1 + }); + } + + case 'asctime': { + // Formats a JSON struct_time string back into a uniform text timestamp + try { + const parsed = JSON.parse(argInput); + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + const dayStr = days[parsed.tm_wday] || 'Mon'; + const monStr = months[parsed.tm_mon - 1] || 'Jan'; + const pad = (num) => String(num).padStart(2, '0'); + + return `${dayStr} ${monStr} ${pad(parsed.tm_mday)} ${pad(parsed.tm_hour)}:${pad(parsed.tm_min)}:${pad(parsed.tm_sec)} ${parsed.tm_year}`; + } catch (e) { + return new Date().toString(); // Fallback if input object is invalid + } + } + + default: + return 'Unknown operation'; + } + } catch (err) { + return `Error: ${err.message}`; + } + + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + templatefunc(args, util){if(this.check(args, util)){ + + } else {throw new Error("Block must be under the When Python Code Starts event")}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getCurrentMutation(args, util) { + // In the interpreter, args.mutation exists (thanks FurryR for notifying me about this, yes that's their username), + // and in the compiler, util.thread.peekStack() works for reporters + return ( + args.mutation || + util.target.blocks.getBlock(util.thread.peekStack())?.mutation || + Scratch.vm.runtime.flyoutBlocks.getBlock(util.thread.peekStack()) + ?.mutation + ); + } + + + } + + + // Based on https://github.com/Xeltalliv/extensions/blob/examples/examples/extension-colors.js + // Add `mutator` +// const runtime = Scratch.vm.runtime; + // @ts-ignore + const cbfsb = runtime._convertBlockForScratchBlocks.bind(runtime); + // @ts-ignore + runtime._convertBlockForScratchBlocks = function (blockInfo, categoryInfo) { + const res = cbfsb(blockInfo, categoryInfo); + if (blockInfo.mutator) { + res.json.mutator = blockInfo.mutator; + } + return res; + }; + + function patchSB() { + // @ts-ignore + const ScratchBlocks = window?.ScratchBlocks; + if (!ScratchBlocks) return; + + Scratch.vm.removeListener("EXTENSION_ADDED", patchSB); + Scratch.vm.removeListener("BLOCKSINFO_UPDATE", patchSB); + + const leftArrowIcon = + `data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNDIuMTUzODUiIGhlaWdodD0iMTQwIiB2aWV3Qm94PSIwLDAsMTQyLjE1Mzg1LDE0MCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2OC45MjMwNywtMTEwKSI+PGcgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjAiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTE2OC45MjMwOCwyNTB2LTE0MGgxNDIuMTUzODV2MTQweiIgZmlsbC1vcGFjaXR5PSIwLjAxNTY5IiBmaWxsPSIjZmZmZmZmIi8+PHBhdGggZD0iTTE5Mi42NTUyOSwxOTZjLTkuMzg4ODQsMCAtMTcsLTcuMTYzNDQgLTE3LC0xNmMwLC04LjgzNjU2IDcuNjExMTYsLTE2IDE3LC0xNmg5NC42ODk0NmM5LjM4ODg0LDAgMTcsNy4xNjM0NCAxNywxNmMwLDguODM2NTYgLTcuNjExMTYsMTYgLTE3LDE2eiIgZmlsbD0iI2ZmZmZmZiIvPjwvZz48L2c+PC9zdmc+` + const rightArrowIcon = + `data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNjgiIGhlaWdodD0iMTU2IiB2aWV3Qm94PSIwLDAsMTY4LDE1NiI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE1NiwtMTAyKSI+PGcgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjAiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTE5Mi42NTUyNywxOTZjLTkuMzg4ODQsMCAtMTcsLTcuMTYzNDQgLTE3LC0xNnYwYzAsLTguODM2NTYgNy42MTExNiwtMTYgMTcsLTE2aDk0LjY4OTQ2YzkuMzg4ODQsMCAxNyw3LjE2MzQ0IDE3LDE2djBjMCw4LjgzNjU2IC03LjYxMTE2LDE2IC0xNywxNnoiIGZpbGw9IiNmZmZmZmYiLz48cGF0aCBkPSJNMjQwLDI0NC4zNTEyN2MtOS42OTA4NiwwIC0xNy41NDY4NiwtOC4yODI3MyAtMTcuNTQ2ODYsLTE4LjV2LTkxLjcwMjU0YzAsLTEwLjIxNzI3IDcuODU2LC0xOC41IDE3LjU0Njg2LC0xOC41djBjOS42OTA4NiwwIDE3LjU0Njg2LDguMjgyNzMgMTcuNTQ2ODYsMTguNXY5MS43MDI1NGMwLDEwLjIxNzI3IC03Ljg1NiwxOC41IC0xNy41NDY4NiwxOC41eiIgZmlsbD0iI2ZmZmZmZiIvPjxwYXRoIGQ9Ik0xNTYsMjU4di0xNTZoMTY4djE1NnoiIGZpbGwtb3BhY2l0eT0iMC4wMTU2OSIgZmlsbD0iI2ZmZmZmZiIvPjwvZz48L2c+PC9zdmc+` + const arrowWidth = 16; + const arrowHeight = 32; + + class FieldImageButton extends ScratchBlocks.FieldImage { + constructor(src, width, height, callback, opt_alt, flip_rtl, noPadding) { + super(src, width, height, opt_alt, flip_rtl); + this._callback = callback.bind(this); + this.noPadding = noPadding; + } + init() { + if (this.fieldGroup_) { + // Image has already been initialized once. + return; + } + super.init(); + this.mouseDownWrapper_ = ScratchBlocks.bindEventWithChecks_( + this.getSvgRoot(), + "mousedown", + this, + this.onMouseDown_ + ); + this.getSvgRoot().style.cursor = "pointer"; + } + showEditor_() { + if (this._callback) { + this._callback(); + } + } + getSize() { + if (!this.size_.width) { + this.render_(); + } + if (!this.noPadding) return this.size_; + return new this.size_.constructor( + Math.max(1, this.size_.width - ScratchBlocks.BlockSvg.SEP_SPACE_X), + this.size_.height + ); + } + EDITABLE = true; + } + + // heavily based on scratch-blocks' procedures code + // https://github.com/TurboWarp/scratch-blocks + ScratchBlocks.Extensions.registerMutator( + "scrtwpmrunpyextender", + { + domToMutation(xmlElement) { + this.inputCount = Math.floor( + Number(xmlElement.getAttribute("inputcount")) + ); + this.inputCount = Math.min( + Math.max(this.minInputs, this.inputCount), + this.maxInputs + ); + if (isNaN(this.inputCount) || !Number.isFinite(this.inputCount)) + this.inputCount = this.minInputs; + this.prevInputCount = this.inputCount; + + this.branchCount = 1 + this.inputCount + (this.extendableDefsEnd.length > 0 ? 1 : 0); + // HACK: fixes alt+drag duplicate not adding blocks inside + this.updateDisplay_(true); + }, + mutationToDom() { + const container = document.createElement("mutation"); + container.setAttribute("inputcount", this.inputCount.toString()); + return container; + }, + + isExtendableInput(input) { + return ( + input.name.startsWith("ARROW_") || + this.extendableDefs.some((def) => input.name.startsWith(def.id)) || + this.extendableDefsStart.some((def) => + input.name.startsWith(def.id) + ) || + this.extendableDefsEnd.some((def) => input.name.startsWith(def.id)) + ); + }, + + // Disconnects all blocks in extendable inputs and returns them. + disconnectOldBlocks_() { + const connectionMap = {}; + const hasEndBlocks = this.extendableDefsEnd.length > 0; + const hasStartBlocks = this.extendableDefsStart.length > 0; + const prevEndIndex = + this.prevInputCount + (this.extendableDefsStart.length > 0); + + // Reattach end blocks when inputs are added/removed + const reattachMap = Object.create(null); + if (hasEndBlocks) { + for (const def of this.extendableDefsEnd) { + const input = this.getInput( + this.getExtendableInput(def.id, prevEndIndex) + ); + if (input && input.connection) { + reattachMap[input.name] = def.id; + } + } + } + + for (const input of this.inputList) { + if (input.connection && this.isExtendableInput(input)) { + const target = input.connection.targetBlock(); + const saveInfo = { + shadow: input.connection.getShadowDom(), + block: target, + }; + + let name = input.name; + if (reattachMap[name]) { + name = this.getExtendableInput( + reattachMap[name], + this.inputCount + hasStartBlocks + ); + if (connectionMap[name]) { + connectionMap["$UNUSED" + name] = connectionMap[name]; + delete connectionMap[name]; + } + } + + if (connectionMap[name]) { + connectionMap["$UNUSED" + name] = saveInfo; + } else { + connectionMap[name] = saveInfo; + } + + // Remove the shadow DOM, then disconnect the block. Otherwise a shadow + // block will respawn instantly, and we'd have to remove it when we remove + // the input. + input.connection.setShadowDom(null); + if (target) { + input.connection.disconnect(); + } + } + } + return connectionMap; + }, + + removeAllInputs_() { + this.inputList = this.inputList.filter((input) => { + if ( + this.isExtendableInput(input) || + (input.type === ScratchBlocks.DUMMY_INPUT && this.clearLabels) + ) { + input.dispose(); + return false; + } + return true; + }); + }, + + // Creates a shadow input for an extendable definition. + attachShadow_(input, def) { + if (!def.shadowType) return; + ScratchBlocks.Events.disable(); + let newBlock; + try { + newBlock = this.workspace.newBlock(def.shadowType); + newBlock.setFieldValue(def.shadowDefault, def.shadowField); + newBlock.setShadow(true); + if (!this.isInsertionMarker()) { + newBlock.initSvg(); + newBlock.render(false); + } + } finally { + ScratchBlocks.Events.enable(); + } + if (ScratchBlocks.Events.isEnabled()) { + ScratchBlocks.Events.fire( + new ScratchBlocks.Events.BlockCreate(newBlock) + ); + } + if (newBlock.outputConnection) + newBlock.outputConnection.connect(input.connection); + else newBlock.previousConnection.connect(input.connection); + }, + buildShadowDom_(def) { + const shadowDom = document.createElement("shadow"); + shadowDom.setAttribute("type", def.shadowType); + const fieldDom = document.createElement("field", null); + fieldDom.setAttribute("name", def.shadowField); + shadowDom.appendChild(fieldDom); + return shadowDom; + }, + + // Populates an argument. + // Puts existing blocks back in or creates new ones. + populateArgument_(connectionMap, id, input, def) { + let oldBlock = null; + let oldShadow = null; + + if (connectionMap && id in connectionMap) { + const saveInfo = connectionMap[id]; + oldBlock = saveInfo["block"]; + oldShadow = saveInfo["shadow"]; + } + + if (connectionMap && oldBlock) { + // Reattach the old block and shadow DOM. + connectionMap[id] = null; + if (oldBlock.outputConnection) + oldBlock.outputConnection.connect(input.connection); + else oldBlock.previousConnection.connect(input.connection); + if (def.shadowType) { + const shadowDom = oldShadow || this.buildShadowDom_(def); + input.connection.setShadowDom(shadowDom); + } + } else { + this.attachShadow_(input, def); + } + }, + + // Removes unused inputs from the VM + cleanInputs() { + const target = Scratch.vm.editingTarget; + if (!target) return; + const blocks = this.isInFlyout + ? Scratch.vm.runtime.flyoutBlocks + : target.blocks; + const vmBlock = blocks.getBlock(this.id); + if (!vmBlock) return; + + const usedInputs = new Set(this.inputList.map((i) => i?.name)); + + const inputs = vmBlock.inputs; + for (const name of Object.keys(inputs)) { + const input = inputs[name]; + if (!usedInputs.has(name)) { + // @ts-ignore + blocks.deleteBlock(input.block); + // @ts-ignore + blocks.deleteBlock(input.shadow); + delete inputs[name]; + } + } + }, + + // Gets an argument name for a prefix + index. + getExtendableInput(prefix, index) { + let id = prefix; + // Special handling for substacks, + // as their names matter for execution + if (prefix === "SUBSTACK") { + index += 1; + if (index > 1) id += index; + } else { + id += index; + } + return id; + }, + + + // The internal create input function. + addInput_(def, i, connectionMap = null) { + const id = this.getExtendableInput(def.id, i); + const input = this.appendInput_(def.type, id); + if (def.type === ScratchBlocks.DUMMY_INPUT) { + input.appendField(def.check); + } else { + if (def.check) { + input.setCheck(def.check); + } + this.populateArgument_(connectionMap, id, input, def); + } + }, + + // The "user create input" function. + insertInput() { + ScratchBlocks.Events.setGroup(true); + const oldMutation = ScratchBlocks.Xml.domToText(this.mutationToDom()); + this.inputCount++; + this.branchCount = 1 + this.inputCount + (this.extendableDefsEnd.length > 0 ? 1 : 0) + + this.updateDisplay_(); + + // i have no idea if this is the correct way or not + const newMutation = ScratchBlocks.Xml.domToText(this.mutationToDom()); + const ev = new ScratchBlocks.Events.BlockChange( + this, + "mutation", + null, + oldMutation, + newMutation + ); + ScratchBlocks.Events.fire(ev); + ScratchBlocks.Events.setGroup(false); + }, + // The "user delete input" function. + deleteInput() { + ScratchBlocks.Events.setGroup(true); + const oldMutation = ScratchBlocks.Xml.domToText(this.mutationToDom()); + this.inputCount--; + + this.branchCount = 1 + this.inputCount + (this.extendableDefsEnd.length > 0 ? 1 : 0); + const plusInputs = this.extendableDefsStart.length > 0 ? 1 : 0; + + for (const def of this.extendableDefs) { + this.removeInput( + this.getExtendableInput(def.id, this.inputCount + plusInputs) + ); + } + this.updateDisplay_(); + + const newMutation = ScratchBlocks.Xml.domToText(this.mutationToDom()); + const ev = new ScratchBlocks.Events.BlockChange( + this, + "mutation", + null, + oldMutation, + newMutation + ); + ScratchBlocks.Events.fire(ev); + ScratchBlocks.Events.setGroup(false); + + this.cleanInputs(); + }, + + createAllInputs_(connectionMap) { + let index = 0; + if (this.extendableDefsStart.length > 0) { + for (const def of this.extendableDefsStart) + this.addInput_(def, index, connectionMap); + index++; + } + for (let i = 0; i < this.inputCount; i++) { + for (const def of this.extendableDefs) + this.addInput_(def, index, connectionMap); + index++; + } + return index; + }, + + addArrowButtons_() { + if (this.inputCount > this.minInputs) { + const leftInput = this.appendDummyInput("ARROW_LEFT"); + const leftArrow = new FieldImageButton( + leftArrowIcon, + arrowWidth, + arrowHeight, + function () { + this.sourceBlock_.deleteInput(); + }, + Scratch.translate({ + default: "Remove input", + description: + "Alt text for the button that removes an input on blocks", + }), + true, + this.inputCount < this.maxInputs + ); + leftInput.appendField(leftArrow); + } + if (this.inputCount < this.maxInputs) { + const rightInput = this.appendDummyInput("ARROW_RIGHT"); + const rightArrow = new FieldImageButton( + rightArrowIcon, + arrowWidth, + arrowHeight, + function () { + this.sourceBlock_.insertInput(); + }, + Scratch.translate({ + default: "Add input", + description: + "Alt text for the button that adds an input on blocks", + }), + true, + false + ); + rightInput.appendField(rightArrow); + } + }, + + // Updates this block's inputs. + updateDisplay_(force) { + if (!this.isInsertionMarker() && !force && this.workspace?.currentGesture_?.isDraggingBlock_ && this.workspace?.currentGesture_?.targetBlock_.type === this.type) + return; + + const wasRendered = this.rendered; + if (this.isInFlyout) ScratchBlocks.Events.disable(); + + this.rendered = false; + this.extendableUpdatedDisplay = true; + + const connectionMap = this.disconnectOldBlocks_(); + this.removeAllInputs_(); + + let index = this.createAllInputs_(connectionMap); + this.addArrowButtons_(); + + if (this.extendableDefsEnd) { + for (const def of this.extendableDefsEnd) { + this.addInput_(def, index, connectionMap); + } + } + + // ========================================== + // CRITICAL FIX: FORCIBLY SYNC THE VM BLOCK DATA + // ========================================== + if (!this.isInsertionMarker()) { + const target = Scratch.vm.editingTarget; + if (target) { + // 1. Calculate actual branches: Base If (1) + User Added Slots (inputCount) + Fallback Else (1) + const computedBranches = 1 + this.inputCount + (this.extendableDefsEnd.length > 0 ? 1 : 0); + + // 2. Fetch the block instance directly from the VM repository + const blocksRepository = this.isInFlyout ? Scratch.vm.runtime.flyoutBlocks : target.blocks; + const vmBlockInstance = blocksRepository.getBlock(this.id); + + if (vmBlockInstance) { + // 3. Directly assign the updated branch bounds onto the block instance! + vmBlockInstance.branchCount = computedBranches; + + // (Optional) Keep a record on the Blockly block state for security + this.branchCount = computedBranches; + } + } + } + // ========================================== + + this.rendered = wasRendered; + if (this.isInFlyout) ScratchBlocks.Events.enable(); + + if (this.rendered && !this.isInsertionMarker()) { + this.initSvg(); + this.render(); + } + } + }, + function () { + // An array of extendable input definitions; + // for each click of the right arrow button, + // all of these inputs will be added + this.extendableDefs = []; + // Inputs to put before any extendable inputs. + // If non-empty, also increases the maximum index by one + this.extendableDefsStart = []; + // Inputs to put after the extendable inputs (after the arrow buttons). + // If non-empty, also increases the maximum index by one + this.extendableDefsEnd = []; + // The default number of inputs. + this.inputCount = 2; + // The minimum number of inputs. + this.minInputs = 1; + // The maximum number of inputs. + this.maxInputs = Infinity; + // If true, clears all blockInfo labels. + this.clearLabels = false; + + // Internal. + this.prevInputCount = this.inputCount; + } + ); + + const createInput = ( + type, // ScratchBlocks.INPUT_VALUE, NEXT_STATEMENT or DUMMY_INPUT + id, // The argument ID (a number will be appended to this) + check = null, // null or "Boolean" (or the label text for DUMMY_INPUTs) + shadowType = undefined, // The type of shadow block (or falsy for none) + shadowField = undefined, // The field to use in the shadow block + shadowDefault = undefined // The default shadow block value + ) => ({ type, id, check, shadowType, shadowField, shadowDefault }); + + // Configuration extensions + ScratchBlocks.Extensions.register("scrtwpmrunpyextender_clear", function () { + this.clearLabels = true; + }); + + ScratchBlocks.Extensions.register("scrtwpmrunpyextender_argsreporter", function () { + this.extendableDefs = [ + createInput(ScratchBlocks.INPUT_VALUE, "ARG", null, "text", "TEXT", "args"), + createInput(ScratchBlocks.DUMMY_INPUT, "WORD", ",") + ]; + + this.inputCount = 1; + + }); + ScratchBlocks.Extensions.register("scrtwpmrunpyextender_string", function () { + this.extendableDefs = [ + createInput(ScratchBlocks.INPUT_VALUE, "ARG", null, "text", "TEXT", "item"), + createInput(ScratchBlocks.DUMMY_INPUT, "WORD", ",") + ]; + + this.extendableDefsEnd = [ + createInput(ScratchBlocks.DUMMY_INPUT, "OLDWORD", "]") + ] + this.inputCount = 1; + + }); + ScratchBlocks.Extensions.register("scrtwpmrunpyextender_fstring", function () { + this.extendableDefs = [ + createInput(ScratchBlocks.INPUT_VALUE, "ARG", null, "text", "TEXT", " 3.14"), + ]; + + this.inputCount = 1; + + }); + ScratchBlocks.Extensions.register("scrtwpmrunpyextender_class", function () { + this.extendableDefs = [ + createInput(ScratchBlocks.INPUT_VALUE, "ARG", null, "text", "TEXT", "item"), + createInput(ScratchBlocks.DUMMY_INPUT, "WORD", ",") + ]; + + this.extendableDefsEnd = [ + createInput(ScratchBlocks.DUMMY_INPUT, "OLDWORD", "] in class") + ] + this.inputCount = 1; + + }); + + // HACK: fixes the flyout, also with dynamic enable/disable addons + const ogInitSvg = ScratchBlocks.BlockSvg.prototype.initSvg; + ScratchBlocks.BlockSvg.prototype.initSvg = function () { + if (this.getExtendableInput && !this.extendableUpdatedDisplay) { + this.updateDisplay_(); + } + return ogInitSvg.call(this); + }; + } + + // https://github.com/LilyMakesThings/extensions/blob/5b9ce572683e403933cab3b23c4a9bbb2a08ecf9/extensions/Lily/Dictionaries.js#L37C1-L45 + if (!("scaffolding" in window)) { + Scratch.vm.on("EXTENSION_ADDED", patchSB); + Scratch.vm.on("BLOCKSINFO_UPDATE", patchSB); + } + Scratch.extensions.register(new RunPython(Scratch.runtime)); + })(Scratch); + + From 51d7f58c258229777e3ad07586824502cdbe707b Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 13:45:08 -0400 Subject: [PATCH 02/15] Create ed.cd --- static/images/ScrTwPm/ed.cd | 1 + 1 file changed, 1 insertion(+) create mode 100644 static/images/ScrTwPm/ed.cd diff --git a/static/images/ScrTwPm/ed.cd b/static/images/ScrTwPm/ed.cd new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/static/images/ScrTwPm/ed.cd @@ -0,0 +1 @@ + From 298a591104dd266adbf0a24aca9116d4a69236c0 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 13:45:23 -0400 Subject: [PATCH 03/15] Add files via upload --- static/images/ScrTwPm/runpython.png | Bin 0 -> 31312 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/ScrTwPm/runpython.png diff --git a/static/images/ScrTwPm/runpython.png b/static/images/ScrTwPm/runpython.png new file mode 100644 index 0000000000000000000000000000000000000000..262827f6c68a125924686ea4c42c83880762801c GIT binary patch literal 31312 zcmX6^bzD>b`$h878z~`dATk;el!gr$NFyE6jVMZYjcz0bg;CO-N=PXsEh*hC-SFGz z`}=3FZLinvec$JO&Uwyr?p=hssyrzXJrNcb7U@d`*;iOt*m_u454Ztr%s25S2JKi_ zUbHV|;aVOuyPq62$MmRvT@qb;k;>t1Nf$Xbyqw4C?&Hn9UL30&4=u>osZ`_OvHCnh z_h?$*dQMwMzREO1GGO2D_wB-Jo|OO(kq*z=;LZBL%MY4w$7XN5R3Bda6npV7n&=sL z=@lh))gzZp%N!>6=BxCL?6aFc>QZL)w^ij@K5%@#B4!q^3oJ8yS-`w1;ufTOX^AV_ zTCV!|#_v>a@=pL;$Gj0K;~?!iYY2gsfW9=ipwp({Z9^Kq8D$`vA6wb=qFf~C>94g$ z3!1-lSCUUpa2lgzC6cCnOUgONQQC<+{um7mYn6brmLFC?v`@~kfdCB0f z%dfj{4;PNO+m6^4E)v={0&kcgnAP5shjp~lXV!E0r~ zeKK{q(>aoH&S%6$a-aA4d!ndVL$sZ#KpNAN3Bf85??7Z z>JbVHJL++AFWzc!`$Nv>Fq_pY`p1IhH~Yq3>rL0A-;e0Mj=%Q%m2s|}uhpG@So&-< zB^9nUl`BqumiGrKw*A~P2U_!+K0e|Ko|m~l^dtA34_8HV1nrlL)dH43@;o;sn84pv>uF<+a0?{~8mTNHvRVN;N1_Ya3AO&3grvlg2w1Alnz zAB_}08lH_8Dsm~{wzqm&qL7xMlk6)$$f zRO3mNRT9|=pgwOp8aFmc()caMN}cDs-5RJE&3&9S{eG{$@e+@MNhD5#{ljzm6yhC< zoTo^{TVxr&l3ClMdQJH^XgW)j*wIj$0n()-q0SU3{$KN#)(G?ACYFsi~2w0Gu#2`_$XFleGZ00sDW96kQ}NaNajcG(ou3bhe`ZzADal(R{HbyD!CXz%a(v7&*@; zLHKW|P^?VDPvx>>`=gSm?Ylr^f{dSs>UvsC++RKSB7d)sY+$)B8pjXP)CLZ~B`Rkx zJ;GINm1PEA$O@Qr9x-P}f|}Oa23cyK_xe0W-1391U!suQV^7ROKjkZc()U1nX+eLf z$9VFT^`A!#4j7s&XC2PQ4VkE9+|xWB5X{JzDDV98`C%Ktjz3SoRu+0^XEse8?qd*$ zeQ$GM6r5LBE-a~SKusyNYG|IEhaKM2k7u@Lrms`-io?X}M|L3Tt4`;IXT-;Pk0bFT zmT}|=J6dhdp4M=)^bk-cXOBGoY5U4bG5AS4?MQeZE70T3f8XR=;Jq;+C^0u%GWXqP z$K>mNG^@6t`@y@rjZAModL(klK%@5G2nX7Ye`sNt_@4cZ35D=Ds8Ci~5*jEid+7Hy z6p1jE1VvWu16%n};H3cY`w^NBAPnOPNAP?L!j5mZ&L>OAqXZv>VRb)WRb}V(*gQ%% zt0SeR*4!#0YxnHl`Lp=C0y~plq79qr@|pf6gW32k4(mIj*ev(^)o(g%L>rSO;Xzpe z%!J&y_X61Spg-q$*kHVS7mz)l3LKUrTA4uKJtYZ5_=kT3P%PNq;dDe4_du9R@jo&r zz|y$RV~)Cp{-46(V+nK8uxgJMPAw9|7u;aYrv4Kj>cCuUR1}RAem&uR!{v76pj4&$ zqPnTtN)8s31*#sG$jVKE>W6)KAw>T|HO2tXtmFY;s2~7Cl((UQUgmYVE5 zUX~}({9o9+gk7pGvVzaSUe@M^maYa(Kvy|dt#|ZQZ*>H9TdRReAW{?Cm5%Vvm6hTK zrU;=*l(yv(Qmp<=Pa5&Qr=B&>77IbK#2-E-i?1R38*(L6=J6F)iEpSlY}wE4QsU8K zF29)`(M_+ws;3j{c%aK`lo2cqBf85^s=TT;49OXkqIZE(06Zdy4S3<29GLWz)f-_1 zCu3i1JWB!}rSo6te37FTE?HDb)}omnm@Qt|$TIu0~i^R9+l!#;I9}T1$LYWm3{&!YOr+qu-prfv%FCRFB$%79A36k~r@}TOn(GL)^ zgs6hp%6=ALZI6(xQ*uD-@@sWG%ytui(xKw>#|sZ7 zJMJcd&pY(V@NoG2cp)#sg2$XS=O@A?oh3H?4;IZ4G{4!Hlx{4AT7fpOvmi(k80iUK-$3m5vm(E`-?lj-)*MWR+PAP9%|r zO#&f&R5z5{IEWcdcsC*_M`}vE^@fSD_h)PTn(I@b>CyBv3SwTuvKz|viqjY3MVj#} zf${jBkD#l~tV!f^Ptqp=E0AageH|I1%bE~H)k|+*^>l@}UzhBAsD95*azi&{HDZl! z&v{P3?ClxwQIHVUxfYmWw{1i@|6jQJ|7t< z4Gy7dmjl0ma-!F(q`qsG&>r0(j4Q<1=H+`R`!%->=^6cAuO(gKN~f*h9S8G!GRYUp zo>-^1z5)W9AdhJb6k-w?OTeNBKn#4!5(_a$yUk7VJf@YcbiZiv$t&ZEW*9!SpJB8- z@kdHKA@M|xir@=YA`s0sK%HYc%KTC_=E>LY#IX=Xi(b2t{i5YAjc>loK1CyB%URAg zF_^>dz35jj&{B-CmWz!yuPkcx>g-`C*5F&k<>-&XfaC@DJB9Bm*c-M{54Y4F+w0_eDeuw@_-b_BznNS_yj8D!0eF{oU`I zm$yuRd6@Tot>0{kgV)`|nTx-!?v9tLDjf}#bsly$`bo;@p9nhCdXWY(1ch(TjE)&E zBtg8gcuVD}A&f9L>%S0jcj!h4!MBh8Q{{M*ibiD5LS=LA8_i|$%mX1SyRgwpxrSh{ zimCA(J(@z5gGI=IUWuA-2OE2%bb93WhT)z6#pRL(dt8>N^Aq3w-KGlFEERvxqmy>N z*tWAXd4~EHgAc!~7jh;>6?9*s^b~|iGJ@4*(fPL};Lt1Y>S&Fc=s;GggA6eQHZ<|SkHfpR4~G`%&9++oQ7nm;AfgE825q72iiF{>aXW577nI=eBPg`;-*{mZTtNWKESEoIT&UBX=E!Re{5~e0Jw-lA zLeYA^FdfWRPnQ#2m`R`PfQk&yulm9~?(&1Um`a@MA~cYWVC&hI3w^Nr**;%ve!;lA za!f$b>&WhL7%7oT%qNopB}i#Q2E9;g9H@E?=%qqTSrw)u<~1=w!rld0eYxmnMT}Zo zSuaOsL=#Dg2yq6-C+7Gy)9v#b&tLERlN0i#rGWH=iQRAaUcae=Mdc_1U_hnZED@gO zU8>yjn;tC!1mhx}f`}B)!HJJSi6r)@0?4@XQr9IpOWf~djJ!1Gpxv%zvzfCuW}R}WNoLml-BxR8zs^?Un7t*Eus8uo=7SV37tm2}yRn@wNt zxG2VdDnP!Z06v$3C(hZ0W_H!>weX=VXp%BJsXI|6m zbh4k*7LGXz7KYVQ6}3TQd7qGNwsb-S=y0~G@hSC9N3A!g={+#J{t+~MGwg_so*TE#O^jf;g^LDg^~Z`zRCu&3pB87IMm~c2 z|7Ls~r!@-~78ws6FT_RUlAb!DtVktVBR!U+%wx^?@;$HEm!2Z80PfXw_V{ooc~Be`%%rt zO(rCWl<#cZcw{rJZ+;5=AtM3<9NVLsxtbP5FO|MmnAADmFY!I#FVY-$b8|Vr&bs29 z`D!CQWmg}^4;R3K8WUmI{7|zd+Erd*imTK?i{MNAxR9d2g8DlWgskn!08pKaIm2}~ z#Ing?CWbO~fkKlQmB^YG=u@ry!9Zq++(8VH;l?E5H+8xJX=1r{pXmBct}we))glk56tf9p@J>wjhy1H zme;X?|1ya>g5_;W=XGQu+^fTmvqBxh3FISopjS!Kg@;36P>zZ9Qm3BbMYfP}#or1v ze#bjE6V)HYpZf-Hl0OreUGh;=g8GU^%!D2#6ukbWDx6L~cfGYVYH~s|$&Q5}X7AUV zlZORY{+{m@plMJOCPd_yzduU+6?*ScHN(gHo6EQrv&LUu)BzsXxc;3Vu37&`3)lN3 zLxHt%m{32}ANRQr3u9BVo2E4bV$6Q)C6Q`z>NU|H8MZM_44)@=VfBpgDK7|B7o%A` zK`y_tSw{yts^F~9eLJD#v~Q1Xrd$lBXR6YnP+zel4JK z$p{M_&kI4m)gr|4R6m831((k zq)V`m-G;QmQ}g$55#xmOVh;=J&j0(>UnONSasRsJxV6r zCD47{@@eD&>&aA?xo-?4+8W=!e?XQMU-hiQHxQJas7e=jhf_Ajq$SkZ?>REgiLO zxWFvsJ3&OeWnqJdCpGWCx~^@=j!Vf#!@I7cHzf%b>eq8eQQ)aa+osVAIicz{w&?SF zAebif9n*v!JJns`AQVnRtg4t65-+}$`AfpNl_@t!zzT2G^VeHtex-|-bH{IWj zwy!VBSk!NBj-{^s?|#G{=+-5J@mxWoZRq*G;Cg)*R9R>&f6s3LZkphDO;Y1{N6@S2 zC)6Yz9I}=FI{Gfk&LwU~84idY3U5zNgRX|qx$IxOw#sr8j;cR-FcSrN;v#BDPYDxG zNl(cFwZY*A!B&gLw+EHIG4u%m@QT^a5K{G zcCB~ljSYVGpF7?{Td)shs+_kkq@OfVejRMS?}b8~_Ho2~q70V)Db~sn?MApRysJcC(%g1(<9U;g(}D(2_BqM?4d)bA;awt|S#h#tHLVDNx2Dkl;hX#Ng9%Loo++T}cqjV?4E^C1B7smY zwFMICwOJ6_Kv}1OtsP^Drzfjx%v=L!&nDPRQCzQ!+QFia9hr}8;4RV^^uL?4yp7^# z=~AN(4hZKjhzRMWR|VEy=kBV1r9y;$LY?+!KM3~Ff-0Z0%p3G@_c(1Amxgo*((m;bG89QH&;UEPfEUvEm$hJ3!EngKm1jKJC*vdtnDBw{g;X)Ygt$0NgCRM z&3CJDfu()oJ!qiOQ%Db>+fjl({z+)&ZZA0YDtAEtZOY**^&uBRv%(;+gJYr{>WPx~ zJJj>6xQ9z6*~SG(L+BgL+#H_{QQkGmH*sn)20Q#t0oCg387Yi&3lQbu0{K;X^Y@zTVI=!8Wap?0qqOB%la zzKe9K{{8)J#cg)__x{=Aw#+xFN)5AHmqY1W{_6oq&Fik9$~ksrnmL^i&?NmSu1A;# zATd^1Sylf8B|zOVdbRc^ceSr!wa=ZN`RKy_hrp83hs$RXUQ?H9oJt0gN0;$FrV5>D zavk2R^m)dX)Jwj<>dm~O5Sx7bCPKtlK$V#%c|Hq?m7)YI@oo2pZ-|Yx_}Veb+qhr$ znfQ8*b>{05V;sdGChV5d}&ZwzMA2Lb=B3#xVtgJ?=V_yss zt-;tLh9^6S9u^|#I@dvvB;sVPaYlaA%fL~W5i0oFVcbvVR`Lpvm@DTuLC1mM;y7(A zrDD~a^=5M6GPw%uEcGn7w7MtKpt(+LZ!bN_B1`vK<%xz|`VZ%g!d!M!gzYRNdR8Ze zkg{5ue3v~bgnylDK(~>UGPWrS^?US|%ey*AEcxeLk1SyE4XyFDRjBV6Jj0DDJ$BLI zJl}Y;LZAOacR!&ItW6(?O!aHzH)$$l!6o))5}P#o9T{w8V`=Nt5>@om`%w|09EKW-wZ z5eUS^U8bjSq?EdAcnZj_Nh2iFgNzCB%QZE2Q^p0JMql=|T}IxAt)E4ZZ5dtsOkKgs zW>iTOi{&&ae%SllZR6lG-D}YocZKhcx-XB`@tU5iPsa}KA>SR03eS>=jWj)? zKW$!pwRCGc8xYx{qd8z(ben3gRo_+cwDP^3UtFnr-?MRs;%viRN_QcIuo}}RB52u= zDtTvwvS)?p)iwWpuH@12=TK4T0yhDQ!T(a5{i%O#*1P6!m)0v2S2I)0XM^kP<(wod zHEtU-Q?>O~%(WppX?&7VrU?)24ID@U#iHnW9$*jrz!DmfqcmSej%l$}DdJg&4h4;b zXpH6a%YeT>eaaidaR0zUsbZ+#(`NAZR*Mnu-P zwVH+O9@vWpQLa=M%b^oXLlv87(|XIiv>@_bUxZ__(mD#SsF4jL>#1ia=&0mZOg!kuSm&xzMCZ4Mo_!{~7HVdSjkbm5UNZt2Pa4Wjae{54b9#}cZOQu;6Ai)ylU*huXM3TZYWnq}_*K73}Ij{CnAEmG4*W;c-)t)03| zBBRox;#w@C;^8aF4G!b8A!}#6+(pv**yhs*`IFz(>K96X2uFFByyGT7ruGIMdb%;O z1%BbS9YVy|s#DZVD??+Ph9Ckj>6A1mdfz;O#dJ|*j?gLgM)xu(Yb1=N4DqAGC{NQ5 z>7MvRHb)6|NZhJPbqd;Dl2CeGxp{;Ves;gvm7)Z%7qQ@y$->^NpmvmKPs3ad)34ac zv!xRPGQ$KZ%}5jV2s%PGi=I=OJxsT7phlg#=$VjjWwgt04QDNwG`b$EEQFAm$gOHo zJ|72Y?ZcW6i*0|Fv>ec%aw5nz1=r}GRV}S;GWgoe-dUwz_I8Okfbs=ID6m9uxFdp?nsgD^y)YDbn(hkp)_ zTpHT4@lk=Y^Y-wPf!+_~Um!@T)gf=Y65-t%swi`H$rmy@-1%MqiBj)g$A?G1-|!~w zx5!d^)H*KidN-JfB?a7p&zOI3!@wp#nBm@tFom~RZ_^^;1?oQ62=ux|0tt}x-?*=G zB0=ekmV8IYC%nz-YNWqdv`h*&)V#s3`fSqTJr}rfGRwgSABj+swRf0lGStT=0P82|1vo8jCL7|LF6itAX4$7Hu9>by5r ze1TZ}Nt7C}D}{Zl-Fd7)@x>WjefE$Gyq@`s8aiX^^eHkvaGMIOit`Mt*eFkp*5j59 zM%mQc7lwlf^MDcSY-wk3PJ0nmQ0BgJj4Bp^d(08FLr+Hu@QhpPd!&;MxWga$C?~LY z^oNPmL^nbCg(`**wK;Qe&8rn{ur?~B4Y|4hPS3#4uXEH$+{VIRFehQqWkq_nH}>fp zR4=E~gNraTxS;Ge&JL&{ct#hMIYEtX0&=5Ef!tz1g|p*MTnAMQjO@#)wSGnZ=9}Xk zjvUqovjn!zO&O4GF$DVi6UM~7?hxFHK<+76OoYsPXhT+#k0XbzV}>se2}yU=P@k@e zJI-u*Zwx7>x3-sSe%g(apLuBYoE;Q%1x&sQ;o>94a$@E>Jx;FzlS(B7jcAwR ze`v^Us-vC?zTrYNY2tf1^CHeC0c~)xX0EZEEf->SkQ&;VJ~sG@XvUmY!R?0 z7oNnTRe(HaLp+vrH(*`02zf#cC4ASWnJZB$t!i@sPCSmZivgw2?^)A5u1UEj$tjD% z&Ee&qtTfhp+%S~1>#-J5spVf;i(Lhw3ZESrY`ynf41QO|mqZQZVQYhrZ0_X3U22gq zE`(|@U}4L}Qu~)9*oC*_rUG1yaUm?SGFYsf8$gr_>U$QwTsc%9H9eTU38gJ3qP_a{ zYsSYa;n{DOf*ghCgGncHHX zWRn!itp$2(y!(E+J;vcHNg_YwN6_M9*4Ym;qOKWkf7=t|aqDMQkDz0!v$fA0e=&a= zXnYy^X3#lH-uoTX!JnEi-~H|R??VIW&)g*rg`qpwv-cwR&SV4~?{z_E&JqYqyIMY3 zYdXJ4+o{*NypbR#ZaLrq5J@t0@Sw$Ha-MU@y_HOph2+p1H=R$F7sBH zcCY@e)IvqM=ZAxXH&3Isc`(VJwxnYUILqwAizn$6(y-@L5%*|be>N^LfqSk_x_y7* zZCaY2SdOBdw^_CyK2q?rY7AHUfBYCU{0K$z#u*Pi#VxaPhrfOKTi`iaet;7r8W5Q* ztUZi)N(V`5bD-+DMch`t0nM0<|9z9v9t;w|EsNCw)67Tw{WhPi{d006V>bouW#W0# zN?~ZDGTwt5GqBtEB*w=%W_?bdQW5^C9ir*wGWGC-Iw(`1Xu{MjU*l7Eq8n5%UT}Hr)IWc&gNZ7a89NZ!g!lN-+F3r&eR?gFVe^2{CfX zmV`+l&WTUO0Y^ssTzQ3*w>CJ49(=C&LHYUbQ+dfh1Ve`Qa-kk3JTKFTP|$mdJQ9J` zq^?29Y+A+yCtElK6DVbB{={E;$q0PDBF7BBQ1x+EWiiPIfzSIPROs1qjUxy)6#lsk z;J93zzQ&|Sf6LYK@XYuXbC*RBzb7UQ)K-$z)Smiquz>+6*KHFVu16*ui_S)d}3NpklzM261B1^j2g%vBVxW4+FS(KRyB-L)Mom=Czma8TY?MB% zBV!+O^W3e5#4p>Au}UB5XO|QRLIYKh$LY9EWH=pQ`k>K5DQSTDlK9lTCK|;AwKS%p zw`6t?^p`^u;UP{P!c&7EoBH@^Zs4t)JkEuHzyieNaF|)PV1@mK{8j@nc?H2ve z-uCM)u(?}I^}h-hs#N45q*e~?TltWA(_whzObav$S<`0&2!{`p?+P65fb;<_>2LQH<|&9_DQB;L8hM{EDN%& z|DawT%o|9SD{z7{Z;SC<)l^SL(u_;Jo_AM~x+S9yBJl>~Hviku+JG0PCDN-GU5(cl zT?`l04i_Sb%gBE@=aKR4L(ggkMB?CjdrkVG9Eik^_$4Jp3VBmzpMlJsMMRUfd0iJRJAusFpVYi>D|4|%CrTKfu}j?=UnxJl1db| znMbRhYK^Vk2JC5+fJitV`ZYb?#Yhh|0F{C0l=6tm3&(DP6S+HuW>PUkFe;>lAr{`G zh@>7#^9q;6Ek+Rw>J=Mr<=0LB`{=j85q$F( z6VxvjB;eIef6cWY2@(K@)Li-BWPfuqyskE=0o}!D9+Zf;%5Ms_DBscD`)hB08tS+g zhz_!BJ4>0{IscqL2VbXUiyS79=m_jK%DpMqXqESP^e)8{$0`LJ{I9VL2*EiO{9v2X z{x*f~{daY8uxU-SGn_~sM?S1Wh^zcufHYki)%!J1bxg8ONo z8i^pab&y{3$-;AtoIt4FrUXhSBf-unVYxIBfX}7`@Xkv`IufzqZ;OE;h5H$ zj+u+OhEIvip3|&dLtsqFNl*#QEt=N^qIR^dtt? zvh<}Q+fsK%Hv>o!Tv1feegvqR+pPPO4Wx^!L+OK3ZX(&S04@vl(q&&~McI_wX8Q~o z87d&2uS@jK?n8zTsM==Qn(nMrlhMzuU)ag1q3*2;M@Kx0GbVJ2Hl>qqMrXoX-vkRy zu9zDi=r(moNIMB5M|4QFOCT~?k{b1YvaeA7*X%kyG1MSJ!lUc%~*5&US$HPrO+@4%3{e(L(wrBF?$(^f|x_;26V6 znZxeLpG77vDm%I-=mw5|Fkq}yuQSUmhyZBtM#YZ>#t+BpqPS&%*SRDp!7)%0@*0nJ z8_y;U7lK-|&tzdVwDbv*bjsIQ3h_FBrd%<$I3U1H30fZFcb$fG&9UyH0@_%x&?6GB zK3$0BXKRAXBfINSw32>llYOEEC9oVNzP?>qSV9(gIh92<$^pi*Q?l zY_e+88WJ*8y-Je$@Ll+n(6$bJ{*s$7Huh7=s&TFfg%sir#sm64FKgnHk&|_qSK`1@ zkC@Bkpx;`A_pui7)eoUaY^YCdb1tp%On#YK=~pi6@oUaSwDiM^^_n@zUq34Tf-Jv& zd}uEt;0RUN$)`&}rs{ydr6NB=`TGIIT-%y~v==z1v~1jS`|Ftk#;Hh8(5TUg9=Yd8%L2M0v)KT%kG;+I$b^zKs8Z?Oy~@Kq)%F^AMPTQ3XSS)Zeq@A ztW3i=Xknu1HAD+4e|B8J!1F?TmP00pDS;e#=_JxV5D5bC+=U& z0zS0;OSXKYOXM&zTM!icEeIqf-($8a(NIlQUdE6L$pRW7hvu zC{h2RuHi70P zqsX_74NceHOBh{#Dmj8ssiAv?QnOU$qK3Tqi|uTQqbgN*&%HFJIlaZ;OY9*~a{fmA zqzn(F3c}3JG3$PlFu$4sQZ+BsZTSjiv}gQY4;4z#Ndj?MJ~ARl1?^<=T=YW0YDjR< zs*1U&5D38w%st`Yk#UcbF+Tb*#B#>QC!RK2s*7KzfCOKRYX8&z$ArMkyFbxSJ(dZ} zSgMehR@h9%@(*`S^omMOtbUb0lo{u!5eA?ZaF{N*uUQf*-nm)~#1VU;0J-o@@bc?i z3F9Or2k~p8+>1lPxh?uVdgD2t%n3OTVh(^9I{B$TW9I&}f`O;45D!rR;5?T!<3Mbu zMi||W$=8j_zL$Y{qdch;!T61r&5N{qu)l7N%UunpV+UAEqXAAJP5L?v6P!`ID_E96 z64Mfh8*93h_xT!U+&ZZF;7Z*^`s8fjwPG&zB+LMekW1=oAF2L2^y==tt49Bmj~73k z)hOCF8OPtLf}Qj|5#{mW+?3nH4f zbosd_a&c0`TETW+LfY6tG>7yU*xaD`{ssHPT4s0o;C*0JMS8}$Pb zp7YCV>p2}Zewd1Noz=%CF?hwO{72_69EbzmQn;!B2cJ5$8`%J=iz%zkgSXY&DpI!Oi z|6X6Omze2{&~^$%v{Ya-5TPE#CRw@Mwx-5BTlflDrC!P=1_(4DH?df#v}elLAeKRX z=zu`@>s%2{RLhq$K>lgI#sqyHFyIucevqRA@9Rj2l&P4p@z8fo>TRz(EwJu7P~KJW zGg5jZkQDM9xGe@ghk0Hyp;e^;;mjhReOY2H-^u4#N7OMv^hBuk+YqY)FBF@6XgfzI z6%-pa&xH^N@NiBs*fj<(!#6hqz1%^GnaM19H%eA5l*lJn!f_|T=&{8p9CG* zrW}~~$6TfEh{MTGX2~Q!hKLj;JphEAiw0=EM%rV;`m;6Yg?gaL0yXD2qnYhqPZ42< z_V+8EeprALq7bk%ebMjnE+t`+KA+Ak?(4XJ)ff|n0F*F3!!-v_zrXjskM z1Ho5p3L;iIQm**;><|FBl>3kKN8Fm=YHob=^{69F(Cb`A;4ojq&j-6o|A}0?vQlp9 zhcVVJE<}*b6CyFsnt=@tJ-5ZRpwL_Oltm6(YB}9M=`$0tYELo}nZ+V=P$A)?g|eXP z0hWFdgvSKWitVbu|Ao`azC@d@N~|&u#!Q)pVwogIm;) zP2}jC-=NgXPyN2st@CCDe!;#)ncVeB~OF zJQo(Wm=wYwS%?Z#{cZzTOnnNY2>IBF0%*NeP_dl6piLgel$Bje`8Ea9_~8QmPLadM zqniLZBGFT>P*ZNI7X+Z)<2rg4uO|4!ChU)?jK~ctSw^b-556{Re`J6I0%( zBc)urAdrChq%A-G_O±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 literal 0 HcmV?d00001 From 6b36eb4d5746fb23932209d42d7ce83f7f4eacb8 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 13:46:12 -0400 Subject: [PATCH 04/15] an extension banner yay --- static/images/ScrTwPm/ed.cd | 1 - 1 file changed, 1 deletion(-) delete mode 100644 static/images/ScrTwPm/ed.cd diff --git a/static/images/ScrTwPm/ed.cd b/static/images/ScrTwPm/ed.cd deleted file mode 100644 index 8b1378917..000000000 --- a/static/images/ScrTwPm/ed.cd +++ /dev/null @@ -1 +0,0 @@ - From 02acc6aaf76739db7b85c57d97df4c28e7c3dcc6 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 14:10:53 -0400 Subject: [PATCH 05/15] Create RunPython.md --- src/lib/Documentation/RunPython.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/lib/Documentation/RunPython.md diff --git a/src/lib/Documentation/RunPython.md b/src/lib/Documentation/RunPython.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/lib/Documentation/RunPython.md @@ -0,0 +1 @@ + From 463adb44fa39e7380bcf619a8a9a11997f31d696 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Tue, 18 Aug 2026 14:11:42 -0400 Subject: [PATCH 06/15] fixed python.js --- static/extensions/ScrTwPm/runpython.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/extensions/ScrTwPm/runpython.js b/static/extensions/ScrTwPm/runpython.js index 66fd73a80..e1507b13b 100644 --- a/static/extensions/ScrTwPm/runpython.js +++ b/static/extensions/ScrTwPm/runpython.js @@ -1661,6 +1661,8 @@ block(){ input(args, util){if(this.check(args, util)){ + if (document.querySelector('#pythonblock')) { + try{ this.pytext = `${this.pytext}
${args.INPUT}`; @@ -1711,6 +1713,8 @@ return new Promise((resolve) => { }catch(error){ } + + } else {throw new Error("The python output must be showing.")} } else {throw new Error("Block must be under the When Python Code Starts event")}} From 05598d062abb4ee190d9ed7c14e565397fc76c31 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Wed, 19 Aug 2026 19:23:34 -0400 Subject: [PATCH 07/15] Add documentation for Python block coding Added comprehensive documentation for running Python scripts using block coding, including details on blocks, strict editing, output display, variables, lists, controls, functions, classes, strings, math, operators, time, and random functionalities. --- src/lib/Documentation/RunPython.md | 952 +++++++++++++++++++++++++++++ 1 file changed, 952 insertions(+) diff --git a/src/lib/Documentation/RunPython.md b/src/lib/Documentation/RunPython.md index 8b1378917..c34f15f1e 100644 --- a/src/lib/Documentation/RunPython.md +++ b/src/lib/Documentation/RunPython.md @@ -1 +1,953 @@ +# Run Python +Python Block coding has come! + +## Blocks + +This extension helps you create python scripts. +Your Python script begins with this block +```scratch +when python code starts ::#4584b6 hat +``` + + + + +```scratch +a yellow python block ::#ffdd55 +``` +Yellow blocks work anywhere in your project +```scratch +a blue python block ::#4584b6 +``` +Blue block sonly work while they are under the when python code starts event hat, unless you turn strict editing off. They are not meant to be used outside of Python scripts. + +### Run Python + +```scratch +when python code starts ::#4584b6 hat +``` + +When python code starts begins the python script. It is ideal to have only one of these in your project at a time. + + +```scratch +run python code ::#ffdd55 +``` +This will start your python script + +--- + +### Strict Editing + +**About strict editing** + +Strict editing makes the blue python blocks only work when they are under the When Python Code Starts hat block. This is similar to how the editor works in Edublocks. If you have it off, it will allow python blocks to be used outside of python scripts (not recommended). However, it can be turned off for certain debugging purposes, but by default it is on to mirror coding in Python as much as possible. Feel free to turn it off if it annoys you. + + +--- + + +```scratch +set strict editing to [on v] ::#ffdd55 +``` +This will turn strict editing on or off. + + +--- + +```scratch + +``` +Self-explanitory + +--- + +### Display Output + +```scratch +show python output ::#ffdd55 +``` +This is like your terminal when you are coding in python. It displays printed text and inputs. It covers the canvas. + +**_Recommened:_** Use this block right before showing the python output. + +--- + +```scratch +hide python output ::#ffdd55 +``` +Self-explanitory. + +**_Note:_**The input blocks will not work if the python output is hidden + +--- + +```scratch +clear python output ::#ffdd55 +``` +Clears the output. When the python script is restarted, the output is not automatically cleared. Use this to clear it. + +--- + +```scratch +(python output ::#ffdd55) +``` +Get the python output that you see on the canvas. + +--- + +### Print + + +```scratch +print [Python is fun!] ::#4584b6 +``` +The simplest of the python statements. Prints the text on the output. + +--- + +### Inputs + +```scratch +input [is Python fun? ] ::#4584b6 +``` +Prints the prompt on the output and awaits an answer. The answer is submitted when the enter key is pressed. +**_IMPORTANT:_** This will NOT work if the Python output is not showing. +**_Note:_** This does not work on mobile devices as it will not prompt the touch keyboard. +**_Note:_** Do NOT click anything else during the prompt until you finish or your python code will stall. + +--- + +```scratch +(entered answer ::#4584b6) +``` +Returns the entered answer to be used in the code. + +--- + + +### Variables + +```scratch +create a variable named [var2] ::#ffdd55 +``` +Create a variable to use in your code. Click the block to create the variable. Then, you need to put this block under the when python code starts hat and above any other blue blocks in the script. + +**Example** +```scratch +when python code starts ::#4584b6 hat +create a variable named [coolvar] :: #ffdd55 +((coolvar v) ::#33546f) = (26) :: #4584b6 stack +print (get ((coolvar v) ::#33546f) :: #4584b6) :: #4584b6 +``` + +--- + +```scratch +delete variable ((var v) ::#6ba4d3) :: #ffdd55 +``` +Deletes the variable (never use this in your python scripts.) + +--- + + +```scratch +get variable ((var v) ::#33546f) :: #4584b6 reporter +``` +Gets the value of the selected variable + +In Python: +```py +variable +``` + +--- + +```scratch +((var v) ::#33546f) = [0] :: #4584b6 stack +``` +Sets the variable to a value + +In Python: +```py +variable = 0 +``` +--- + + +### Lists + + +```scratch +create a list named [list2] ::#ffdd55 +``` +Create a list to use in your code. Click the block to create the list. Then, you need to put this block under the when python code starts hat and above any other blue blocks in the script. + +**Example** +```scratch +when python code starts ::#4584b6 hat +create a variable named [cool list] :: #ffdd55 +((cool list v) ::#33546f) = () :: #4584b6 stack +print (get ((cool list v) ::#33546f) :: #4584b6) :: #4584b6 +``` + +--- + +```scratch +delete list ((list v) ::#6ba4d3) :: #ffdd55 +``` +Deletes the list (never use this in your python scripts.) + +--- + + +```scratch +get list ((list v) ::#33546f) :: #4584b6 reporter +``` +Gets the value of the selected list + +In Python: +```py +lst +``` + +--- + +```scratch +((list v) ::#33546f) = \[ [item] @addInput \] :: #4584b6 stack +``` +Sets the list to an array. The block is expandable if you want to add more items + +In Python: +```py +lst = ["item","item2"] +``` + +--- + + +```scratch +((list v) ::#33546f) = [["Python", "3.14"]] :: #4584b6 stack +``` +This is the same as the previous block except it is not expandable and the input is a full list + +In Python: +```py +lst = ["Python","3.14"] +``` + +--- + + + +```scratch +((list v) ::#33546f) .reverse\(\) :: #4584b6 stack +``` +This will reverse the list and save the new list. + +In python: +```py +lst.reverse() +``` + + +--- + + +```scratch +((list v) ::#33546f) . [append v] [item2] :: #4584b6 stack +``` +Based on the dropdown... +* append: Appends (adds) the value to the end of the list +* remove: Removes the value from the list + + +In python: +```py +lst.append("item2") +``` + + +--- + + + +```scratch +((list v) ::#33546f) . extend [["item4","item5"\]] :: #4584b6 stack +``` +Marges the list with the input list + +In Python: +```py +lst.extend(["item4","item5"]) +``` + + +--- + + +```scratch +((list v) ::#33546f) . insert [item3], [0] :: #4584b6 stack +``` +Inserts the item to the specified index + +**_Note:_** Python is zero-indexed, meaning the first item of a list is cosidered at position 0, the second item is considered at position 1, and so on. + +If this block was used, "item3" would be inserted as the first item in the list. + +In python: +```py +lst.insert("item3", 0) +``` + + +--- + + + +```scratch +((list v) ::#33546f) . pop [item] :: #4584b6 reporter +``` +If the list contains the value, it will return the value then remove it from the list + +In python: +```py +lst.pop("item") +``` + + +--- + + +```scratch +((list v) ::#33546f) \( [0] \) :: #4584b6 reporter +``` +Returns the item at the specified index + +**_Remember_:_** Python is zero-indexed, meaning the first item of a list is cosidered at position 0, the second item is considered at position 1, and so on. + +In python: +```py +lst[0] +``` + +--- + + +```scratch +((list v) ::#33546f) .index [item]:: #4584b6 reporter +``` +Returns the index of the specified item in the list, if it is not found it will return -1 + + +--- + + +### Controls + + +```scratch +if <> : { +} :: #4584b6 loop +``` +Runs the inside code if the boolean is true + +In python: +```py +if bool : + #runs if true +``` + + +--- + + +```scratch +elif <> : { +} :: #4584b6 loop +``` +Runs the inside code if the boolean is true and the above if statement is false. You can add many elifs after an if + +In python: +```py +if bool : + #it was false +elif bool : + #runs if true +``` + + +--- + + +```scratch +else : { +} :: #4584b6 loop +``` +The last condition. If the if statement and all elifs are false, this code runs. + +In python: +```py +if bool : + #this was false +elif bool : + #this was also false +elif bool : + #this was false too?!?!?! +else bool : + #this runs +``` + +**Example** + +```scratch +if <[21] [== v] [36] :: #4584b6> : { +print [21 is 36] :: #4584b6 +} :: #4584b6 loop +elif <[48] [== v] [36] :: #4584b6> : { +print [48 is 36] :: #4584b6 +} :: #4584b6 loop +else :{ +print [i guess 48 and 21 are not 36] :: #4584b6 +} :: #4584b6 loop +``` +This will print +``` +"i guess 48 and 21 are not 36" +``` + +--- + +```scratch +while <> : { + +} :: #4584b6 loop +``` +Runs the inside code while the boolean is true + +In python: +```py +while bool : + #runs while true +``` + +--- + +```scratch +for ((var v) ::#33546f) in [["item1","item2","item3"\]] : { + +} :: #4584b6 loop +``` +Loops through all items in the list, and the var will be the item on which the loop is in + +In python: +```py +for var in lst: + #see the example for a better explnation +``` + +**Example** + +```scratch +for ((var v) ::#33546f) in [["item1","item2","item3"\]] : { +print (get variable ((var v) ::#33546f) :: #4584b6) :: #4584b6 +} :: #4584b6 loop +``` +This will print +``` +item1 +item2 +item3 +``` + +--- + +```scratch +range [5] :: #4584b6 reporter +``` +Creates an array of positive integers from 0 to the specified length. +Useful in for loops. +**_Remember:_** Python is zero-indexed, so range (5) would end at 4 + +In python: +```py +range(5) +``` + +--- + +```scratch +range [2] , [7] :: #4584b6 reporter +``` +Creates an array of integers counting up from the first number to the specified length. +Useful in for loops. +**_Remember:_** Python is zero-indexed, so range (2,7) would end at 6 + +In python: +```py +range(2,7) +``` + +--- + + + +### Functions + +```scratch +def [my_func] \( [hello, goodbye] \) : { +} :: #4584b6 loop +``` +Defines a function with parameters. If your functions don't use parameters, leave the second input blank + +In python: +```py +def my_func (hello, goodbye) : + #stuff the function does... +``` +**_Remember:_** You can only use a function after it is defined in the python script. +**_Helpful Hint:_** For advanced users, use * before the last parameter name to use *args. **kwargs is not supported yet. + +--- + +```scratch +return [1] :: #4584b6 cap +``` +return a value at the end of a function + +In python: +```py +return 1 +``` + +--- + + +```scratch +[my_func] \( [one,two] \) :: #4584b6 +``` +call a function. If your funciton has parameters, pass the arguements in the same order that you defined them in the function. + +In python: +```py +my_func("one","two") +``` + +**Example** + +```scratch +def [my_func] \( [hello, goodbye] \) : { +print (parameter [hello] :: #4584b6) :: #4584b6 +} :: #4584b6 loop +[my_func] \( [one,two] \) :: #4584b6 +``` + +When the function is called, +* parameter **hello** = "one" +* parameter **goodbye** = "two" + +This will print "one" because the hello parameter was first and so is "one" in the list of args + +--- + + + +```scratch +parameter [hello] :: #4584b6 reporter +``` +Access the values of the parameter inside your function when it is called + +In python: +```py +def my_func (parameter) : + parameter # this is a parameter +``` + +--- + + +```scratch +[my_func] \( [one,two] \) :: #4584b6 reporter +``` +Use this to call a function if it returns a value + +In python: +```py +my_func("one","two") +``` + +--- + + +```scratch +[args], @addInput :: #4584b6 reporter +``` +an expandable block to list parameters + +In python: +```py +def my_func (data, use, issue_date, target): # a lot of parameters! +``` + +--- + + +### Classes + +```scratch +class [My_Class] : { +} :: #4594b6 loop +``` +Create a class + +In python: +```py +class My_Class: + # data... +``` + +--- + + +```scratch +def [class_func] \( self, [hello, goodbye] \) in class : { +} :: #4594b6 loop +``` +This is the same as defining a normal function, except it is used in a class. It is able to change properties in a class. + +In python: +```py +def class_func (self, hello, goodbye): + #Do something +``` + +--- + + +```scratch +self. [class_var] = [0] in class :: #4584b6 +``` +Define a property in a class. This will define a variable. + +In python: +```py +self.class_var = 0 +``` + +--- + + +```scratch +self. [class_list] = \[ [item] @addInput \] in class :: #4584b6 +``` +Define a property in a class. This will define a list. It is expandable to add items. + + + +In python: +```py +self.class_list = ["item","item2"] +``` + +--- + + +```scratch +from class [My_class].[class_func] \( [one,two] \) :: #4584b6 +``` +Run a function from a class. For more details on how to run functions, scroll up to the define my_func block. + +In python: +```py +My_class.class_func("one","two") +``` + +--- + + +```scratch +from class [My_class].[class_func] \( [one,two] \) :: #4584b6 reporter +``` +Run a function from a class to return a value. For more details on how to run functions, scroll up to the define my_func block. + +In python: +```py +range(2,7) +``` + +--- + + +```scratch +from class [My_class].[class_var] :: #4584b6 reporter +``` +Get a property from a class + +In python: +```py +My_class.class_var +``` + +--- + + +### Strings + +```scratch +f [Python] [ 3.14] @addInput :: #4584b6 reporter +``` +Make an f-string. Basically an expandable join block. + +In python: +```py +f"Python 3.14" +``` + +--- + + +```scratch +[PyThOn].[upper () v] :: #4584b6 reporter +``` +Based on the dropdown +* upper(): Converts to uppercase +* lower(): Converts to lowercase +* title(): Converts to title case + + +In python: +```py +string.upper() +``` + +--- + + +```scratch +[Python is fun] .replace [fun], [awesome]:: #4584b6 reporter +``` +Replaces the word fun with awesome in "Python is fun" + +In python: +```py +string.replace("fun", "awesome") +``` + +--- + + +```scratch +[ whitespace? ]. [strip() v]:: #4584b6 reporter +``` +Based on the dropdown +* strip(): trims whitespace on both sides +* lstrip(): trims whitespace from the front (left) +* rstrip(): trims whitespace from the back (right) + +In python: +```py +string.strip() +``` + +--- + + +```scratch +[Python is fun] \( [0]\):: #4584b6 reporter +``` +Get the letter at the specified index in the string + +**_Note:_** Python is zero-indexed, meaning the first item of a list is cosidered at position 0, the second item is considered at position 1, and so on. + +In python: +```py +string[0] +``` + +--- + + +```scratch +[Python is fun]. count [n]:: #4584b6 reporter +``` +Count the number of times a character appears in a string + +In python: +```py +string.count("n") +``` + +--- + + +```scratch +[Python is fun]. count [n], [3], [9]:: #4584b6 reporter +``` +Count the number of times a character appears in a section of a string + +In python: +```py +string.count("n", 3, 9) +``` + +--- + + +```scratch +[Python is fun] .[find v] [n]:: #4584b6 reporter +``` +Find a character. Based on the dropdown +* find(): search from the left (first occurance) +* rfind(): search from the right (last occurance) +In python: +```py +string.find("n") +``` + +--- + + +### Math + + +```scratch +[16] [+ v] [16]:: #4584b6 reporter +``` +Add, subtract, multiply, or divide numbers + +In python: +```py +16 + 16 +``` + +--- + + +```scratch +math. [sqrt v] \( [16] \):: #4584b6 reporter +``` +Run various math functions + +In python: +```py +math.sqrt(16) +``` + +--- + + +```scratch +math. [pi v]:: #4584b6 reporter +``` +Get various math constants + +In python: +```py +math.pi +``` + +--- + + +```scratch +math. [pow v] \( [4], [2]\):: #4584b6 reporter +``` +Run various math operations + +In python: +```py +math.pow(4, 2) +``` + +--- + + +### Operators + +```scratch +[21] [== v] [36]:: #4584b6 boolean +``` +Complete various checks between values +== means is equal to +!= means not equal to + +In python: +```py +21 == 36 +``` + +--- + + +```scratch +<> [and v] <>:: #4584b6 boolean +``` +Based on the dropdown +* and: returns true if both conditions are true +* or: returns true if either condition is true (or both) + +In python: +```py +bool and bool +``` + +--- + + +```scratch +not <>:: #4584b6 boolean +``` +Returns true if the boolean is false + +In python: +```py +not bool +``` + + +--- + +### Time + + +```scratch +time.sleep [2]:: #4584b6 +``` +Waits the number of seconds + +In python: +```py +time.sleep(2) +``` + +--- + + +```scratch +time. [time() v] []:: #4584b6 reporter +``` +Return various time calculations + +In python: +```py +time.time() +``` + +--- + + +### Random + + +```scratch +random.choice [["yes","no"\]] :: #4584b6 reporter +``` +Get a randomly selected value from the list + +In python: +```py +random.choice(["yes","no"]) +``` + + From df663ce7b0ed1a1b082e32d6cd4da04241f9d958 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Wed, 19 Aug 2026 19:27:26 -0400 Subject: [PATCH 08/15] Add RunPython documentation to pages.js --- src/lib/Documentation/pages.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/Documentation/pages.js b/src/lib/Documentation/pages.js index 546137a0f..bab0fc58a 100644 --- a/src/lib/Documentation/pages.js +++ b/src/lib/Documentation/pages.js @@ -42,6 +42,9 @@ import ProjectInterfaces from "./ProjectInterfaces.md?raw"; // Date Format V2 import DateFormatV2 from "./DateFormatV2.md?raw"; +// Run Python +import DateFormatV2 from "./RunPython.md?raw"; + export default { // the key is the path to the docs page // so you can do "sharkpool-particle-tools" for example @@ -81,5 +84,8 @@ export default { // Project Interfaces "ProjectInterfaces": ProjectInterfaces, + // Run Python + "RunPython": RunPython, + "DateFormatV2": DateFormatV2 }; From e66e70c6acc3914b26f48c2a66254f233e19e415 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Wed, 19 Aug 2026 19:32:58 -0400 Subject: [PATCH 09/15] Add Python block coding extension Added a new Python block coding extension with documentation. --- src/lib/extensions.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/extensions.js b/src/lib/extensions.js index 84d1c063a..0bfa2cab6 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -50,6 +50,15 @@ export default [ banner: "pooiod/B2Dimg.svg", creator: "pooiod7", }, + { + name: "Python", + description: "Python block coding has come! Like EduBlocks, but better.", + code: "ScrTwPm/RunPython.js", + banner: "ScrTwPm/python.png", + creator: "ScrTwPm", + documentation: "RunPython", + isGitHub: true, + }, { name: "Lighting", description: "A fast, powerful and easy-to-use lighting engine powered by WebGL!", From 05378aada0621aaf0edcf1be483fecd9842b8812 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Wed, 19 Aug 2026 19:34:20 -0400 Subject: [PATCH 10/15] Python extension --- src/lib/extensions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/extensions.js b/src/lib/extensions.js index 0bfa2cab6..975c075f3 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -53,8 +53,8 @@ export default [ { name: "Python", description: "Python block coding has come! Like EduBlocks, but better.", - code: "ScrTwPm/RunPython.js", - banner: "ScrTwPm/python.png", + code: "ScrTwPm/runpython.js", + banner: "ScrTwPm/runpython.png", creator: "ScrTwPm", documentation: "RunPython", isGitHub: true, From 16e44a61c860d800323e85f97ed460d1440b08d2 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Thu, 20 Aug 2026 14:09:10 -0400 Subject: [PATCH 11/15] yay --- static/extensions/ScrTwPm/runpython.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/extensions/ScrTwPm/runpython.js b/static/extensions/ScrTwPm/runpython.js index e1507b13b..8bd0ede31 100644 --- a/static/extensions/ScrTwPm/runpython.js +++ b/static/extensions/ScrTwPm/runpython.js @@ -1,3 +1,6 @@ +// Credits: +// SharkPool Extra Controls +// CST1229 https://github.com/CST1229/ (function(Scratch) { 'use strict'; From a099139ac58ec1078eafc317f3b9770bd899b582 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Thu, 20 Aug 2026 14:10:30 -0400 Subject: [PATCH 12/15] Enhance Python block coding description Updated the description of the Python block coding feature to include additional context about mirroring Python code in Penguinmod. --- src/lib/extensions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/extensions.js b/src/lib/extensions.js index 975c075f3..a676ebd1e 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -52,7 +52,7 @@ export default [ }, { name: "Python", - description: "Python block coding has come! Like EduBlocks, but better.", + description: "Python block coding has come! Like EduBlocks, but better. Mirror python code in Penguinmod!", code: "ScrTwPm/runpython.js", banner: "ScrTwPm/runpython.png", creator: "ScrTwPm", From 191b13dd36e08bfc033ab8c8220e3eabfdb09313 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Thu, 20 Aug 2026 14:11:55 -0400 Subject: [PATCH 13/15] editied Corrected spelling of 'true' in boolean explanations. --- src/lib/Documentation/RunPython.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/Documentation/RunPython.md b/src/lib/Documentation/RunPython.md index c34f15f1e..6b2551f99 100644 --- a/src/lib/Documentation/RunPython.md +++ b/src/lib/Documentation/RunPython.md @@ -1,5 +1,6 @@ # Run Python Python Block coding has come! +Mirror python in PenguinMod! ## Blocks @@ -883,8 +884,8 @@ In python: <> [and v] <>:: #4584b6 boolean ``` Based on the dropdown -* and: returns true if both conditions are true -* or: returns true if either condition is true (or both) +* and: returns true if both conditions are ture +* or: returns true if either condition is ture (or both) In python: ```py From 815e1b8c8b1647b121adb4678f57e74ccd20caf9 Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Thu, 20 Aug 2026 14:19:02 -0400 Subject: [PATCH 14/15] Implement input prompt alert in runpython.js Added an alert function 'abtinp' to provide information about input prompts in the Python environment. --- static/extensions/ScrTwPm/runpython.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/static/extensions/ScrTwPm/runpython.js b/static/extensions/ScrTwPm/runpython.js index 8bd0ede31..792c22b35 100644 --- a/static/extensions/ScrTwPm/runpython.js +++ b/static/extensions/ScrTwPm/runpython.js @@ -1,6 +1,3 @@ -// Credits: -// SharkPool Extra Controls -// CST1229 https://github.com/CST1229/ (function(Scratch) { 'use strict'; @@ -355,6 +352,12 @@ { blockType: Scratch.BlockType.LABEL, text: "Inputs" }, + + { + opcode: 'abtinp', + blockType: Scratch.BlockType.BUTTON, + text: 'About inputs', + }, { opcode: 'input', @@ -1164,6 +1167,12 @@ aboutstrict(){ window.alert("Strict editing makes the blue python blocks only work when they are under the When Python Code Starts hat block. This is similar to how the editor works in Edublocks. If you have it off, it will allow python blocks to be used outside of python scripts (not recommended). However, it can be turned off for certain debugging purposes, but by default it is on to mirror coding in Python as much as possible. Feel free to turn it off if it annoys you.") } +abtinp(){ + window.alert(`Prints the prompt on the output and awaits an answer. The answer is submitted when the enter key is pressed. 1) IMPORTANT: This will NOT work if the Python output is not showing. 2) This does not work on mobile devices as it will not prompt the touch keyboard. 3) Do NOT click anything else during the prompt until you finish or your python code will stall. +`) + +} + range = (start, end) => JSON.stringify(Array.from({ length: end - start }, (_, i) => start + i)) From 06dff2a10c0f96ad0cf53ded9dc747059ddf66af Mon Sep 17 00:00:00 2001 From: ScrTwPm Date: Thu, 20 Aug 2026 14:22:22 -0400 Subject: [PATCH 15/15] Add credits to notes in extensions.js Added notes crediting CST1229 and Sharkool for contributions. --- src/lib/extensions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/extensions.js b/src/lib/extensions.js index a676ebd1e..988c209fc 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -58,6 +58,7 @@ export default [ creator: "ScrTwPm", documentation: "RunPython", isGitHub: true, + notes: "Credit to CST1229 (github) and Sharkool (github)" }, { name: "Lighting",