#!/usr/bin/python3 import re import typing as T from dataclasses import dataclass from pathlib import Path GRID_MAX_SQUARES = 50 GRID_SQUARE_SIZE = 12 GRID_SQUARE_MARGIN = 2 LEGEND_SQUARE_SIZE = 12 LEGEND_SQUARE_MARGIN = 2 LEGEND_ROW_PADDING = 3 LEGEND_MARGIN = 15 TEXT_SIZE = 15 TEXT_MARGIN = 5 DOCS_DIR = Path(__file__).parent PROGRESS_TXT_FILE = DOCS_DIR / "progress.txt" PROGRESS_SVG_FILE = DOCS_DIR / "progress.svg" COLOR_DECOMPILED = "green" COLOR_TODO = "lightpink" @dataclass class Function: name: str offset: int size: int flags: str @property def is_decompiled(self): return "+" in self.flags def collect_functions() -> T.Iterable[Function]: for line in PROGRESS_TXT_FILE.open(): if line.startswith("#"): continue func_name, offset, size, flags = re.split(r"\s+", line.strip()) yield Function( name=func_name, offset=int(offset, 16), size=int(size, 16), flags=flags, ) @dataclass class BoundingBox: x1: int y1: int x2: int y2: int class Shape: @property def bounds(self) -> BoundingBox: raise NotImplementedError("not implemented") def render(self) -> str: raise NotImplementedError("not implemented") def get_common_bbox(shapes: T.List[Shape]) -> BoundingBox: return BoundingBox( x1=min(shape.bounds.x1 for shape in shapes), y1=min(shape.bounds.y1 for shape in shapes), x2=max(shape.bounds.x2 for shape in shapes), y2=max(shape.bounds.y2 for shape in shapes), ) @dataclass class Square(Shape): x: int y: int color: str size: int = GRID_SQUARE_SIZE @property def bounds(self) -> BoundingBox: return BoundingBox( x1=self.x, y1=self.y, x2=self.x + self.size, y2=self.y + self.size ) def render(self) -> str: return ( f"' ) @dataclass class Text(Shape): x: int y: int text: str @property def bounds(self) -> BoundingBox: return BoundingBox( x1=self.x, y1=self.y, x2=self.x, y2=self.y + TEXT_SIZE ) def render(self) -> str: return ( f'' f"{self.text}" f"" ) @dataclass class LegendText(Shape): x: int y: int color: str text: str @property def _square(self) -> Square: return Square( x=self.x, y=self.y + (TEXT_SIZE - LEGEND_SQUARE_SIZE) / 2, color=self.color, size=LEGEND_SQUARE_SIZE, ) @property def _text(self) -> Text: return Text( x=LEGEND_SQUARE_SIZE + TEXT_MARGIN, y=self.y, text=self.text, ) @property def bounds(self) -> BoundingBox: return get_common_bbox([self._square, self._text]) def render(self) -> str: return self._square.render() + self._text.render() def render_svg(functions: T.List[Function]) -> T.Iterable[Shape]: for i, function in enumerate(functions): x = (i % GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN) y = (i // GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN) if function.is_decompiled: color = COLOR_DECOMPILED else: color = COLOR_TODO yield Square(x=x, y=y, color=color) y += GRID_SQUARE_SIZE + LEGEND_MARGIN ready_functions = sum(function.is_decompiled for function in functions) total_functions = len(functions) ready_size = sum( function.size for function in functions if function.is_decompiled ) total_size = sum(function.size for function in functions) for (color, text) in [ ( COLOR_DECOMPILED, ( f"Functions decompiled (count): " f"{ready_functions/total_functions:.02%}" ), ), ( COLOR_DECOMPILED, f"Functions decompiled (bytesize): {ready_size/total_size:.02%}", ), ( COLOR_TODO, ( f"Functions remaining (count): " f"{(total_functions-ready_functions)/total_functions:.02%}" ), ), ( COLOR_TODO, ( f"Functions remaining (bytesize): " f"{(total_size-ready_size)/total_size:.02%}" ), ), ]: yield LegendText( x=0, y=y, color=color, text=text, ) y += TEXT_SIZE + LEGEND_ROW_PADDING def main() -> None: functions = list(collect_functions()) with PROGRESS_SVG_FILE.open("w") as handle: shapes = list(render_svg(functions)) bbox = get_common_bbox(shapes) print( f'', file=handle, ) for shape in shapes: print(shape.render(), file=handle) print("", file=handle) if __name__ == "__main__": main()