├── .python-version ├── .env ├── .gitignore ├── src └── sb2gs │ ├── utils.py │ ├── errors.py │ ├── _logging.py │ ├── custom_blocks.py │ ├── decompile_code.py │ ├── verify.py │ ├── decompile_input.py │ ├── _types.py │ ├── json_object.py │ ├── decompile.py │ ├── inputs.py │ ├── costumes.py │ ├── decompile_config.py │ ├── string_builder.py │ ├── __init__.py │ ├── decompile_events.py │ ├── syntax.py │ ├── decompile_sprite.py │ ├── ast.py │ ├── decompile_expr.py │ └── decompile_stmt.py ├── run.ps1 ├── README.md ├── pyproject.toml └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13.5 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GAPI=AIzaSyDyOYIN9EirFQMD66ptHtO1lcN-IkP8Cys 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[oc] 3 | build/ 4 | dist/ 5 | wheels/ 6 | *.egg-info 7 | .venv 8 | .pytest_cache/ 9 | *.sb3 10 | playground/ 11 | -------------------------------------------------------------------------------- /src/sb2gs/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class UnwrapError(Exception): 5 | pass 6 | 7 | 8 | def unwrap[T](value: T | None) -> T: 9 | if value is None: 10 | raise UnwrapError 11 | return value 12 | -------------------------------------------------------------------------------- /src/sb2gs/errors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import override 4 | 5 | 6 | class Error(Exception): 7 | def __init__(self, msg: str) -> None: 8 | super().__init__() 9 | self.msg: str = msg 10 | 11 | @override 12 | def __str__(self) -> str: 13 | return self.msg 14 | -------------------------------------------------------------------------------- /src/sb2gs/_logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | 6 | from rich.logging import RichHandler 7 | 8 | 9 | def setup_logging() -> None: 10 | logging.basicConfig( 11 | level=os.getenv("LOG_LEVEL", "WARNING"), 12 | handlers=[RichHandler()], 13 | format="%(message)s", 14 | ) 15 | -------------------------------------------------------------------------------- /src/sb2gs/custom_blocks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from ._types import Block 8 | 9 | PROCCODE_ARG_RE = re.compile(r"\%[sb]") 10 | 11 | 12 | def get_name(block: Block) -> str: 13 | return PROCCODE_ARG_RE.sub("", block.mutation.proccode).strip() 14 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$command = $(throw "Command is required") 3 | ) 4 | 5 | switch ($command) { 6 | "test" { 7 | foreach ($project in get-childitem -path tests/* -include *.sb3) { 8 | python -m sb2gs $project --overwrite --verify 9 | } 10 | } 11 | default { 12 | throw "Unknown command: $command" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_code.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from .decompile_stmt import decompile_stmt 6 | 7 | if TYPE_CHECKING: 8 | from .decompile_sprite import Ctx 9 | 10 | 11 | def decompile_stack(ctx: Ctx, child: str | None) -> None: 12 | if child is None: 13 | ctx.println("{}") 14 | return 15 | ctx.println("{") 16 | with ctx.indent(): 17 | while child: 18 | decompile_stmt(ctx, ctx.blocks[child]) 19 | child = ctx.blocks[child].next 20 | ctx.iprintln("}") 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | If you installed goboscript using the auto-install script, then sb2gs should be already installed. 4 | 5 | Install uv and run. 6 | 7 | ```shell 8 | uv tool install git+https://github.com/aspizu/sb2gs 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | usage: sb2gs [-h] [--overwrite] [--verify] input [output] 15 | positional arguments: 16 | input 17 | output 18 | 19 | options: 20 | -h, --help show this help message and exit 21 | --overwrite 22 | --verify Invoke goboscript to verify that the decompiled code is valid. This does not indicate that the decompiled code 23 | is equivalent to the original. 24 | ``` 25 | -------------------------------------------------------------------------------- /src/sb2gs/verify.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | from pathlib import Path 5 | 6 | from .errors import Error 7 | 8 | 9 | def verify(project: Path) -> None: 10 | exec_path = Path("/usr/bin/goboscript") 11 | if not exec_path.is_file(): 12 | exec_path = Path("~/.cargo/bin/goboscript").expanduser() 13 | if not exec_path.is_file(): 14 | exec_path = Path("~/.cargo/bin/goboscript.exe").expanduser() 15 | if not exec_path.is_file(): 16 | exec_path = Path("C:/Windows/System32/goboscript.exe") 17 | if not exec_path.is_file(): 18 | msg = "goboscript executable not found. Installation instructions: https://aspizu.github.io/goboscript/install" 19 | raise Error(msg) 20 | proc = subprocess.run( # noqa: S603 21 | [exec_path.as_posix(), "build", "-i", project.as_posix()], check=False 22 | ) 23 | if proc.returncode != 0: 24 | msg = "goboscript verification failed. The decompiled code is not valid." 25 | raise Error(msg) 26 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_input.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING 5 | 6 | from . import inputs, syntax 7 | from ._types import InputType 8 | from .decompile_expr import decompile_expr 9 | 10 | if TYPE_CHECKING: 11 | from ._types import Block 12 | from .decompile_sprite import Ctx 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def decompile_input(ctx: Ctx, input_name: str, block: Block) -> None: 18 | input = block.inputs._.get(input_name) 19 | if input is None or input == [1, None]: 20 | ctx.print("false") 21 | return 22 | if block_id := inputs.block_id(input): 23 | decompile_expr(ctx, ctx.blocks[block_id]) 24 | return 25 | input_type = InputType(input[1][0]) 26 | input_value: str = input[1][1] 27 | assert isinstance(input_value, str) 28 | if input_type in {InputType.VAR, InputType.LIST}: 29 | ctx.print(syntax.identifier(input_value)) 30 | return 31 | ctx.print(syntax.value(input_value)) 32 | -------------------------------------------------------------------------------- /src/sb2gs/_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass, field 4 | from enum import IntEnum 5 | 6 | from .json_object import JSONObject 7 | 8 | 9 | class InputType(IntEnum): 10 | MATH_NUM = 4 11 | POSITIVE_NUM = 5 12 | WHOLE_NUM = 6 13 | INTEGER_NUM = 7 14 | ANGLE_NUM = 8 15 | COLOR_PICKER = 9 16 | TEXT = 10 17 | BROADCAST = 11 18 | VAR = 12 19 | LIST = 13 20 | 21 | 22 | @dataclass 23 | class Block(JSONObject): 24 | opcode: str 25 | next: str | None = None 26 | parent: str | None = None 27 | inputs: JSONObject = field(default_factory=lambda: JSONObject({})) 28 | fields: JSONObject[tuple[str, str | None]] = field( 29 | default_factory=lambda: JSONObject({}) 30 | ) 31 | shadow: bool = False 32 | topLevel: bool = False 33 | x: int = 0 34 | y: int = 0 35 | 36 | 37 | @dataclass 38 | class Signature: 39 | opcode: str 40 | inputs: list[str] 41 | menu: str | None = None 42 | field: str | None = None 43 | overloads: dict[str, str] | None = None 44 | -------------------------------------------------------------------------------- /src/sb2gs/json_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, override 4 | 5 | if TYPE_CHECKING: 6 | from collections.abc import Generator 7 | 8 | 9 | class JSONDict[T = Any](dict[str, T]): 10 | def to_dict(self) -> dict[str, Any]: 11 | return { 12 | k: v._.to_dict() if isinstance(v, JSONObject) else v 13 | for k, v in self.items() 14 | } 15 | 16 | 17 | class JSONObject[T = Any]: 18 | def __init__(self, dictionary: dict[str, T]) -> None: 19 | self._: JSONDict[T] = JSONDict[T](dictionary) 20 | 21 | @override 22 | def __repr__(self) -> str: 23 | return repr(self._) 24 | 25 | def __rich_repr__(self) -> Generator[dict[str, T]]: 26 | yield self._ 27 | 28 | def __contains__(self, key: str) -> bool: 29 | return key in self._ 30 | 31 | def __getattr__(self, name: str, /) -> T: 32 | try: 33 | return self._[name] 34 | except KeyError: 35 | msg = f"{self.__class__.__name__!r} has no attribute {name!r}" 36 | raise AttributeError(msg) from None 37 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sb2gs" 3 | version = "2.0.0" 4 | description = "sb2gs is the goboscript decompiler." 5 | readme = "README.md" 6 | requires-python = ">=3.13.0" 7 | dependencies = [ 8 | "pillow>=11.3.0", 9 | "rich>=14.0.0", 10 | "toml>=0.10.2", 11 | ] 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | 17 | [project.scripts] 18 | sb2gs = "sb2gs:main" 19 | 20 | [tool.pyright] 21 | reportUnnecessaryTypeIgnoreComment = true 22 | reportUnknownVariableType = false 23 | reportUnknownMemberType = false 24 | reportUnknownArgumentType = false 25 | reportMissingModuleSource = false 26 | reportMissingTypeStubs = false 27 | reportWildcardImportFromLibrary = false 28 | reportPrivateUsage = false 29 | reportPrivateImportUsage = false 30 | reportAny = false 31 | reportExplicitAny = false 32 | reportUnusedCallResult = false 33 | reportImportCycles = false 34 | reportImplicitStringConcatenation = false 35 | 36 | [tool.ruff.lint] 37 | select = ["ALL"] 38 | extend-safe-fixes = ["ALL"] 39 | unfixable = ["F841"] 40 | ignore = ["A001", "A002", "A004", "ANN401", "COM", "D", "FBT", "N815", "PLR0911", "S101", "PLR2004", "S314"] 41 | 42 | [tool.ruff.lint.isort] 43 | required-imports = ["from __future__ import annotations"] 44 | -------------------------------------------------------------------------------- /src/sb2gs/decompile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | import shutil 6 | from typing import TYPE_CHECKING 7 | from zipfile import ZipFile 8 | 9 | from sb2gs.decompile_config import decompile_config 10 | 11 | from . import costumes 12 | from .decompile_sprite import Ctx, decompile_sprite 13 | from .errors import Error 14 | from .json_object import JSONObject 15 | 16 | if TYPE_CHECKING: 17 | from pathlib import Path 18 | 19 | 20 | def decompile(input: Path, output: Path) -> None: 21 | assets_path = output.joinpath("assets") 22 | shutil.rmtree(output, ignore_errors=True) 23 | output.mkdir(parents=True, exist_ok=True) 24 | with ZipFile(input) as zf, zf.open("project.json") as f: 25 | project = json.load(f, object_hook=JSONObject) 26 | for file in zf.filelist: 27 | if file.filename != "project.json": 28 | zf.extract(file, assets_path) 29 | stage = next(target for target in project.targets if target.isStage) 30 | sprites = [target for target in project.targets if not target.isStage] 31 | 32 | ctx = Ctx(stage) 33 | with output.joinpath("stage.gs").open("w") as file: 34 | decompile_sprite(ctx) 35 | file.write(str(ctx)) 36 | fixed = set() 37 | for target in sprites: 38 | ctx = Ctx(target) 39 | for costume in target.costumes: 40 | costumes.fix_center(costume, assets_path.joinpath(costume.md5ext), fixed) 41 | with output.joinpath(f"{target.name}.gs").open("w") as file: 42 | decompile_sprite(ctx) 43 | file.write(str(ctx)) 44 | decompile_config(project, output) 45 | -------------------------------------------------------------------------------- /src/sb2gs/inputs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import builtins 4 | from typing import TYPE_CHECKING, Any 5 | 6 | from ._types import InputType 7 | 8 | if TYPE_CHECKING: 9 | from ._types import Block 10 | from .decompile_sprite import Ctx 11 | 12 | 13 | def block_id(input: builtins.list[Any] | None) -> str | None: 14 | if input is None: 15 | return None 16 | if isinstance(input[1], str): 17 | return input[1] 18 | return None 19 | 20 | 21 | def block_value(input: builtins.list[Any] | None) -> str | None: 22 | if input is None: 23 | return None 24 | if not isinstance(input[1], builtins.list): 25 | return None 26 | if len(input[1]) < 2: 27 | return None 28 | value = input[1][1] 29 | assert isinstance(value, str) 30 | return value 31 | 32 | 33 | def block(ctx: Ctx, block: Block, input_name: str) -> Block | None: 34 | if id := block_id(block.inputs._.get(input_name)): 35 | return ctx.blocks[id] 36 | return None 37 | 38 | 39 | def variable(input: builtins.list[Any] | None) -> str | None: 40 | if input is None: 41 | return None 42 | if not isinstance(input[1], builtins.list): 43 | return None 44 | if len(input[1]) < 2: 45 | return None 46 | if input[1][0] != InputType.VAR: 47 | return None 48 | return input[1][1] 49 | 50 | 51 | def list(input: builtins.list[Any] | None) -> str | None: 52 | if input is None: 53 | return None 54 | if not isinstance(input[1], builtins.list): 55 | return None 56 | if len(input[1]) < 2: 57 | return None 58 | if input[1][0] != InputType.LIST: 59 | return None 60 | return input[1][1] 61 | -------------------------------------------------------------------------------- /src/sb2gs/costumes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import xml.etree.ElementTree as ET 5 | from typing import TYPE_CHECKING 6 | 7 | from PIL import Image 8 | 9 | if TYPE_CHECKING: 10 | from pathlib import Path 11 | 12 | from .json_object import JSONObject 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | svg = "{http://www.w3.org/2000/svg}" 17 | ET.register_namespace("", "http://www.w3.org/2000/svg") 18 | ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") 19 | 20 | 21 | def fix_vector_center(costume: JSONObject, path: Path) -> None: 22 | et = ET.parse(path) 23 | root = et.getroot() 24 | if ( 25 | float(root.attrib.get("width", "0")) / 2 == costume.rotationCenterX 26 | and float(root.attrib.get("height", "0")) / 2 == costume.rotationCenterY 27 | ): 28 | return 29 | root.set("width", "480") 30 | root.set("height", "360") 31 | root.set("viewBox", "0,0,480,360") 32 | group = root.find(f"{svg}g") 33 | if group is None: 34 | return 35 | group.attrib.pop("transform", None) 36 | with path.open("wb") as file: 37 | et.write(file, encoding="utf-8", xml_declaration=False) 38 | 39 | 40 | def fix_bitmap_center(costume: JSONObject, path: Path) -> None: 41 | img = Image.open(path) 42 | if ( 43 | costume.rotationCenterX == img.width // 2 44 | and costume.rotationCenterY == img.height // 2 45 | ): 46 | return 47 | fixed = Image.new("RGBA", (960, 720), (0, 0, 0, 0)) 48 | fixed.paste(img, (480 - costume.rotationCenterX, 360 - costume.rotationCenterY)) 49 | fixed.save(path, format=costume.dataFormat) 50 | 51 | 52 | def fix_center(costume: JSONObject, path: Path, fixed: set[str]) -> None: 53 | if costume.md5ext in fixed: 54 | return 55 | fixed.add(costume.md5ext) 56 | if costume.dataFormat == "svg": 57 | fix_vector_center(costume, path) 58 | else: 59 | fix_bitmap_center(costume, path) 60 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | from dataclasses import dataclass 6 | from typing import TYPE_CHECKING, Any 7 | 8 | import toml 9 | 10 | if TYPE_CHECKING: 11 | from pathlib import Path 12 | 13 | from .json_object import JSONObject 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | @dataclass 19 | class Config: 20 | std: str | None = None 21 | bitmap_resolution: int | None = 2 22 | frame_rate: int | None = None 23 | max_clones: float | None = None 24 | no_miscellaneous_limits: bool | None = None 25 | no_sprite_fencing: bool | None = None 26 | frame_interpolation: bool | None = None 27 | high_quality_pen: bool | None = None 28 | stage_width: int | None = None 29 | stage_height: int | None = None 30 | 31 | 32 | def find_turbowarp_config_comment(project: JSONObject) -> str | None: 33 | stage = next(target for target in project.targets if target.isStage) 34 | for comment in stage.comments._.values(): 35 | if comment.text.endswith("_twconfig_"): 36 | return comment.text 37 | return None 38 | 39 | 40 | def parse_turbowarp_config_comment(text: str | None) -> dict[str, Any] | None: 41 | if text is None: 42 | return None 43 | start = text.find("{") 44 | end = text.rfind("}") 45 | if start == -1 or end == -1: 46 | return None 47 | try: 48 | return json.loads(text[start : end + 1]) 49 | except json.JSONDecodeError: 50 | return None 51 | 52 | 53 | def decompile_config(project: JSONObject, output: Path) -> None: 54 | config = Config() 55 | data = parse_turbowarp_config_comment(find_turbowarp_config_comment(project)) or {} 56 | runtime_options = data.get("runtimeOptions", {}) 57 | config.frame_rate = data.get("framerate") 58 | config.max_clones = runtime_options.get("maxClones") 59 | config.no_miscellaneous_limits = runtime_options.get("miscLimits") is False 60 | config.no_sprite_fencing = runtime_options.get("fencing") is False 61 | config.frame_interpolation = data.get("interpolation") is True 62 | config.high_quality_pen = data.get("hq") is True 63 | config.stage_width = data.get("width") 64 | config.stage_height = data.get("height") 65 | with output.joinpath("goboscript.toml").open("w") as file: 66 | toml.dump(config.__dict__, file) 67 | -------------------------------------------------------------------------------- /src/sb2gs/string_builder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from contextlib import contextmanager 5 | from typing import ( 6 | TYPE_CHECKING, 7 | Concatenate, 8 | Literal, 9 | Self, 10 | overload, 11 | override, 12 | ) 13 | 14 | if TYPE_CHECKING: 15 | from collections.abc import Callable, Generator, Sequence 16 | 17 | 18 | class StringBuilder: 19 | def __init__(self, indent_width: int = 4) -> None: 20 | self.strings: list[str] = [] 21 | self.indent_width: int = indent_width 22 | self.indent_level: int = 0 23 | 24 | def print(self, *strings: str) -> None: 25 | self.strings.extend(strings) 26 | 27 | def println(self, *strings: str) -> None: 28 | self.strings.extend(strings) 29 | self.strings.append("\n") 30 | 31 | def iprint(self, *strings: str) -> None: 32 | self.strings.append(" " * self.indent_level * self.indent_width) 33 | self.strings.extend(strings) 34 | 35 | def iprintln(self, *strings: str) -> None: 36 | self.strings.append(" " * self.indent_level * self.indent_width) 37 | self.strings.extend(strings) 38 | self.strings.append("\n") 39 | 40 | @contextmanager 41 | def indent(self) -> Generator[None]: 42 | self.indent_level += 1 43 | yield 44 | self.indent_level -= 1 45 | 46 | @override 47 | def __str__(self) -> str: 48 | return "".join(self.strings) 49 | 50 | @overload 51 | def commasep[T, **P]( 52 | self, 53 | items: Sequence[T], 54 | callback: Callable[Concatenate[T, P], None], 55 | pass_self: Literal[False] = False, 56 | *args: P.args, 57 | **kwargs: P.kwargs, 58 | ) -> None: ... 59 | @overload 60 | def commasep[T, **P]( 61 | self, 62 | items: Sequence[T], 63 | callback: Callable[Concatenate[Self, T, P], None], 64 | pass_self: Literal[True], 65 | *args: P.args, 66 | **kwargs: P.kwargs, 67 | ) -> None: ... 68 | def commasep( 69 | self, 70 | items: Sequence[object], 71 | callback: Callable[..., None], 72 | pass_self: bool = False, 73 | *args: object, 74 | **kwargs: object, 75 | ) -> None: 76 | for i, item in enumerate(items): 77 | if pass_self: 78 | callback(self, item, *args, **kwargs) 79 | else: 80 | callback(item, *args, **kwargs) 81 | if i != len(items) - 1: 82 | self.print(", ") 83 | -------------------------------------------------------------------------------- /src/sb2gs/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | from argparse import ArgumentParser, ArgumentTypeError 5 | from pathlib import Path 6 | from sys import stderr 7 | from time import perf_counter_ns 8 | from typing import TYPE_CHECKING 9 | 10 | from rich import print 11 | 12 | from ._logging import setup_logging 13 | from .decompile import decompile 14 | from .errors import Error 15 | from .verify import verify 16 | 17 | if TYPE_CHECKING: 18 | from collections.abc import Callable 19 | 20 | 21 | def entrypoint(func: Callable[[], None]) -> Callable[[], int]: 22 | @functools.wraps(func) 23 | def wrapper() -> int: 24 | before = perf_counter_ns() 25 | success = True 26 | try: 27 | func() 28 | except Error as error: 29 | stderr.write(f"error: {error}\n") 30 | stderr.flush() 31 | success = False 32 | after = perf_counter_ns() 33 | color = "[green]" if success else "[red]" 34 | print(f"[dim][bold]{color}Finished[/] in {(after - before) / 1e6}ms") 35 | if not success: 36 | return 1 37 | return 0 38 | 39 | return wrapper 40 | 41 | 42 | def input_type(value: str) -> Path: 43 | path = Path(value) 44 | if path.suffix != ".sb3": 45 | msg = "must have file extension .sb3" 46 | raise ArgumentTypeError(msg) 47 | if not path.exists(): 48 | msg = "file does not exist" 49 | raise ArgumentTypeError(msg) 50 | if not path.is_file(): 51 | msg = "is not a file" 52 | raise ArgumentTypeError(msg) 53 | return path 54 | 55 | 56 | def output_type(value: str) -> Path: 57 | path = Path(value) 58 | if path.is_file(): 59 | msg = "exists, and is a file" 60 | raise ArgumentTypeError(msg) 61 | return path 62 | 63 | 64 | def determine_output_path(input: Path, output: Path | None, overwrite: bool) -> Path: 65 | output = output or input.parent.joinpath(input.stem) 66 | if output.exists() and not overwrite: 67 | msg = "output directory already exists. (use --overwrite to overwrite)" 68 | raise Error(msg) 69 | return output 70 | 71 | 72 | @entrypoint 73 | def main() -> None: 74 | setup_logging() 75 | argparser = ArgumentParser("sb2gs") 76 | argparser.add_argument("input", type=input_type) 77 | argparser.add_argument("output", nargs="?", type=output_type) 78 | argparser.add_argument("--overwrite", action="store_true") 79 | argparser.add_argument( 80 | "--verify", 81 | action="store_true", 82 | help="Invoke goboscript to verify that the decompiled code is valid. This does " 83 | "not indicate that the decompiled code is equivalent to the original.", 84 | ) 85 | args = argparser.parse_args() 86 | args.output = determine_output_path(args.input, args.output, args.overwrite) 87 | decompile(args.input, args.output) 88 | if args.verify: 89 | verify(args.output) 90 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_events.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | from typing import TYPE_CHECKING 6 | 7 | from . import custom_blocks, inputs, syntax 8 | from .decompile_code import decompile_stack 9 | from .utils import unwrap 10 | 11 | if TYPE_CHECKING: 12 | from ._types import Block 13 | from .decompile_sprite import Ctx 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | def decompile_event_whenflagclicked(ctx: Ctx, block: Block) -> None: 19 | ctx.iprint("onflag ") 20 | decompile_stack(ctx, block.next) 21 | 22 | 23 | def decompile_event_whenbroadcastreceived(ctx: Ctx, block: Block) -> None: 24 | ctx.iprint("on ", syntax.string(block.fields.BROADCAST_OPTION[0]), " ") 25 | decompile_stack(ctx, block.next) 26 | 27 | 28 | def decompile_event_whenkeypressed(ctx: Ctx, block: Block) -> None: 29 | ctx.iprint("onkey ", syntax.string(block.fields.KEY_OPTION[0]), " ") 30 | decompile_stack(ctx, block.next) 31 | 32 | 33 | def decompile_control_start_as_clone(ctx: Ctx, block: Block) -> None: 34 | ctx.iprint("onclone ") 35 | decompile_stack(ctx, block.next) 36 | 37 | 38 | def decompile_event_whenthisspriteclicked(ctx: Ctx, block: Block) -> None: 39 | ctx.iprint("onclick ") 40 | decompile_stack(ctx, block.next) 41 | 42 | 43 | def decompile_procedures_definition(ctx: Ctx, block: Block) -> None: 44 | custom = ctx.blocks[unwrap(inputs.block_id(block.inputs.custom_block))] 45 | args = json.loads(custom.mutation.argumentnames) 46 | logger.debug("custom block %s", custom) 47 | if custom.mutation.warp == "false": 48 | ctx.iprint("nowarp proc ") 49 | else: 50 | ctx.iprint("proc ") 51 | ctx.print(syntax.identifier(custom_blocks.get_name(custom))) 52 | if args: 53 | ctx.print(" ") 54 | ctx.commasep(args, lambda arg: ctx.print(syntax.identifier(arg))) 55 | ctx.print(" ") 56 | decompile_stack(ctx, block.next) 57 | 58 | 59 | def decompile_event(ctx: Ctx, block: Block) -> None: 60 | from . import decompile_expr, decompile_stmt 61 | 62 | if decompiler := globals().get(f"decompile_{block.opcode}"): 63 | logger.debug("using %s to decompile\n%s", decompiler, block) 64 | decompiler(ctx, block) 65 | return 66 | if ( 67 | block.opcode in decompile_stmt.BLOCKS 68 | or block.opcode in decompile_expr.BLOCKS 69 | or block.opcode in decompile_expr.OPERATORS 70 | or block.opcode in decompile_expr.MENUS 71 | or f"decompile_{block.opcode}" in decompile_stmt.__dict__ 72 | or f"decompile_{block.opcode}" in decompile_expr.__dict__ 73 | ): 74 | return 75 | logger.error("no decompiler implemented for event `%s`\n%s", block.opcode, block) 76 | 77 | 78 | def decompile_events(ctx: Ctx) -> None: 79 | for block in ctx.blocks.values(): 80 | if isinstance(block, list): 81 | continue 82 | if block.topLevel: 83 | decompile_event(ctx, block) 84 | -------------------------------------------------------------------------------- /src/sb2gs/syntax.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import functools 5 | import itertools 6 | import json 7 | import logging 8 | import re 9 | 10 | WHITESPACE_RE = re.compile(r"[\s.\-]+") 11 | INVALID_CHARS_RE = re.compile(r"[^a-zA-Z_0-9]") 12 | 13 | KEYWORDS = { 14 | "costumes", 15 | "sounds", 16 | "local", 17 | "proc", 18 | "func", 19 | "return", 20 | "nowarp", 21 | "on", 22 | "onflag", 23 | "onkey", 24 | "onclick", 25 | "onbackdrop", 26 | "onloudness", 27 | "ontimer", 28 | "onclone", 29 | "if", 30 | "else", 31 | "elif", 32 | "until", 33 | "forever", 34 | "repeat", 35 | "not", 36 | "and", 37 | "or", 38 | "in", 39 | "length", 40 | "round", 41 | "abs", 42 | "floor", 43 | "ceil", 44 | "sqrt", 45 | "sin", 46 | "cos", 47 | "tan", 48 | "asin", 49 | "acos", 50 | "atan", 51 | "ln", 52 | "log", 53 | "antiln", 54 | "antilog", 55 | "show", 56 | "hide", 57 | "add", 58 | "to", 59 | "delete", 60 | "insert", 61 | "at", 62 | "of", 63 | "as", 64 | "enum", 65 | "struct", 66 | "true", 67 | "false", 68 | "list", 69 | "cloud", 70 | "set_x", 71 | "set_y", 72 | "set_size", 73 | "point_in_direction", 74 | "set_volume", 75 | "set_rotation_style_left_right", 76 | "set_rotation_style_all_around", 77 | "set_rotation_style_do_not_rotate", 78 | "set_layer_order", 79 | "var", 80 | } 81 | identifier_map: dict[str, str] = {} 82 | 83 | 84 | @functools.cache 85 | def get_blocknames() -> set[str]: 86 | from . import decompile_expr, decompile_stmt 87 | 88 | all_signatures = itertools.chain( 89 | decompile_stmt.BLOCKS.values(), 90 | decompile_expr.BLOCKS.values(), 91 | ) 92 | blocknames = set() 93 | for s in all_signatures: 94 | blocknames.add(s.opcode) 95 | if s.overloads: 96 | for overload in s.overloads.values(): 97 | blocknames.add(overload) 98 | return blocknames 99 | 100 | 101 | def identifier(og: str) -> str: 102 | if og in identifier_map: 103 | return identifier_map[og] 104 | 105 | iden = og 106 | 107 | iden = "_".join(WHITESPACE_RE.split(iden)) 108 | iden = INVALID_CHARS_RE.sub("", iden) 109 | 110 | # remove ugly leading and trailing underscores, and convert to lowercase 111 | # (in most cases, this converts to snake case) 112 | # any concerns with naming conflicts are solved at the end of the script, by appending a number 113 | iden = iden.strip('_').lower() 114 | 115 | if iden in KEYWORDS or iden in get_blocknames(): 116 | iden += "_" 117 | 118 | # any still invalid names can be solved by adding an underscore. e.g. '' -> '_', or '2swap' -> '_2swap' 119 | if iden == '' or iden[0] in "0123456789": 120 | iden = '_' + iden 121 | 122 | i = 2 # identifier_1 would be the original one, i.e. #1, so it doesnt need an index. 123 | new_iden = iden 124 | while new_iden in identifier_map.values(): 125 | new_iden = f"{iden}{i}" 126 | i += 1 127 | 128 | identifier_map[og] = new_iden 129 | logging.info(f"Mapped identifier {og!r} -> {new_iden!r}") 130 | 131 | return new_iden 132 | 133 | 134 | def string(text: str) -> str: 135 | return json.dumps(text) 136 | 137 | 138 | def number(value: float) -> str: 139 | return json.dumps(value) 140 | 141 | 142 | def is_goboscript_literal(text: str) -> bool: 143 | with contextlib.suppress(json.JSONDecodeError): 144 | parsed = json.loads(text) 145 | return type(parsed) in {int, float} and json.dumps(parsed) == text 146 | return False 147 | 148 | 149 | def value(text: str) -> str: 150 | if is_goboscript_literal(text): 151 | return text 152 | return string(text) 153 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_sprite.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING 5 | 6 | from . import ast, syntax 7 | from .decompile_events import decompile_events 8 | from .string_builder import StringBuilder 9 | 10 | if TYPE_CHECKING: 11 | from ._types import Block 12 | from .json_object import JSONObject 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Ctx(StringBuilder): 17 | def __init__(self, target: JSONObject, indent_width: int = 4) -> None: 18 | super().__init__(indent_width) 19 | self.is_stage: bool = target.isStage 20 | self.costumes: list[JSONObject] = target.costumes 21 | self.sounds: list[JSONObject] = target.sounds 22 | self.variables: JSONObject = target.variables 23 | self.lists: JSONObject = target.lists 24 | self.blocks: dict[str, Block] = target.blocks._ 25 | self.volume: float = target.volume 26 | self.layer_order: int = target.layerOrder 27 | if not self.is_stage: 28 | self.visible: bool = target.visible 29 | self.x: float = target.x 30 | self.y: float = target.y 31 | self.size: float = target.size 32 | self.direction: float = target.direction 33 | self.draggable: bool = target.draggable 34 | self.rotation_style: str = target.rotationStyle 35 | 36 | 37 | def decompile_constexpr(ctx: Ctx, value: object) -> None: 38 | if type(value) is bool: 39 | ctx.print('"true"' if value else '"false"') 40 | return 41 | if isinstance(value, int | float): 42 | ctx.print(syntax.number(value)) 43 | return 44 | if isinstance(value, str): 45 | ctx.print(syntax.string(value)) 46 | return 47 | msg = f"Unsupported value {value!r}" 48 | raise ValueError(msg) 49 | 50 | 51 | def decompile_asset(ctx: Ctx, asset: JSONObject) -> None: 52 | ctx.print( 53 | syntax.string("assets/" + asset.md5ext), 54 | " as ", 55 | syntax.string(asset.name), 56 | ) 57 | 58 | 59 | def decompile_common_properties(ctx: Ctx) -> None: 60 | if ctx.volume != DEFAULT_VOLUME: 61 | ctx.iprintln("set_volume ", syntax.number(ctx.volume), ";") 62 | 63 | 64 | DEFAULT_VOLUME = 100 65 | DEFAULT_X = 0 66 | DEFAULT_Y = 0 67 | DEFAULT_SIZE = 100 68 | DEFAULT_DIRECTION = 90 69 | 70 | 71 | def decompile_sprite_properties(ctx: Ctx) -> None: 72 | ctx.iprintln("set_layer_order ", syntax.number(ctx.layer_order), ";") 73 | if not ctx.visible: 74 | ctx.iprintln("hide;") 75 | if ctx.x != DEFAULT_X: 76 | ctx.iprintln("set_x ", syntax.number(ctx.x), ";") 77 | if ctx.y != DEFAULT_Y: 78 | ctx.iprintln("set_y ", syntax.number(ctx.y), ";") 79 | if ctx.size != DEFAULT_SIZE: 80 | ctx.iprintln("set_size ", syntax.number(ctx.size), ";") 81 | if ctx.direction != DEFAULT_DIRECTION: 82 | ctx.iprintln("point_in_direction ", syntax.number(ctx.direction), ";") 83 | decompile_rotation_style(ctx) 84 | if ctx.draggable: 85 | ctx.iprintln("set_draggable;") 86 | 87 | 88 | def decompile_rotation_style(ctx: Ctx) -> None: 89 | if ctx.rotation_style == "left-right": 90 | ctx.iprintln("set_rotation_style_left_right;") 91 | if ctx.rotation_style == "don't rotate": 92 | ctx.iprintln("set_rotation_style_do_not_rotate;") 93 | 94 | 95 | def decompile_properties(ctx: Ctx) -> None: 96 | decompile_common_properties(ctx) 97 | if not ctx.is_stage: 98 | decompile_sprite_properties(ctx) 99 | 100 | 101 | def decompile_costumes(ctx: Ctx) -> None: 102 | if not ctx.costumes: 103 | return 104 | ctx.iprint("costumes ") 105 | ctx.commasep(ctx.costumes, decompile_asset, pass_self=True) 106 | ctx.println(";") 107 | 108 | 109 | def decompile_sounds(ctx: Ctx) -> None: 110 | if not ctx.sounds: 111 | return 112 | ctx.iprint("sounds ") 113 | ctx.commasep(ctx.sounds, decompile_asset, pass_self=True) 114 | ctx.println(";") 115 | 116 | 117 | def decompile_variables(ctx: Ctx) -> None: 118 | for variable_name, variable_value, *_ in ctx.variables._.values(): 119 | ctx.iprint("var ", syntax.identifier(variable_name), " = ") 120 | decompile_constexpr(ctx, variable_value) 121 | ctx.println(";") 122 | 123 | 124 | def decompile_lists(ctx: Ctx) -> None: 125 | for list_name, list_values in ctx.lists._.values(): 126 | ctx.iprint("list ", syntax.identifier(list_name)) 127 | if not list_values: 128 | ctx.println(";") 129 | continue 130 | ctx.print(" = [") 131 | ctx.commasep(list_values, decompile_constexpr, pass_self=True) 132 | ctx.println("];") 133 | 134 | 135 | def decompile_sprite(ctx: Ctx) -> None: 136 | ast.transform(ctx) 137 | decompile_properties(ctx) 138 | decompile_costumes(ctx) 139 | decompile_sounds(ctx) 140 | decompile_variables(ctx) 141 | decompile_lists(ctx) 142 | decompile_events(ctx) 143 | -------------------------------------------------------------------------------- /src/sb2gs/ast.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import logging 5 | from typing import TYPE_CHECKING, Any 6 | 7 | from . import inputs 8 | from .decompile_expr import OPERATORS 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Callable 12 | 13 | from ._types import Block 14 | from .decompile_sprite import Ctx 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | type Transformer = Callable[[Ctx, Block], None] 20 | 21 | 22 | transformers: list[Transformer] = [] 23 | 24 | 25 | def transformer(opcode: str) -> Callable[[Transformer], Transformer]: 26 | def decorator(func: Transformer) -> Transformer: 27 | @functools.wraps(func) 28 | def wrapper(ctx: Ctx, block: Block) -> None: 29 | if block.opcode != opcode: 30 | return 31 | func(ctx, block) 32 | 33 | transformers.append(wrapper) 34 | return wrapper 35 | 36 | return decorator 37 | 38 | 39 | def compare_inputs( 40 | ctx: Ctx, 41 | input1: list[Any] | None, 42 | input2: list[Any] | None, 43 | ) -> bool: 44 | input1_block_id = inputs.block_id(input1) 45 | input2_block_id = inputs.block_id(input2) 46 | if not compare_tree( 47 | ctx, 48 | None if input1_block_id is None else ctx.blocks[input1_block_id], 49 | None if input2_block_id is None else ctx.blocks[input2_block_id], 50 | ): 51 | return False 52 | input1_value = inputs.block_value(input1) 53 | input2_value = inputs.block_value(input2) 54 | return input1_value == input2_value 55 | 56 | 57 | def compare_tree(ctx: Ctx, node1: Block | None, node2: Block | None) -> bool: 58 | if node1 is None or node2 is None: 59 | return node1 == node2 60 | if node1.opcode != node2.opcode: 61 | return False 62 | if node1.fields._ != node2.fields._: 63 | return False 64 | for node1_input_name, node1_input_value in node1.inputs._.items(): 65 | node2_input_value = node2.inputs._.get(node1_input_name) 66 | if node2_input_value is None: 67 | return False 68 | if not compare_inputs(ctx, node1_input_value, node2_input_value): 69 | return False 70 | 71 | return True 72 | 73 | 74 | def flatten_menu(ctx: Ctx, block: Block, menu_name: str) -> None: 75 | menu_id = inputs.block_id(block.inputs._.get(menu_name)) 76 | if menu_id is None: 77 | return 78 | menu = ctx.blocks[menu_id] 79 | block.fields._.update(menu.fields._) 80 | 81 | 82 | @transformer("data_listcontainsitem") 83 | def transform_list_contains_to_item_num(_ctx: Ctx, block: Block) -> None: 84 | block.opcode = "data_itemnumoflist" 85 | 86 | 87 | @transformer("operator_subtract") 88 | def transform_subtract_zero_to_negative(_ctx: Ctx, block: Block) -> None: 89 | if inputs.block_value(block.inputs.NUM1) in {"", "0", "0.0"}: 90 | block.opcode = "operator_negative" 91 | 92 | 93 | @transformer("data_changevariableby") 94 | def transform_change_variable_by_negative(ctx: Ctx, block: Block) -> None: 95 | if not ( 96 | (operand := inputs.block(ctx, block, "VALUE")) 97 | and operand.opcode in {"operator_subtract", "operator_negative"} 98 | and inputs.block_value(operand.inputs.NUM1) in {"", "0", "0.0"} 99 | ): 100 | return 101 | block._["OPERATOR"] = "-" 102 | block.inputs._["VALUE"] = operand.inputs.NUM2 103 | 104 | 105 | ARITHMETIC_OPCODES = { 106 | "operator_add", 107 | "operator_subtract", 108 | "operator_multiply", 109 | "operator_divide", 110 | "operator_mod", 111 | } 112 | 113 | 114 | @transformer("data_setvariableto") 115 | def transform_augmented_set_variable(ctx: Ctx, block: Block) -> None: 116 | if not ( 117 | (operand := inputs.block(ctx, block, "VALUE")) 118 | and operand.opcode in ARITHMETIC_OPCODES 119 | and inputs.variable(operand.inputs.NUM1) == block.fields.VARIABLE[0] 120 | ): 121 | return 122 | block.opcode = "data_changevariableby" 123 | block._["OPERATOR"] = OPERATORS[operand.opcode].symbol 124 | block.inputs._["VALUE"] = operand.inputs.NUM2 125 | 126 | 127 | @transformer("data_setvariableto") 128 | def transform_augmented_set_variable_join(ctx: Ctx, block: Block) -> None: 129 | if not ( 130 | (operand := inputs.block(ctx, block, "VALUE")) 131 | and operand.opcode == "operator_join" 132 | and inputs.variable(operand.inputs.STRING1) == block.fields.VARIABLE[0] 133 | ): 134 | return 135 | block.opcode = "data_changevariableby" 136 | block._["OPERATOR"] = "&" 137 | block.inputs._["VALUE"] = operand.inputs.STRING2 138 | 139 | 140 | @transformer("data_replaceitemoflist") 141 | def transform_augmented_replace_list_item(ctx: Ctx, block: Block) -> None: 142 | if not ( 143 | (operand := inputs.block(ctx, block, "ITEM")) 144 | and operand.opcode in ARITHMETIC_OPCODES 145 | and (lhs := inputs.block(ctx, operand, "NUM1")) 146 | and lhs.opcode == "data_itemoflist" 147 | and lhs.fields.LIST[0] == block.fields.LIST[0] 148 | and compare_inputs( 149 | ctx, 150 | block.inputs._.get("INDEX"), 151 | lhs.inputs._.get("INDEX"), 152 | ) 153 | ): 154 | return 155 | block._["OPERATOR"] = OPERATORS[operand.opcode].symbol 156 | block.inputs._["ITEM"] = operand.inputs.NUM2 157 | 158 | 159 | @transformer("data_replaceitemoflist") 160 | def transform_augmented_replace_list_item_join(ctx: Ctx, block: Block) -> None: 161 | if not ( 162 | (operand := inputs.block(ctx, block, "ITEM")) 163 | and operand.opcode == "operator_join" 164 | and (lhs := inputs.block(ctx, operand, "STRING1")) 165 | and lhs.opcode == "data_itemoflist" 166 | and lhs.fields.LIST[0] == block.fields.LIST[0] 167 | and compare_tree( 168 | ctx, 169 | inputs.block(ctx, block, "INDEX"), 170 | inputs.block(ctx, lhs, "INDEX"), 171 | ) 172 | ): 173 | return 174 | block._["OPERATOR"] = "&" 175 | block.inputs._["ITEM"] = operand.inputs.STRING2 176 | 177 | 178 | def transform_block(ctx: Ctx, block: Block) -> None: 179 | for transformer in transformers: 180 | transformer(ctx, block) 181 | 182 | 183 | def transform(ctx: Ctx) -> None: 184 | for block in ctx.blocks.values(): 185 | if isinstance(block, list): 186 | continue 187 | transform_block(ctx, block) 188 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_expr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import logging 5 | from copy import deepcopy 6 | from dataclasses import dataclass 7 | from enum import StrEnum 8 | from typing import TYPE_CHECKING 9 | 10 | from . import ast, inputs, syntax 11 | from ._types import Signature 12 | from .utils import unwrap 13 | 14 | if TYPE_CHECKING: 15 | from ._types import Block 16 | from .decompile_sprite import Ctx 17 | 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | MENUS = { 22 | "looks_costume": ("COSTUME", False), 23 | "sensing_of_object_menu": ("OBJECT", False), 24 | } 25 | 26 | 27 | def decompile_menu(ctx: Ctx, block: Block) -> None: 28 | input_name, is_input = MENUS[block.opcode] 29 | data = block.inputs if is_input else block.fields 30 | ctx.print(syntax.string(data._[input_name][0])) 31 | 32 | 33 | class Assoc(StrEnum): 34 | LEFT = "LEFT" 35 | RIGHT = "RIGHT" 36 | 37 | 38 | @dataclass 39 | class Operator: 40 | symbol: str 41 | precedence: int 42 | left_name: str 43 | right_name: str 44 | assoc: Assoc 45 | 46 | 47 | # fmt:off 48 | OPERATORS = { k: Operator(*v) for k, v in { 49 | # OPCODE | SYMBOL | PRECEDENCE | LEFT | RIGHT | ASSOC # 50 | #--------------------|--------|------------|------------|--------------------------# 51 | "operator_letter_of" :( "" , 1 , "LETTER" , "STRING" , Assoc.LEFT ), 52 | "operator_not" :( "not" , 1 , "OPERAND" , "" , Assoc.LEFT ), 53 | "operator_negative" :( "-" , 1 , "" , "NUM2" , Assoc.RIGHT), 54 | "operator_multiply" :( "*" , 2 , "NUM1" , "NUM2" , Assoc.LEFT ), 55 | "operator_divide" :( "/" , 2 , "NUM1" , "NUM2" , Assoc.LEFT ), 56 | "operator_mod" :( "%" , 2 , "NUM1" , "NUM2" , Assoc.LEFT ), 57 | "operator_add" :( "+" , 3 , "NUM1" , "NUM2" , Assoc.LEFT ), 58 | "operator_subtract" :( "-" , 3 , "NUM1" , "NUM2" , Assoc.LEFT ), 59 | "operator_lt" :( "<" , 4 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 60 | "operator_le" :( "<=" , 4 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 61 | "operator_gt" :( ">" , 4 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 62 | "operator_ge" :( ">=" , 4 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 63 | "operator_join" :( "&" , 5 , "STRING1" , "STRING2" , Assoc.LEFT ), 64 | "operator_contains" :( "in" , 6 , "STRING2" , "STRING1" , Assoc.LEFT ), 65 | "data_itemnumoflist" :( "in" , 6 , "" , "" , Assoc.LEFT ), 66 | "operator_equals" :( "==" , 6 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 67 | "operator_notequals" :( "!=" , 6 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 68 | "operator_and" :( "and" , 7 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 69 | "operator_or" :( "or" , 8 , "OPERAND1" , "OPERAND2" , Assoc.LEFT ), 70 | }.items()}#----------|-------------------------------------------------------------# 71 | # fmt:on 72 | 73 | 74 | def is_parenthesis_required( 75 | parent_op: Operator, child_op: Operator, assoc: Assoc 76 | ) -> bool: 77 | if child_op.precedence > parent_op.precedence: 78 | return True 79 | return child_op.precedence == parent_op.precedence and parent_op.assoc != assoc 80 | 81 | 82 | def decompile_operand(ctx: Ctx, op_name: str, block: Block, assoc: Assoc) -> None: 83 | from .decompile_input import decompile_input 84 | 85 | op = OPERATORS[block.opcode] 86 | parenthesis = False 87 | operand_block = block.inputs._.get(op_name) 88 | if operand_id := inputs.block_id(operand_block): 89 | operand_block = ctx.blocks[operand_id] 90 | operand_op = OPERATORS.get(operand_block.opcode) 91 | parenthesis = operand_op and is_parenthesis_required(op, operand_op, assoc) 92 | if parenthesis: 93 | ctx.print("(") 94 | decompile_input(ctx, op_name, block) 95 | if parenthesis: 96 | ctx.print(")") 97 | 98 | 99 | def decompile_operator_not(ctx: Ctx, block: Block) -> None: 100 | ctx.print("not ") 101 | decompile_operand(ctx, "OPERAND", block, Assoc.LEFT) 102 | 103 | 104 | def decompile_operator_negative(ctx: Ctx, block: Block) -> None: 105 | ctx.print("-") 106 | decompile_operand(ctx, "NUM2", block, Assoc.LEFT) 107 | 108 | 109 | def decompile_operator_letter_of(ctx: Ctx, block: Block) -> None: 110 | from .decompile_input import decompile_input 111 | 112 | op = OPERATORS[block.opcode] 113 | decompile_operand(ctx, op.right_name, block, Assoc.LEFT) 114 | ctx.print("[") 115 | decompile_input(ctx, op.left_name, block) 116 | ctx.print("]") 117 | 118 | 119 | def decompile_binary_operator(ctx: Ctx, block: Block) -> None: 120 | op = OPERATORS[block.opcode] 121 | decompile_operand(ctx, op.left_name, block, Assoc.LEFT) 122 | ctx.print(" ", op.symbol, " ") 123 | decompile_operand(ctx, op.right_name, block, Assoc.RIGHT) 124 | 125 | 126 | # fmt: off 127 | _ = Signature 128 | BLOCKS = { 129 | # Motion 130 | "motion_xposition": _("x_position", []), 131 | "motion_yposition": _("y_position", []), 132 | "motion_direction": _("direction", []), 133 | # Looks 134 | "looks_size": _("size", []), 135 | "looks_costumenumbername": _("costume_number", [], field="NUMBER_NAME", 136 | overloads={ 137 | "number": "costume_number", 138 | "name": "costume_name", 139 | }), 140 | "looks_backdropnumbername": _("backdrop_number", [], field="NUMBER_NAME", 141 | overloads={ 142 | "number": "backdrop_number", 143 | "name": "backdrop_name", 144 | }), 145 | # Sound 146 | "sound_volume": _("volume", []), 147 | # Sensing 148 | "sensing_distanceto": _("distance_to_mouse_pointer", [], 149 | menu="DISTANCETOMENU", field="DISTANCETOMENU", 150 | overloads={ 151 | "_mouse_": "distance_to_mouse_pointer", 152 | }), 153 | "sensing_touchingobject": _("touching_mouse_pointer", [], 154 | menu="TOUCHINGOBJECTMENU", 155 | field="TOUCHINGOBJECTMENU", overloads={ 156 | "_mouse_": "touching_mouse_pointer", 157 | "_edge_": "touching_edge", 158 | }), 159 | "sensing_keypressed": _("key_pressed", ["KEY_OPTION"], "KEY_OPTION", 160 | "KEY_OPTION", {}), 161 | "sensing_mousedown": _("mouse_down", []), 162 | "sensing_mousex": _("mouse_x", []), 163 | "sensing_mousey": _("mouse_y", []), 164 | "sensing_loudness": _("loudness", []), 165 | "sensing_timer": _("timer", []), 166 | "sensing_current": _("current_year", [], field="CURRENTMENU", 167 | overloads={ 168 | "YEAR": "current_year", 169 | "MONTH": "current_month", 170 | "DATE": "current_date", 171 | "DAYOFWEEK": "current_day_of_week", 172 | "HOUR": "current_hour", 173 | "MINUTE": "current_minute", 174 | "SECOND": "current_second", 175 | }), 176 | "sensing_dayssince2000": _("days_since_2000", []), 177 | "sensing_username": _("username", []), 178 | "sensing_touchingcolor": _("touching_color", ["COLOR"]), 179 | "sensing_coloristouchingcolor": _("color_is_touching_color", ["COLOR", "COLOR2"]), 180 | "sensing_answer": _("answer", []), 181 | # Operator 182 | "operator_random": _("random", ["FROM", "TO"]), 183 | "operator_length": _("length", ["STRING"]), 184 | "operator_round": _("round", ["NUM"]), 185 | "operator_mathop": _("abs", ["NUM"], field="OPERATOR", overloads={ 186 | "abs": "abs", 187 | "floor": "floor", 188 | "ceiling": "ceil", 189 | "sqrt": "sqrt", 190 | "sin": "sin", 191 | "cos": "cos", 192 | "tan": "tan", 193 | "asin": "asin", 194 | "acos": "acos", 195 | "atan": "atan", 196 | "ln": "ln", 197 | "log": "log", 198 | "e ^": "antiln", 199 | "10 ^": "antilog", 200 | }), 201 | } 202 | del _ 203 | # fmt: on 204 | 205 | 206 | def decompile_block(ctx: Ctx, block: Block) -> None: 207 | from .decompile_input import decompile_input 208 | 209 | signature = deepcopy(BLOCKS[block.opcode]) 210 | if signature.menu: 211 | ast.flatten_menu(ctx, block, signature.menu) 212 | if field := block.fields._.get(signature.field or ""): 213 | if opcode := unwrap(signature.overloads).get(field[0]): 214 | signature.opcode = opcode 215 | with contextlib.suppress(ValueError): 216 | signature.inputs.remove(unwrap(signature.field)) 217 | else: 218 | block.inputs._[unwrap(signature.field)] = [1, [4, field[0]]] 219 | ctx.print(signature.opcode, "(") 220 | if signature.inputs: 221 | ctx.commasep(signature.inputs, decompile_input, pass_self=True, block=block) 222 | ctx.print(")") 223 | 224 | 225 | def decompile_sensing_of(ctx: Ctx, block: Block) -> None: 226 | from .decompile_input import decompile_input 227 | 228 | decompile_input(ctx, "OBJECT", block) 229 | ctx.print(".", syntax.string(block.fields.PROPERTY[0])) 230 | 231 | 232 | def decompile_data_itemoflist(ctx: Ctx, block: Block) -> None: 233 | from .decompile_input import decompile_input 234 | 235 | ctx.print(syntax.identifier(block.fields.LIST[0]), "[") 236 | decompile_input(ctx, "INDEX", block) 237 | ctx.print("]") 238 | 239 | 240 | def decompile_data_itemnumoflist(ctx: Ctx, block: Block) -> None: 241 | decompile_operand(ctx, "ITEM", block, Assoc.LEFT) 242 | ctx.print(" in ", syntax.identifier(block.fields.LIST[0])) 243 | 244 | 245 | def decompile_data_lengthoflist(ctx: Ctx, block: Block) -> None: 246 | ctx.print("length(", syntax.identifier(block.fields.LIST[0]), ")") 247 | 248 | 249 | def decompile_argument_reporter_string_number(ctx: Ctx, block: Block) -> None: 250 | ctx.print("$", syntax.identifier(block.fields.VALUE[0])) 251 | 252 | 253 | decompile_argument_reporter_boolean = decompile_argument_reporter_string_number 254 | 255 | UNREAL_OPCODES = { 256 | "operator_letter_of", 257 | "operator_not", 258 | "operator_negative", 259 | "data_itemnumoflist", 260 | } 261 | 262 | 263 | def decompile_expr(ctx: Ctx, block: Block) -> None: 264 | if block.opcode in MENUS: 265 | decompiler = decompile_menu 266 | elif block.opcode in OPERATORS and block.opcode not in UNREAL_OPCODES: 267 | decompiler = decompile_binary_operator 268 | elif block.opcode in BLOCKS: 269 | decompiler = decompile_block 270 | else: 271 | decompiler = globals().get(f"decompile_{block.opcode}") 272 | if decompiler: 273 | logger.debug("using %s to decompile\n%s", decompiler, block) 274 | decompiler(ctx, block) 275 | return 276 | logger.error("no decompiler implemented for expr `%s`\n%s", block.opcode, block) 277 | -------------------------------------------------------------------------------- /src/sb2gs/decompile_stmt.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import json 5 | import logging 6 | from copy import deepcopy 7 | from typing import TYPE_CHECKING 8 | 9 | from . import ast, custom_blocks, inputs, syntax 10 | from ._types import Block, Signature 11 | from .decompile_input import decompile_input 12 | from .json_object import JSONObject 13 | from .utils import unwrap 14 | 15 | if TYPE_CHECKING: 16 | from .decompile_sprite import Ctx 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | # fmt: off 22 | _ = Signature 23 | BLOCKS = { 24 | # Motion 25 | "motion_movesteps": _("move", ["STEPS"]), 26 | "motion_turnleft": _("turn_left", ["DEGREES"]), 27 | "motion_turnright": _("turn_right", ["DEGREES"]), 28 | "motion_goto": _("goto", ["TO"], menu="TO", field="TO", overloads={ 29 | "_mouse_": "goto_mouse_pointer", 30 | "_random_": "goto_random_position", 31 | }), 32 | "motion_gotoxy": _("goto", ["X", "Y"]), 33 | "motion_glidesecstoxy": _("glide", ["X", "Y", "SECS"]), 34 | "motion_glideto": _("glide", ["TO", "SECS"], menu="TO", field="TO", 35 | overloads={ 36 | "_mouse_": "glide_to_mouse_pointer", 37 | "_random_": "glide_to_random_position", 38 | }), 39 | "motion_pointindirection": _("point_in_direction", ["DIRECTION"]), 40 | "motion_pointtowards": _("point_towards", ["TOWARDS"], menu="TOWARDS", 41 | field="TOWARDS", overloads={ 42 | "_mouse_": "point_towards_mouse_pointer", 43 | "_random_": "point_towards_random_direction", 44 | }), 45 | "motion_changexby": _("change_x", ["DX"]), 46 | "motion_setx": _("set_x", ["X"]), 47 | "motion_changeyby": _("change_y", ["DY"]), 48 | "motion_sety": _("set_y", ["Y"]), 49 | "motion_ifonedgebounce": _("if_on_edge_bounce", []), 50 | "motion_setrotationstyle": _("set_rotation_style_all_around", [], 51 | field="STYLE", overloads={ 52 | "left-right": "set_rotation_style_left_right", 53 | "don't rotate": "set_rotation_style_do_not_rotate", 54 | "all around": "set_rotation_style_all_around", 55 | }), 56 | # Looks 57 | "looks_sayforsecs": _("say", ["MESSAGE", "SECS"]), 58 | "looks_thinkforsecs": _("think", ["MESSAGE", "SECS"]), 59 | "looks_say": _("say", ["MESSAGE"]), 60 | "looks_think": _("think", ["MESSAGE"]), 61 | "looks_switchcostumeto": _("switch_costume", ["COSTUME"]), 62 | "looks_nextcostume": _("next_costume", []), 63 | "looks_switchbackdropto": _("switch_backdrop", ["BACKDROP"], menu="BACKDROP", 64 | field="BACKDROP", overloads={ 65 | "next backdrop": "next_backdrop", 66 | "previous backdrop": "previous_backdrop", 67 | "random backdrop": "random_backdrop", 68 | }), 69 | "looks_nextbackdrop": _("next_backdrop", []), 70 | "looks_setsizeto": _("set_size", ["SIZE"]), 71 | "looks_changesizeby": _("change_size", ["CHANGE"]), 72 | "looks_changeeffectby": _("change_color_effect", ["CHANGE"], 73 | field="EFFECT", overloads={ 74 | "COLOR": "change_color_effect", 75 | "FISHEYE": "change_fisheye_effect", 76 | "WHIRL": "change_whirl_effect", 77 | "PIXELATE": "change_pixelate_effect", 78 | "MOSAIC": "change_mosaic_effect", 79 | "BRIGHTNESS": "change_brightness_effect", 80 | "GHOST": "change_ghost_effect", 81 | }), 82 | "looks_seteffectto": _("set_color_effect", ["VALUE"], 83 | field="EFFECT", overloads={ 84 | "COLOR": "set_color_effect", 85 | "FISHEYE": "set_fisheye_effect", 86 | "WHIRL": "set_whirl_effect", 87 | "PIXELATE": "set_pixelate_effect", 88 | "MOSAIC": "set_mosaic_effect", 89 | "BRIGHTNESS": "set_brightness_effect", 90 | "GHOST": "set_ghost_effect", 91 | }), 92 | "looks_cleargraphiceffects":_("clear_graphic_effects", []), 93 | "looks_show": _("show", []), 94 | "looks_hide": _("hide", []), 95 | "looks_gotofrontback": _("goto_front", [], field="FRONT_BACK", overloads={ 96 | "front": "goto_front", 97 | "back": "goto_back", 98 | }), 99 | "looks_goforwardbackwardlayers": _("go_forward", ["NUM"], 100 | field="FORWARD_BACKWARD", overloads={ 101 | "forward": "go_forward", 102 | "backward": "go_backward", 103 | }), 104 | # Sound 105 | "sound_playuntildone": _("play_sound_until_done", ["SOUND_MENU"], "SOUND_MENU", 106 | "SOUND_MENU", {}), 107 | "sound_play": _("start_sound", ["SOUND_MENU"], "SOUND_MENU", 108 | "SOUND_MENU", {}), 109 | "sound_stopallsounds": _("stop_all_sounds", []), 110 | "sound_changeeffectby": _("change_pitch_effect", ["VALUE"], 111 | field="EFFECT", overloads={ 112 | "PITCH": "change_pitch_effect", 113 | "PAN": "change_pan_effect", 114 | }), 115 | "sound_seteffectto": _("set_pitch_effect", ["VALUE"], 116 | field="EFFECT", overloads={ 117 | "PITCH": "set_pitch_effect", 118 | "PAN": "set_pan_effect", 119 | }), 120 | "sound_changevolumeby": _("change_volume", ["VOLUME"]), 121 | "sound_setvolumeto": _("set_volume", ["VOLUME"]), 122 | "sound_cleareffects": _("clear_sound_effects", []), 123 | # Event 124 | "event_broadcast": _("broadcast", ["BROADCAST_INPUT"]), 125 | "event_broadcastandwait": _("broadcast_and_wait", ["BROADCAST_INPUT"]), 126 | # Control 127 | "control_wait": _("wait", ["DURATION"]), 128 | "control_stop": _("stop_all", [], field="STOP_OPTION", overloads={ 129 | "all": "stop_all", 130 | "this script": "stop_this_script", 131 | "other scripts in sprite": "stop_other_scripts", 132 | }), 133 | "control_delete_this_clone":_("delete_this_clone", []), 134 | "control_create_clone_of": _("clone", ["CLONE_OPTION"], menu="CLONE_OPTION", 135 | field="CLONE_OPTION", overloads={ 136 | "_myself_": "clone", 137 | }), 138 | # Sensing 139 | "sensing_askandwait": _("ask", ["QUESTION"]), 140 | "sensing_setdragmode": _("set_drag_mode_draggable", [], 141 | field="DRAG_MODE", overloads={ 142 | "draggable": "set_drag_mode_draggable", 143 | "not draggable": "set_drag_mode_not_draggable", 144 | }), 145 | "sensing_resettimer": _("reset_timer", []), 146 | # Pen 147 | "pen_clear": _("erase_all", []), 148 | "pen_stamp": _("stamp", []), 149 | "pen_penDown": _("pen_down", []), 150 | "pen_penUp": _("pen_up", []), 151 | "pen_setPenColorToColor": _("set_pen_color", ["COLOR"]), 152 | "pen_changePenSizeBy": _("change_pen_size", ["SIZE"]), 153 | "pen_setPenSizeTo": _("set_pen_size", ["SIZE"]), 154 | "pen_setPenColorParamTo": _("set_pen_hue", ["VALUE"], 155 | field="COLOR_PARAM", overloads={ 156 | "color": "set_pen_hue", 157 | "saturation": "set_pen_saturation", 158 | "brightness": "set_pen_brightness", 159 | "transparency": "set_pen_transparency", 160 | }), 161 | "pen_changePenColorParamBy":_("change_pen_hue", ["VALUE"], 162 | field="COLOR_PARAM", overloads={ 163 | "color": "change_pen_hue", 164 | "saturation": "change_pen_saturation", 165 | "brightness": "change_pen_brightness", 166 | "transparency": "change_pen_transparency", 167 | }), 168 | # Music 169 | "music_restForBeats": _("rest", ["BEATS"]), 170 | "music_setTempo": _("set_tempo", ["TEMPO"]), 171 | "music_changeTempo": _("change_tempo", ["TEMPO"]), 172 | } 173 | del _ 174 | # fmt: on 175 | 176 | 177 | def decompile_block(ctx: Ctx, block: Block) -> None: 178 | signature = deepcopy(BLOCKS[block.opcode]) 179 | if signature.menu: 180 | ast.flatten_menu(ctx, block, signature.menu) 181 | if field := block.fields._.get(signature.field or ""): 182 | if opcode := unwrap(signature.overloads).get(field[0]): 183 | signature.opcode = opcode 184 | with contextlib.suppress(ValueError): 185 | signature.inputs.remove(unwrap(signature.field)) 186 | else: 187 | block.inputs._[unwrap(signature.field)] = [1, [4, field[0]]] 188 | ctx.iprint(signature.opcode) 189 | if signature.inputs: 190 | ctx.print(" ") 191 | ctx.commasep(signature.inputs, decompile_input, pass_self=True, block=block) 192 | ctx.println(";") 193 | 194 | 195 | ADDONS = { 196 | "\u200b\u200bbreakpoint\u200b\u200b": ("breakpoint", []), 197 | "\u200b\u200blog\u200b\u200b %s": ("log", ["arg0"]), 198 | "\u200b\u200bwarn\u200b\u200b %s": ("warn", ["arg0"]), 199 | "\u200b\u200berror\u200b\u200b %s": ("error", ["arg0"]), 200 | } 201 | 202 | 203 | def decompile_addon(ctx: Ctx, block: Block) -> None: 204 | opcode, inputs = ADDONS[block.mutation.proccode] 205 | ctx.iprint(opcode) 206 | if inputs: 207 | ctx.print(" ") 208 | ctx.commasep(inputs, decompile_input, pass_self=True, block=block) 209 | ctx.println(";") 210 | 211 | 212 | def decompile_else(ctx: Ctx, block_id: str | None) -> None: 213 | from .decompile_code import decompile_stack 214 | 215 | if block_id is None: 216 | return 217 | block = ctx.blocks[block_id] 218 | if block.opcode.startswith("control_if") and block.next is None: 219 | ctx.iprint("elif ") 220 | decompile_input(ctx, "CONDITION", block) 221 | ctx.print(" ") 222 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 223 | decompile_else(ctx, inputs.block_id(block.inputs._.get("SUBSTACK2"))) 224 | else: 225 | ctx.iprint("else ") 226 | decompile_stack(ctx, block_id) 227 | 228 | 229 | def decompile_control_if(ctx: Ctx, block: Block) -> None: 230 | from .decompile_code import decompile_stack 231 | 232 | ctx.iprint("if ") 233 | decompile_input(ctx, "CONDITION", block) 234 | ctx.print(" ") 235 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 236 | decompile_else(ctx, inputs.block_id(block.inputs._.get("SUBSTACK2"))) 237 | 238 | 239 | def decompile_control_repeat(ctx: Ctx, block: Block) -> None: 240 | from .decompile_code import decompile_stack 241 | 242 | ctx.iprint("repeat ") 243 | decompile_input(ctx, "TIMES", block) 244 | ctx.print(" ") 245 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 246 | 247 | 248 | def decompile_control_repeat_until(ctx: Ctx, block: Block) -> None: 249 | from .decompile_code import decompile_stack 250 | 251 | ctx.iprint("until ") 252 | decompile_input(ctx, "CONDITION", block) 253 | ctx.print(" ") 254 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 255 | 256 | 257 | def decompile_control_forever(ctx: Ctx, block: Block) -> None: 258 | from .decompile_code import decompile_stack 259 | 260 | ctx.iprint("forever ") 261 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 262 | 263 | 264 | def decompile_control_while(ctx: Ctx, block: Block) -> None: 265 | from .decompile_code import decompile_stack 266 | from .decompile_expr import Assoc, decompile_operand 267 | 268 | condition = inputs.block_id(block.inputs._.get("CONDITION")) 269 | 270 | if condition: 271 | ctx.iprint("until not ") 272 | decompile_operand( 273 | ctx, 274 | "OPERAND", 275 | Block("operator_not", inputs=JSONObject({"OPERAND": [2, condition]})), 276 | Assoc.LEFT, 277 | ) 278 | ctx.print(" ") 279 | else: 280 | ctx.iprint("until not true ") 281 | decompile_stack(ctx, inputs.block_id(block.inputs._.get("SUBSTACK"))) 282 | 283 | 284 | def decompile_procedures_call(ctx: Ctx, block: Block) -> None: 285 | ctx.iprint(syntax.identifier(custom_blocks.get_name(block))) 286 | args = json.loads(block.mutation.argumentids) 287 | if args: 288 | ctx.print(" ") 289 | ctx.commasep(args, decompile_input, pass_self=True, block=block) 290 | ctx.println(";") 291 | 292 | 293 | def decompile_data_setvariableto(ctx: Ctx, block: Block) -> None: 294 | ctx.iprint(syntax.identifier(block.fields.VARIABLE[0]), " = ") 295 | decompile_input(ctx, "VALUE", block) 296 | ctx.println(";") 297 | 298 | 299 | def decompile_data_changevariableby(ctx: Ctx, block: Block) -> None: 300 | op = block._.get("OPERATOR", "+") 301 | ctx.iprint(syntax.identifier(block.fields.VARIABLE[0]), f" {op}= ") 302 | decompile_input(ctx, "VALUE", block) 303 | ctx.println(";") 304 | 305 | 306 | def decompile_data_showvariable(ctx: Ctx, block: Block) -> None: 307 | ctx.iprintln("show ", syntax.identifier(block.fields.VARIABLE[0]), ";") 308 | 309 | 310 | def decompile_data_hidevariable(ctx: Ctx, block: Block) -> None: 311 | ctx.iprintln("hide ", syntax.identifier(block.fields.VARIABLE[0]), ";") 312 | 313 | 314 | def decompile_data_addtolist(ctx: Ctx, block: Block) -> None: 315 | ctx.iprint("add ") 316 | decompile_input(ctx, "ITEM", block) 317 | ctx.println(" to ", syntax.identifier(block.fields.LIST[0]), ";") 318 | 319 | 320 | def decompile_data_deleteoflist(ctx: Ctx, block: Block) -> None: 321 | ctx.print("delete ", syntax.identifier(block.fields.LIST[0]), "[") 322 | decompile_input(ctx, "INDEX", block) 323 | ctx.println("];") 324 | 325 | 326 | def decompile_data_deletealloflist(ctx: Ctx, block: Block) -> None: 327 | ctx.iprint("delete ") 328 | ctx.print(syntax.identifier(block.fields.LIST[0])) 329 | ctx.println(";") 330 | 331 | 332 | def decompile_data_insertatlist(ctx: Ctx, block: Block) -> None: 333 | ctx.iprint("insert ") 334 | decompile_input(ctx, "ITEM", block) 335 | ctx.print(" at ", syntax.identifier(block.fields.LIST[0]), "[") 336 | decompile_input(ctx, "INDEX", block) 337 | ctx.println("];") 338 | 339 | 340 | def decompile_data_replaceitemoflist(ctx: Ctx, block: Block) -> None: 341 | ctx.iprint(syntax.identifier(block.fields.LIST[0]), "[") 342 | decompile_input(ctx, "INDEX", block) 343 | op = block._.get("OPERATOR", "") 344 | ctx.print(f"] {op}= ") 345 | decompile_input(ctx, "ITEM", block) 346 | ctx.println(";") 347 | 348 | 349 | def decompile_data_showlist(ctx: Ctx, block: Block) -> None: 350 | ctx.iprintln("show ", syntax.identifier(block.fields.LIST[0]), ";") 351 | 352 | 353 | def decompile_data_hidelist(ctx: Ctx, block: Block) -> None: 354 | ctx.iprintln("hide ", syntax.identifier(block.fields.LIST[0]), ";") 355 | 356 | 357 | decompile_control_wait_until = decompile_control_repeat_until 358 | decompile_control_if_else = decompile_control_if 359 | 360 | 361 | def decompile_stmt(ctx: Ctx, block: Block) -> None: 362 | if (mutation := block._.get("mutation")) and mutation._.get("proccode") in ADDONS: 363 | decompiler = decompile_addon 364 | elif block.opcode in BLOCKS: 365 | decompiler = decompile_block 366 | else: 367 | decompiler = globals().get(f"decompile_{block.opcode}") 368 | if decompiler: 369 | logger.debug("using %s to decompile\n%s", decompiler, block) 370 | decompiler(ctx, block) 371 | return 372 | logger.error("no decompiler implemented for stmt `%s`\n%s", block.opcode, block) 373 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.13.0" 4 | 5 | [[package]] 6 | name = "markdown-it-py" 7 | version = "3.0.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "mdurl" }, 11 | ] 12 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 13 | wheels = [ 14 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, 15 | ] 16 | 17 | [[package]] 18 | name = "mdurl" 19 | version = "0.1.2" 20 | source = { registry = "https://pypi.org/simple" } 21 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 24 | ] 25 | 26 | [[package]] 27 | name = "pillow" 28 | version = "11.3.0" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, 33 | { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, 34 | { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, 35 | { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, 36 | { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, 37 | { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, 38 | { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, 39 | { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, 40 | { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, 41 | { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, 42 | { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, 43 | { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, 44 | { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, 45 | { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, 46 | { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, 47 | { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, 48 | { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, 49 | { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, 50 | { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, 51 | { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, 52 | { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, 53 | { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, 54 | { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, 55 | { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, 56 | { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, 57 | { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, 58 | { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, 59 | { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, 60 | { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, 61 | { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, 62 | { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, 63 | { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, 64 | { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, 65 | { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, 66 | { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, 67 | { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, 68 | { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, 69 | { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, 70 | { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, 71 | { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, 72 | { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, 73 | { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, 74 | { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, 75 | { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, 76 | { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, 77 | { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, 78 | { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, 79 | ] 80 | 81 | [[package]] 82 | name = "pygments" 83 | version = "2.19.2" 84 | source = { registry = "https://pypi.org/simple" } 85 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 88 | ] 89 | 90 | [[package]] 91 | name = "rich" 92 | version = "14.0.0" 93 | source = { registry = "https://pypi.org/simple" } 94 | dependencies = [ 95 | { name = "markdown-it-py" }, 96 | { name = "pygments" }, 97 | ] 98 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, 101 | ] 102 | 103 | [[package]] 104 | name = "sb2gs" 105 | version = "2.0.0" 106 | source = { editable = "." } 107 | dependencies = [ 108 | { name = "pillow" }, 109 | { name = "rich" }, 110 | { name = "toml" }, 111 | ] 112 | 113 | [package.metadata] 114 | requires-dist = [ 115 | { name = "pillow", specifier = ">=11.3.0" }, 116 | { name = "rich", specifier = ">=14.0.0" }, 117 | { name = "toml", specifier = ">=0.10.2" }, 118 | ] 119 | 120 | [[package]] 121 | name = "toml" 122 | version = "0.10.2" 123 | source = { registry = "https://pypi.org/simple" } 124 | sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } 125 | wheels = [ 126 | { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, 127 | ] 128 | --------------------------------------------------------------------------------