mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-05-02 22:57:59 +03:00
238 lines
5.7 KiB
Python
Executable file
238 lines
5.7 KiB
Python
Executable file
#!/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 = "forestgreen"
|
|
COLOR_NAMED = "lightpink"
|
|
COLOR_TODO = "mistyrose"
|
|
|
|
|
|
@dataclass
|
|
class Function:
|
|
name: str
|
|
offset: int
|
|
size: int
|
|
flags: str
|
|
|
|
@property
|
|
def is_decompiled(self):
|
|
return "+" in self.flags
|
|
|
|
@property
|
|
def is_named(self):
|
|
return not self.name.startswith("sub_")
|
|
|
|
|
|
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"<rect "
|
|
f'width="{self.size}" '
|
|
f'height="{self.size}" '
|
|
f'x="{self.x}" '
|
|
f'y="{self.y}" '
|
|
f'fill="{self.color}"/>'
|
|
)
|
|
|
|
|
|
@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'<text alignment-baseline="central" '
|
|
f'x="{self.x}" y="{self.y + TEXT_SIZE/2}" '
|
|
f'style="font-family: sans-serif; font-size: {TEXT_SIZE}px">'
|
|
f"{self.text}"
|
|
f"</text>"
|
|
)
|
|
|
|
|
|
@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(all_functions: T.List[Function]) -> T.Iterable[Shape]:
|
|
for i, function in enumerate(all_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
|
|
elif function.is_named:
|
|
color = COLOR_NAMED
|
|
else:
|
|
color = COLOR_TODO
|
|
yield Square(x=x, y=y, color=color)
|
|
|
|
y += GRID_SQUARE_SIZE + LEGEND_MARGIN
|
|
|
|
ready_functions = [func for func in all_functions if func.is_decompiled]
|
|
named_functions = [
|
|
func
|
|
for func in all_functions
|
|
if func.is_named and func not in ready_functions
|
|
]
|
|
todo_functions = [
|
|
func
|
|
for func in all_functions
|
|
if func not in ready_functions and func not in named_functions
|
|
]
|
|
|
|
def sum_size(functions: T.Iterable[Function]) -> int:
|
|
return sum(func.size for func in functions)
|
|
|
|
for (color, text, functions) in [
|
|
(COLOR_DECOMPILED, "Functions decompiled", ready_functions),
|
|
(
|
|
COLOR_NAMED,
|
|
"Functions not decompiled, but with known names",
|
|
named_functions,
|
|
),
|
|
(
|
|
COLOR_TODO,
|
|
"Functions not decompiled, with unknown names",
|
|
todo_functions,
|
|
),
|
|
]:
|
|
yield LegendText(
|
|
x=0,
|
|
y=y,
|
|
color=color,
|
|
text=f"{text} (count): {len(functions)/len(all_functions):.02%}",
|
|
)
|
|
y += TEXT_SIZE + LEGEND_ROW_PADDING
|
|
|
|
yield LegendText(
|
|
x=0,
|
|
y=y,
|
|
color=color,
|
|
text=f"{text} (bytesize): {sum_size(functions)/sum_size(all_functions):.02%}",
|
|
)
|
|
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'<svg version="1.1" '
|
|
f'width="{bbox.x2}" '
|
|
f'height="{bbox.y2}" '
|
|
f'xmlns="http://www.w3.org/2000/svg">',
|
|
file=handle,
|
|
)
|
|
|
|
for shape in shapes:
|
|
print(shape.render(), file=handle)
|
|
|
|
print("</svg>", file=handle)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|