diff --git a/scratchattach/__init__.py b/scratchattach/__init__.py index 0ec563e..eaa8307 100644 --- a/scratchattach/__init__.py +++ b/scratchattach/__init__.py @@ -23,3 +23,5 @@ from .site.classroom import Classroom, get_classroom from .site.user import User, get_user from .site._base import BaseSiteComponent + +from . import editor diff --git a/scratchattach/editor/__init__.py b/scratchattach/editor/__init__.py new file mode 100644 index 0000000..0e3a245 --- /dev/null +++ b/scratchattach/editor/__init__.py @@ -0,0 +1,21 @@ +""" +scratchattach.editor (sbeditor v2) - a library for all things sb3 +""" + +from .asset import Asset, Costume, Sound +from .project import Project +from .extension import Extensions, Extension +from .mutation import Mutation, Argument, parse_proc_code +from .meta import Meta, set_meta_platform +from .sprite import Sprite +from .block import Block +from .prim import Prim, PrimTypes +from .backpack_json import load_script as load_script_from_backpack +from .twconfig import TWConfig, is_valid_twconfig +from .inputs import Input, ShadowStatuses +from .field import Field +from .vlb import Variable, List, Broadcast +from .comment import Comment +from .monitor import Monitor + +from .build_defaulting import add_chain, add_comment, add_block diff --git a/scratchattach/editor/asset.py b/scratchattach/editor/asset.py new file mode 100644 index 0000000..30280e5 --- /dev/null +++ b/scratchattach/editor/asset.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from hashlib import md5 +import requests + +from . import base, commons, sprite, build_defaulting + + +@dataclass(init=True, repr=True) +class AssetFile: + filename: str + _data: bytes = field(repr=False, default=None) + _md5: str = field(repr=False, default=None) + + @property + def data(self): + if self._data is None: + # Download and cache + rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/") + if rq.status_code != 200: + raise ValueError(f"Can't download asset {self.filename}\nIs not uploaded to scratch! Response: {rq.text}") + + self._data = rq.content + + return self._data + + @property + def md5(self): + if self._md5 is None: + self._md5 = md5(self.data).hexdigest() + + return self._md5 + + +class Asset(base.SpriteSubComponent): + def __init__(self, + name: str = "costume1", + file_name: str = "b7853f557e4426412e64bb3da6531a99.svg", + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Represents a generic asset. Can be a sound or an image. + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets + """ + try: + asset_id, data_format = file_name.split('.') + except ValueError: + raise ValueError(f"Invalid file name: {file_name}, # of '.' in {file_name} ({file_name.count('.')}) != 2; " + f"(too many/few values to unpack)") + self.name = name + + self.id = asset_id + self.data_format = data_format + + super().__init__(_sprite) + + def __repr__(self): + return f"Asset<{self.name!r}>" + + @property + def folder(self): + return commons.get_folder_name(self.name) + + @property + def name_nfldr(self): + return commons.get_name_nofldr(self.name) + + @property + def file_name(self): + return f"{self.id}.{self.data_format}" + + @property + def md5ext(self): + return self.file_name + + @property + def parent(self): + if self.project is None: + return self.sprite + else: + return self.project + + @property + def asset_file(self) -> AssetFile: + for asset_file in self.parent.asset_data: + if asset_file.filename == self.file_name: + return asset_file + + # No pre-existing asset file object; create one and add it to the project + asset_file = AssetFile(self.file_name) + self.project.asset_data.append(asset_file) + return asset_file + + @staticmethod + def from_json(data: dict): + _name = data.get("name") + _file_name = data.get("md5ext") + if _file_name is None: + if "dataFormat" in data and "assetId" in data: + _id = data["assetId"] + _data_format = data["dataFormat"] + _file_name = f"{_id}.{_data_format}" + + return Asset(_name, _file_name) + + def to_json(self) -> dict: + return { + "name": self.name, + + "assetId": self.id, + "md5ext": self.file_name, + "dataFormat": self.data_format, + } + + """ + @staticmethod + def from_file(fp: str, name: str = None): + image_types = ("png", "jpg", "jpeg", "svg") + sound_types = ("wav", "mp3") + + # Should save data as well so it can be uploaded to scratch if required (add to project asset data) + ... + """ + + +class Costume(Asset): + def __init__(self, + name: str = "Cat", + file_name: str = "b7853f557e4426412e64bb3da6531a99.svg", + + bitmap_resolution=None, + rotation_center_x: int | float = 48, + rotation_center_y: int | float = 50, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + A costume. An asset with additional properties + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes + """ + super().__init__(name, file_name, _sprite) + + self.bitmap_resolution = bitmap_resolution + self.rotation_center_x = rotation_center_x + self.rotation_center_y = rotation_center_y + + @staticmethod + def from_json(data): + _asset_load = Asset.from_json(data) + + bitmap_resolution = data.get("bitmapResolution") + + rotation_center_x = data["rotationCenterX"] + rotation_center_y = data["rotationCenterY"] + return Costume(_asset_load.name, _asset_load.file_name, + + bitmap_resolution, rotation_center_x, rotation_center_y) + + def to_json(self) -> dict: + _json = super().to_json() + _json.update({ + "bitmapResolution": self.bitmap_resolution, + "rotationCenterX": self.rotation_center_x, + "rotationCenterY": self.rotation_center_y + }) + return _json + + +class Sound(Asset): + def __init__(self, + name: str = "pop", + file_name: str = "83a9787d4cb6f3b7632b4ddfebf74367.wav", + + rate: int = None, + sample_count: int = None, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + A sound. An asset with additional properties + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Sounds + """ + super().__init__(name, file_name, _sprite) + + self.rate = rate + self.sample_count = sample_count + + @staticmethod + def from_json(data): + _asset_load = Asset.from_json(data) + + rate = data.get("rate") + sample_count = data.get("sampleCount") + return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count) + + def to_json(self) -> dict: + _json = super().to_json() + commons.noneless_update(_json, { + "rate": self.rate, + "sampleCount": self.sample_count + }) + return _json diff --git a/scratchattach/editor/backpack_json.py b/scratchattach/editor/backpack_json.py new file mode 100644 index 0000000..5e101ea --- /dev/null +++ b/scratchattach/editor/backpack_json.py @@ -0,0 +1,117 @@ +""" +Module to deal with the backpack's weird JSON format, by overriding with new load methods +""" +from __future__ import annotations + +from . import block, prim, field, inputs, mutation, sprite + + +def parse_prim_fields(_fields: dict[str]) -> tuple[str | None, str | None, str | None]: + """ + Function for reading the fields in a backpack **primitive** + """ + for key, value in _fields.items(): + key: str + value: dict[str, str] + prim_value, prim_name, prim_id = (None,) * 3 + if key == "NUM": + prim_value = value.get("value") + else: + prim_name = value.get("value") + prim_id = value.get("id") + + # There really should only be 1 item, and this function can only return for that item + return prim_value, prim_name, prim_id + return (None,) * 3 + + +class BpField(field.Field): + """ + A normal field but with a different load method + """ + + @staticmethod + def from_json(data: dict[str, str]) -> field.Field: + # We can very simply convert it to the regular format + data = [data.get("value"), data.get("id")] + return field.Field.from_json(data) + + +class BpInput(inputs.Input): + """ + A normal input but with a different load method + """ + + @staticmethod + def from_json(data: dict[str, str]) -> inputs.Input: + # The actual data is stored in a separate prim block + _id = data.get("shadow") + _obscurer_id = data.get("block") + + if _obscurer_id == _id: + # If both the shadow and obscurer are the same, then there is no actual obscurer + _obscurer_id = None + # We cannot work out the shadow status yet since that is located in the primitive + return inputs.Input(None, _id=_id, _obscurer_id=_obscurer_id) + + +class BpBlock(block.Block): + """ + A normal block but with a different load method + """ + + @staticmethod + def from_json(data: dict) -> prim.Prim | block.Block: + """ + Load a block in the **backpack** JSON format + :param data: A dictionary (not list) + :return: A new block/prim object + """ + _opcode = data["opcode"] + + _x, _y = data.get("x"), data.get("y") + if prim.is_prim_opcode(_opcode): + # This is actually a prim + prim_value, prim_name, prim_id = parse_prim_fields(data["fields"]) + return prim.Prim(prim.PrimTypes.find(_opcode, "opcode"), + prim_value, prim_name, prim_id) + + _next_id = data.get("next") + _parent_id = data.get("parent") + + _shadow = data.get("shadow", False) + _top_level = data.get("topLevel", _parent_id is None) + + _inputs = {} + for _input_code, _input_data in data.get("inputs", {}).items(): + _inputs[_input_code] = BpInput.from_json(_input_data) + + _fields = {} + for _field_code, _field_data in data.get("fields", {}).items(): + _fields[_field_code] = BpField.from_json(_field_data) + + if "mutation" in data: + _mutation = mutation.Mutation.from_json(data["mutation"]) + else: + _mutation = None + + return block.Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id, + _parent_id=_parent_id) + + +def load_script(_script_data: list[dict]) -> sprite.Sprite: + """ + Loads a script into a sprite from the backpack JSON format + :param _script_data: Backpack script JSON data + :return: a blockchain object containing the script + """ + # Using a sprite since it simplifies things, e.g. local global loading + _blockchain = sprite.Sprite() + + for _block_data in _script_data: + _block = BpBlock.from_json(_block_data) + _block.sprite = _blockchain + _blockchain.blocks[_block_data["id"]] = _block + + _blockchain.link_subcomponents() + return _blockchain diff --git a/scratchattach/editor/base.py b/scratchattach/editor/base.py new file mode 100644 index 0000000..58bac9d --- /dev/null +++ b/scratchattach/editor/base.py @@ -0,0 +1,142 @@ +""" +Editor base classes +""" + +from __future__ import annotations + +import copy +import json +from abc import ABC, abstractmethod +from io import TextIOWrapper +from typing import Any, TYPE_CHECKING, BinaryIO + +if TYPE_CHECKING: + from . import project, sprite, block, mutation, asset + +from . import build_defaulting + + +class Base(ABC): + def dcopy(self): + """ + :return: A **deep** copy of self + """ + return copy.deepcopy(self) + + def copy(self): + """ + :return: A **shallow** copy of self + """ + return copy.copy(self) + + +class JSONSerializable(Base, ABC): + @staticmethod + @abstractmethod + def from_json(data: dict | list | Any): + pass + + @abstractmethod + def to_json(self) -> dict | list | Any: + pass + + def save_json(self, name: str = ''): + data = self.to_json() + with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f: + json.dump(data, f) + + +class JSONExtractable(JSONSerializable, ABC): + @staticmethod + @abstractmethod + def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: str = None) -> tuple[ + str, list[asset.AssetFile], str]: + """ + Automatically extracts the JSON data as a string, as well as providing auto naming + :param data: Either a string of JSON, sb3 file as bytes or as a file object + :param load_assets: Whether to extract assets as well (if applicable) + :param _name: Any provided name (will automatically find one otherwise) + :return: tuple of the name, asset data & json as a string + """ + ... + + +class ProjectSubcomponent(JSONSerializable, ABC): + def __init__(self, _project: project.Project = None): + self.project = _project + + +class SpriteSubComponent(JSONSerializable, ABC): + def __init__(self, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + if _sprite is build_defaulting.SPRITE_DEFAULT: + _sprite = build_defaulting.current_sprite() + + self.sprite = _sprite + + # @property + # def sprite(self): + # if self._sprite is None: + # print("ok, ", build_defaulting.current_sprite()) + # return build_defaulting.current_sprite() + # else: + # return self._sprite + + # @sprite.setter + # def sprite(self, value): + # self._sprite = value + + @property + def project(self) -> project.Project: + return self.sprite.project + + +class IDComponent(SpriteSubComponent, ABC): + def __init__(self, _id: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + self.id = _id + super().__init__(_sprite) + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.id}>" + + +class NamedIDComponent(IDComponent, ABC): + """ + Base class for Variables, Lists and Broadcasts (Name + ID + sprite) + """ + + def __init__(self, _id: str, name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + self.name = name + super().__init__(_id, _sprite) + + def __repr__(self): + return f"<{self.__class__.__name__} '{self.name}'>" + + +class BlockSubComponent(JSONSerializable, ABC): + def __init__(self, _block: block.Block = None): + self.block = _block + + @property + def sprite(self) -> sprite.Sprite: + return self.block.sprite + + @property + def project(self) -> project.Project: + return self.sprite.project + + +class MutationSubComponent(JSONSerializable, ABC): + def __init__(self, _mutation: mutation.Mutation = None): + self.mutation = _mutation + + @property + def block(self) -> block.Block: + return self.mutation.block + + @property + def sprite(self) -> sprite.Sprite: + return self.block.sprite + + @property + def project(self) -> project.Project: + return self.sprite.project diff --git a/scratchattach/editor/block.py b/scratchattach/editor/block.py new file mode 100644 index 0000000..fa43a8c --- /dev/null +++ b/scratchattach/editor/block.py @@ -0,0 +1,507 @@ +from __future__ import annotations + +import warnings +from typing import Iterable, Self + +from . import base, sprite, mutation, field, inputs, commons, vlb, blockshape, prim, comment, build_defaulting +from ..utils import exceptions + + +class Block(base.SpriteSubComponent): + def __init__(self, _opcode: str, _shadow: bool = False, _top_level: bool = None, + _mutation: mutation.Mutation = None, _fields: dict[str, field.Field] = None, + _inputs: dict[str, inputs.Input] = None, x: int = 0, y: int = 0, pos: tuple[int, int] = None, + + _next: Block = None, _parent: Block = None, + *, _next_id: str = None, _parent_id: str = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + # Defaulting for args + if _fields is None: + _fields = {} + if _inputs is None: + _inputs = {} + + if pos is not None: + x, y = pos + + self.opcode = _opcode + self.is_shadow = _shadow + self.is_top_level = _top_level + + self.x, self.y = x, y + + self.mutation = _mutation + self.fields = _fields + self.inputs = _inputs + + self._next_id = _next_id + """ + Temporarily stores id of next block. Will be used later during project instantiation to find the next block object + """ + self._parent_id = _parent_id + """ + Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object + """ + + self.next = _next + self.parent = _parent + + self.check_toplevel() + + super().__init__(_sprite) + self.link_subcomponents() + + def __repr__(self): + return f"Block<{self.opcode!r}>" + + def link_subcomponents(self): + if self.mutation: + self.mutation.block = self + + for iterable in (self.fields.values(), self.inputs.values()): + for subcomponent in iterable: + subcomponent.block = self + + def add_input(self, name: str, _input: inputs.Input) -> Self: + self.inputs[name] = _input + for val in (_input.value, _input.obscurer): + if isinstance(val, Block): + val.parent = self + return self + + def add_field(self, name: str, _field: field.Field) -> Self: + self.fields[name] = _field + return self + + def set_mutation(self, _mutation: mutation.Mutation) -> Self: + self.mutation = _mutation + _mutation.block = self + _mutation.link_arguments() + return self + + def set_comment(self, _comment: comment.Comment) -> Self: + _comment.block = self + self.sprite.add_comment(_comment) + + return self + + def check_toplevel(self): + self.is_top_level = self.parent is None + + if not self.is_top_level: + self.x, self.y = None, None + + @property + def target(self): + """ + Alias for sprite + """ + return self.sprite + + @property + def block_shape(self) -> blockshape.BlockShape: + """ + Search for the blockshape stored in blockshape.py + :return: The block's block shape (by opcode) + """ + _shape = blockshape.BlockShapes.find(self.opcode, "opcode") + if _shape is None: + warnings.warn(f"No blockshape {self.opcode!r} exists! Defaulting to {blockshape.BlockShapes.UNDEFINED}") + return blockshape.BlockShapes.UNDEFINED + return _shape + + @property + def can_next(self): + """ + :return: Whether the block *can* have a next block (basically checks if it's not a cap block, also considering the behaviour of control_stop) + """ + _shape = self.block_shape + if _shape.is_cap is not blockshape.MUTATION_DEPENDENT: + return _shape.is_attachable + else: + if self.mutation is None: + # If there's no mutation, let's just assume yes + warnings.warn(f"{self} has no mutation! Assuming we can add block ;-;") + return True + + return self.mutation.has_next + + @property + def id(self) -> str | None: + """ + Work out the id of this block by searching through the sprite dictionary + """ + # warnings.warn(f"Using block IDs can cause consistency issues and is not recommended") + # This property is used when converting comments to JSON (we don't want random warning when exporting a project) + for _block_id, _block in self.sprite.blocks.items(): + if _block is self: + return _block_id + + # Let's just automatically assign ourselves an id + self.sprite.add_block(self) + + @property + def parent_id(self): + if self.parent is not None: + return self.parent.id + else: + return None + + @property + def next_id(self): + if self.next is not None: + return self.next.id + else: + return None + + @property + def relatives(self) -> list[Block]: + """ + :return: A list of blocks which are related to this block (e.g. parent, next, inputs) + """ + _ret = [] + + def yield_block(_block: Block | None): + if isinstance(_block, Block): + _ret.append(_block) + + yield_block(self.next) + yield_block(self.parent) + + return _ret + + @property + def children(self) -> list[Block | prim.Prim]: + """ + :return: A list of blocks that are inside of this block, **NOT INCLUDING THE ATTACHED BLOCK** + """ + _children = [] + for _input in self.inputs.values(): + if isinstance(_input.value, Block) or isinstance(_input.value, prim.Prim): + _children.append(_input.value) + + if _input.obscurer is not None: + _children.append(_input.obscurer) + return _children + + @property + def previous_chain(self): + if self.parent is None: + return [self] + + return [self] + self.parent.previous_chain + + @property + def attached_chain(self): + if self.next is None: + return [self] + + return [self] + self.next.attached_chain + + @property + def complete_chain(self): + # Both previous and attached chains start with self + return self.previous_chain[:1:-1] + self.attached_chain + + @property + def top_level_block(self): + """ + same as the old stack_parent property from sbedtior v1 + """ + return self.previous_chain[-1] + + @property + def bottom_level_block(self): + return self.attached_chain[-1] + + @property + def stack_tree(self): + """ + :return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block + """ + _tree = [self] + for child in self.children: + if isinstance(child, prim.Prim): + _tree.append(child) + elif isinstance(child, Block): + _tree.append(child.stack_tree) + + if self.next: + _tree += self.next.stack_tree + + return _tree + + @property + def category(self): + """ + Works out what category of block this is using the opcode. Does not perform validation + """ + return self.opcode.split('_')[0] + + @property + def is_input(self): + """ + :return: Whether this block is an input obscurer or value + """ + return self.parent_input is not None + + @property + def is_next_block(self): + """ + :return: Whether this block is attached (as next block) to a previous block and not an input + """ + return self.parent and not self.is_input + + @property + def parent_input(self): + if not self.parent: + return None + + for _input in self.parent.inputs.values(): + if _input.obscurer is self or _input.value is self: + return _input + return None + + @property + def new_id(self): + return self.sprite.new_id + + @property + def comment(self) -> comment.Comment | None: + for _comment in self.sprite.comments: + if _comment.block is self: + return _comment + return None + + @property + def turbowarp_block_opcode(self): + """ + :return: The 'opcode' if this is a turbowarp block: e.g. + - log + - breakpoint + - error + - warn + - is compiled? + - is turbowarp? + - is forkphorus? + If it's not one, just returns None + """ + if self.opcode == "procedures_call": + if self.mutation: + if self.mutation.proc_code: + # \u200B is a zero-width space + if self.mutation.proc_code == "\u200B\u200Bbreakpoint\u200B\u200B": + return "breakpoint" + elif self.mutation.proc_code == "\u200B\u200Blog\u200B\u200B %s": + return "log" + elif self.mutation.proc_code == "\u200B\u200Berror\u200B\u200B %s": + return "error" + elif self.mutation.proc_code == "\u200B\u200Bwarn\u200B\u200B %s": + return "warn" + + elif self.opcode == "argument_reporter_boolean": + arg = self.fields.get("VALUE") + + if arg is not None: + arg = arg.value + if isinstance(arg, str): + arg = arg.lower() + + if arg == "is turbowarp?": + return "is_turbowarp?" + + elif arg == "is compiled?": + return "is_compiled?" + + elif arg == "is forkphorus?": + return "is_forkphorus?" + + return None + + @property + def is_turbowarp_block(self): + return self.turbowarp_block_opcode is not None + + @staticmethod + def from_json(data: dict) -> Block: + """ + Load a block from the JSON dictionary. + :param data: a dictionary (not list) + :return: The new Block object + """ + _opcode = data["opcode"] + + _x, _y = data.get("x"), data.get("y") + + _next_id = data.get("next") + _parent_id = data.get("parent") + + _shadow = data.get("shadow", False) + _top_level = data.get("topLevel", _parent_id is None) + + _inputs = {} + for _input_code, _input_data in data.get("inputs", {}).items(): + _inputs[_input_code] = inputs.Input.from_json(_input_data) + + _fields = {} + for _field_code, _field_data in data.get("fields", {}).items(): + _fields[_field_code] = field.Field.from_json(_field_data) + + if "mutation" in data: + _mutation = mutation.Mutation.from_json(data["mutation"]) + else: + _mutation = None + + return Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id, + _parent_id=_parent_id) + + def to_json(self) -> dict: + self.check_toplevel() + + _json = { + "opcode": self.opcode, + "next": self.next_id, + "parent": self.parent_id, + "inputs": {_id: _input.to_json() for _id, _input in self.inputs.items()}, + "fields": {_id: _field.to_json() for _id, _field in self.fields.items()}, + "shadow": self.is_shadow, + "topLevel": self.is_top_level, + } + _comment = self.comment + if _comment: + commons.noneless_update(_json, { + "comment": _comment.id + }) + + if self.is_top_level: + commons.noneless_update(_json, { + "x": self.x, + "y": self.y, + }) + + if self.mutation is not None: + commons.noneless_update(_json, { + "mutation": self.mutation.to_json(), + }) + + return _json + + def link_using_sprite(self, link_subs: bool = True): + if link_subs: + self.link_subcomponents() + + if self.mutation: + self.mutation.link_arguments() + + if self._parent_id is not None: + self.parent = self.sprite.find_block(self._parent_id, "id") + if self.parent is not None: + self._parent_id = None + + if self._next_id is not None: + self.next = self.sprite.find_block(self._next_id, "id") + if self.next is not None: + self._next_id = None + + for _block in self.relatives: + _block.sprite = self.sprite + + for _field in self.fields.values(): + if _field.id is not None: + new_value = self.sprite.find_vlb(_field.id, "id") + if new_value is None: + # We probably need to add a local global variable + _type = _field.type + + if _type == field.Types.VARIABLE: + # Create a new variable + new_value = vlb.Variable(commons.gen_id(), + _field.value) + elif _type == field.Types.LIST: + # Create a list + new_value = vlb.List(commons.gen_id(), + _field.value) + elif _type == field.Types.BROADCAST: + # Create a broadcast + new_value = vlb.Broadcast(commons.gen_id(), + _field.value) + else: + # Something probably went wrong + warnings.warn( + f"Could not find {_field.id!r} in {self.sprite}. Can't create a new {_type} so we gave a warning") + + if new_value is not None: + self.sprite.add_local_global(new_value) + + # Check again since there may have been a newly created VLB + if new_value is not None: + _field.value = new_value + _field.id = None + + for _input in self.inputs.values(): + _input.link_using_block() + + # Adding/removing block + def attach_block(self, new: Block) -> Block: + if not self.can_next: + raise exceptions.BadBlockShape(f"{self.block_shape} cannot be stacked onto") + elif new.block_shape.is_hat or not new.block_shape.is_stack: + raise exceptions.BadBlockShape(f"{new.block_shape} is not stackable") + + new.parent = self + new.next = self.next + + self.next = new + + new.check_toplevel() + self.sprite.add_block(new) + + return new + + def duplicate_single_block(self) -> Block: + return self.attach_block(self.dcopy()) + + def attach_chain(self, *chain: Iterable[Block]) -> Block: + attaching_block = self + for _block in chain: + attaching_block = attaching_block.attach_block(_block) + + return attaching_block + + def duplicate_chain(self) -> Block: + return self.bottom_level_block.attach_chain( + *map(Block.dcopy, self.attached_chain) + ) + + def slot_above(self, new: Block) -> Block: + if not new.can_next: + raise exceptions.BadBlockShape(f"{new.block_shape} cannot be stacked onto") + + elif self.block_shape.is_hat or not self.block_shape.is_stack: + raise exceptions.BadBlockShape(f"{self.block_shape} is not stackable") + + new.parent, new.next = self.parent, self + + self.parent = new + + if new.parent: + new.parent.next = new + + return self.sprite.add_block(new) + + def delete_single_block(self): + if self.is_next_block: + self.parent.next = self.next + + if self.next: + self.next.parent = self.parent + + if self.is_top_level: + self.next.is_top_level = True + self.next.x, self.next.y = self.next.x, self.next.y + + self.sprite.remove_block(self) + + def delete_chain(self): + for _block in self.attached_chain: + _block.delete_single_block() + diff --git a/scratchattach/editor/blockshape.py b/scratchattach/editor/blockshape.py new file mode 100644 index 0000000..ebfa661 --- /dev/null +++ b/scratchattach/editor/blockshape.py @@ -0,0 +1,353 @@ +""" +Enums stating the shape of a block from its opcode (i.e: stack, c-mouth, cap, hat etc) +""" +from __future__ import annotations + +# Perhaps this should be merged with pallet.py +from dataclasses import dataclass +from typing import Final + +from . import commons +from ..utils.enums import _EnumWrapper + + +class _MutationDependent(commons.Singleton): + def __bool__(self): + raise TypeError("Need mutation data to work out attribute value.") + + +MUTATION_DEPENDENT: Final[_MutationDependent] = _MutationDependent() +"""Value used when mutation data is required to work out the attribute value""" + + +@dataclass(init=True, repr=True) +class BlockShape: + """ + A class that describes the shape of a block; e.g. is it a stack, c-mouth, cap, hat reporter, boolean or menu block? + """ + is_stack: bool | _MutationDependent = False # Most blocks - e.g. move [10] steps + is_c_mouth: bool | _MutationDependent = False # Has substack - e.g. repeat + is_cap: bool | _MutationDependent = False # No next - e.g. forever + is_hat: bool | _MutationDependent = False # No parent - e.g. when gf clicked + is_reporter: bool | _MutationDependent = False # (reporter) + is_boolean: bool | _MutationDependent = False # + is_menu: bool | _MutationDependent = False # Shadow reporters, e.g. costume menu + opcode: str = None + + @property + def is_attachable(self): + if self.is_cap is MUTATION_DEPENDENT: + raise TypeError( + "Can't tell if the block is attachable because we can't be sure if it is a cap block or not (stop block)") + return not self.is_cap and not self.is_reporter + + +class BlockShapes(_EnumWrapper): + MOTION_MOVESTEPS = BlockShape(is_stack=True, opcode="motion_movesteps") + MOTION_TURNRIGHT = BlockShape(is_stack=True, opcode="motion_turnright") + MOTION_TURNLEFT = BlockShape(is_stack=True, opcode="motion_turnleft") + MOTION_GOTO = BlockShape(is_stack=True, opcode="motion_goto") + MOTION_GOTOXY = BlockShape(is_stack=True, opcode="motion_gotoxy") + MOTION_GLIDETO = BlockShape(is_stack=True, opcode="motion_glideto") + MOTION_GLIDESECSTOXY = BlockShape(is_stack=True, opcode="motion_glidesecstoxy") + MOTION_POINTINDIRECTION = BlockShape(is_stack=True, opcode="motion_pointindirection") + MOTION_POINTTOWARDS = BlockShape(is_stack=True, opcode="motion_pointtowards") + MOTION_CHANGEXBY = BlockShape(is_stack=True, opcode="motion_changexby") + MOTION_SETX = BlockShape(is_stack=True, opcode="motion_setx") + MOTION_CHANGEYBY = BlockShape(is_stack=True, opcode="motion_changeyby") + MOTION_SETY = BlockShape(is_stack=True, opcode="motion_sety") + MOTION_IFONEDGEBOUNCE = BlockShape(is_stack=True, opcode="motion_ifonedgebounce") + MOTION_SETROTATIONSTYLE = BlockShape(is_stack=True, opcode="motion_setrotationstyle") + MOTION_XPOSITION = BlockShape(is_reporter=True, opcode="motion_xposition") + MOTION_YPOSITION = BlockShape(is_reporter=True, opcode="motion_yposition") + MOTION_DIRECTION = BlockShape(is_reporter=True, opcode="motion_direction") + MOTION_SCROLL_RIGHT = BlockShape(is_stack=True, opcode="motion_scroll_right") + MOTION_SCROLL_UP = BlockShape(is_stack=True, opcode="motion_scroll_up") + MOTION_ALIGN_SCENE = BlockShape(is_stack=True, opcode="motion_align_scene") + MOTION_XSCROLL = BlockShape(is_reporter=True, opcode="motion_xscroll") + MOTION_YSCROLL = BlockShape(is_reporter=True, opcode="motion_yscroll") + MOTION_GOTO_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="motion_goto_menu") + MOTION_GLIDETO_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="motion_glideto_menu") + MOTION_POINTTOWARDS_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="motion_pointtowards_menu") + + LOOKS_SAYFORSECS = BlockShape(is_stack=True, opcode="looks_sayforsecs") + LOOKS_SAY = BlockShape(is_stack=True, opcode="looks_say") + LOOKS_THINKFORSECS = BlockShape(is_stack=True, opcode="looks_thinkforsecs") + LOOKS_THINK = BlockShape(is_stack=True, opcode="looks_think") + LOOKS_SWITCHCOSTUMETO = BlockShape(is_stack=True, opcode="looks_switchcostumeto") + LOOKS_NEXTCOSTUME = BlockShape(is_stack=True, opcode="looks_nextcostume") + LOOKS_SWITCHBACKDROPTO = BlockShape(is_stack=True, opcode="looks_switchbackdropto") + LOOKS_SWITCHBACKDROPTOANDWAIT = BlockShape(is_stack=True, opcode="looks_switchbackdroptoandwait") + LOOKS_NEXTBACKDROP = BlockShape(is_stack=True, opcode="looks_nextbackdrop") + LOOKS_CHANGESIZEBY = BlockShape(is_stack=True, opcode="looks_changesizeby") + LOOKS_SETSIZETO = BlockShape(is_stack=True, opcode="looks_setsizeto") + LOOKS_CHANGEEFFECTBY = BlockShape(is_stack=True, opcode="looks_changeeffectby") + LOOKS_SETEFFECTTO = BlockShape(is_stack=True, opcode="looks_seteffectto") + LOOKS_CLEARGRAPHICEFFECTS = BlockShape(is_stack=True, opcode="looks_cleargraphiceffects") + LOOKS_SHOW = BlockShape(is_stack=True, opcode="looks_show") + LOOKS_HIDE = BlockShape(is_stack=True, opcode="looks_hide") + LOOKS_GOTOFRONTBACK = BlockShape(is_stack=True, opcode="looks_gotofrontback") + LOOKS_GOFORWARDBACKWARDLAYERS = BlockShape(is_stack=True, opcode="looks_goforwardbackwardlayers") + LOOKS_COSTUMENUMBERNAME = BlockShape(is_reporter=True, opcode="looks_costumenumbername") + LOOKS_BACKDROPNUMBERNAME = BlockShape(is_reporter=True, opcode="looks_backdropnumbername") + LOOKS_SIZE = BlockShape(is_reporter=True, opcode="looks_size") + LOOKS_HIDEALLSPRITES = BlockShape(is_stack=True, opcode="looks_hideallsprites") + LOOKS_SETSTRETCHTO = BlockShape(is_stack=True, opcode="looks_setstretchto") + LOOKS_CHANGESTRETCHBY = BlockShape(is_stack=True, opcode="looks_changestretchby") + LOOKS_COSTUME = BlockShape(is_reporter=True, is_menu=True, opcode="looks_costume") + LOOKS_BACKDROPS = BlockShape(is_reporter=True, is_menu=True, opcode="looks_backdrops") + + SOUND_PLAYUNTILDONE = BlockShape(is_stack=True, opcode="sound_playuntildone") + SOUND_PLAY = BlockShape(is_stack=True, opcode="sound_play") + SOUND_STOPALLSOUNDS = BlockShape(is_stack=True, opcode="sound_stopallsounds") + SOUND_CHANGEEFFECTBY = BlockShape(is_stack=True, opcode="sound_changeeffectby") + SOUND_SETEFFECTTO = BlockShape(is_stack=True, opcode="sound_seteffectto") + SOUND_CLEAREFFECTS = BlockShape(is_stack=True, opcode="sound_cleareffects") + SOUND_CHANGEVOLUMEBY = BlockShape(is_stack=True, opcode="sound_changevolumeby") + SOUND_SETVOLUMETO = BlockShape(is_stack=True, opcode="sound_setvolumeto") + SOUND_VOLUME = BlockShape(is_reporter=True, opcode="sound_volume") + SOUND_SOUNDS_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="sound_sounds_menu") + + EVENT_WHENFLAGCLICKED = BlockShape(is_hat=True, opcode="event_whenflagclicked") + EVENT_WHENKEYPRESSED = BlockShape(is_hat=True, opcode="event_whenkeypressed") + EVENT_WHENTHISSPRITECLICKED = BlockShape(is_hat=True, opcode="event_whenthisspriteclicked") + EVENT_WHENSTAGECLICKED = BlockShape(is_hat=True, opcode="event_whenstageclicked") + EVENT_WHENBACKDROPSWITCHESTO = BlockShape(is_hat=True, opcode="event_whenbackdropswitchesto") + EVENT_WHENGREATERTHAN = BlockShape(is_hat=True, opcode="event_whengreaterthan") + EVENT_WHENBROADCASTRECEIVED = BlockShape(is_hat=True, opcode="event_whenbroadcastreceived") + EVENT_BROADCAST = BlockShape(is_stack=True, opcode="event_broadcast") + EVENT_BROADCASTANDWAIT = BlockShape(is_stack=True, opcode="event_broadcastandwait") + EVENT_WHENTOUCHINGOBJECT = BlockShape(is_hat=True, opcode="event_whentouchingobject") + EVENT_BROADCAST_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="event_broadcast_menu") + EVENT_TOUCHINGOBJECTMENU = BlockShape(is_reporter=True, is_menu=True, opcode="event_touchingobjectmenu") + + CONTROL_WAIT = BlockShape(is_stack=True, opcode="control_wait") + CONTROL_FOREVER = BlockShape(is_c_mouth=True, is_stack=True, is_cap=True, opcode="control_forever") + CONTROL_IF = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_if") + CONTROL_IF_ELSE = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_if_else") + CONTROL_WAIT_UNTIL = BlockShape(is_stack=True, opcode="control_wait_until") + CONTROL_REPEAT_UNTIL = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_repeat_until") + CONTROL_STOP = BlockShape(is_stack=True, is_cap=MUTATION_DEPENDENT, opcode="control_stop") + CONTROL_START_AS_CLONE = BlockShape(is_hat=True, opcode="control_start_as_clone") + CONTROL_CREATE_CLONE_OF = BlockShape(is_stack=True, opcode="control_create_clone_of") + CONTROL_DELETE_THIS_CLONE = BlockShape(is_stack=True, is_cap=True, opcode="control_delete_this_clone") + CONTROL_FOR_EACH = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_for_each") + CONTROL_WHILE = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_while") + CONTROL_GET_COUNTER = BlockShape(is_reporter=True, opcode="control_get_counter") + CONTROL_INCR_COUNTER = BlockShape(is_stack=True, opcode="control_incr_counter") + CONTROL_CLEAR_COUNTER = BlockShape(is_stack=True, opcode="control_clear_counter") + CONTROL_ALL_AT_ONCE = BlockShape(is_c_mouth=True, is_stack=True, opcode="control_all_at_once") + CONTROL_CREATE_CLONE_OF_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="control_create_clone_of_menu") + + SENSING_TOUCHINGOBJECT = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_touchingobject") + SENSING_TOUCHINGCOLOR = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_touchingcolor") + SENSING_COLORISTOUCHINGCOLOR = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_coloristouchingcolor") + SENSING_DISTANCETO = BlockShape(is_reporter=True, opcode="sensing_distanceto") + SENSING_ASKANDWAIT = BlockShape(is_stack=True, opcode="sensing_askandwait") + SENSING_ANSWER = BlockShape(is_reporter=True, opcode="sensing_answer") + SENSING_KEYPRESSED = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_keypressed") + SENSING_MOUSEDOWN = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_mousedown") + SENSING_MOUSEX = BlockShape(is_reporter=True, opcode="sensing_mousex") + SENSING_MOUSEY = BlockShape(is_reporter=True, opcode="sensing_mousey") + SENSING_SETDRAGMODE = BlockShape(is_stack=True, opcode="sensing_setdragmode") + SENSING_LOUDNESS = BlockShape(is_reporter=True, opcode="sensing_loudness") + SENSING_TIMER = BlockShape(is_reporter=True, opcode="sensing_timer") + SENSING_RESETTIMER = BlockShape(is_stack=True, opcode="sensing_resettimer") + SENSING_OF = BlockShape(is_reporter=True, opcode="sensing_of") + SENSING_CURRENT = BlockShape(is_reporter=True, opcode="sensing_current") + SENSING_DAYSSINCE2000 = BlockShape(is_reporter=True, opcode="sensing_dayssince2000") + SENSING_USERNAME = BlockShape(is_reporter=True, opcode="sensing_username") + SENSING_LOUD = BlockShape(is_reporter=True, is_boolean=True, opcode="sensing_loud") + SENSING_USERID = BlockShape(is_reporter=True, opcode="sensing_userid") + SENSING_TOUCHINGOBJECTMENU = BlockShape(is_reporter=True, is_menu=True, opcode="sensing_touchingobjectmenu") + SENSING_DISTANCETOMENU = BlockShape(is_reporter=True, is_menu=True, opcode="sensing_distancetomenu") + SENSING_KEYOPTIONS = BlockShape(is_reporter=True, is_menu=True, opcode="sensing_keyoptions") + SENSING_OF_OBJECT_MENU = BlockShape(is_reporter=True, is_menu=True, opcode="sensing_of_object_menu") + + OPERATOR_ADD = BlockShape(is_reporter=True, opcode="operator_add") + OPERATOR_SUBTRACT = BlockShape(is_reporter=True, opcode="operator_subtract") + OPERATOR_MULTIPLY = BlockShape(is_reporter=True, opcode="operator_multiply") + OPERATOR_DIVIDE = BlockShape(is_reporter=True, opcode="operator_divide") + OPERATOR_RANDOM = BlockShape(is_reporter=True, opcode="operator_random") + OPERATOR_GT = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_gt") + OPERATOR_LT = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_lt") + OPERATOR_EQUALS = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_equals") + OPERATOR_AND = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_and") + OPERATOR_OR = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_or") + OPERATOR_NOT = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_not") + OPERATOR_JOIN = BlockShape(is_reporter=True, opcode="operator_join") + OPERATOR_LETTER_OF = BlockShape(is_reporter=True, opcode="operator_letter_of") + OPERATOR_LENGTH = BlockShape(is_reporter=True, opcode="operator_length") + OPERATOR_CONTAINS = BlockShape(is_reporter=True, is_boolean=True, opcode="operator_contains") + OPERATOR_MOD = BlockShape(is_reporter=True, opcode="operator_mod") + OPERATOR_ROUND = BlockShape(is_reporter=True, opcode="operator_round") + OPERATOR_MATHOP = BlockShape(is_reporter=True, opcode="operator_mathop") + + DATA_VARIABLE = BlockShape(is_reporter=True, opcode="data_variable") + DATA_SETVARIABLETO = BlockShape(is_stack=True, opcode="data_setvariableto") + DATA_CHANGEVARIABLEBY = BlockShape(is_stack=True, opcode="data_changevariableby") + DATA_SHOWVARIABLE = BlockShape(is_stack=True, opcode="data_showvariable") + DATA_HIDEVARIABLE = BlockShape(is_stack=True, opcode="data_hidevariable") + DATA_LISTCONTENTS = BlockShape(is_reporter=True, opcode="data_listcontents") + DATA_ADDTOLIST = BlockShape(is_stack=True, opcode="data_addtolist") + DATA_DELETEOFLIST = BlockShape(is_stack=True, opcode="data_deleteoflist") + DATA_DELETEALLOFLIST = BlockShape(is_stack=True, opcode="data_deletealloflist") + DATA_INSERTATLIST = BlockShape(is_stack=True, opcode="data_insertatlist") + DATA_REPLACEITEMOFLIST = BlockShape(is_stack=True, opcode="data_replaceitemoflist") + DATA_ITEMOFLIST = BlockShape(is_reporter=True, is_boolean=True, opcode="data_itemoflist") + DATA_ITEMNUMOFLIST = BlockShape(is_reporter=True, opcode="data_itemnumoflist") + DATA_LENGTHOFLIST = BlockShape(is_reporter=True, opcode="data_lengthoflist") + DATA_LISTCONTAINSITEM = BlockShape(is_reporter=True, is_boolean=True, opcode="data_listcontainsitem") + DATA_SHOWLIST = BlockShape(is_stack=True, opcode="data_showlist") + DATA_HIDELIST = BlockShape(is_stack=True, opcode="data_hidelist") + DATA_LISTINDEXALL = BlockShape(is_reporter=True, is_menu=True, opcode="data_listindexall") + DATA_LISTINDEXRANDOM = BlockShape(is_reporter=True, is_menu=True, opcode="data_listindexrandom") + + PROCEDURES_DEFINITION = BlockShape(is_hat=True, opcode="procedures_definition") + PROCEDURES_CALL = BlockShape(is_stack=True, opcode="procedures_call") + PROCEDURES_DECLARATION = BlockShape(is_stack=True, opcode="procedures_declaration") + PROCEDURES_PROTOTYPE = BlockShape(is_stack=True, opcode="procedures_prototype") + + ARGUMENT_REPORTER_STRING_NUMBER = BlockShape(is_reporter=True, opcode="argument_reporter_string_number") + ARGUMENT_REPORTER_BOOLEAN = BlockShape(is_reporter=True, is_boolean=True, opcode="argument_reporter_boolean") + ARGUMENT_EDITOR_REPORTER = BlockShape(is_reporter=True, is_boolean=True, opcode="argument_editor_reporter") + ARGUMENT_EDITOR_STRING_NUMBER = BlockShape(is_reporter=True, opcode="argument_editor_string_number") + + MUSIC_PLAYDRUMFORBEATS = BlockShape(is_stack=True, opcode="music_playDrumForBeats") + MUSIC_RESTFORBEATS = BlockShape(is_stack=True, opcode="music_restForBeats") + MUSIC_PLAYNOTEFORBEATS = BlockShape(is_stack=True, opcode="music_playNoteForBeats") + MUSIC_SETINSTRUMENT = BlockShape(is_stack=True, opcode="music_setInstrument") + MUSIC_SETTEMPO = BlockShape(is_stack=True, opcode="music_setTempo") + MUSIC_CHANGETEMPO = BlockShape(is_stack=True, opcode="music_changeTempo") + MUSIC_GETTEMPO = BlockShape(is_reporter=True, opcode="music_getTempo") + MUSIC_MIDIPLAYDRUMFORBEATS = BlockShape(is_stack=True, opcode="music_midiPlayDrumForBeats") + MUSIC_MIDISETINSTRUMENT = BlockShape(is_stack=True, opcode="music_midiSetInstrument") + MUSIC_MENU_DRUM = BlockShape(is_reporter=True, is_menu=True, opcode="music_menu_DRUM") + MUSIC_MENU_INSTRUMENT = BlockShape(is_reporter=True, is_menu=True, opcode="music_menu_INSTRUMENT") + + PEN_CLEAR = BlockShape(is_stack=True, opcode="pen_clear") + PEN_STAMP = BlockShape(is_stack=True, opcode="pen_stamp") + PEN_PENDOWN = BlockShape(is_stack=True, opcode="pen_penDown") + PEN_PENUP = BlockShape(is_stack=True, opcode="pen_penUp") + PEN_SETPENCOLORTOCOLOR = BlockShape(is_stack=True, opcode="pen_setPenColorToColor") + PEN_CHANGEPENCOLORPARAMBY = BlockShape(is_stack=True, opcode="pen_changePenColorParamBy") + PEN_SETPENCOLORPARAMTO = BlockShape(is_stack=True, opcode="pen_setPenColorParamTo") + PEN_CHANGEPENSIZEBY = BlockShape(is_stack=True, opcode="pen_changePenSizeBy") + PEN_SETPENSIZETO = BlockShape(is_stack=True, opcode="pen_setPenSizeTo") + PEN_SETPENHUETONUMBER = BlockShape(is_stack=True, opcode="pen_setPenHueToNumber") + PEN_CHANGEPENHUEBY = BlockShape(is_stack=True, opcode="pen_changePenHueBy") + PEN_SETPENSHADETONUMBER = BlockShape(is_stack=True, opcode="pen_setPenShadeToNumber") + PEN_CHANGEPENSHADEBY = BlockShape(is_stack=True, opcode="pen_changePenShadeBy") + PEN_MENU_COLORPARAM = BlockShape(is_reporter=True, is_menu=True, opcode="pen_menu_colorParam") + + VIDEOSENSING_WHENMOTIONGREATERTHAN = BlockShape(is_hat=True, opcode="videoSensing_whenMotionGreaterThan") + VIDEOSENSING_VIDEOON = BlockShape(is_reporter=True, opcode="videoSensing_videoOn") + VIDEOSENSING_VIDEOTOGGLE = BlockShape(is_stack=True, opcode="videoSensing_videoToggle") + VIDEOSENSING_SETVIDEOTRANSPARENCY = BlockShape(is_stack=True, opcode="videoSensing_setVideoTransparency") + VIDEOSENSING_MENU_ATTRIBUTE = BlockShape(is_reporter=True, is_menu=True, opcode="videoSensing_menu_ATTRIBUTE") + VIDEOSENSING_MENU_SUBJECT = BlockShape(is_reporter=True, is_menu=True, opcode="videoSensing_menu_SUBJECT") + VIDEOSENSING_MENU_VIDEO_STATE = BlockShape(is_reporter=True, is_menu=True, opcode="videoSensing_menu_VIDEO_STATE") + + TEXT2SPEECH_SPEAKANDWAIT = BlockShape(is_stack=True, opcode="text2speech_speakAndWait") + TEXT2SPEECH_SETVOICE = BlockShape(is_stack=True, opcode="text2speech_setVoice") + TEXT2SPEECH_SETLANGUAGE = BlockShape(is_stack=True, opcode="text2speech_setLanguage") + TEXT2SPEECH_MENU_VOICES = BlockShape(is_reporter=True, is_menu=True, opcode="text2speech_menu_voices") + TEXT2SPEECH_MENU_LANGUAGES = BlockShape(is_reporter=True, is_menu=True, opcode="text2speech_menu_languages") + TRANSLATE_GETTRANSLATE = BlockShape(is_reporter=True, opcode="translate_getTranslate") + TRANSLATE_GETVIEWERLANGUAGE = BlockShape(is_reporter=True, opcode="translate_getViewerLanguage") + TRANSLATE_MENU_LANGUAGES = BlockShape(is_reporter=True, is_menu=True, opcode="translate_menu_languages") + + MAKEYMAKEY_WHENMAKEYKEYPRESSED = BlockShape(is_hat=True, opcode="makeymakey_whenMakeyKeyPressed") + MAKEYMAKEY_WHENCODEPRESSED = BlockShape(is_hat=True, opcode="makeymakey_whenCodePressed") + MAKEYMAKEY_MENU_KEY = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_KEY") + MAKEYMAKEY_MENU_SEQUENCE = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_SEQUENCE") + + MICROBIT_WHENBUTTONPRESSED = BlockShape(opcode="microbit_whenButtonPressed") + MICROBIT_ISBUTTONPRESSED = BlockShape(opcode="microbit_isButtonPressed") + MICROBIT_WHENGESTURE = BlockShape(opcode="microbit_whenGesture") + MICROBIT_DISPLAYSYMBOL = BlockShape(opcode="microbit_displaySymbol") + MICROBIT_DISPLAYTEXT = BlockShape(opcode="microbit_displayText") + MICROBIT_DISPLAYCLEAR = BlockShape(opcode="microbit_displayClear") + MICROBIT_WHENTILTED = BlockShape(opcode="microbit_whenTilted") + MICROBIT_ISTILTED = BlockShape(opcode="microbit_isTilted") + MICROBIT_GETTILTANGLE = BlockShape(opcode="microbit_getTiltAngle") + MICROBIT_WHENPINCONNECTED = BlockShape(opcode="microbit_whenPinConnected") + MICROBIT_MENU_BUTTONS = BlockShape(opcode="microbit_menu_buttons") + MICROBIT_MENU_GESTURES = BlockShape(opcode="microbit_menu_gestures") + MICROBIT_MENU_TILTDIRECTIONANY = BlockShape(opcode="microbit_menu_tiltDirectionAny") + MICROBIT_MENU_TILTDIRECTION = BlockShape(opcode="microbit_menu_tiltDirection") + MICROBIT_MENU_TOUCHPINS = BlockShape(opcode="microbit_menu_touchPins") + MICROBIT_MENU_PINSTATE = BlockShape(opcode="microbit_menu_pinState") + + EV3_MOTORTURNCLOCKWISE = BlockShape(opcode="ev3_motorTurnClockwise") + EV3_MOTORTURNCOUNTERCLOCKWISE = BlockShape(opcode="ev3_motorTurnCounterClockwise") + EV3_MOTORSETPOWER = BlockShape(opcode="ev3_motorSetPower") + EV3_GETMOTORPOSITION = BlockShape(opcode="ev3_getMotorPosition") + EV3_WHENBUTTONPRESSED = BlockShape(opcode="ev3_whenButtonPressed") + EV3_WHENDISTANCELESSTHAN = BlockShape(opcode="ev3_whenDistanceLessThan") + EV3_WHENBRIGHTNESSLESSTHAN = BlockShape(opcode="ev3_whenBrightnessLessThan") + EV3_BUTTONPRESSED = BlockShape(opcode="ev3_buttonPressed") + EV3_GETDISTANCE = BlockShape(opcode="ev3_getDistance") + EV3_GETBRIGHTNESS = BlockShape(opcode="ev3_getBrightness") + EV3_BEEP = BlockShape(opcode="ev3_beep") + EV3_MENU_MOTORPORTS = BlockShape(opcode="ev3_menu_motorPorts") + EV3_MENU_SENSORPORTS = BlockShape(opcode="ev3_menu_sensorPorts") + + BOOST_MOTORONFOR = BlockShape(opcode="boost_motorOnFor") + BOOST_MOTORONFORROTATION = BlockShape(opcode="boost_motorOnForRotation") + BOOST_MOTORON = BlockShape(opcode="boost_motorOn") + BOOST_MOTOROFF = BlockShape(opcode="boost_motorOff") + BOOST_SETMOTORPOWER = BlockShape(opcode="boost_setMotorPower") + BOOST_SETMOTORDIRECTION = BlockShape(opcode="boost_setMotorDirection") + BOOST_GETMOTORPOSITION = BlockShape(opcode="boost_getMotorPosition") + BOOST_WHENCOLOR = BlockShape(opcode="boost_whenColor") + BOOST_SEEINGCOLOR = BlockShape(opcode="boost_seeingColor") + BOOST_WHENTILTED = BlockShape(opcode="boost_whenTilted") + BOOST_GETTILTANGLE = BlockShape(opcode="boost_getTiltAngle") + BOOST_SETLIGHTHUE = BlockShape(opcode="boost_setLightHue") + BOOST_MENU_MOTOR_ID = BlockShape(opcode="boost_menu_MOTOR_ID") + BOOST_MENU_MOTOR_DIRECTION = BlockShape(opcode="boost_menu_MOTOR_DIRECTION") + BOOST_MENU_MOTOR_REPORTER_ID = BlockShape(opcode="boost_menu_MOTOR_REPORTER_ID") + BOOST_MENU_COLOR = BlockShape(opcode="boost_menu_COLOR") + BOOST_MENU_TILT_DIRECTION_ANY = BlockShape(opcode="boost_menu_TILT_DIRECTION_ANY") + BOOST_MENU_TILT_DIRECTION = BlockShape(opcode="boost_menu_TILT_DIRECTION") + + WEDO2_MOTORONFOR = BlockShape(opcode="wedo2_motorOnFor") + WEDO2_MOTORON = BlockShape(opcode="wedo2_motorOn") + WEDO2_MOTOROFF = BlockShape(opcode="wedo2_motorOff") + WEDO2_STARTMOTORPOWER = BlockShape(opcode="wedo2_startMotorPower") + WEDO2_SETMOTORDIRECTION = BlockShape(opcode="wedo2_setMotorDirection") + WEDO2_SETLIGHTHUE = BlockShape(opcode="wedo2_setLightHue") + WEDO2_WHENDISTANCE = BlockShape(opcode="wedo2_whenDistance") + WEDO2_WHENTILTED = BlockShape(opcode="wedo2_whenTilted") + WEDO2_GETDISTANCE = BlockShape(opcode="wedo2_getDistance") + WEDO2_ISTILTED = BlockShape(opcode="wedo2_isTilted") + WEDO2_GETTILTANGLE = BlockShape(opcode="wedo2_getTiltAngle") + WEDO2_PLAYNOTEFOR = BlockShape(opcode="wedo2_playNoteFor") + WEDO2_MENU_MOTOR_ID = BlockShape(opcode="wedo2_menu_MOTOR_ID") + WEDO2_MENU_MOTOR_DIRECTION = BlockShape(opcode="wedo2_menu_MOTOR_DIRECTION") + WEDO2_MENU_OP = BlockShape(opcode="wedo2_menu_OP") + WEDO2_MENU_TILT_DIRECTION_ANY = BlockShape(opcode="wedo2_menu_TILT_DIRECTION_ANY") + WEDO2_MENU_TILT_DIRECTION = BlockShape(opcode="wedo2_menu_TILT_DIRECTION") + + GDXFOR_WHENGESTURE = BlockShape(opcode="gdxfor_whenGesture") + GDXFOR_WHENFORCEPUSHEDORPULLED = BlockShape(opcode="gdxfor_whenForcePushedOrPulled") + GDXFOR_GETFORCE = BlockShape(opcode="gdxfor_getForce") + GDXFOR_WHENTILTED = BlockShape(opcode="gdxfor_whenTilted") + GDXFOR_ISTILTED = BlockShape(opcode="gdxfor_isTilted") + GDXFOR_GETTILT = BlockShape(opcode="gdxfor_getTilt") + GDXFOR_ISFREEFALLING = BlockShape(opcode="gdxfor_isFreeFalling") + GDXFOR_GETSPINSPEED = BlockShape(opcode="gdxfor_getSpinSpeed") + GDXFOR_GETACCELERATION = BlockShape(opcode="gdxfor_getAcceleration") + GDXFOR_MENU_GESTUREOPTIONS = BlockShape(opcode="gdxfor_menu_gestureOptions") + GDXFOR_MENU_PUSHPULLOPTIONS = BlockShape(opcode="gdxfor_menu_pushPullOptions") + GDXFOR_MENU_TILTANYOPTIONS = BlockShape(opcode="gdxfor_menu_tiltAnyOptions") + GDXFOR_MENU_TILTOPTIONS = BlockShape(opcode="gdxfor_menu_tiltOptions") + GDXFOR_MENU_AXISOPTIONS = BlockShape(opcode="gdxfor_menu_axisOptions") + + COREEXAMPLE_EXAMPLEOPCODE = BlockShape(is_reporter=True, opcode="coreExample_exampleOpcode") + COREEXAMPLE_EXAMPLEWITHINLINEIMAGE = BlockShape(is_stack=True, opcode="coreExample_exampleWithInlineImage") + + NOTE = BlockShape(is_reporter=True, is_menu=True, opcode="note") + MATRIX = BlockShape(is_reporter=True, is_menu=True, opcode="matrix") + UNDEFINED = BlockShape(is_hat=True, is_cap=True, opcode="red_hat_block") diff --git a/scratchattach/editor/build_defaulting.py b/scratchattach/editor/build_defaulting.py new file mode 100644 index 0000000..b7d22f7 --- /dev/null +++ b/scratchattach/editor/build_defaulting.py @@ -0,0 +1,47 @@ +""" +Module which stores the 'default' or 'current' selected Sprite/project (stored as a stack) which makes it easier to write scratch code directly in Python +""" +from __future__ import annotations + +from typing import Iterable, TYPE_CHECKING, Final + +if TYPE_CHECKING: + from . import sprite, block, prim, comment +from . import commons + + +class _SetSprite(commons.Singleton): + def __repr__(self): + return f'' + + +SPRITE_DEFAULT: Final[_SetSprite] = _SetSprite() + +_sprite_stack: list[sprite.Sprite] = [] + + +def stack_add_sprite(_sprite: sprite.Sprite): + _sprite_stack.append(_sprite) + + +def current_sprite() -> sprite.Sprite | None: + if len(_sprite_stack) == 0: + return None + return _sprite_stack[-1] + + +def pop_sprite(_sprite: sprite.Sprite) -> sprite.Sprite | None: + assert _sprite_stack.pop() == _sprite + return _sprite + + +def add_block(_block: block.Block | prim.Prim) -> block.Block | prim.Prim: + return current_sprite().add_block(_block) + + +def add_chain(*chain: Iterable[block.Block, prim.Prim]) -> block.Block | prim.Prim: + return current_sprite().add_chain(*chain) + + +def add_comment(_comment: comment.Comment): + return current_sprite().add_comment(_comment) diff --git a/scratchattach/editor/comment.py b/scratchattach/editor/comment.py new file mode 100644 index 0000000..d436e24 --- /dev/null +++ b/scratchattach/editor/comment.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from . import base, block, sprite, build_defaulting + + +class Comment(base.IDComponent): + def __init__(self, _id: str = None, _block: block.Block = None, x: int = 0, y: int = 0, width: int = 200, + height: int = 200, minimized: bool = False, text: str = '', *, _block_id: str = None, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT, pos: tuple[int, int] = None): + self.block = _block + self._block_id = _block_id + """ + ID of connected block. Will be set to None upon sprite initialization when the block attribute is updated to the relevant Block. + """ + if pos is not None: + x, y = pos + + self.x = x + self.y = y + + self.width = width + self.height = height + + self.minimized = minimized + self.text = text + + super().__init__(_id, _sprite) + + def __repr__(self): + return f"Comment<{self.text[:10]!r}...>" + + @property + def block_id(self): + if self.block is not None: + return self.block.id + elif self._block_id is not None: + return self._block_id + else: + return None + + @staticmethod + def from_json(data: tuple[str, dict]): + assert len(data) == 2 + _id, data = data + + _block_id = data.get("blockId") + + x = data.get("x", 0) + y = data.get("y", 0) + + width = data.get("width", 100) + height = data.get("height", 100) + + minimized = data.get("minimized", False) + text = data.get("text") + + ret = Comment(_id, None, x, y, width, height, minimized, text, _block_id=_block_id) + return ret + + def to_json(self) -> dict: + return { + "blockId": self.block_id, + "x": self.x, "y": self.y, + "width": self.width, "height": self.height, + "minimized": self.minimized, + "text": self.text, + } + + def link_using_sprite(self): + if self._block_id is not None: + self.block = self.sprite.find_block(self._block_id, "id") + if self.block is not None: + self._block_id = None diff --git a/scratchattach/editor/commons.py b/scratchattach/editor/commons.py new file mode 100644 index 0000000..2341aa9 --- /dev/null +++ b/scratchattach/editor/commons.py @@ -0,0 +1,243 @@ +""" +Shared functions used by the editor module +""" +from __future__ import annotations + +import json +import random +import string +from typing import Final, Any + +from ..utils import exceptions + +DIGITS: Final[tuple[str]] = tuple("0123456789") + +ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation + + +# Strangely enough, it seems like something in string.punctuation causes issues. Not sure why + + +def _read_json_number(_str: str) -> float | int: + ret = '' + + minus = _str[0] == '-' + if minus: + ret += '-' + _str = _str[1:] + + def read_fraction(sub: str): + sub_ret = '' + if sub[0] == '.': + sub_ret += '.' + sub = sub[1:] + while sub[0] in DIGITS: + sub_ret += sub[0] + sub = sub[1:] + + return sub_ret, sub + + def read_exponent(sub: str): + sub_ret = '' + if sub[0].lower() == 'e': + sub_ret += sub[0] + sub = sub[1:] + + if sub[0] in "-+": + sub_ret += sub[0] + sub = sub[1:] + + if sub[0] not in DIGITS: + raise exceptions.UnclosedJSONError(f"Invalid exponent {sub}") + + while sub[0] in DIGITS: + sub_ret += sub[0] + sub = sub[1:] + + return sub_ret + + if _str[0] == '0': + ret += '0' + _str = _str[1:] + + elif _str[0] in DIGITS[1:9]: + while _str[0] in DIGITS: + ret += _str[0] + _str = _str[1:] + + frac, _str = read_fraction(_str) + ret += frac + + ret += read_exponent(_str) + + return json.loads(ret) + + +def consume_json(_str: str, i: int = 0) -> str | float | int | dict | list | bool | None: + """ + *'gobble up some JSON until we hit something not quite so tasty'* + + Reads a JSON string and stops at the natural end (i.e. when brackets close, or when quotes end, etc.) + """ + # Named by ChatGPT + section = ''.join(_str[i:]) + if section.startswith("true"): + return True + elif section.startswith("false"): + return False + elif section.startswith("null"): + return None + elif section[0] in "0123456789.-": + return _read_json_number(section) + + depth = 0 + json_text = '' + out_string = True + + for char in section: + json_text += char + + if char == '"': + if len(json_text) > 1: + unescaped = json_text[-2] != '\\' + else: + unescaped = True + if unescaped: + out_string ^= True + if out_string: + depth -= 1 + else: + depth += 1 + + if out_string: + if char in "[{": + depth += 1 + elif char in "}]": + depth -= 1 + + if depth == 0 and json_text.strip(): + return json.loads(json_text.strip()) + + raise exceptions.UnclosedJSONError(f"Unclosed JSON string, read {json_text}") + + +def is_partial_json(_str: str, i: int = 0) -> bool: + try: + consume_json(_str, i) + return True + + except exceptions.UnclosedJSONError: + return False + + except ValueError: + return False + + +def is_valid_json(_str: Any) -> bool: + try: + json.loads(_str) + return True + except ValueError: + return False + except TypeError: + return False + + +def noneless_update(obj: dict, update: dict) -> None: + for key, value in update.items(): + if value is not None: + obj[key] = value + + +def remove_nones(obj: dict) -> None: + """ + Removes all None values from a dict. + :param obj: Dictionary to remove all None values. + """ + nones = [] + for key, value in obj.items(): + if value is None: + nones.append(key) + for key in nones: + del obj[key] + + +def safe_get(lst: list | tuple, _i: int, default: Any = None) -> Any: + if len(lst) <= _i: + return default + else: + return lst[_i] + + +def trim_final_nones(lst: list) -> list: + """ + Removes the last None values from a list until a non-None value is hit. + :param lst: list which will **not** be modified. + """ + i = len(lst) + for item in lst[::-1]: + if item is not None: + break + i -= 1 + return lst[:i] + + +def dumps_ifnn(obj: Any) -> str: + if obj is None: + return None + else: + return json.dumps(obj) + + +def gen_id() -> str: + # The old 'naïve' method but that chances of a repeat are so miniscule + # Have to check if whitespace chars break it + # May later add checking within sprites so that we don't need such long ids (we can save space this way) + return ''.join(random.choices(ID_CHARS, k=20)) + + +def sanitize_fn(filename: str): + """ + Removes illegal chars from a filename + :return: Sanitized filename + """ + # Maybe could import a slugify module, but it's a bit overkill + ret = '' + for char in filename: + if char in string.ascii_letters + string.digits + "-_": + ret += char + else: + ret += '_' + return ret + + +def get_folder_name(name: str) -> str | None: + if name.startswith('//'): + return None + + if '//' in name: + return name.split('//')[0] + else: + return None + + +def get_name_nofldr(name: str) -> str: + """ + Get the sprite/asset name without the folder name + """ + fldr = get_folder_name(name) + if fldr is None: + return name + else: + return name[len(fldr) + 2:] + + +class Singleton(object): + _instance: Singleton + + def __new__(cls, *args, **kwargs): + if hasattr(cls, "_instance"): + return cls._instance + else: + cls._instance = super(Singleton, cls).__new__(cls) + return cls._instance diff --git a/scratchattach/editor/extension.py b/scratchattach/editor/extension.py new file mode 100644 index 0000000..81bfed0 --- /dev/null +++ b/scratchattach/editor/extension.py @@ -0,0 +1,43 @@ +from __future__ import annotations + + +from dataclasses import dataclass + +from . import base +from ..utils import enums + + +@dataclass(init=True, repr=True) +class Extension(base.JSONSerializable): + code: str + name: str = None + + def __eq__(self, other): + return self.code == other.code + + @staticmethod + def from_json(data: str): + assert isinstance(data, str) + _extension = Extensions.find(data, "code") + if _extension is None: + _extension = Extension(data) + + return _extension + + def to_json(self) -> str: + return self.code + + +class Extensions(enums._EnumWrapper): + BOOST = Extension("boost", "LEGO BOOST Extension") + EV3 = Extension("ev3", "LEGO MINDSTORMS EV3 Extension") + GDXFOR = Extension("gdxfor", "Go Direct Force & Acceleration Extension") + MAKEYMAKEY = Extension("makeymakey", "Makey Makey Extension") + MICROBIT = Extension("microbit", "micro:bit Extension") + MUSIC = Extension("music", "Music Extension") + PEN = Extension("pen", "Pen Extension") + TEXT2SPEECH = Extension("text2speech", "Text to Speech Extension") + TRANSLATE = Extension("translate", "Translate Extension") + VIDEOSENSING = Extension("videoSensing", "Video Sensing Extension") + WEDO2 = Extension("wedo2", "LEGO Education WeDo 2.0 Extension") + COREEXAMPLE = Extension("coreExample", "CoreEx Extension") diff --git a/scratchattach/editor/field.py b/scratchattach/editor/field.py new file mode 100644 index 0000000..34afed1 --- /dev/null +++ b/scratchattach/editor/field.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + + +if TYPE_CHECKING: + from . import block, vlb + +from . import base, commons + + +class Types: + VARIABLE: Final[str] = "variable" + LIST: Final[str] = "list" + BROADCAST: Final[str] = "broadcast" + DEFAULT: Final[str] = "default" + + +class Field(base.BlockSubComponent): + def __init__(self, _value: str | vlb.Variable | vlb.List | vlb.Broadcast, _id: str = None, *, _block: block.Block = None): + """ + A field for a scratch block + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=it.%5B9%5D-,fields,element%2C%20which%20is%20the%20ID%20of%20the%20field%27s%20value.%5B10%5D,-shadow + """ + self.value = _value + self.id = _id + """ + ID of associated VLB. Will be used to get VLB object during sprite initialisation, where it will be replaced with 'None' + """ + super().__init__(_block) + + def __repr__(self): + if self.id is not None: + # This shouldn't occur after sprite initialisation + return f"" + else: + return f"" + + @property + def value_id(self): + if self.id is not None: + return self.id + else: + if hasattr(self.value, "id"): + return self.value.id + else: + return None + + @property + def value_str(self): + if not isinstance(self.value, base.NamedIDComponent): + return self.value + else: + return self.value.name + + @property + def name(self) -> str: + for _name, _field in self.block.fields.items(): + if _field is self: + return _name + + @property + def type(self): + """ + Infer the type of value that this field holds + :return: A string (from field.Types) as a name of the type + """ + if "variable" in self.name.lower(): + return Types.VARIABLE + elif "list" in self.name.lower(): + return Types.LIST + elif "broadcast" in self.name.lower(): + return Types.BROADCAST + else: + return Types.DEFAULT + + @staticmethod + def from_json(data: list[str, str | None]): + # Sometimes you may have a stray field with no id. Not sure why + while len(data) < 2: + data.append(None) + data = data[:2] + + _value, _id = data + return Field(_value, _id) + + def to_json(self) -> dict: + return commons.trim_final_nones([ + self.value_str, self.value_id + ]) diff --git a/scratchattach/editor/inputs.py b/scratchattach/editor/inputs.py new file mode 100644 index 0000000..ff14a01 --- /dev/null +++ b/scratchattach/editor/inputs.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import warnings +from typing import Final + +from . import block +from . import base, commons, prim +from dataclasses import dataclass + + +@dataclass(init=True) +class ShadowStatus: + idx: int + name: str + + def __repr__(self): + return f"" + + +class ShadowStatuses: + # Not an enum so you don't need to do .value + # Uh why? + HAS_SHADOW: Final[ShadowStatus] = ShadowStatus(1, "has shadow") + NO_SHADOW: Final[ShadowStatus] = ShadowStatus(2, "no shadow") + OBSCURED: Final[ShadowStatus] = ShadowStatus(3, "obscured") + + @classmethod + def find(cls, idx: int) -> ShadowStatus: + for status in (cls.HAS_SHADOW, cls.NO_SHADOW, cls.OBSCURED): + if status.idx == idx: + return status + + if not 1 <= idx <= 3: + raise ValueError(f"Invalid ShadowStatus idx={idx}") + + +class Input(base.BlockSubComponent): + def __init__(self, _shadow: ShadowStatus | None = ShadowStatuses.HAS_SHADOW, _value: prim.Prim | block.Block | str = None, _id: str = None, + _obscurer: prim.Prim | block.Block | str = None, *, _obscurer_id: str = None, _block: block.Block = None): + """ + An input for a scratch block + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=inputs,it.%5B9%5D + """ + super().__init__(_block) + + # If the shadow is None, we'll have to work it out later + self.shadow = _shadow + + self.value: prim.Prim | block.Block = _value + self.obscurer: prim.Prim | block.Block = _obscurer + + self._id = _id + """ + ID referring to the input value. Upon project initialisation, this will be set to None and the value attribute will be set to the relevant object + """ + self._obscurer_id = _obscurer_id + """ + ID referring to the obscurer. Upon project initialisation, this will be set to None and the obscurer attribute will be set to the relevant block + """ + + def __repr__(self): + if self._id is not None: + return f"" + else: + return f"" + + @staticmethod + def from_json(data: list): + _shadow = ShadowStatuses.find(data[0]) + + _value, _id = None, None + if isinstance(data[1], list): + _value = prim.Prim.from_json(data[1]) + else: + _id = data[1] + + _obscurer_data = commons.safe_get(data, 2) + + _obscurer, _obscurer_id = None, None + if isinstance(_obscurer_data, list): + _obscurer = prim.Prim.from_json(_obscurer_data) + else: + _obscurer_id = _obscurer_data + return Input(_shadow, _value, _id, _obscurer, _obscurer_id=_obscurer_id) + + def to_json(self) -> list: + data = [self.shadow.idx] + + def add_pblock(pblock: prim.Prim | block.Block | None): + """ + Adds a primitive or a block to the data in the right format + """ + if pblock is None: + return + + if isinstance(pblock, prim.Prim): + data.append(pblock.to_json()) + + elif isinstance(pblock, block.Block): + data.append(pblock.id) + + else: + warnings.warn(f"Bad prim/block {pblock!r} of type {type(pblock)}") + + add_pblock(self.value) + add_pblock(self.obscurer) + + return data + + def link_using_block(self): + # Link to value + if self._id is not None: + new_value = self.sprite.find_block(self._id, "id") + if new_value is not None: + self.value = new_value + self._id = None + + # Link to obscurer + if self._obscurer_id is not None: + new_block = self.sprite.find_block(self._obscurer_id, "id") + if new_block is not None: + self.obscurer = new_block + self._obscurer_id = None + + # Link value to sprite + if isinstance(self.value, prim.Prim): + self.value.sprite = self.sprite + self.value.link_using_sprite() + + # Link obscurer to sprite + if self.obscurer is not None: + self.obscurer.sprite = self.sprite diff --git a/scratchattach/editor/meta.py b/scratchattach/editor/meta.py new file mode 100644 index 0000000..0f1fb56 --- /dev/null +++ b/scratchattach/editor/meta.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +from . import base, commons + + +@dataclass(init=True, repr=True) +class PlatformMeta(base.JSONSerializable): + name: str = None + url: str = field(repr=True, default=None) + + def __bool__(self): + return self.name is not None or self.url is not None + + def to_json(self) -> dict: + _json = {"name": self.name, "url": self.url} + commons.remove_nones(_json) + return _json + + @staticmethod + def from_json(data: dict | None): + if data is None: + return PlatformMeta() + else: + return PlatformMeta(data.get("name"), data.get("url")) + + +DEFAULT_VM = "0.1.0" +DEFAULT_AGENT = "scratchattach.editor by https://scratch.mit.edu/users/timmccool/" +DEFAULT_PLATFORM = PlatformMeta("scratchattach", "https://github.com/timMcCool/scratchattach/") + +EDIT_META = True +META_SET_PLATFORM = False + + +def set_meta_platform(true_false: bool = False): + global META_SET_PLATFORM + META_SET_PLATFORM = true_false + + +class Meta(base.JSONSerializable): + def __init__(self, semver: str = "3.0.0", vm: str = DEFAULT_VM, agent: str = DEFAULT_AGENT, + platform: PlatformMeta = None): + """ + Represents metadata of the project + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Metadata + """ + if platform is None and META_SET_PLATFORM: + platform = DEFAULT_PLATFORM.dcopy() + + self.semver = semver + self.vm = vm + self.agent = agent + self.platform = platform + + if not self.vm_is_valid: + raise ValueError( + f"{vm!r} does not match pattern '^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)' - maybe try '0.0.0'?") + + def __repr__(self): + data = f"{self.semver} : {self.vm} : {self.agent}" + if self.platform: + data += f": {self.platform}" + + return f"Meta<{data}>" + + @property + def vm_is_valid(self): + # Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it + return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None + + def to_json(self): + _json = { + "semver": self.semver, + "vm": self.vm, + "agent": self.agent + } + + if self.platform: + _json["platform"] = self.platform.to_json() + return _json + + @staticmethod + def from_json(data): + if data is None: + data = "" + + semver = data["semver"] + vm = data.get("vm") + agent = data.get("agent") + platform = PlatformMeta.from_json(data.get("platform")) + + if EDIT_META or vm is None: + vm = DEFAULT_VM + + if EDIT_META or agent is None: + agent = DEFAULT_AGENT + + if EDIT_META: + if META_SET_PLATFORM and not platform: + platform = DEFAULT_PLATFORM.dcopy() + + return Meta(semver, vm, agent, platform) diff --git a/scratchattach/editor/monitor.py b/scratchattach/editor/monitor.py new file mode 100644 index 0000000..0c20486 --- /dev/null +++ b/scratchattach/editor/monitor.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import project + +from . import base + + +class Monitor(base.ProjectSubcomponent): + def __init__(self, reporter: base.NamedIDComponent = None, + mode: str = "default", + opcode: str = "data_variable", + params: dict = None, + sprite_name: str = None, + value=0, + width: int | float = 0, + height: int | float = 0, + x: int | float = 5, + y: int | float = 5, + visible: bool = False, + slider_min: int | float = 0, + slider_max: int | float = 100, + is_discrete: bool = True, *, reporter_id: str = None, _project: project.Project = None): + """ + Represents a variable/list monitor + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Monitors + """ + assert isinstance(reporter, base.SpriteSubComponent) or reporter is None + + self.reporter_id = reporter_id + """ + ID referencing the VLB being referenced. Replaced with None during project instantiation, where the reporter attribute is updated + """ + + self.reporter = reporter + if params is None: + params = {} + + self.mode = mode + + self.opcode = opcode + self.params = params + + self.sprite_name = sprite_name + + self.value = value + + self.width, self.height = width, height + self.x, self.y = x, y + + self.visible = visible + + self.slider_min, self.slider_max = slider_min, slider_max + self.is_discrete = is_discrete + + super().__init__(_project) + + def __repr__(self): + return f"Monitor<{self.opcode}>" + + @property + def id(self): + if self.reporter is not None: + return self.reporter.id + # if isinstance(self.reporter, str): + # return self.reporter + # else: + # return self.reporter.id + else: + return self.reporter_id + + @staticmethod + def from_json(data: dict): + _id = data["id"] + # ^^ NEED TO FIND REPORTER OBJECT + + mode = data["mode"] + + opcode = data["opcode"] + params: dict = data["params"] + + sprite_name = data["spriteName"] + + value = data["value"] + + width, height = data["width"], data["height"] + x, y = data["x"], data["y"] + + visible = data["visible"] + + if "isDiscrete" in data.keys(): + slider_min, slider_max = data["sliderMin"], data["sliderMax"] + is_discrete = data["isDiscrete"] + else: + slider_min, slider_max, is_discrete = None, None, None + + return Monitor(None, mode, opcode, params, sprite_name, value, width, height, x, y, visible, slider_min, + slider_max, is_discrete, reporter_id=_id) + + def to_json(self): + _json = { + "id": self.id, + "mode": self.mode, + + "opcode": self.opcode, + "params": self.params, + + "spriteName": self.sprite_name, + + "value": self.value, + + "width": self.width, + "height": self.height, + + "x": self.x, + "y": self.y, + + "visible": self.visible + } + if self.is_discrete is not None: + _json["sliderMin"] = self.slider_min + _json["sliderMax"] = self.slider_max + _json["isDiscrete"] = self.is_discrete + + return _json + + def link_using_project(self): + assert self.project is not None + + if self.opcode in ("data_variable", "data_listcontents", "event_broadcast_menu"): + new_vlb = self.project.find_vlb(self.reporter_id, "id") + if new_vlb is not None: + self.reporter = new_vlb + self.reporter_id = None + + # @staticmethod + # def from_reporter(reporter: Block, _id: str = None, mode: str = "default", + # opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0, + # height: int | float = 0, + # x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0, + # slider_max: int | float = 100, is_discrete: bool = True, params: dict = None): + # if "reporter" not in reporter.stack_type: + # warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'") + # elif "(menu)" in reporter.stack_type: + # warnings.warn(f"{reporter} is a menu block; the monitor will return '0'") + # # Maybe add note that length of list doesn't work fsr?? idk + # if _id is None: + # _id = reporter.opcode + # if opcode is None: + # opcode = reporter.opcode # .replace('_', ' ') + + # if params is None: + # params = {} + # for field in reporter.fields: + # if field.value_id is None: + # params[field.id] = field.value + # else: + # params[field.id] = field.value, field.value_id + + # return Monitor( + # _id, + # mode, + # opcode, + + # params, + # sprite_name, + # value, + + # width, height, + # x, y, + # visible, + # slider_min, slider_max, is_discrete + # ) diff --git a/scratchattach/editor/mutation.py b/scratchattach/editor/mutation.py new file mode 100644 index 0000000..91b764f --- /dev/null +++ b/scratchattach/editor/mutation.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +import json +import warnings +from dataclasses import dataclass +from typing import TYPE_CHECKING, Iterable, Any + +from . import base, commons +from ..utils import enums + +if TYPE_CHECKING: + from . import block + + +@dataclass(init=True) +class ArgumentType(base.Base): + type: str + proc_str: str + + def __eq__(self, other): + if isinstance(other, enums._EnumWrapper): + other = other.value + + assert isinstance(other, ArgumentType) + + return self.type == other.type + + def __repr__(self): + return f"" + + @property + def default(self) -> str | None: + if self.proc_str == "%b": + return "false" + elif self.proc_str == "%s": + return '' + else: + return None + + +@dataclass(init=True, repr=True) +class ArgSettings(base.Base): + ids: bool + names: bool + defaults: bool + + def __int__(self): + return (int(self.ids) + + int(self.names) + + int(self.defaults)) + + def __eq__(self, other): + return (self.ids == other.ids and + self.names == other.names and + self.defaults == other.defaults) + + def __gt__(self, other): + return int(self) > int(other) + + def __lt__(self, other): + return int(self) > int(other) + + +@dataclass(init=True, repr=True) +class Argument(base.MutationSubComponent): + name: str + default: str = '' + + _id: str = None + """ + Argument ID: Will be used to replace other parameters during block instantiation. + """ + + @property + def index(self): + return self.mutation.arguments.index(self) + + @property + def type(self) -> None | ArgumentType: + i = 0 + goal = self.index + for token in parse_proc_code(self.mutation.proc_code): + if isinstance(token, ArgumentType): + if i == goal: + return token + i += 1 + + @staticmethod + def from_json(data: dict | list | Any): + warnings.warn("No from_json method defined for Arguments (yet?)") + + def to_json(self) -> dict | list | Any: + warnings.warn("No to_json method defined for Arguments (yet?)") + + def link_using_mutation(self): + if self._id is None: + self._id = self.block.new_id + + +class ArgTypes(enums._EnumWrapper): + BOOLEAN = ArgumentType("boolean", "%b") + NUMBER_OR_TEXT = ArgumentType("number or text", "%s") + + +def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None: + if _proc_code is None: + return None + token = '' + tokens = [] + + last_char = '' + for char in _proc_code: + if last_char == '%': + if char in "sb": + # If we've hit an %s or %b + token = token[:-1] + # Clip the % sign off the token + + if token != '': + # Make sure not to append an empty token + tokens.append(token) + + # Add the parameter token + token = f"%{char}" + if token == "%b": + tokens.append(ArgTypes.BOOLEAN.value.dcopy()) + elif token == "%s": + tokens.append(ArgTypes.NUMBER_OR_TEXT.value.dcopy()) + + token = '' + continue + + token += char + last_char = char + + if token != '': + tokens.append(token) + + return tokens + + +class Mutation(base.BlockSubComponent): + def __init__(self, _tag_name: str = "mutation", _children: list = None, _proc_code: str = None, + _is_warp: bool = None, _arguments: list[Argument] = None, _has_next: bool = None, + _argument_settings: ArgSettings = None, *, + _block: block.Block = None): + """ + Mutation for Control:stop block and procedures + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Mutations + """ + # Defaulting for args + if _children is None: + _children = [] + + if _argument_settings is None: + if _arguments: + _argument_settings = ArgSettings( + _arguments[0]._id is None, + _arguments[0].name is None, + _arguments[0].default is None + ) + else: + _argument_settings = ArgSettings(False, False, False) + + self.tag_name = _tag_name + self.children = _children + + self.proc_code = _proc_code + self.is_warp = _is_warp + self.arguments = _arguments + self.og_argument_settings = _argument_settings + + self.has_next = _has_next + + super().__init__(_block) + + def __repr__(self): + if self.arguments is not None: + return f"Mutation" + else: + return f"Mutation" + + @property + def argument_ids(self): + if self.arguments is not None: + return [_arg._id for _arg in self.arguments] + else: + return None + + @property + def argument_names(self): + if self.arguments is not None: + return [_arg.name for _arg in self.arguments] + else: + return None + + @property + def argument_defaults(self): + if self.arguments is not None: + return [_arg.default for _arg in self.arguments] + else: + return None + + @property + def argument_settings(self) -> ArgSettings: + return ArgSettings(bool(commons.safe_get(self.argument_ids, 0)), + bool(commons.safe_get(self.argument_names, 0)), + bool(commons.safe_get(self.argument_defaults, 0))) + + @property + def parsed_proc_code(self) -> list[str, ArgumentType] | None: + return parse_proc_code(self.proc_code) + + @staticmethod + def from_json(data: dict) -> Mutation: + assert isinstance(data, dict) + + _tag_name = data.get("tagName", "mutation") + _children = data.get("children", []) + + # procedures_prototype & procedures_call attrs + _proc_code = data.get("proccode") + _is_warp = data.get("warp") + if isinstance(_is_warp, str): + _is_warp = json.loads(_is_warp) + + _argument_ids = data.get("argumentids") + # For some reason these are stored as JSON strings + if _argument_ids is not None: + _argument_ids = json.loads(_argument_ids) + + # procedures_prototype attrs + _argument_names = data.get("argumentnames") + _argument_defaults = data.get("argumentdefaults") + # For some reason these are stored as JSON strings + if _argument_names is not None: + assert isinstance(_argument_names, str) + _argument_names = json.loads(_argument_names) + if _argument_defaults is not None: + assert isinstance(_argument_defaults, str) + _argument_defaults = json.loads(_argument_defaults) + _argument_settings = ArgSettings(_argument_ids is not None, + _argument_names is not None, + _argument_defaults is not None) + + # control_stop attrs + _has_next = data.get("hasnext") + if isinstance(_has_next, str): + _has_next = json.loads(_has_next) + + def get(_lst: list | tuple | None, _idx: int): + if _lst is None: + return None + + if len(_lst) <= _idx: + return None + else: + return _lst[_idx] + + if _argument_ids is None: + _arguments = None + else: + _arguments = [] + for i, _arg_id in enumerate(_argument_ids): + _arg_name = get(_argument_names, i) + _arg_default = get(_argument_defaults, i) + + _arguments.append(Argument(_arg_name, _arg_default, _arg_id)) + + return Mutation(_tag_name, _children, _proc_code, _is_warp, _arguments, _has_next, _argument_settings) + + def to_json(self) -> dict | None: + _json = { + "tagName": self.tag_name, + "children": self.children, + } + commons.noneless_update(_json, { + "proccode": self.proc_code, + "warp": commons.dumps_ifnn(self.is_warp), + "argumentids": commons.dumps_ifnn(self.argument_ids), + "argumentnames": commons.dumps_ifnn(self.argument_names), + "argumentdefaults": commons.dumps_ifnn(self.argument_defaults), + + "hasNext": commons.dumps_ifnn(self.has_next) + }) + + return _json + + def link_arguments(self): + if self.arguments is None: + return + + # You only need to fetch argument data if you actually have arguments + if len(self.arguments) > 0: + if self.arguments[0].name is None: + # This requires linking + _proc_uses = self.sprite.find_block(self.argument_ids, "argument ids", True) + # Note: Sometimes there may not be any argument ids provided. There will be no way to find out the names + # Technically, defaults can be found by using the proc code + for _use in _proc_uses: + if _use.mutation.argument_settings > self.argument_settings: + self.arguments = _use.mutation.arguments + if int(self.argument_settings) == 3: + # If all of our argument data is filled, we can stop early + return + + # We can still work out argument defaults from parsing the proc code + if self.arguments[0].default is None: + _parsed = self.parsed_proc_code + _arg_phs: Iterable[ArgumentType] = filter(lambda tkn: isinstance(tkn, ArgumentType), + _parsed) + for i, _arg_ph in enumerate(_arg_phs): + self.arguments[i].default = _arg_ph.default + + for _argument in self.arguments: + _argument.mutation = self + _argument.link_using_mutation() diff --git a/scratchattach/editor/pallete.py b/scratchattach/editor/pallete.py new file mode 100644 index 0000000..587fb48 --- /dev/null +++ b/scratchattach/editor/pallete.py @@ -0,0 +1,91 @@ +""" +Collection of block information, stating input/field names and opcodes +New version of sbuild.py + +May want to completely change this later +""" +from __future__ import annotations + +from dataclasses import dataclass + +from . import prim +from ..utils.enums import _EnumWrapper + + +@dataclass(init=True, repr=True) +class FieldUsage: + name: str + value_type: prim.PrimTypes = None + + +@dataclass(init=True, repr=True) +class SpecialFieldUsage(FieldUsage): + name: str + attrs: list[str] = None + if attrs is None: + attrs = [] + + value_type: None = None + + +@dataclass(init=True, repr=True) +class InputUsage: + name: str + value_type: prim.PrimTypes = None + default_obscurer: BlockUsage = None + + +@dataclass(init=True, repr=True) +class BlockUsage: + opcode: str + fields: list[FieldUsage] = None + if fields is None: + fields = [] + + inputs: list[InputUsage] = None + if inputs is None: + inputs = [] + + +class BlockUsages(_EnumWrapper): + # Special Enum blocks + MATH_NUMBER = BlockUsage( + "math_number", + [SpecialFieldUsage("NUM", ["name", "value"])] + ) + MATH_POSITIVE_NUMBER = BlockUsage( + "math_positive_number", + [SpecialFieldUsage("NUM", ["name", "value"])] + ) + MATH_WHOLE_NUMBER = BlockUsage( + "math_whole_number", + [SpecialFieldUsage("NUM", ["name", "value"])] + ) + MATH_INTEGER = BlockUsage( + "math_integer", + [SpecialFieldUsage("NUM", ["name", "value"])] + ) + MATH_ANGLE = BlockUsage( + "math_angle", + [SpecialFieldUsage("NUM", ["name", "value"])] + ) + COLOUR_PICKER = BlockUsage( + "colour_picker", + [SpecialFieldUsage("COLOUR", ["name", "value"])] + ) + TEXT = BlockUsage( + "text", + [SpecialFieldUsage("TEXT", ["name", "value"])] + ) + EVENT_BROADCAST_MENU = BlockUsage( + "event_broadcast_menu", + [SpecialFieldUsage("BROADCAST_OPTION", ["name", "id", "value", "variableType"])] + ) + DATA_VARIABLE = BlockUsage( + "data_variable", + [SpecialFieldUsage("VARIABLE", ["name", "id", "value", "variableType"])] + ) + DATA_LISTCONTENTS = BlockUsage( + "data_listcontents", + [SpecialFieldUsage("LIST", ["name", "id", "value", "variableType"])] + ) diff --git a/scratchattach/editor/prim.py b/scratchattach/editor/prim.py new file mode 100644 index 0000000..b787b48 --- /dev/null +++ b/scratchattach/editor/prim.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import warnings +from dataclasses import dataclass +from typing import Callable, Final + +from . import base, sprite, vlb, commons, build_defaulting +from ..utils import enums, exceptions + + +@dataclass(init=True, repr=True) +class PrimType(base.JSONSerializable): + code: int + name: str + attrs: list = None + opcode: str = None + + def __eq__(self, other): + if isinstance(other, str): + return self.name == other + elif isinstance(other, enums._EnumWrapper): + other = other.value + return super().__eq__(other) + + @staticmethod + def from_json(data: int): + return PrimTypes.find(data, "code") + + def to_json(self) -> int: + return self.code + + +BASIC_ATTRS: Final[tuple[str]] = ("value",) +VLB_ATTRS: Final[tuple[str]] = ("name", "id", "x", "y") + + +class PrimTypes(enums._EnumWrapper): + # Yeah, they actually do have opcodes + NUMBER = PrimType(4, "number", BASIC_ATTRS, "math_number") + POSITIVE_NUMBER = PrimType(5, "positive number", BASIC_ATTRS, "math_positive_number") + POSITIVE_INTEGER = PrimType(6, "positive integer", BASIC_ATTRS, "math_whole_number") + INTEGER = PrimType(7, "integer", BASIC_ATTRS, "math_integer") + ANGLE = PrimType(8, "angle", BASIC_ATTRS, "math_angle") + COLOR = PrimType(9, "color", BASIC_ATTRS, "colour_picker") + STRING = PrimType(10, "string", BASIC_ATTRS, "text") + BROADCAST = PrimType(11, "broadcast", VLB_ATTRS, "event_broadcast_menu") + VARIABLE = PrimType(12, "variable", VLB_ATTRS, "data_variable") + LIST = PrimType(13, "list", VLB_ATTRS, "data_listcontents") + + @classmethod + def find(cls, value, by: str, apply_func: Callable = None) -> PrimType: + return super().find(value, by, apply_func=apply_func) + + +def is_prim_opcode(opcode: str): + return opcode in PrimTypes.all_of("opcode") and opcode is not None + + +class Prim(base.SpriteSubComponent): + def __init__(self, _primtype: PrimType | PrimTypes, _value: str | vlb.Variable | vlb.List | vlb.Broadcast = None, + _name: str = None, _id: str = None, _x: int = None, + _y: int = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Class representing a Scratch string, number, angle, variable etc. + Technically blocks but behave differently + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=A%20few%20blocks,13 + """ + if isinstance(_primtype, PrimTypes): + _primtype = _primtype.value + + self.type = _primtype + + self.value = _value + + self.name = _name + """ + Once you get the object associated with this primitive (sprite.link_prims()), + the name will be removed and the value will be changed from ``None`` + """ + self.value_id = _id + """ + It's not an object accessed by id, but it may reference an object with an id. + + ---- + + Once you get the object associated with it (sprite.link_prims()), + the id will be removed and the value will be changed from ``None`` + """ + + self.x = _x + self.y = _y + + super().__init__(_sprite) + + def __repr__(self): + if self.is_basic: + return f"Prim<{self.type.name}: {self.value}>" + elif self.is_vlb: + return f"Prim<{self.type.name}: {self.value}>" + else: + return f"Prim<{self.type.name}>" + + @property + def is_vlb(self): + return self.type.attrs == VLB_ATTRS + + @property + def is_basic(self): + return self.type.attrs == BASIC_ATTRS + + @staticmethod + def from_json(data: list): + assert isinstance(data, list) + + _type_idx = data[0] + _prim_type = PrimTypes.find(_type_idx, "code") + + _value, _name, _value_id, _x, _y = (None,) * 5 + if _prim_type.attrs == BASIC_ATTRS: + assert len(data) == 2 + _value = data[1] + + elif _prim_type.attrs == VLB_ATTRS: + assert len(data) in (3, 5) + _name, _value_id = data[1:3] + + if len(data) == 5: + _x, _y = data[3:] + + return Prim(_prim_type, _value, _name, _value_id, _x, _y) + + def to_json(self) -> list: + if self.type.attrs == BASIC_ATTRS: + return [self.type.code, self.value] + else: + return commons.trim_final_nones([self.type.code, self.value.name, self.value.id, self.x, self.y]) + + def link_using_sprite(self): + # Link prim to var/list/broadcast + if self.is_vlb: + if self.type.name == "variable": + self.value = self.sprite.find_variable(self.value_id, "id") + + elif self.type.name == "list": + self.value = self.sprite.find_list(self.value_id, "id") + + elif self.type.name == "broadcast": + self.value = self.sprite.find_broadcast(self.value_id, "id") + else: + # This should never happen + raise exceptions.BadVLBPrimitiveError(f"{self} claims to be VLB, but is {self.type.name}") + + if self.value is None: + if not self.project: + new_vlb = vlb.construct(self.type.name.lower(), self.value_id, self.name) + self.sprite.add_local_global(new_vlb) + self.value = new_vlb + + else: + new_vlb = vlb.construct(self.type.name.lower(), self.value_id, self.name) + self.sprite.stage.add_vlb(new_vlb) + + warnings.warn( + f"Prim has unknown {self.type.name} id; adding as global variable") + self.name = None + self.value_id = None + + @property + def can_next(self): + return False diff --git a/scratchattach/editor/project.py b/scratchattach/editor/project.py new file mode 100644 index 0000000..029601c --- /dev/null +++ b/scratchattach/editor/project.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +import json +import os +import warnings +from io import BytesIO, TextIOWrapper +from typing import Iterable, Generator, BinaryIO +from zipfile import ZipFile + +from . import base, meta, extension, monitor, sprite, asset, vlb, twconfig, comment, commons +from ..site import session +from ..site.project import get_project +from ..utils import exceptions + + +class Project(base.JSONExtractable): + def __init__(self, _name: str = None, _meta: meta.Meta = None, _extensions: Iterable[extension.Extension] = (), + _monitors: Iterable[monitor.Monitor] = (), _sprites: Iterable[sprite.Sprite] = (), *, + _asset_data: list[asset.AssetFile] = None, _session: session.Session = None): + # Defaulting for list parameters + if _meta is None: + _meta = meta.Meta() + if _asset_data is None: + _asset_data = [] + + self._session = _session + + self.name = _name + + self.meta = _meta + self.extensions = _extensions + self.monitors = list(_monitors) + self.sprites = list(_sprites) + + self.asset_data = _asset_data + + self._tw_config_comment = None + + # Link subcomponents + for iterable in (self.monitors, self.sprites): + for _subcomponent in iterable: + _subcomponent.project = self + + # Link sprites + _stage_count = 0 + + for _sprite in self.sprites: + if _sprite.is_stage: + _stage_count += 1 + + _sprite.link_subcomponents() + + # Link monitors + for _monitor in self.monitors: + _monitor.link_using_project() + + if _stage_count != 1: + raise exceptions.InvalidStageCount(f"Project {self}") + + def __repr__(self): + _ret = "Project<" + if self.name is not None: + _ret += f"name={self.name}, " + _ret += f"meta={self.meta}" + _ret += '>' + return _ret + + @property + def stage(self) -> sprite.Sprite: + for _sprite in self.sprites: + if _sprite.is_stage: + return _sprite + warnings.warn(f"Could not find stage for {self.name}") + + def to_json(self) -> dict: + _json = { + "targets": [_sprite.to_json() for _sprite in self.sprites], + "monitors": [_monitor.to_json() for _monitor in self.monitors], + "extensions": [_extension.to_json() for _extension in self.extensions], + "meta": self.meta.to_json(), + } + + return _json + + @property + def assets(self) -> Generator[asset.Asset, None, None]: + for _sprite in self.sprites: + for _asset in _sprite.assets: + yield _asset + + @property + def tw_config_comment(self) -> comment.Comment | None: + for _comment in self.stage.comments: + if twconfig.is_valid_twconfig(_comment.text): + return _comment + return None + + @property + def tw_config(self) -> twconfig.TWConfig | None: + _comment = self.tw_config_comment + if _comment: + return twconfig.TWConfig.from_str(_comment.text) + return None + + @property + def all_ids(self): + _ret = [] + for _sprite in self.sprites: + _ret += _sprite.all_ids + return _ret + + @property + def new_id(self): + return commons.gen_id() + + @staticmethod + def from_json(data: dict): + assert isinstance(data, dict) + + # Load metadata + _meta = meta.Meta.from_json(data.get("meta")) + + # Load extensions + _extensions = [] + for _extension_data in data.get("extensions", []): + _extensions.append(extension.Extension.from_json(_extension_data)) + + # Load monitors + _monitors = [] + for _monitor_data in data.get("monitors", []): + _monitors.append(monitor.Monitor.from_json(_monitor_data)) + + # Load sprites (targets) + _sprites = [] + for _sprite_data in data.get("targets", []): + _sprites.append(sprite.Sprite.from_json(_sprite_data)) + + return Project(None, _meta, _extensions, _monitors, _sprites) + + @staticmethod + def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: str = None): + """ + Load project JSON and assets from an .sb3 file/bytes/file path + :return: Project name, asset data, json string + """ + _dir_for_name = None + + if _name is None: + if hasattr(data, "name"): + _dir_for_name = data.name + + if not isinstance(_name, str) and _name is not None: + _name = str(_name) + + if isinstance(data, bytes): + data = BytesIO(data) + + elif isinstance(data, str): + _dir_for_name = data + data = open(data, "rb") + + if _name is None and _dir_for_name is not None: + # Remove any directory names and the file extension + _name = _dir_for_name.split('/')[-1] + _name = '.'.join(_name.split('.')[:-1]) + + asset_data = [] + with data: + # For if the sb3 is just JSON (e.g. if it's exported from scratchattach) + if commons.is_valid_json(data): + json_str = data + + else: + with ZipFile(data) as archive: + json_str = archive.read("project.json") + + # Also load assets + if load_assets: + for filename in archive.namelist(): + if filename != "project.json": + md5_hash = filename.split('.')[0] + + asset_data.append( + asset.AssetFile(filename, archive.read(filename), md5_hash) + ) + + else: + warnings.warn( + "Loading sb3 without loading assets. When exporting the project, there may be errors due to assets not being uploaded to the Scratch website") + + return _name, asset_data, json_str + + @classmethod + def from_sb3(cls, data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: str = None): + """ + Load a project from an .sb3 file/bytes/file path + """ + _name, asset_data, json_str = cls.load_json(data, load_assets, _name) + data = json.loads(json_str) + + project = cls.from_json(data) + project.name = _name + project.asset_data = asset_data + + return project + + @staticmethod + def from_id(project_id: int, _name: str = None): + _proj = get_project(project_id) + data = json.loads(_proj.get_json()) + + if _name is None: + _name = _proj.title + _name = str(_name) + + _proj = Project.from_json(data) + _proj.name = _name + return _proj + + def find_vlb(self, value: str | None, by: str = "name", + multiple: bool = False) -> vlb.Variable | vlb.List | vlb.Broadcast | list[ + vlb.Variable | vlb.List | vlb.Broadcast]: + _ret = [] + for _sprite in self.sprites: + val = _sprite.find_vlb(value, by, multiple) + if multiple: + _ret += val + else: + if val is not None: + return val + if multiple: + return _ret + + def find_sprite(self, value: str | None, by: str = "name", + multiple: bool = False) -> sprite.Sprite | list[sprite.Sprite]: + _ret = [] + for _sprite in self.sprites: + if by == "name": + _val = _sprite.name + else: + _val = getattr(_sprite, by) + + if _val == value: + if multiple: + _ret.append(_sprite) + else: + return _sprite + + if multiple: + return _ret + + def export(self, fp: str, *, auto_open: bool = False, export_as_zip: bool = True): + data = self.to_json() + + if export_as_zip: + with ZipFile(fp, "w") as archive: + for _asset in self.assets: + asset_file = _asset.asset_file + if asset_file.filename not in archive.namelist(): + archive.writestr(asset_file.filename, asset_file.data) + + archive.writestr("project.json", json.dumps(data)) + else: + with open(fp, "w") as json_file: + json.dump(data, json_file) + + if auto_open: + os.system(f"explorer.exe \"{fp}\"") + + def add_monitor(self, _monitor: monitor.Monitor) -> monitor.Monitor: + _monitor.project = self + _monitor.reporter_id = self.new_id + self.monitors.append(_monitor) diff --git a/scratchattach/editor/sbuild.py b/scratchattach/editor/sbuild.py new file mode 100644 index 0000000..7b57c30 --- /dev/null +++ b/scratchattach/editor/sbuild.py @@ -0,0 +1,2835 @@ +from __future__ import annotations + +from .. import editor +# Copied from sbuild so we have to make a few wrappers ;-; +# May need to recreate this from scratch. In which case, it is to be done in palette.py +class Block(editor.Block): + ... + +class Input(editor.Input): + ... +class Field(editor.Field): + ... +class Variable(editor.Variable): + ... +class List(editor.List): + ... +class Broadcast(editor.Broadcast): + ... +class Mutation(editor.Mutation): + ... + + +class Motion: + class MoveSteps(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_movesteps", _shadow=shadow, pos=pos) + + def set_steps(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STEPS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class TurnRight(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_turnright", _shadow=shadow, pos=pos) + + def set_degrees(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DEGREES", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class TurnLeft(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_turnleft", _shadow=shadow, pos=pos) + + def set_degrees(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DEGREES", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GoTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_goto", _shadow=shadow, pos=pos) + + def set_to(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GoToMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_goto_menu", _shadow=shadow, pos=pos) + + def set_to(self, value: str = "_random_", value_id: str = None): + return self.add_field(Field("TO", value, value_id)) + + class GoToXY(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_gotoxy", _shadow=shadow, pos=pos) + + def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GlideTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_glideto", _shadow=shadow, pos=pos) + + def set_secs(self, value, input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_to(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GlideToMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_glideto_menu", _shadow=shadow, pos=pos) + + def set_to(self, value: str = "_random_", value_id: str = None): + return self.add_field(Field("TO", value, value_id)) + + class GlideSecsToXY(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_glidesecstoxy", _shadow=shadow, pos=pos) + + def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_secs(self, value, input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class PointInDirection(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_pointindirection", _shadow=shadow, pos=pos) + + def set_direction(self, value, input_type: str | int = "angle", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DIRECTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class PointTowards(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_pointtowards", _shadow=shadow, pos=pos) + + def set_towards(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TOWARDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class PointTowardsMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_pointtowards_menu", _shadow=shadow, pos=pos) + + def set_towards(self, value: str = "_mouse_", value_id: str = None): + return self.add_field(Field("TOWARDS", value, value_id)) + + class ChangeXBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_changexby", _shadow=shadow, pos=pos) + + def set_dx(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("DX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangeYBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_changeyby", _shadow=shadow, pos=pos) + + def set_dy(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("DY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetX(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_setx", _shadow=shadow, pos=pos) + + def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetY(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_sety", _shadow=shadow, pos=pos) + + def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class IfOnEdgeBounce(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_ifonedgebounce", _shadow=shadow, pos=pos) + + class SetRotationStyle(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_setrotationstyle", _shadow=shadow, pos=pos) + + def set_style(self, value: str = "all around", value_id: str = None): + return self.add_field(Field("STYLE", value, value_id)) + + class XPosition(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_xposition", _shadow=shadow, pos=pos) + + class YPosition(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_yposition", _shadow=shadow, pos=pos) + + class Direction(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_direction", _shadow=shadow, pos=pos) + + class ScrollRight(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_scroll_right", _shadow=shadow, pos=pos) + + def set_distance(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DISTANCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ScrollUp(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_scroll_up", _shadow=shadow, pos=pos) + + def set_distance(self, value, input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DISTANCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class AlignScene(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_align_scene", _shadow=shadow, pos=pos) + + def set_alignment(self, value: str = "bottom-left", value_id: str = None): + return self.add_field(Field("ALIGNMENT", value, value_id)) + + class XScroll(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_xscroll", _shadow=shadow, pos=pos) + + class YScroll(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "motion_yscroll", _shadow=shadow, pos=pos) + + +class Looks: + class SayForSecs(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_sayforsecs", _shadow=shadow, pos=pos) + + def set_message(self, value="Hello!", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + def set_secs(self, value=2, input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + class Say(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_say", _shadow=shadow, pos=pos) + + def set_message(self, value="Hello!", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + class ThinkForSecs(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_thinkforsecs", _shadow=shadow, pos=pos) + + def set_message(self, value="Hmm...", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + def set_secs(self, value=2, input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + class Think(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_think", _shadow=shadow, pos=pos) + + def set_message(self, value="Hmm...", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + class SwitchCostumeTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_switchcostumeto", _shadow=shadow, pos=pos) + + def set_costume(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COSTUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Costume(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_costume", _shadow=shadow, pos=pos) + + def set_costume(self, value: str = "costume1", value_id: str = None): + return self.add_field(Field("COSTUME", value, value_id)) + + class NextCostume(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_nextcostume", _shadow=shadow, pos=pos) + + class SwitchBackdropTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_switchbackdropto", _shadow=shadow, pos=pos) + + def set_backdrop(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BACKDROP", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Backdrops(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_backdrops", _shadow=shadow, pos=pos) + + def set_backdrop(self, value: str = "costume1", value_id: str = None): + return self.add_field(Field("BACKDROP", value, value_id)) + + class SwitchBackdropToAndWait(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_switchbackdroptoandwait", _shadow=shadow, pos=pos) + + def set_backdrop(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BACKDROP", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class NextBackdrop(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_nextbackdrop", _shadow=shadow, pos=pos) + + class ChangeSizeBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_changesizeby", _shadow=shadow, pos=pos) + + def set_change(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetSizeTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_setsizeto", _shadow=shadow, pos=pos) + + def set_size(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangeEffectBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_changeeffectby", _shadow=shadow, pos=pos) + + def set_change(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_effect(self, value: str = "COLOR", value_id: str = None): + return self.add_field(Field("EFFECT", value, value_id)) + + class SetEffectTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_seteffectto", _shadow=shadow, pos=pos) + + def set_value(self, value="0", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_effect(self, value: str = "COLOR", value_id: str = None): + return self.add_field(Field("EFFECT", value, value_id)) + + class ClearGraphicEffects(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_cleargraphiceffects", _shadow=shadow, pos=pos) + + class Hide(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_hide", _shadow=shadow, pos=pos) + + class Show(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_show", _shadow=shadow, pos=pos) + + class GoToFrontBack(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_gotofrontback", _shadow=shadow, pos=pos) + + def set_front_back(self, value: str = "front", value_id: str = None): + return self.add_field(Field("FRONT_BACK", value, value_id)) + + class GoForwardBackwardLayers(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_goforwardbackwardlayers", _shadow=shadow, pos=pos) + + def set_num(self, value="1", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_fowrward_backward(self, value: str = "forward", value_id: str = None): + return self.add_field(Field("FORWARD_BACKWARD", value, value_id)) + + class CostumeNumberName(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_costumenumbername", _shadow=shadow, pos=pos) + + def set_number_name(self, value: str = "string", value_id: str = None): + return self.add_field(Field("NUMBER_NAME", value, value_id)) + + class BackdropNumberName(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_backdropnumbername", _shadow=shadow, pos=pos) + + def set_number_name(self, value: str = "number", value_id: str = None): + return self.add_field(Field("NUMBER_NAME", value, value_id)) + + class Size(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_size", _shadow=shadow, pos=pos) + + class HideAllSprites(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_hideallsprites", _shadow=shadow, pos=pos) + + class SetStretchTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_setstretchto", _shadow=shadow, pos=pos) + + def set_stretch(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRETCH", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangeStretchBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "looks_changestretchby", _shadow=shadow, pos=pos) + + def set_change(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + +class Sounds: + class Play(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_play", _shadow=shadow, pos=pos) + + def set_sound_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SOUND_MENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SoundsMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_sounds_menu", _shadow=shadow, pos=pos) + + def set_sound_menu(self, value: str = "pop", value_id: str = None): + return self.add_field(Field("SOUND_MENU", value, value_id)) + + class PlayUntilDone(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_playuntildone", _shadow=shadow, pos=pos) + + def set_sound_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SOUND_MENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class StopAllSounds(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_stopallsounds", _shadow=shadow, pos=pos) + + class ChangeEffectBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_changeeffectby", _shadow=shadow, pos=pos) + + def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_effect(self, value: str = "PITCH", value_id: str = None): + return self.add_field(Field("EFFECT", value, value_id)) + + class SetEffectTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_seteffectto", _shadow=shadow, pos=pos) + + def set_value(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_effect(self, value: str = "PITCH", value_id: str = None): + return self.add_field(Field("EFFECT", value, value_id)) + + class ClearEffects(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_cleareffects", _shadow=shadow, pos=pos) + + class ChangeVolumeBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_changevolumeby", _shadow=shadow, pos=pos) + + def set_volume(self, value="-10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VOLUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetVolumeTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_setvolumeto", _shadow=shadow, pos=pos) + + def set_volume(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VOLUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Volume(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sound_volume", _shadow=shadow, pos=pos) + + +class Events: + class WhenFlagClicked(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenflagclicked", _shadow=shadow, pos=pos) + + class WhenKeyPressed(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenkeypressed", _shadow=shadow, pos=pos) + + def set_key_option(self, value: str = "space", value_id: str = None): + return self.add_field(Field("KEY_OPTION", value, value_id)) + + class WhenThisSpriteClicked(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenthisspriteclicked", _shadow=shadow, pos=pos) + + class WhenStageClicked(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenstageclicked", _shadow=shadow, pos=pos) + + class WhenBackdropSwitchesTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenbackdropswitchesto", _shadow=shadow, pos=pos) + + def set_backdrop(self, value: str = "backdrop1", value_id: str = None): + return self.add_field(Field("BACKDROP", value, value_id)) + + class WhenGreaterThan(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whengreaterthan", _shadow=shadow, pos=pos) + + def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_when_greater_than_menu(self, value: str = "LOUDNESS", value_id: str = None): + return self.add_field(Field("WHENGREATERTHANMENU", value, value_id)) + + class WhenBroadcastReceived(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whenbroadcastreceived", _shadow=shadow, pos=pos) + + def set_broadcast_option(self, value="message1", value_id: str = "I didn't get an id..."): + return self.add_field(Field("BROADCAST_OPTION", value, value_id)) + + class Broadcast(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_broadcast", _shadow=shadow, pos=pos) + + def set_broadcast_input(self, value="message1", input_type: str | int = "broadcast", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BROADCAST_INPUT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class BroadcastAndWait(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_broadcastandwait", _shadow=shadow, pos=pos) + + def set_broadcast_input(self, value="message1", input_type: str | int = "broadcast", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BROADCAST_INPUT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class WhenTouchingObject(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_whentouchingobject", _shadow=shadow, pos=pos) + + def set_touching_object_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TOUCHINGOBJECTMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class TouchingObjectMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "event_touchingobjectmenu", _shadow=shadow, pos=pos) + + def set_touching_object_menu(self, value: str = "_mouse_", value_id: str = None): + return self.add_field(Field("TOUCHINGOBJECTMENU", value, value_id)) + + +class Control: + class Wait(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_wait", _shadow=shadow, pos=pos) + + def set_duration(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DURATION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Forever(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_forever", _shadow=shadow, pos=pos, can_next=False) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + class If(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_if", _shadow=shadow, pos=pos) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + class IfElse(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_if_else", _shadow=shadow, pos=pos) + + def set_substack1(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_substack2(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK2", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + class WaitUntil(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_wait_until", _shadow=shadow, pos=pos) + + def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("CONDITION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class RepeatUntil(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_repeat_until", _shadow=shadow, pos=pos) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + class While(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_while", _shadow=shadow, pos=pos) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + class Stop(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_stop", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_stop_option(self, value: str = "all", value_id: str = None): + return self.add_field(Field("STOP_OPTION", value, value_id)) + + def set_hasnext(self, has_next: bool = True): + self.mutation.has_next = has_next + return self + + class StartAsClone(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_start_as_clone", _shadow=shadow, pos=pos) + + class CreateCloneOf(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_create_clone_of", _shadow=shadow, pos=pos) + + def set_clone_option(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("CLONE_OPTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class CreateCloneOfMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_create_clone_of_menu", _shadow=shadow, pos=pos) + + def set_clone_option(self, value: str = "_myself_", value_id: str = None): + return self.add_field(Field("CLONE_OPTION", value, value_id)) + + class DeleteThisClone(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_delete_this_clone", _shadow=shadow, pos=pos, can_next=False) + + class ForEach(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_for_each", _shadow=shadow, pos=pos) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_value(self, value="5", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("VALUE", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + def set_variable(self, value: str = "i", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class GetCounter(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_get_counter", _shadow=shadow, pos=pos) + + class IncrCounter(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_incr_counter", _shadow=shadow, pos=pos) + + class ClearCounter(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_clear_counter", _shadow=shadow, pos=pos) + + class AllAtOnce(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "control_all_at_once", _shadow=shadow, pos=pos) + + def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, + input_id: str = None): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) + return self.add_input(inp) + + +class Sensing: + class TouchingObject(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_touchingobject", _shadow=shadow, pos=pos) + + def set_touching_object_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TOUCHINGOBJECTMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class TouchingObjectMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_touchingobjectmenu", _shadow=shadow, pos=pos) + + def set_touching_object_menu(self, value: str = "_mouse_", value_id: str = None): + return self.add_field(Field("TOUCHINGOBJECTMENU", value, value_id)) + + class TouchingColor(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_touchingcolor", _shadow=shadow, pos=pos) + + def set_color(self, value="#0000FF", input_type: str | int = "color", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ColorIsTouchingColor(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_coloristouchingcolor", _shadow=shadow, pos=pos) + + def set_color1(self, value="#0000FF", input_type: str | int = "color", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_color2(self, value="#00FF00", input_type: str | int = "color", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class DistanceTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_distanceto", _shadow=shadow, pos=pos) + + def set_distance_to_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DISTANCETOMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class DistanceToMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_distancetomenu", _shadow=shadow, pos=pos) + + def set_distance_to_menu(self, value: str = "_mouse_", value_id: str = None): + return self.add_field(Field("DISTANCETOMENU", value, value_id)) + + class Loud(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_loud", _shadow=shadow, pos=pos) + + class AskAndWait(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_askandwait", _shadow=shadow, pos=pos) + + def set_question(self, value="What's your name?", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("QUESTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + ) + + class Answer(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_answer", _shadow=shadow, pos=pos) + + class KeyPressed(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_keypressed", _shadow=shadow, pos=pos) + + def set_key_option(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("KEY_OPTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class KeyOptions(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_keyoptions", _shadow=shadow, pos=pos) + + def set_key_option(self, value: str = "space", value_id: str = None): + return self.add_field(Field("KEY_OPTION", value, value_id)) + + class MouseDown(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_mousedown", _shadow=shadow, pos=pos) + + class MouseX(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_mousex", _shadow=shadow, pos=pos) + + class MouseY(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_mousey", _shadow=shadow, pos=pos) + + class SetDragMode(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_setdragmode", _shadow=shadow, pos=pos) + + def set_drag_mode(self, value: str = "draggable", value_id: str = None): + return self.add_field(Field("DRAG_MODE", value, value_id)) + + class Loudness(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_loudness", _shadow=shadow, pos=pos) + + class Timer(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_timer", _shadow=shadow, pos=pos) + + class ResetTimer(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_resettimer", _shadow=shadow, pos=pos) + + class Of(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_of", _shadow=shadow, pos=pos) + + def set_object(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OBJECT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_property(self, value: str = "backdrop #", value_id: str = None): + return self.add_field(Field("PROPERTY", value, value_id)) + + class OfObjectMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_of_object_menu", _shadow=shadow, pos=pos) + + def set_object(self, value: str = "_stage_", value_id: str = None): + return self.add_field(Field("OBJECT", value, value_id)) + + class Current(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_current", _shadow=shadow, pos=pos) + + def set_current_menu(self, value: str = "YEAR", value_id: str = None): + return self.add_field(Field("CURRENTMENU", value, value_id)) + + class DaysSince2000(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_dayssince2000", _shadow=shadow, pos=pos) + + class Username(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_username", _shadow=shadow, pos=pos) + + class UserID(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "sensing_userid", _shadow=shadow, pos=pos) + + +class Operators: + class Add(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_add", _shadow=shadow, pos=pos) + + def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Subtract(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_subtract", _shadow=shadow, pos=pos) + + def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Multiply(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_multiply", _shadow=shadow, pos=pos) + + def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Divide(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_divide", _shadow=shadow, pos=pos) + + def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Random(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_random", _shadow=shadow, pos=pos) + + def set_from(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("FROM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_to(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GT(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_gt", _shadow=shadow, pos=pos) + + def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class LT(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_lt", _shadow=shadow, pos=pos) + + def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Equals(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_equals", _shadow=shadow, pos=pos) + + def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class And(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_and", _shadow=shadow, pos=pos) + + def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Or(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_or", _shadow=shadow, pos=pos) + + def set_operand1(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operand2(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Not(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_not", _shadow=shadow, pos=pos) + + def set_operand(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("OPERAND", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Join(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_join", _shadow=shadow, pos=pos) + + def set_string1(self, value="apple ", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_string2(self, value="banana", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class LetterOf(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_letter_of", _shadow=shadow, pos=pos) + + def set_letter(self, value="1", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("LETTER", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_string(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Length(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_length", _shadow=shadow, pos=pos) + + def set_string(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Contains(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_contains", _shadow=shadow, pos=pos) + + def set_string1(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_string2(self, value="a", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("STRING2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Mod(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_mod", _shadow=shadow, pos=pos) + + def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Round(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_round", _shadow=shadow, pos=pos) + + def set_num(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MathOp(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "operator_mathop", _shadow=shadow, pos=pos) + + def set_num(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_operator(self, value: str = "abs", value_id: str = None): + return self.add_field(Field("OPERATOR", value, value_id)) + + +class Data: + class VariableArr(Block): + def __init__(self, value, input_type: str | int = "variable", shadow_status: int = None, *, + pos: tuple[int | float, int | float] = (0, 0)): + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + inp = Input(None, value, input_type, shadow_status) + if inp.type_str == "block": + arr = inp.json[0] + else: + arr = inp.json[1][-1] + + super().__init__(array=arr, pos=pos) + + class Variable(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_variable", _shadow=shadow, pos=pos) + + def set_variable(self, value: str | Variable = "variable", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class SetVariableTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_setvariableto", _shadow=shadow, pos=pos) + + def set_value(self, value="0", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_variable(self, value: str | Variable = "variable", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class ChangeVariableBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_changevariableby", _shadow=shadow, pos=pos) + + def set_value(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_variable(self, value: str | Variable = "variable", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class ShowVariable(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_showvariable", _shadow=shadow, pos=pos) + + def set_variable(self, value: str | Variable = "variable", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class HideVariable(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_hidevariable", _shadow=shadow, pos=pos) + + def set_variable(self, value: str | Variable = "variable", value_id: str = None): + return self.add_field(Field("VARIABLE", value, value_id)) + + class ListArr(Block): + def __init__(self, value, input_type: str | int = "list", shadow_status: int = None, *, + pos: tuple[int | float, int | float] = (0, 0)): + inp = Input(None, value, input_type, shadow_status) + if inp.type_str == "block": + arr = inp.json[0] + else: + arr = inp.json[1][-1] + + super().__init__(array=arr, pos=pos) + + class ListContents(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_listcontents", _shadow=shadow, pos=pos) + + def set_list(self, value: str | List = "my list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class AddToList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_addtolist", _shadow=shadow, pos=pos) + + def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class DeleteOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_deleteoflist", _shadow=shadow, pos=pos) + + def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class InsertAtList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_insertatlist", _shadow=shadow, pos=pos) + + def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class DeleteAllOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_deletealloflist", _shadow=shadow, pos=pos) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ReplaceItemOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_replaceitemoflist", _shadow=shadow, pos=pos) + + def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ItemOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_itemoflist", _shadow=shadow, pos=pos) + + def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ItemNumOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_itemnumoflist", _shadow=shadow, pos=pos) + + def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class LengthOfList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_lengthoflist", _shadow=shadow, pos=pos) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ListContainsItem(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_listcontainsitem", _shadow=shadow, pos=pos) + + def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ShowList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_showlist", _shadow=shadow, pos=pos) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class HideList(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_hidelist", _shadow=shadow, pos=pos) + + def set_list(self, value: str | List = "list", value_id: str = None): + return self.add_field(Field("LIST", value, value_id)) + + class ListIndexAll(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_listindexall", _shadow=shadow, pos=pos) + + class ListIndexRandom(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "data_listindexrandom", _shadow=shadow, pos=pos) + + +class Proc: + class Definition(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "procedures_definition", _shadow=shadow, pos=pos) + + def set_custom_block(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("custom_block", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Call(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "procedures_call", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_proc_code(self, proc_code: str = ''): + self.mutation.proc_code = proc_code + return self + + def set_argument_ids(self, *argument_ids: list[str]): + self.mutation.argument_ids = argument_ids + return self + + def set_warp(self, warp: bool = True): + self.mutation.warp = warp + return self + + def set_arg(self, arg, value='', input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input(arg, value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class Declaration(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "procedures_declaration", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_proc_code(self, proc_code: str = ''): + self.mutation.proc_code = proc_code + return self + + class Prototype(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "procedures_prototype", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_proc_code(self, proc_code: str = ''): + self.mutation.proc_code = proc_code + return self + + def set_argument_ids(self, *argument_ids: list[str]): + self.mutation.argument_ids = argument_ids + return self + + def set_argument_names(self, *argument_names: list[str]): + self.mutation.argument_names = list(argument_names) + return self + + def set_argument_defaults(self, *argument_defaults: list[str]): + self.mutation.argument_defaults = argument_defaults + return self + + def set_warp(self, warp: bool = True): + self.mutation.warp = warp + return self + + def set_arg(self, arg, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input(arg, value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + +class Args: + class EditorBoolean(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "argument_editor_boolean", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_text(self, value: str = "foo", value_id: str = None): + return self.add_field(Field("TEXT", value, value_id)) + + class EditorStringNumber(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "argument_editor_string_number", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_text(self, value: str = "foo", value_id: str = None): + return self.add_field(Field("TEXT", value, value_id)) + + class ReporterBoolean(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "argument_reporter_boolean", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_value(self, value: str = "boolean", value_id: str = None): + return self.add_field(Field("VALUE", value, value_id)) + + class ReporterStringNumber(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "argument_reporter_string_number", _shadow=shadow, pos=pos, mutation=Mutation()) + + def set_value(self, value: str = "boolean", value_id: str = None): + return self.add_field(Field("VALUE", value, value_id)) + + +class Addons: + class IsTurbowarp(Args.ReporterBoolean): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_value("is turbowarp?") + + class IsCompiled(Args.ReporterBoolean): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_value("is compiled?") + + class IsForkphorus(Args.ReporterBoolean): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_value("is forkphorus?") + + class Breakpoint(Proc.Call): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_proc_code("​​breakpoint​​") + + class Log(Proc.Call): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_proc_code("​​log​​ %s") + self.set_argument_ids("arg0") + + def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + + class Warn(Proc.Call): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_proc_code("​​warn​​ %s") + self.set_argument_ids("arg0") + + def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + + class Error(Proc.Call): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(_shadow=shadow, pos=pos) + self.set_proc_code("​​error​​ %s") + self.set_argument_ids("arg0") + + def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) + + +class Pen: + class Clear(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_clear", _shadow=shadow, pos=pos) + + class Stamp(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_stamp", _shadow=shadow, pos=pos) + + class PenDown(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_penDown", _shadow=shadow, pos=pos) + + class PenUp(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_penUp", _shadow=shadow, pos=pos) + + class SetPenColorToColor(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_setPenColorToColor", _shadow=shadow, pos=pos) + + def set_color(self, value="#FF0000", input_type: str | int = "color", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangePenParamBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_changePenColorParamBy", _shadow=shadow, pos=pos) + + def set_param(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR_PARAM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetPenParamTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_setPenColorParamTo", _shadow=shadow, pos=pos) + + def set_param(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("COLOR_PARAM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangePenSizeBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_changePenSizeBy", _shadow=shadow, pos=pos) + + def set_size(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetPenSizeTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_setPenSizeTo", _shadow=shadow, pos=pos) + + def set_size(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetPenHueTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_setPenHueToNumber", _shadow=shadow, pos=pos) + + def set_hue(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("HUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangePenHueBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_changePenHueBy", _shadow=shadow, pos=pos) + + def set_hue(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("HUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetPenShadeTo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_setPenShadeToNumber", _shadow=shadow, pos=pos) + + def set_shade(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SHADE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangePenShadeBy(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_changePenShadeBy", _shadow=shadow, pos=pos) + + def set_shade(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SHADE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ColorParamMenu(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "pen_menu_colorParam", _shadow=shadow, pos=pos) + + def set_color_param(self, value: str = "color", value_id: str = None): + return self.add_field(Field("colorParam", value, value_id)) + + +class Music: + class PlayDrumForBeats(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_playDrumForBeats", _shadow=shadow, pos=pos) + + def set_drum(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DRUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class PlayNoteForBeats(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_playDrumForBeats", _shadow=shadow, pos=pos) + + def set_note(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("NOTE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class RestForBeats(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_restForBeats", _shadow=shadow, pos=pos) + + def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetTempo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_setTempo", _shadow=shadow, pos=pos) + + def set_beats(self, value="60", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TEMPO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class ChangeTempo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_changeTempo", _shadow=shadow, pos=pos) + + def set_beats(self, value="60", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TEMPO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class GetTempo(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_getTempo", _shadow=shadow, pos=pos) + + class SetInstrument(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_setInstrument", _shadow=shadow, pos=pos) + + def set_instrument(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INSTRUMENT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MidiPlayDrumForBeats(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_midiPlayDrumForBeats", _shadow=shadow, pos=pos) + + def set_drum(self, value="123", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("DRUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_beats(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MidiSetInstrument(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_midiSetInstrument", _shadow=shadow, pos=pos) + + def set_instrument(self, value="6", input_type: str | int = "positive integer", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("INSTRUMENT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuDrum(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_menu_DRUM", _shadow=shadow, pos=pos) + + def set_drum(self, value: str = "1", value_id: str = None): + return self.add_field(Field("DRUM", value, value_id)) + + class MenuInstrument(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "music_menu_INSTRUMENT", _shadow=shadow, pos=pos) + + def set_instrument(self, value: str = "1", value_id: str = None): + return self.add_field(Field("INSTRUMENT", value, value_id)) + + +class VideoSensing: + class WhenMotionGreaterThan(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_whenMotionGreaterThan", _shadow=shadow, pos=pos) + + def set_reference(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("REFERENCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class VideoOn(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_videoOn", _shadow=shadow, pos=pos) + + def set_attribute(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("ATTRIBUTE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_subject(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SUBJECT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuAttribute(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_menu_ATTRIBUTE", _shadow=shadow, pos=pos) + + def set_attribute(self, value: str = "motion", value_id: str = None): + return self.add_field(Field("ATTRIBUTE", value, value_id)) + + class MenuSubject(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_menu_SUBJECT", _shadow=shadow, pos=pos) + + def set_subject(self, value: str = "this sprite", value_id: str = None): + return self.add_field(Field("SUBJECT", value, value_id)) + + class VideoToggle(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_videoToggle", _shadow=shadow, pos=pos) + + def set_video_state(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VIDEO_STATE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuVideoState(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_menu_VIDEO_STATE", _shadow=shadow, pos=pos) + + def set_video_state(self, value: str = "on", value_id: str = None): + return self.add_field(Field("VIDEO_STATE", value, value_id)) + + class SetVideoTransparency(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "videoSensing_setVideoTransparency", _shadow=shadow, pos=pos) + + def set_transparency(self, value: str = "50", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("TRANSPARENCY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + +class Text2Speech: + class SpeakAndWait(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "text2speech_speakAndWait", _shadow=shadow, pos=pos) + + def set_words(self, value: str = "50", input_type: str | int = "number", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("WORDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class SetVoice(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "text2speech_setVoice", _shadow=shadow, pos=pos) + + def set_voice(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("VOICE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuVoices(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "text2speech_menu_voices", _shadow=shadow, pos=pos) + + def set_voices(self, value: str = "ALTO", value_id: str = None): + return self.add_field(Field("voices", value, value_id)) + + class SetLanguage(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "text2speech_setLanguage", _shadow=shadow, pos=pos) + + def set_language(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("LANGUAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuLanguages(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "text2speech_menu_languages", _shadow=shadow, pos=pos) + + def set_languages(self, value: str = "en", value_id: str = None): + return self.add_field(Field("languages", value, value_id)) + + +class Translate: + class GetTranslate(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "translate_getTranslate", _shadow=shadow, pos=pos) + + def set_words(self, value="hello!", input_type: str | int = "string", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("WORDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + def set_language(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("LANGUAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuLanguages(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "translate_menu_languages", _shadow=shadow, pos=pos) + + def set_languages(self, value: str = "sv", value_id: str = None): + return self.add_field(Field("languages", value, value_id)) + + class GetViewerLanguage(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "translate_getViewerLanguage", _shadow=shadow, pos=pos) + + +class MakeyMakey: + class WhenMakeyKeyPressed(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "makeymakey_whenMakeyKeyPressed", _shadow=shadow, pos=pos) + + def set_key(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("KEY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuKey(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "makeymakey_menu_KEY", _shadow=shadow, pos=pos) + + def set_key(self, value: str = "SPACE", value_id: str = None): + return self.add_field(Field("KEY", value, value_id)) + + class WhenCodePressed(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "makeymakey_whenCodePressed", _shadow=shadow, pos=pos) + + def set_sequence(self, value, input_type: str | int = "block", shadow_status: int = 1, *, + input_id: str = None, obscurer: str | Block = None): + + if isinstance(value, Block): + value = self.target.add_block(value) + elif isinstance(value, list) or isinstance(value, tuple): + if isinstance(value[0], Block): + value = self.target.link_chain(value) + return self.add_input( + Input("SEQUENCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) + + class MenuSequence(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "makeymakey_menu_SEQUENCE", _shadow=shadow, pos=pos) + + def set_key(self, value: str = "LEFT UP RIGHT", value_id: str = None): + return self.add_field(Field("SEQUENCE", value, value_id)) + + +class CoreExample: + class ExampleOpcode(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "coreExample_exampleOpcode", _shadow=shadow, pos=pos) + + class ExampleWithInlineImage(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "coreExample_exampleWithInlineImage", _shadow=shadow, pos=pos) + + +class OtherBlocks: + class Note(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "note", _shadow=shadow, pos=pos) + + def set_note(self, value: str = "60", value_id: str = None): + return self.add_field(Field("NOTE", value, value_id)) + + class Matrix(Block): + def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): + super().__init__(None, "matrix", _shadow=shadow, pos=pos) + + def set_note(self, value: str = "0101010101100010101000100", value_id: str = None): + return self.add_field(Field("MATRIX", value, value_id)) + + class RedHatBlock(Block): + def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): + # Note: There is no single opcode for the red hat block as the block is simply the result of an error + # The opcode here has been set to 'redhatblock' to make it obvious what is going on + + # (It's not called red_hat_block because then TurboWarp thinks that it's supposed to find an extension + # called red) + + # Appendix: You **CAN** actually add comments to this block, however it will make the block misbehave in the + # editor. The link between the comment and the block will not be visible, but will be visible with the + # corresponding TurboWarp addon + super().__init__(None, "redhatblock", _shadow=shadow, pos=pos, can_next=False) diff --git a/scratchattach/editor/sprite.py b/scratchattach/editor/sprite.py new file mode 100644 index 0000000..5cfd1ec --- /dev/null +++ b/scratchattach/editor/sprite.py @@ -0,0 +1,586 @@ +from __future__ import annotations + +import json +import warnings +from io import BytesIO, TextIOWrapper +from typing import Any, BinaryIO +from zipfile import ZipFile +from typing import Iterable, TYPE_CHECKING +from . import base, project, vlb, asset, comment, prim, block, commons, build_defaulting +if TYPE_CHECKING: + from . import asset + +class Sprite(base.ProjectSubcomponent, base.JSONExtractable): + def __init__(self, is_stage: bool = False, name: str = '', _current_costume: int = 1, _layer_order: int = None, + _volume: int = 100, + _broadcasts: list[vlb.Broadcast] = None, + _variables: list[vlb.Variable] = None, _lists: list[vlb.List] = None, + _costumes: list[asset.Costume] = None, _sounds: list[asset.Sound] = None, + _comments: list[comment.Comment] = None, _prims: dict[str, prim.Prim] = None, + _blocks: dict[str, block.Block] = None, + # Stage only: + _tempo: int | float = 60, _video_state: str = "off", _video_transparency: int | float = 50, + _text_to_speech_language: str = "en", _visible: bool = True, + # Sprite only: + _x: int | float = 0, _y: int | float = 0, _size: int | float = 100, _direction: int | float = 90, + _draggable: bool = False, _rotation_style: str = "all around", + + *, _project: project.Project = None): + """ + Represents a sprite or the stage (known internally as a Target) + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets + """ + # Defaulting for list parameters + if _broadcasts is None: + _broadcasts = [] + if _variables is None: + _variables = [] + if _lists is None: + _lists = [] + if _costumes is None: + _costumes = [] + if _sounds is None: + _sounds = [] + if _comments is None: + _comments = [] + if _prims is None: + _prims = {} + if _blocks is None: + _blocks = {} + + self.is_stage = is_stage + self.name = name + self.current_costume = _current_costume + self.layer_order = _layer_order + self.volume = _volume + + self.broadcasts = _broadcasts + self.variables = _variables + self.lists = _lists + self._local_globals = [] + + self.costumes = _costumes + self.sounds = _sounds + + self.comments = _comments + self.prims = _prims + self.blocks = _blocks + + self.tempo = _tempo + self.video_state = _video_state + self.video_transparency = _video_transparency + self.text_to_speech_language = _text_to_speech_language + self.visible = _visible + self.x, self.y = _x, _y + self.size = _size + self.direction = _direction + self.draggable = _draggable + self.rotation_style = _rotation_style + + self.asset_data = [] + + super().__init__(_project) + + # Assign sprite + for iterable in (self.vlbs, self.comments, self.assets, self.prims.values(), self.blocks.values()): + for sub_component in iterable: + sub_component.sprite = self + + def __repr__(self): + return f"Sprite<{self.name}>" + + def __enter__(self): + build_defaulting.stack_add_sprite(self) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + build_defaulting.pop_sprite(self) + + def link_subcomponents(self): + self.link_prims() + self.link_blocks() + self.link_comments() + + def link_prims(self): + """ + Link primitives to corresponding VLB objects (requires project attribute) + """ + for _prim in self.prims.values(): + _prim.link_using_sprite() + + def link_blocks(self): + """ + Link blocks to sprite/to other blocks + """ + for _block_id, _block in self.blocks.items(): + _block.link_using_sprite() + + def link_comments(self): + for _comment in self.comments: + _comment.link_using_sprite() + + def add_local_global(self, _vlb: base.NamedIDComponent): + self._local_globals.append(_vlb) + _vlb.sprite = self + + def add_variable(self, _variable: vlb.Variable): + self.variables.append(_variable) + _variable.sprite = self + + def add_list(self, _list: vlb.List): + self.variables.append(_list) + _list.sprite = self + + def add_broadcast(self, _broadcast: vlb.Broadcast): + self.variables.append(_broadcast) + _broadcast.sprite = self + + def add_vlb(self, _vlb: base.NamedIDComponent): + if isinstance(_vlb, vlb.Variable): + self.add_variable(_vlb) + elif isinstance(_vlb, vlb.List): + self.add_list(_vlb) + elif isinstance(_vlb, vlb.Broadcast): + self.add_broadcast(_vlb) + else: + warnings.warn(f"Invalid 'VLB' {_vlb} of type: {type(_vlb)}") + + def add_block(self, _block: block.Block | prim.Prim) -> block.Block | prim.Prim: + if _block.sprite is self: + if _block in self.blocks.values(): + return _block + + _block.sprite = self + + if isinstance(_block, block.Block): + self.blocks[self.new_id] = _block + _block.link_using_sprite() + + elif isinstance(_block, prim.Prim): + self.prims[self.new_id] = _block + _block.link_using_sprite() + + return _block + + def add_chain(self, *chain: Iterable[block.Block | prim.Prim]) -> block.Block | prim.Prim: + """ + Adds a list of blocks to the sprite **AND RETURNS THE FIRST BLOCK** + :param chain: + :return: + """ + chain = tuple(chain) + + _prev = self.add_block(chain[0]) + + for _block in chain[1:]: + _prev = _prev.attach_block(_block) + + return chain[0] + + def add_comment(self, _comment: comment.Comment) -> comment.Comment: + _comment.sprite = self + if _comment.id is None: + _comment.id = self.new_id + + self.comments.append(_comment) + _comment.link_using_sprite() + + return _comment + + def remove_block(self, _block: block.Block): + for key, val in self.blocks.items(): + if val is _block: + del self.blocks[key] + return + + @property + def folder(self): + return commons.get_folder_name(self.name) + + @property + def name_nfldr(self): + return commons.get_name_nofldr(self.name) + + @property + def vlbs(self) -> list[base.NamedIDComponent]: + """ + :return: All vlbs associated with the sprite. No local globals are added + """ + return self.variables + self.lists + self.broadcasts + + @property + def assets(self) -> list[asset.Costume | asset.Sound]: + return self.costumes + self.sounds + + @property + def stage(self) -> Sprite: + return self.project.stage + + @property + def new_id(self): + return commons.gen_id() + + @staticmethod + def from_json(data: dict): + _is_stage = data["isStage"] + _name = data["name"] + _current_costume = data.get("currentCostume", 1) + _layer_order = data.get("layerOrder", 1) + _volume = data.get("volume", 100) + + # Read VLB + def read_idcomponent(attr_name: str, cls: type[base.IDComponent]): + _vlbs = [] + for _vlb_id, _vlb_data in data.get(attr_name, {}).items(): + _vlbs.append(cls.from_json((_vlb_id, _vlb_data))) + return _vlbs + + _variables = read_idcomponent("variables", vlb.Variable) + _lists = read_idcomponent("lists", vlb.List) + _broadcasts = read_idcomponent("broadcasts", vlb.Broadcast) + + # Read assets + _costumes = [] + for _costume_data in data.get("costumes", []): + _costumes.append(asset.Costume.from_json(_costume_data)) + _sounds = [] + for _sound_data in data.get("sounds", []): + _sounds.append(asset.Sound.from_json(_sound_data)) + + # Read comments + _comments = read_idcomponent("comments", comment.Comment) + + # Read blocks/prims + _prims = {} + _blocks = {} + for _block_id, _block_data in data.get("blocks", {}).items(): + if isinstance(_block_data, list): + # Prim + _prims[_block_id] = prim.Prim.from_json(_block_data) + else: + # Block + _blocks[_block_id] = block.Block.from_json(_block_data) + + # Stage/sprite specific vars + _tempo, _video_state, _video_transparency, _text_to_speech_language = (None,) * 4 + _visible, _x, _y, _size, _direction, _draggable, _rotation_style = (None,) * 7 + if _is_stage: + _tempo = data["tempo"] + _video_state = data["videoState"] + _video_transparency = data["videoTransparency"] + _text_to_speech_language = data["textToSpeechLanguage"] + else: + _visible = data["visible"] + _x = data["x"] + _y = data["y"] + _size = data["size"] + _direction = data["direction"] + _draggable = data["draggable"] + _rotation_style = data["rotationStyle"] + + return Sprite(_is_stage, _name, _current_costume, _layer_order, _volume, _broadcasts, _variables, _lists, + _costumes, + _sounds, _comments, _prims, _blocks, + + _tempo, _video_state, _video_transparency, _text_to_speech_language, + _visible, _x, _y, _size, _direction, _draggable, _rotation_style + ) + + def to_json(self) -> dict: + _json = { + "isStage": self.is_stage, + "name": self.name, + "currentCostume": self.current_costume, + "volume": self.volume, + "layerOrder": self.layer_order, + + "variables": {_variable.id: _variable.to_json() for _variable in self.variables}, + "lists": {_list.id: _list.to_json() for _list in self.lists}, + "broadcasts": {_broadcast.id: _broadcast.to_json() for _broadcast in self.broadcasts}, + + "blocks": {_block_id: _block.to_json() for _block_id, _block in (self.blocks | self.prims).items()}, + "comments": {_comment.id: _comment.to_json() for _comment in self.comments}, + + "costumes": [_costume.to_json() for _costume in self.costumes], + "sounds": [_sound.to_json() for _sound in self.sounds] + } + + if self.is_stage: + _json.update({ + "tempo": self.tempo, + "videoTransparency": self.video_transparency, + "videoState": self.video_state, + "textToSpeechLanguage": self.text_to_speech_language + }) + else: + _json.update({ + "visible": self.visible, + + "x": self.x, "y": self.y, + "size": self.size, + "direction": self.direction, + + "draggable": self.draggable, + "rotationStyle": self.rotation_style + }) + + return _json + + # Finding/getting from list/dict attributes + def find_asset(self, value: str, by: str = "name", multiple: bool = False, a_type: type=None) -> asset.Asset | asset.Sound | asset.Costume | list[asset.Asset | asset.Sound | asset.Costume]: + if a_type is None: + a_type = asset.Asset + + _ret = [] + by = by.lower() + for _asset in self.assets: + if not isinstance(_asset, a_type): + continue + + # Defaulting + compare = getattr(_asset, by) + + if compare == value: + if multiple: + _ret.append(_asset) + else: + return _asset + + if multiple: + return _ret + + def find_variable(self, value: str, by: str = "name", multiple: bool = False) -> vlb.Variable | list[vlb.Variable]: + _ret = [] + by = by.lower() + for _variable in self.variables + self._local_globals: + if not isinstance(_variable, vlb.Variable): + continue + + if by == "id": + compare = _variable.id + else: + # Defaulting + compare = _variable.name + if compare == value: + if multiple: + _ret.append(_variable) + else: + return _variable + # Search in stage for global variables + if self.project: + if not self.is_stage: + if multiple: + _ret += self.stage.find_variable(value, by, True) + else: + return self.stage.find_variable(value, by) + + if multiple: + return _ret + + def find_list(self, value: str, by: str = "name", multiple: bool = False) -> vlb.List | list[vlb.List]: + _ret = [] + by = by.lower() + for _list in self.lists + self._local_globals: + if not isinstance(_list, vlb.List): + continue + if by == "id": + compare = _list.id + else: + # Defaulting + compare = _list.name + if compare == value: + if multiple: + _ret.append(_list) + else: + return _list + # Search in stage for global lists + if self.project: + if not self.is_stage: + if multiple: + _ret += self.stage.find_list(value, by, True) + else: + return self.stage.find_list(value, by) + + if multiple: + return _ret + + def find_broadcast(self, value: str, by: str = "name", multiple: bool = False) -> vlb.Broadcast | list[ + vlb.Broadcast]: + _ret = [] + by = by.lower() + for _broadcast in self.broadcasts + self._local_globals: + if not isinstance(_broadcast, vlb.Broadcast): + continue + if by == "id": + compare = _broadcast.id + else: + # Defaulting + compare = _broadcast.name + if compare == value: + if multiple: + _ret.append(_broadcast) + else: + return _broadcast + # Search in stage for global broadcasts + if self.project: + if not self.is_stage: + if multiple: + _ret += self.stage.find_broadcast(value, by, True) + else: + return self.stage.find_broadcast(value, by) + + if multiple: + return _ret + + def find_vlb(self, value: str, by: str = "name", + multiple: bool = False) -> vlb.Variable | vlb.List | vlb.Broadcast | list[ + vlb.Variable | vlb.List | vlb.Broadcast]: + if multiple: + return self.find_variable(value, by, True) + \ + self.find_list(value, by, True) + \ + self.find_broadcast(value, by, True) + else: + _ret = self.find_variable(value, by) + if _ret is not None: + return _ret + _ret = self.find_list(value, by) + if _ret is not None: + return _ret + return self.find_broadcast(value, by) + + def find_block(self, value: str | Any, by: str, multiple: bool = False) -> block.Block | prim.Prim | list[ + block.Block | prim.Prim]: + _ret = [] + by = by.lower() + for _block_id, _block in (self.blocks | self.prims).items(): + _block: block.Block | prim.Prim + + is_block = isinstance(_block, block.Block) + is_prim = isinstance(_block, prim.Prim) + + compare = None + if by == "id": + compare = _block_id + elif by == "argument ids": + if is_prim: + continue + + if _block.mutation is not None: + compare = _block.mutation.argument_ids + elif by == "opcode": + if is_prim: + continue + + # Defaulting + compare = _block.opcode + else: + if is_block: + compare = _block.opcode + else: + compare = _block.value + + if compare == value: + if multiple: + _ret.append(_block) + else: + return _block + # Search in stage for global variables + if self.project: + if not self.is_stage: + if multiple: + _ret += self.stage.find_block(value, by, True) + else: + return self.stage.find_block(value, by) + + if multiple: + return _ret + + def export(self, fp: str = None, *, export_as_zip: bool = True): + if fp is None: + fp = commons.sanitize_fn(f"{self.name}.sprite3") + + data = self.to_json() + + if export_as_zip: + with ZipFile(fp, "w") as archive: + for _asset in self.assets: + asset_file = _asset.asset_file + if asset_file.filename not in archive.namelist(): + archive.writestr(asset_file.filename, asset_file.data) + + archive.writestr("sprite.json", json.dumps(data)) + else: + with open(fp, "w") as json_file: + json.dump(data, json_file) + + @property + def all_ids(self): + ret = [] + for _vlb in self.vlbs + self._local_globals: + ret.append(_vlb.id) + for iterator in self.blocks.keys(), self.prims.keys(): + ret += list(iterator) + + return ret + @staticmethod + def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: str = None): + _dir_for_name = None + + if _name is None: + if hasattr(data, "name"): + _dir_for_name = data.name + + if not isinstance(_name, str) and _name is not None: + _name = str(_name) + + if isinstance(data, bytes): + data = BytesIO(data) + + elif isinstance(data, str): + _dir_for_name = data + data = open(data, "rb") + + if _name is None and _dir_for_name is not None: + # Remove any directory names and the file extension + _name = _dir_for_name.split('/')[-1] + _name = '.'.join(_name.split('.')[:-1]) + + asset_data = [] + with data: + # For if the sprite3 is just JSON (e.g. if it's exported from scratchattach) + if commons.is_valid_json(data): + json_str = data + + else: + with ZipFile(data) as archive: + json_str = archive.read("sprite.json") + + # Also load assets + if load_assets: + + for filename in archive.namelist(): + if filename != "sprite.json": + md5_hash = filename.split('.')[0] + + asset_data.append( + asset.AssetFile(filename, archive.read(filename), md5_hash) + ) + else: + warnings.warn( + "Loading sb3 without loading assets. When exporting the project, there may be errors due to assets not being uploaded to the Scratch website") + + return _name, asset_data, json_str + + @classmethod + def from_sprite3(cls, data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: str = None): + """ + Load a project from an .sb3 file/bytes/file path + """ + _name, asset_data, json_str = cls.load_json(data, load_assets, _name) + data = json.loads(json_str) + + sprite = cls.from_json(data) + # Sprites already have names, so we probably don't want to set it + # sprite.name = _name + sprite.asset_data = asset_data + return sprite diff --git a/scratchattach/editor/todo.md b/scratchattach/editor/todo.md new file mode 100644 index 0000000..203a81e --- /dev/null +++ b/scratchattach/editor/todo.md @@ -0,0 +1,105 @@ +# Things to add to scratchattach.editor (sbeditor v2) + +## All + +- [ ] Docstrings +- [x] Dealing with stuff from the backpack (it's in a weird format): This may require a whole separate module +- [ ] Getter functions (`@property`) instead of directly editing attrs (make them protected attrs) +- [ ] Check if whitespace chars break IDs +- [ ] Maybe blockchain should be renamed to 'script' +- [ ] Perhaps use sprites as blockchain wrappers due to their existing utility (loading of local globals etc) +- [ ] bs4 styled search function +- [ ] ScratchJR project parser (lol) +- [ ] Error checking (for when you need to specify sprite etc) +- [x] Split json unpacking and the use of .from_json method so that it is easy to just extract json data (but not parse + it) + +## Project + +- [x] Asset list +- [ ] Obfuscation +- [x] Detection for twconfig +- [x] Edit twconfig +- [ ] Find targets + +## Block + +### Finding blocks/attrs + +- [x] Top level block (stack parent) +- [x] Previous chain +- [x] Attached chain +- [x] Complete chain +- [x] Block categories +- [x] Block shape attr aka stack type (Stack/hat/c-mouth/end/reporter/boolean detection) +- [x] `can_next` property +- [x] `is_input` property: Check if block is an input obscurer +- [x] `parent_input` property: Get input that this block obscures +- [x] `stack_tree` old 'subtree' property: Get the 'ast' of this blockchain (a 'tree' structure - well actually a list + of lists) +- [x] `children` property - list of all blocks with this block as a parent except next block (any input obscurers) +- [x] Detection for turbowarp debug blocks + (proc codes: + `"​​log​​ %s", + "​​breakpoint​​", + "​​error​​ %s", + "​​warn​​ %s"` - note: they all have ZWSPs) +- [x] Detection for `` and `` and `` booleans + +### Adding/removing blocks + +- [x] Add block to sprite +- [x] Duplicating (single) block +- [x] Attach block +- [x] Duplicating blockchain +- [x] Slot above (if possible - raise error if not) +- [x] Attach blockchain +- [x] Delete block +- [x] Delete blockchain +- [x] Add/edit inputs +- [x] Add/edit fields +- [x] Add mutation +- [x] Add comment +- [x] Get comment + +## Mutation + +- [ ] Proc code builder +- [x] get type of argument (bool/str) inside argument class +- [ ] to/from json for args? + +## Sprite + +### Finding ID components + +- [x] Find var/list/broadcast +- [x] Find block/prim +- [ ] Add costume/sound +- [ ] Add var/list/broadcast +- [ ] Add arbitrary block/blockchain +- [ ] Asset count +- [ ] Obfuscation +- [ ] Var/list/broadcast/block/comment/whole id list (like `_EnumWrapper.all_of`) +- [ ] Get custom blocks list +- [ ] With statements for sprite to allow for choosing default sprite + +## Vars/lists/broadcasts + +- [ ] idk + +## Monitors + +- [ ] Get relevant var/list if applicable +- [ ] Generate from block + +## Assets + +- [x] Download assets +- [ ] Upload asset +- [ ] Load from file (auto-detect type) + +## Pallet + +- [ ] Add all block defaults (like sbuild.py) +- [ ] Actions (objects that behave like blocks but add multiple blocks - e.g. a 'superjoin' block that you can use to + join more than 2 strings with one block (by actually building multiple join blocks)) \ No newline at end of file diff --git a/scratchattach/editor/twconfig.py b/scratchattach/editor/twconfig.py new file mode 100644 index 0000000..858bd00 --- /dev/null +++ b/scratchattach/editor/twconfig.py @@ -0,0 +1,113 @@ +""" +Parser for TurboWarp settings configuration +""" + +from __future__ import annotations + +import json +import math +from dataclasses import dataclass +from typing import Any + +from . import commons, base + +_START = """Configuration for https://turbowarp.org/ +You can move, resize, and minimize this comment, but don't edit it by hand. This comment can be deleted to remove the stored settings. +""" +_END = " // _twconfig_" + + +@dataclass(init=True, repr=True) +class TWConfig(base.JSONSerializable): + framerate: int = None, + interpolation: bool = False, + hq_pen: bool = False, + max_clones: float | int | None = None, + misc_limits: bool = True, + fencing: bool = True + width: int = None + height: int = None + + @staticmethod + def from_json(data: dict) -> TWConfig: + # Non-runtime options + _framerate = data.get("framerate") + _interpolation = data.get("interpolation", False) + _hq_pen = data.get("hq", False) + + # Runtime options + _runtime_options = data.get("runtimeOptions", {}) + + # Luckily for us, the JSON module actually accepts the 'Infinity' literal. Otherwise, it would be a right pain + _max_clones = _runtime_options.get("maxClones") + _misc_limits = _runtime_options.get("miscLimits", True) + _fencing = _runtime_options.get("fencing", True) + + # Custom stage size + _width = data.get("width") + _height = data.get("height") + + return TWConfig(_framerate, _interpolation, _hq_pen, _max_clones, _misc_limits, _fencing, _width, _height) + + def to_json(self) -> dict: + runtime_options = {} + commons.noneless_update( + runtime_options, + { + "maxClones": self.max_clones, + "miscLimits": none_if_eq(self.misc_limits, True), + "fencing": none_if_eq(self.fencing, True) + }) + + data = {} + commons.noneless_update(data, { + "framerate": self.framerate, + "runtimeOptions": runtime_options, + "interpolation": none_if_eq(self.interpolation, False), + "hq": none_if_eq(self.hq_pen, False), + "width": self.width, + "height": self.height + }) + return data + + @property + def infinite_clones(self): + return self.max_clones == math.inf + + @staticmethod + def from_str(string: str): + return TWConfig.from_json(get_twconfig_data(string)) + + +def is_valid_twconfig(string: str) -> bool: + """ + Checks if some text is TWConfig (does not check the JSON itself) + :param string: text (from a comment) + :return: Boolean whether it is TWConfig + """ + + if string.startswith(_START) and string.endswith(_END): + json_part = string[len(_START):-len(_END)] + if commons.is_valid_json(json_part): + return True + return False + + +def get_twconfig_data(string: str) -> dict | None: + try: + return json.loads(string[len(_START):-len(_END)]) + except ValueError: + return None + + +def none_if_eq(data, compare) -> Any | None: + """ + Returns None if data and compare are the same + :param data: Original data + :param compare: Data to compare + :return: Either the original data or None + """ + if data == compare: + return None + else: + return data diff --git a/scratchattach/editor/vlb.py b/scratchattach/editor/vlb.py new file mode 100644 index 0000000..174f0d8 --- /dev/null +++ b/scratchattach/editor/vlb.py @@ -0,0 +1,134 @@ +""" +Variables, lists & broadcasts +""" +# Perhaps ids should not be stored in these objects, but in the sprite, similarly +# to how blocks/prims are stored + +from __future__ import annotations + +from typing import Literal + +from . import base, sprite, build_defaulting +from ..utils import exceptions + + +class Variable(base.NamedIDComponent): + def __init__(self, _id: str, _name: str, _value: str | int | float = None, _is_cloud: bool = False, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Class representing a variable. + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=variables,otherwise%20not%20present + """ + if _value is None: + _value = 0 + + self.value = _value + self.is_cloud = _is_cloud + + super().__init__(_id, _name, _sprite) + + @property + def is_global(self): + """ + Works out whethere a variable is global based on whether the sprite is a stage + :return: Whether this variable is a global variable. + """ + return self.sprite.is_stage + + @staticmethod + def from_json(data: tuple[str, tuple[str, str | int | float] | tuple[str, str | int | float, bool]]): + """ + Read data in format: (variable id, variable JSON) + """ + assert len(data) == 2 + _id, data = data + + assert len(data) in (2, 3) + _name, _value = data[:2] + + if len(data) == 3: + _is_cloud = data[2] + else: + _is_cloud = False + + return Variable(_id, _name, _value, _is_cloud) + + def to_json(self) -> tuple[str, str | int | float, bool] | tuple[str, str | int | float]: + """ + Returns Variable data as a tuple + """ + if self.is_cloud: + _ret = self.name, self.value, True + else: + _ret = self.name, self.value + + return _ret + + +class List(base.NamedIDComponent): + def __init__(self, _id: str, _name: str, _value: list[str | int | float] = None, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Class representing a list. + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=lists,as%20an%20array + """ + if _value is None: + _value = [] + + self.value = _value + super().__init__(_id, _name, _sprite) + + @staticmethod + def from_json(data: tuple[str, tuple[str, str | int | float] | tuple[str, str | int | float, bool]]): + """ + Read data in format: (variable id, variable JSON) + """ + assert len(data) == 2 + _id, data = data + + assert len(data) == 2 + _name, _value = data + + return List(_id, _name, _value) + + def to_json(self) -> tuple[str, tuple[str, str | int | float, bool] | tuple[str, str | int | float]]: + """ + Returns List data as a tuple + """ + return self.name, self.value + + +class Broadcast(base.NamedIDComponent): + def __init__(self, _id: str, _name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Class representing a broadcast. + https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=broadcasts,in%20the%20stage + """ + super().__init__(_id, _name, _sprite) + + @staticmethod + def from_json(data: tuple[str, str]): + assert len(data) == 2 + _id, _name = data + + return Broadcast(_id, _name) + + def to_json(self) -> str: + """ + :return: Broadcast as JSON (just a string of its name) + """ + return self.name + + +def construct(vlb_type: Literal["variable", "list", "broadcast"], _id: str = None, _name: str = None, + _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT) -> Variable | List | Broadcast: + if vlb_type == "variable": + vlb_type = Variable + elif vlb_type == "list": + vlb_type = List + elif vlb_type == "broadcast": + vlb_type = Broadcast + else: + raise exceptions.InvalidVLBName(f"Bad VLB {vlb_type!r}") + + return vlb_type(_id, _name, _sprite) diff --git a/scratchattach/other/project_json_capabilities.py b/scratchattach/other/project_json_capabilities.py index 5c02246..4e11459 100644 --- a/scratchattach/other/project_json_capabilities.py +++ b/scratchattach/other/project_json_capabilities.py @@ -1,6 +1,7 @@ """Project JSON reading and editing capabilities. This code is still in BETA, there are still bugs and potential consistency issues to be fixed. New features will be added.""" +# Note: You may want to make this into multiple files for better organisation from __future__ import annotations import hashlib @@ -9,20 +10,15 @@ import string import zipfile from abc import ABC, abstractmethod - from ..utils import exceptions from ..utils.commons import empty_project_json from ..utils.requests import Requests as requests - - # noinspection PyPep8Naming def load_components(json_data: list, ComponentClass: type, target_list: list): for element in json_data: component = ComponentClass() component.from_json(element) target_list.append(component) - - class ProjectBody: class BaseProjectBodyComponent(ABC): def __init__(self, **entries): @@ -30,28 +26,21 @@ def __init__(self, **entries): self.id = None # Update attributes from entries dict: self.__dict__.update(entries) - @abstractmethod def from_json(self, data: dict): pass - @abstractmethod def to_json(self): pass - def _generate_new_id(self): """ Generates a new id and updates the id. - Warning: When done on Block objects, the next_id attribute of the parent block and the parent_id attribute of the next block will NOT be updated by this method. """ self.id = ''.join(random.choices(string.ascii_letters + string.digits, k=20)) - class Block(BaseProjectBodyComponent): - # Thanks to @MonkeyBean2 for some scripts - def from_json(self, data: dict): self.opcode = data["opcode"] # The name of the block self.next_id = data.get("next", None) # The id of the block attached below this block @@ -63,23 +52,18 @@ def from_json(self, data: dict): self.mutation = data.get("mutation", None) # For custom blocks self.x = data.get("x", None) # x position if topLevel self.y = data.get("y", None) # y position if topLevel - def to_json(self): output = {"opcode": self.opcode, "next": self.next_id, "parent": self.parent_id, "inputs": self.input_data, "fields": self.fields, "shadow": self.shadow, "topLevel": self.topLevel, "mutation": self.mutation, "x": self.x, "y": self.y} return {k: v for k, v in output.items() if v} - def attached_block(self): return self.sprite.block_by_id(self.next_id) - def previous_block(self): return self.sprite.block_by_id(self.parent_id) - def top_level_block(self): block = self return block - def previous_chain(self): # to implement: a method that detects circular block chains (to make sure this method terminates) chain = [] @@ -88,7 +72,6 @@ def previous_chain(self): block = block.previous_block() chain.insert(0, block) return chain - def attached_chain(self): chain = [] block = self @@ -96,10 +79,8 @@ def attached_chain(self): block = block.attached_block() chain.append(block) return chain - def complete_chain(self): return self.previous_chain() + [self] + self.attached_chain() - def duplicate_single_block(self): new_block = ProjectBody.Block(**self.__dict__) new_block.parent_id = None @@ -107,7 +88,6 @@ def duplicate_single_block(self): new_block._generate_new_id() self.sprite.blocks.append(new_block) return new_block - def duplicate_chain(self): blocks_to_dupe = [self] + self.attached_chain() duped = [] @@ -122,62 +102,46 @@ def duplicate_chain(self): duped.append(new_block) self.sprite.blocks += duped return duped - def _reattach(self, new_parent_id, new_next_id_of_old_parent): if self.parent_id is not None: old_parent_block = self.sprite.block_by_id(self.parent_id) self.sprite.blocks.remove(old_parent_block) old_parent_block.next_id = new_next_id_of_old_parent self.sprite.blocks.append(old_parent_block) - self.parent_id = new_parent_id - if self.parent_id is not None: new_parent_block = self.sprite.block_by_id(self.parent_id) self.sprite.blocks.remove(new_parent_block) new_parent_block.next_id = self.id self.sprite.blocks.append(new_parent_block) - self.topLevel = new_parent_id is None - def reattach_single_block(self, new_parent_id): old_parent_id = str(self.parent_id) self._reattach(new_parent_id, self.next_id) - if self.next_id is not None: old_next_block = self.sprite.block_by_id(self.next_id) self.sprite.blocks.remove(old_next_block) old_next_block.parent_id = old_parent_id self.sprite.blocks.append(old_next_block) - self.next_id = None - def reattach_chain(self, new_parent_id): self._reattach(new_parent_id, None) - def delete_single_block(self): self.sprite.blocks.remove(self) - self.reattach_single_block(None, self.next_id) - def delete_chain(self): self.sprite.blocks.remove(self) - self.reattach_chain(None) - to_delete = self.attached_chain() for block in to_delete: self.sprite.blocks.remove(block) - def inputs_as_blocks(self): if self.input_data is None: return None inputs = [] for input in self.input_data: inputs.append(self.sprite.block_by_id(self.input_data[input][1])) - class Sprite(BaseProjectBodyComponent): - def from_json(self, data: dict): self.isStage = data["isStage"] self.name = data["name"] @@ -221,7 +185,6 @@ def from_json(self, data: dict): self.direction = data.get("direction", None) self.draggable = data.get("draggable", None) self.rotationStyle = data.get("rotationStyle", None) - def to_json(self): return_data = dict(self.__dict__) if "projectBody" in return_data: @@ -239,42 +202,34 @@ def to_json(self): return_data["costumes"] = [custome.to_json() for custome in self.costumes] return_data["sounds"] = [sound.to_json() for sound in self.sounds] return return_data - def variable_by_id(self, variable_id): matching = list(filter(lambda x: x.id == variable_id, self.variables)) if matching == []: return None return matching[0] - def list_by_id(self, list_id): matching = list(filter(lambda x: x.id == list_id, self.lists)) if matching == []: return None return matching[0] - def variable_by_name(self, variable_name): matching = list(filter(lambda x: x.name == variable_name, self.variables)) if matching == []: return None return matching[0] - def list_by_name(self, list_name): matching = list(filter(lambda x: x.name == list_name, self.lists)) if matching == []: return None return matching[0] - def block_by_id(self, block_id): matching = list(filter(lambda x: x.id == block_id, self.blocks)) if matching == []: return None return matching[0] - # -- Functions to modify project contents -- - def create_sound(self, asset_content, *, name="new sound", dataFormat="mp3", rate=4800, sampleCount=4800): data = asset_content if isinstance(asset_content, bytes) else open(asset_content, "rb").read() - new_asset_id = hashlib.md5(data).hexdigest() new_asset = ProjectBody.Asset(assetId=new_asset_id, name=name, id=new_asset_id, dataFormat=dataFormat, rate=rate, sampleCound=sampleCount, md5ext=new_asset_id + "." + dataFormat, @@ -289,11 +244,9 @@ def create_sound(self, asset_content, *, name="new sound", dataFormat="mp3", rat else: self._session.upload_asset(data, asset_id=new_asset_id, file_ext=dataFormat) return new_asset - def create_costume(self, asset_content, *, name="new costume", dataFormat="svg", rotationCenterX=0, rotationCenterY=0): data = asset_content if isinstance(asset_content, bytes) else open(asset_content, "rb").read() - new_asset_id = hashlib.md5(data).hexdigest() new_asset = ProjectBody.Asset(assetId=new_asset_id, name=name, id=new_asset_id, dataFormat=dataFormat, rotationCenterX=rotationCenterX, rotationCenterY=rotationCenterY, @@ -309,76 +262,59 @@ def create_costume(self, asset_content, *, name="new costume", dataFormat="svg", else: self._session.upload_asset(data, asset_id=new_asset_id, file_ext=dataFormat) return new_asset - def create_variable(self, name, *, value=0, is_cloud=False): new_var = ProjectBody.Variable(name=name, value=value, is_cloud=is_cloud) self.variables.append(new_var) return new_var - def create_list(self, name, *, value=[]): new_list = ProjectBody.List(name=name, value=value) self.lists.append(new_list) return new_list - def add_block(self, block, *, parent_id=None): block.parent_id = None block.next_id = None if parent_id is not None: block.reattach_single_block(parent_id) self.blocks.append(block) - def add_block_chain(self, block_chain, *, parent_id=None): parent = parent_id for block in block_chain: self.add_block(block, parent_id=parent) parent = str(block.id) - class Variable(BaseProjectBodyComponent): - def __init__(self, **entries): super().__init__(**entries) if self.id is None: self._generate_new_id() - def from_json(self, data: list): self.name = data[0] self.saved_value = data[1] self.is_cloud = len(data) == 3 - def to_json(self): if self.is_cloud: return [self.name, self.saved_value, True] else: return [self.name, self.saved_value] - def make_cloud_variable(self): self.is_cloud = True - class List(BaseProjectBodyComponent): - def __init__(self, **entries): super().__init__(**entries) if self.id is None: self._generate_new_id() - def from_json(self, data: list): self.name = data[0] self.saved_content = data[1] - def to_json(self): return [self.name, self.saved_content] - class Monitor(BaseProjectBodyComponent): - def from_json(self, data: dict): self.__dict__.update(data) - def to_json(self): return_data = dict(self.__dict__) if "projectBody" in return_data: return_data.pop("projectBody") return return_data - def target(self): if not hasattr(self, "projectBody"): print("Can't get represented object because the origin projectBody of this monitor is not saved") @@ -387,22 +323,18 @@ def target(self): return self.projectBody.sprite_by_name(self.spriteName).variable_by_name(self.params["VARIABLE"]) if "LIST" in self.params: return self.projectBody.sprite_by_name(self.spriteName).list_by_name(self.params["LIST"]) - class Asset(BaseProjectBodyComponent): - def from_json(self, data: dict): self.__dict__.update(data) self.id = self.assetId self.filename = self.md5ext self.download_url = f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}" - def to_json(self): return_data = dict(self.__dict__) return_data.pop("filename") return_data.pop("id") return_data.pop("download_url") return return_data - def download(self, *, filename=None, dir=""): if not (dir.endswith("/") or dir.endswith("\\")): dir = dir + "/" @@ -420,7 +352,6 @@ def download(self, *, filename=None, dir=""): "Failed to download asset" ) ) - def __init__(self, *, sprites=[], monitors=[], extensions=[], meta=[{"agent": None}], _session=None): # sprites are called "targets" in the initial API response self.sprites = sprites @@ -428,7 +359,6 @@ def __init__(self, *, sprites=[], monitors=[], extensions=[], meta=[{"agent": No self.extensions = extensions self.meta = meta self._session = _session - def from_json(self, data: dict): """ Imports the project data from a dict that contains the raw project json @@ -445,10 +375,9 @@ def from_json(self, data: dict): # Save origin of monitor in Monitor object: for monitor in self.monitors: monitor.projectBody = self - # Set extensions and meta attributs: + # Set extensions and meta attributs: self.extensions = data["extensions"] self.meta = data["meta"] - def to_json(self): """ Returns a valid project JSON dict with the contents of this project @@ -459,47 +388,36 @@ def to_json(self): return_data["extensions"] = self.extensions return_data["meta"] = self.meta return return_data - # -- Functions to get info -- - def blocks(self): return [block for sprite in self.sprites for block in sprite.blocks] - def block_count(self): return len(self.blocks()) - def assets(self): return [sound for sprite in self.sprites for sound in sprite.sounds] + [costume for sprite in self.sprites for costume in sprite.costumes] - def asset_count(self): return len(self.assets()) - def variable_by_id(self, variable_id): for sprite in self.sprites: r = sprite.variable_by_id(variable_id) if r is not None: return r - def list_by_id(self, list_id): for sprite in self.sprites: r = sprite.list_by_id(list_id) if r is not None: return r - def sprite_by_name(self, sprite_name): matching = list(filter(lambda x: x.name == sprite_name, self.sprites)) if matching == []: return None return matching[0] - def user_agent(self): return self.meta["agent"] - def save(self, *, filename=None, dir=""): """ Saves the project body to the given directory. - Args: filename (str): The name that will be given to the downloaded file. dir (str): The path of the directory the file will be saved in. @@ -511,20 +429,14 @@ def save(self, *, filename=None, dir=""): filename = filename.replace(".sb3", "") with open(f"{dir}{filename}.sb3", "w") as d: json.dump(self.to_json(), d, indent=4) - - def get_empty_project_pb(): pb = ProjectBody() pb.from_json(empty_project_json) return pb - - def get_pb_from_dict(project_json: dict): pb = ProjectBody() pb.from_json(project_json) return pb - - def _load_sb3_file(path_to_file): try: with open(path_to_file, "r") as r: @@ -538,14 +450,10 @@ def _load_sb3_file(path_to_file): return json.loads(file.read()) else: raise ValueError("specified sb3 archive doesn't contain project.json") - - def read_sb3_file(path_to_file): pb = ProjectBody() pb.from_json(_load_sb3_file(path_to_file)) return pb - - def download_asset(asset_id_with_file_ext, *, filename=None, dir=""): if not (dir.endswith("/") or dir.endswith("\\")): dir = dir + "/" diff --git a/scratchattach/site/backpack_asset.py b/scratchattach/site/backpack_asset.py index f0b4269..48b65aa 100644 --- a/scratchattach/site/backpack_asset.py +++ b/scratchattach/site/backpack_asset.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json import time import logging from ._base import BaseSiteComponent -from ..utils.requests import Requests as requests from ..utils import exceptions +from ..utils.requests import Requests as requests + class BackpackAsset(BaseSiteComponent): @@ -37,26 +39,64 @@ def __init__(self, **entries): self.__dict__.update(entries) def update(self): - logging.warning("Warning: BackpackAsset objects can't be updated") + print("Warning: BackpackAsset objects can't be updated") return False # Objects of this type cannot be updated + def _update_from_dict(self, data) -> bool: - try: self.id = data["id"] - except Exception: pass - try: self.type = data["type"] - except Exception: pass - try: self.mime = data["mime"] - except Exception: pass - try: self.name = data["name"] - except Exception: pass - try: self.filename = data["body"] - except Exception: pass - try: self.thumbnail_url = "https://backpack.scratch.mit.edu/"+data["thumbnail"] - except Exception: pass - try: self.download_url = "https://backpack.scratch.mit.edu/"+data["body"] - except Exception: pass + try: + self.id = data["id"] + except Exception: + pass + try: + self.type = data["type"] + except Exception: + pass + try: + self.mime = data["mime"] + except Exception: + pass + try: + self.name = data["name"] + except Exception: + pass + try: + self.filename = data["body"] + except Exception: + pass + try: + self.thumbnail_url = "https://backpack.scratch.mit.edu/" + data["thumbnail"] + except Exception: + pass + try: + self.download_url = "https://backpack.scratch.mit.edu/" + data["body"] + except Exception: + pass return True + @property + def _data_bytes(self) -> bytes: + try: + return requests.get(self.download_url).content + except Exception as e: + raise exceptions.FetchError(f"Failed to download asset: {e}") + + @property + def file_ext(self): + return self.filename.split(".")[-1] + + @property + def is_json(self): + return self.file_ext == "json" + + @property + def data(self) -> dict | list | int | None | str | bytes | float: + if self.is_json: + return json.loads(self._data_bytes) + else: + # It's either a zip + return self._data_bytes + def download(self, *, fp: str = ''): """ Downloads the asset content to the given directory. The given filename is equal to the value saved in the .filename attribute. @@ -66,18 +106,7 @@ def download(self, *, fp: str = ''): """ if not (fp.endswith("/") or fp.endswith("\\")): fp = fp + "/" - try: - response = requests.get( - self.download_url, - timeout=10, - ) - open(f"{fp}{self.filename}", "wb").write(response.content) - except Exception as e: - raise ( - exceptions.FetchError( - "Failed to download asset: "+str(e) - ) - ) + open(f"{fp}{self.filename}", "wb").write(self._data_bytes) def delete(self): self._assert_auth() diff --git a/scratchattach/site/classroom.py b/scratchattach/site/classroom.py index 6e7fb2b..5021274 100644 --- a/scratchattach/site/classroom.py +++ b/scratchattach/site/classroom.py @@ -64,7 +64,13 @@ def update(self): response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/") soup = BeautifulSoup(response.text, "html.parser") + headings = soup.find_all("h1") + for heading in headings: + if heading.text == "Whoops! Our server is Scratch'ing its head": + raise exceptions.ClassroomNotFound(f"Classroom id {self.id} is not closed and cannot be found.") + # id, title, description, status, date_start (iso format), educator/username + title = soup.find("title").contents[0][:-len(" on Scratch")] overviews = soup.find_all("p", {"class": "overview"}) diff --git a/scratchattach/site/project.py b/scratchattach/site/project.py index 0cc33e7..00e900c 100644 --- a/scratchattach/site/project.py +++ b/scratchattach/site/project.py @@ -268,6 +268,25 @@ def download(self, *, filename=None, dir=""): ) ) + def get_json(self) -> str: + """ + Downloads the project json and returns it as a string + """ + try: + self.update() + response = requests.get( + f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}", + timeout=10, + ) + return response.text + + except Exception: + raise ( + exceptions.FetchError( + "Method only works for projects created with Scratch 3" + ) + ) + def body(self): """ Method only works for project created with Scratch 3. diff --git a/scratchattach/site/session.py b/scratchattach/site/session.py index d513a6c..6788d17 100644 --- a/scratchattach/site/session.py +++ b/scratchattach/site/session.py @@ -1039,6 +1039,7 @@ def login(username, password, *, timeout=10) -> Session: _headers["Cookie"] = "scratchcsrftoken=a;scratchlanguage=en;" request = requests.post( "https://scratch.mit.edu/login/", json={"username": username, "password": password}, headers=_headers, + timeout=timeout, errorhandling = False ) try: diff --git a/scratchattach/site/studio.py b/scratchattach/site/studio.py index 80875b2..b412dbe 100644 --- a/scratchattach/site/studio.py +++ b/scratchattach/site/studio.py @@ -10,6 +10,7 @@ from ..utils.requests import Requests as requests + class Studio(BaseSiteComponent): """ Represents a Scratch studio. @@ -121,7 +122,7 @@ def unfollow(self): timeout=10, ) - def comments(self, *, limit=40, offset=0): + def comments(self, *, limit=40, offset=0) -> list[comment.Comment]: """ Returns the comments posted on the studio (except for replies. To get replies use :meth:`scratchattach.studio.Studio.get_comment_replies`). diff --git a/scratchattach/site/user.py b/scratchattach/site/user.py index e05b792..0e247e2 100644 --- a/scratchattach/site/user.py +++ b/scratchattach/site/user.py @@ -263,7 +263,7 @@ def studios(self, *, limit=40, offset=0): studios.append(_studio) return studios - def projects(self, *, limit=40, offset=0): + def projects(self, *, limit=40, offset=0) -> list[project.Project]: """ Returns: list: The user's shared projects diff --git a/scratchattach/utils/commons.py b/scratchattach/utils/commons.py index 33ef63d..f08fdf3 100644 --- a/scratchattach/utils/commons.py +++ b/scratchattach/utils/commons.py @@ -17,8 +17,7 @@ "x-csrftoken": "a", "x-requested-with": "XMLHttpRequest", "referer": "https://scratch.mit.edu", -} # headers recommended for accessing API endpoints that don't require verification - +} empty_project_json: Final = { 'targets': [ { diff --git a/scratchattach/utils/enums.py b/scratchattach/utils/enums.py index 67d4d91..bbb7246 100644 --- a/scratchattach/utils/enums.py +++ b/scratchattach/utils/enums.py @@ -37,8 +37,14 @@ def apply_func(x): item_obj = item.value try: - if apply_func(getattr(item_obj, by)) == value: + if by is None: + _val = item_obj + else: + _val = getattr(item_obj, by) + + if apply_func(_val) == value: return item_obj + except TypeError: pass diff --git a/scratchattach/utils/exceptions.py b/scratchattach/utils/exceptions.py index a8b7526..63fd49d 100644 --- a/scratchattach/utils/exceptions.py +++ b/scratchattach/utils/exceptions.py @@ -91,7 +91,6 @@ class CommentNotFound(Exception): # Invalid inputs - class InvalidLanguage(Exception): """ Raised when an invalid language/language code/language object is provided, for TTS or Translate @@ -105,7 +104,6 @@ class InvalidTTSGender(Exception): """ pass - # API errors: class LoginFailure(Exception): @@ -209,3 +207,54 @@ class WebsocketServerError(Exception): """ pass + + +# Editor errors: + +class UnclosedJSONError(Exception): + """ + Raised when a JSON string is never closed. + """ + pass + + +class BadVLBPrimitiveError(Exception): + """ + Raised when a Primitive claiming to be a variable/list/broadcast actually isn't + """ + pass + + +class UnlinkedVLB(Exception): + """ + Raised when a Primitive cannot be linked to variable/list/broadcast because the provided ID does not have an associated variable/list/broadcast + """ + pass + + +class InvalidStageCount(Exception): + """ + Raised when a project has too many or too few Stage sprites + """ + pass + + +class InvalidVLBName(Exception): + """ + Raised when an invalid VLB name is provided (not variable, list or broadcast) + """ + pass + + +class BadBlockShape(Exception): + """ + Raised when the block shape cannot allow for the operation + """ + pass + + +class BadScript(Exception): + """ + Raised when the block script cannot allow for the operation + """ + pass