tfbscript 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tfbscript/__init__.py +21 -0
- tfbscript/__main__.py +52 -0
- tfbscript/ansi.py +203 -0
- tfbscript/arena.py +119 -0
- tfbscript/binary.py +84 -0
- tfbscript/debug.py +14 -0
- tfbscript/editor/__init__.py +3 -0
- tfbscript/editor/app.py +141 -0
- tfbscript/editor/fonts/ms-sans-serif/MS Sans Serif.ttf +0 -0
- tfbscript/editor/fonts/ms-sans-serif/license.txt +4 -0
- tfbscript/editor/fonts/ms-sans-serif/readme.txt +26 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/license.txt +4 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/readme.txt +26 -0
- tfbscript/editor/fonts.py +98 -0
- tfbscript/editor/search.py +200 -0
- tfbscript/editor/style.py +141 -0
- tfbscript/editor/widgets.py +321 -0
- tfbscript/opcodes/__init__.py +104 -0
- tfbscript/opcodes/base.py +362 -0
- tfbscript/opcodes/block.py +34 -0
- tfbscript/opcodes/enums.py +219 -0
- tfbscript/opcodes/op_behavior_implementation.py +39 -0
- tfbscript/opcodes/op_change_membership.py +62 -0
- tfbscript/opcodes/op_check_fov.py +73 -0
- tfbscript/opcodes/op_check_membership.py +41 -0
- tfbscript/opcodes/op_check_message.py +79 -0
- tfbscript/opcodes/op_check_reference.py +61 -0
- tfbscript/opcodes/op_check_value.py +66 -0
- tfbscript/opcodes/op_comment.py +43 -0
- tfbscript/opcodes/op_control.py +80 -0
- tfbscript/opcodes/op_create_variable.py +42 -0
- tfbscript/opcodes/op_cut_scene.py +44 -0
- tfbscript/opcodes/op_dec_value.py +43 -0
- tfbscript/opcodes/op_displace.py +111 -0
- tfbscript/opcodes/op_find_subset.py +49 -0
- tfbscript/opcodes/op_find_variable.py +28 -0
- tfbscript/opcodes/op_for_each.py +65 -0
- tfbscript/opcodes/op_if_else.py +105 -0
- tfbscript/opcodes/op_inc_value.py +43 -0
- tfbscript/opcodes/op_loop_value.py +28 -0
- tfbscript/opcodes/op_move_from.py +47 -0
- tfbscript/opcodes/op_move_to.py +99 -0
- tfbscript/opcodes/op_play_animation.py +46 -0
- tfbscript/opcodes/op_play_sound.py +42 -0
- tfbscript/opcodes/op_prescript.py +26 -0
- tfbscript/opcodes/op_print.py +53 -0
- tfbscript/opcodes/op_remove.py +43 -0
- tfbscript/opcodes/op_reset.py +42 -0
- tfbscript/opcodes/op_run_as_player.py +26 -0
- tfbscript/opcodes/op_send_message.py +53 -0
- tfbscript/opcodes/op_set_behavior.py +41 -0
- tfbscript/opcodes/op_set_reference.py +52 -0
- tfbscript/opcodes/op_set_value.py +52 -0
- tfbscript/opcodes/op_shutdown.py +26 -0
- tfbscript/opcodes/op_slide_value.py +81 -0
- tfbscript/opcodes/op_spawn_actor.py +96 -0
- tfbscript/opcodes/op_startup.py +26 -0
- tfbscript/opcodes/op_stop_sound.py +26 -0
- tfbscript/opcodes/op_teleport_to.py +47 -0
- tfbscript/opcodes/op_turn_to.py +33 -0
- tfbscript/opcodes/op_use_camera.py +68 -0
- tfbscript/payload.py +40 -0
- tfbscript/reference.py +926 -0
- tfbscript/rhs.py +326 -0
- tfbscript/script.py +160 -0
- tfbscript/string_table.py +101 -0
- tfbscript-0.1.0.dist-info/METADATA +116 -0
- tfbscript-0.1.0.dist-info/RECORD +72 -0
- tfbscript-0.1.0.dist-info/WHEEL +4 -0
- tfbscript-0.1.0.dist-info/entry_points.txt +2 -0
- tfbscript-0.1.0.dist-info/licenses/LICENSE +201 -0
tfbscript/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Parser and decompiler for TFB script (.ai) files."""
|
|
2
|
+
|
|
3
|
+
from tfbscript.binary import BinaryReader
|
|
4
|
+
from tfbscript.payload import PayloadReader
|
|
5
|
+
from tfbscript.reference import Reference
|
|
6
|
+
from tfbscript.rhs import Rhs
|
|
7
|
+
from tfbscript.script import ScriptFile
|
|
8
|
+
from tfbscript.string_table import StringTable, StringTableEntry
|
|
9
|
+
from tfbscript.editor import open_editor, editor_from_filepath
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BinaryReader",
|
|
13
|
+
"PayloadReader",
|
|
14
|
+
"Reference",
|
|
15
|
+
"Rhs",
|
|
16
|
+
"ScriptFile",
|
|
17
|
+
"StringTable",
|
|
18
|
+
"StringTableEntry",
|
|
19
|
+
"open_editor",
|
|
20
|
+
"editor_from_filepath"
|
|
21
|
+
]
|
tfbscript/__main__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""CLI: decompile TFB script (.ai) files to pretty-printed pseudo-source.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m tfbscript example_scripts/Teleporter.ai
|
|
5
|
+
python -m tfbscript --no-color example_scripts/*.ai
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from tfbscript.ansi import disable_colors, enable_colors
|
|
12
|
+
from tfbscript.script import ScriptFile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="tfbscript",
|
|
18
|
+
description="Decompile TFB script (.ai) files to pretty-printed pseudo-source.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("files", nargs="+", help="one or more .ai script files")
|
|
21
|
+
color_group = parser.add_mutually_exclusive_group()
|
|
22
|
+
color_group.add_argument(
|
|
23
|
+
"--no-color",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="disable ANSI syntax coloring (auto-disabled when piped or NO_COLOR is set)",
|
|
26
|
+
)
|
|
27
|
+
color_group.add_argument(
|
|
28
|
+
"--color",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="force ANSI syntax coloring even when piped",
|
|
31
|
+
)
|
|
32
|
+
args = parser.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
if args.no_color:
|
|
35
|
+
disable_colors()
|
|
36
|
+
elif args.color:
|
|
37
|
+
enable_colors()
|
|
38
|
+
|
|
39
|
+
status = 0
|
|
40
|
+
for path in args.files:
|
|
41
|
+
if len(args.files) > 1:
|
|
42
|
+
print(f"===== {path} =====")
|
|
43
|
+
try:
|
|
44
|
+
ScriptFile.from_path(path).print_tree()
|
|
45
|
+
except (OSError, ValueError) as error:
|
|
46
|
+
print(f"{path}: {error}", file=sys.stderr)
|
|
47
|
+
status = 1
|
|
48
|
+
return status
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
sys.exit(main())
|
tfbscript/ansi.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Centralized ANSI syntax highlighting for TFBScript pretty-printing.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from tfbscript.ansi import keyword
|
|
5
|
+
|
|
6
|
+
print(keyword("var"))
|
|
7
|
+
|
|
8
|
+
Coloring is on by default when stdout is a real terminal, and off when
|
|
9
|
+
piped/redirected or when the NO_COLOR env var is set (https://no-color.org).
|
|
10
|
+
Override explicitly with enable_colors() / disable_colors() / set_colors_enabled(),
|
|
11
|
+
e.g. to add a --no-color CLI flag to a script.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
_RESET = "\033[0m"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if sys.platform == "win32":
|
|
22
|
+
|
|
23
|
+
def _enable_windows_vt100() -> bool:
|
|
24
|
+
"""Turn on ANSI (virtual terminal) processing for the Windows console."""
|
|
25
|
+
try:
|
|
26
|
+
import ctypes
|
|
27
|
+
|
|
28
|
+
STD_OUTPUT_HANDLE = -11
|
|
29
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
30
|
+
|
|
31
|
+
kernel32 = ctypes.windll.kernel32
|
|
32
|
+
handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
33
|
+
|
|
34
|
+
mode = ctypes.c_uint32()
|
|
35
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
return bool(
|
|
39
|
+
kernel32.SetConsoleMode(
|
|
40
|
+
handle,
|
|
41
|
+
mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
except Exception: # noqa: BLE001
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
else:
|
|
48
|
+
|
|
49
|
+
def _enable_windows_vt100() -> bool:
|
|
50
|
+
"""No-op on non-Windows platforms."""
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _detect_default() -> bool:
|
|
55
|
+
if os.environ.get("NO_COLOR") is not None:
|
|
56
|
+
return False
|
|
57
|
+
if os.environ.get("FORCE_COLOR") is not None:
|
|
58
|
+
return True
|
|
59
|
+
if not getattr(sys.stdout, "isatty", lambda: False)():
|
|
60
|
+
return False
|
|
61
|
+
if sys.platform == "win32":
|
|
62
|
+
return _enable_windows_vt100()
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_enabled = _detect_default()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def colors_enabled() -> bool:
|
|
70
|
+
return _enabled
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_colors_enabled(value: bool) -> None:
|
|
74
|
+
global _enabled
|
|
75
|
+
_enabled = value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def enable_colors() -> None:
|
|
79
|
+
set_colors_enabled(True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def disable_colors() -> None:
|
|
83
|
+
set_colors_enabled(False)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _fg_rgb(r: int, g: int, b: int) -> str:
|
|
87
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fg(hex_color: str) -> str:
|
|
91
|
+
hex_color = hex_color.lstrip("#")
|
|
92
|
+
r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
|
93
|
+
return _fg_rgb(r, g, b)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Color(Enum):
|
|
97
|
+
"""Syntax categories, colored after the One Dark Pro theme."""
|
|
98
|
+
|
|
99
|
+
# Punctuation
|
|
100
|
+
PARENTHESES = "#5C6370" # muted gray
|
|
101
|
+
|
|
102
|
+
# Operators
|
|
103
|
+
OPERATOR = "#56B6C2" # cyan
|
|
104
|
+
COMPARISON = "#56B6C2" # cyan
|
|
105
|
+
|
|
106
|
+
# Language keywords
|
|
107
|
+
KEYWORD = "#C678DD" # purple
|
|
108
|
+
FLOW_CONTROL = "#E06C75" # red
|
|
109
|
+
|
|
110
|
+
# Literals
|
|
111
|
+
STRING = "#98C379" # green
|
|
112
|
+
NUMBER = "#D19A66" # orange
|
|
113
|
+
TYPE = "#E5C07B" # yellow
|
|
114
|
+
BUILTIN = "#E5C07B" # yellow
|
|
115
|
+
|
|
116
|
+
# Identifiers
|
|
117
|
+
VARIABLE = "#E06C75" # red
|
|
118
|
+
VARIABLE_GLOBAL = "#61AFEF" # blue
|
|
119
|
+
METHOD = "#61AFEF" # blue
|
|
120
|
+
|
|
121
|
+
# Comments
|
|
122
|
+
COMMENT = "#5C6370" # muted gray
|
|
123
|
+
|
|
124
|
+
INLINE_FUNC_OP = "#6458EC"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def color_text(text: object, color: Color) -> str:
|
|
128
|
+
"""Wrap text in `color`'s ANSI code; passes it through unchanged if coloring is off."""
|
|
129
|
+
if not _enabled:
|
|
130
|
+
return str(text)
|
|
131
|
+
return f"{_fg(color.value)}{text}{_RESET}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Shorthand for each syntax category, e.g. keyword("if") instead of
|
|
135
|
+
# color_text("if", Color.KEYWORD) -- use these when building source_line()
|
|
136
|
+
# output for opcodes so call sites read like the syntax they represent.
|
|
137
|
+
def parentheses(text: object) -> str:
|
|
138
|
+
return color_text(text, Color.PARENTHESES)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def operator(text: object) -> str:
|
|
142
|
+
return color_text(text, Color.OPERATOR)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def comparison(text: object) -> str:
|
|
146
|
+
return color_text(text, Color.COMPARISON)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def keyword(text: object) -> str:
|
|
150
|
+
return color_text(text, Color.KEYWORD)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def flow_control(text: object) -> str:
|
|
154
|
+
return color_text(text, Color.FLOW_CONTROL)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def string(text: object) -> str:
|
|
158
|
+
return color_text(text, Color.STRING)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def number(text: object) -> str:
|
|
162
|
+
return color_text(text, Color.NUMBER)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def type_(text: object) -> str:
|
|
166
|
+
return color_text(text, Color.TYPE)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def builtin(text: object) -> str:
|
|
170
|
+
return color_text(text, Color.BUILTIN)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def variable(text: object) -> str:
|
|
174
|
+
return color_text(text, Color.VARIABLE)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def variable_global(text: object) -> str:
|
|
178
|
+
return color_text(text, Color.VARIABLE_GLOBAL)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def method(text: object) -> str:
|
|
182
|
+
return color_text(text, Color.METHOD)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def comment(text: object) -> str:
|
|
186
|
+
return color_text(text, Color.COMMENT)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def rgb_square(r: int, g: int, b: int) -> str:
|
|
190
|
+
"""A small ■ swatch rendered in the given RGB, e.g. to preview a Color32 value."""
|
|
191
|
+
if not _enabled:
|
|
192
|
+
return ""
|
|
193
|
+
return f"{_fg_rgb(r, g, b)}■ {_RESET}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def quoted_string(text: str) -> str:
|
|
197
|
+
return string(f'"{text}"')
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def func_call(func_name: str, *args: str, add_semicolon=True) -> str:
|
|
201
|
+
"""Format a function call with syntax highlighting."""
|
|
202
|
+
args_str = ", ".join(args)
|
|
203
|
+
return f"{method(func_name)}{parentheses('(')}{args_str}{parentheses(')')}{';' if add_semicolon else ''}"
|
tfbscript/arena.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The opcode-arena size stored in the .ai header.
|
|
2
|
+
|
|
3
|
+
The 4-byte field after the magic string is not opaque: it is the size, in
|
|
4
|
+
dwords, of the arena the loader allocates for the script's instruction objects.
|
|
5
|
+
From ``TFBScript::read`` @ 004349e0 in Game.exe::
|
|
6
|
+
|
|
7
|
+
FUN_00432920(&count); // the header dword
|
|
8
|
+
DAT_0061f890 = FUN_00550170(count * 4); // allocate the arena
|
|
9
|
+
|
|
10
|
+
``read`` then carves ``1 + instructionCount`` dwords off the front for the
|
|
11
|
+
instruction pointer array (plus its NULL terminator), and every instruction's
|
|
12
|
+
``instantiate`` (vtable +0x30) bumps ``DAT_0061f890`` by that opcode's object
|
|
13
|
+
size. So::
|
|
14
|
+
|
|
15
|
+
header = 1 + instructionCount + sum(objectSize(opcode) / 4)
|
|
16
|
+
|
|
17
|
+
Two accounting details, both recovered from the corpus rather than assumed:
|
|
18
|
+
|
|
19
|
+
* instructions with ``flags.no_handler`` (bit 6) set are **still counted** --
|
|
20
|
+
the exporter reserves space for them even though the loader never constructs
|
|
21
|
+
them;
|
|
22
|
+
* every 0xFF control-block marker costs 4 dwords, including the behavior
|
|
23
|
+
implementations past the first three.
|
|
24
|
+
|
|
25
|
+
The per-opcode sizes below were solved as a linear system over the 577 shipped
|
|
26
|
+
scripts (one equation per file, one unknown per opcode) and reproduce the
|
|
27
|
+
stored header exactly for 575 of them. The five that could be cross-checked
|
|
28
|
+
against Game.exe's ``parse`` return values all agree: set value 0x18, control
|
|
29
|
+
0x10, check message 0x1c, send message 0x1c, cut-scene 0x0c.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import TYPE_CHECKING, Iterator
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from tfbscript.opcodes.base import Opcode
|
|
36
|
+
from tfbscript.script import ScriptFile
|
|
37
|
+
|
|
38
|
+
# Cost of one 0xFF control-block marker (prescript/startup/shutdown/behavior).
|
|
39
|
+
MARKER_DWORDS = 4
|
|
40
|
+
|
|
41
|
+
# opcode table name -> object size in dwords.
|
|
42
|
+
OPCODE_OBJECT_DWORDS: dict[str, int] = {
|
|
43
|
+
"comment:": 0,
|
|
44
|
+
"create variable": 0,
|
|
45
|
+
"if/else": 2,
|
|
46
|
+
"cut-scene": 3,
|
|
47
|
+
"dec value": 3,
|
|
48
|
+
"inc value": 3,
|
|
49
|
+
"play animation": 3,
|
|
50
|
+
"play sound": 3,
|
|
51
|
+
"remove": 3,
|
|
52
|
+
"reset": 3,
|
|
53
|
+
"run as player": 3,
|
|
54
|
+
"set behavior": 3,
|
|
55
|
+
"stop sound": 3,
|
|
56
|
+
"check reference": 4,
|
|
57
|
+
"control": 4,
|
|
58
|
+
"for each": 4,
|
|
59
|
+
"print": 4,
|
|
60
|
+
"set reference": 4,
|
|
61
|
+
"change membership": 5,
|
|
62
|
+
"check membership": 5,
|
|
63
|
+
"loop value": 5,
|
|
64
|
+
"turn to": 5,
|
|
65
|
+
"check value": 6,
|
|
66
|
+
"find subset": 6,
|
|
67
|
+
"find variable": 6,
|
|
68
|
+
"set value": 6,
|
|
69
|
+
"use camera": 6,
|
|
70
|
+
"check message": 7,
|
|
71
|
+
"move from": 7,
|
|
72
|
+
"move to": 7,
|
|
73
|
+
"send message": 7,
|
|
74
|
+
"spawn actor": 8,
|
|
75
|
+
"teleport to": 9,
|
|
76
|
+
"check fov": 12,
|
|
77
|
+
"displace": 12,
|
|
78
|
+
"slide value": 17,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _walk(ops: "list[Opcode]") -> "Iterator[Opcode]":
|
|
83
|
+
for op in ops:
|
|
84
|
+
yield op
|
|
85
|
+
yield from _walk(op.children)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _marker_classes() -> tuple[type, ...]:
|
|
89
|
+
from tfbscript.opcodes.op_behavior_implementation import OpBehaviorImplementation
|
|
90
|
+
from tfbscript.opcodes.op_prescript import OpPrescript
|
|
91
|
+
from tfbscript.opcodes.op_shutdown import OpShutdown
|
|
92
|
+
from tfbscript.opcodes.op_startup import OpStartup
|
|
93
|
+
|
|
94
|
+
return (OpPrescript, OpStartup, OpShutdown, OpBehaviorImplementation)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def arena_dwords(script: "ScriptFile") -> int | None:
|
|
98
|
+
"""The header dword this script needs, or None if it can't be computed.
|
|
99
|
+
|
|
100
|
+
Returns None when the script contains an opcode with no known object size
|
|
101
|
+
(an unrecognised name, or the generic `Opcode` fallback) -- callers should
|
|
102
|
+
fall back to the value the file was read with rather than guess.
|
|
103
|
+
"""
|
|
104
|
+
from tfbscript.opcodes.base import OPCODE_REGISTRY
|
|
105
|
+
|
|
106
|
+
name_of = {cls: name for name, cls in OPCODE_REGISTRY.items()}
|
|
107
|
+
markers = _marker_classes()
|
|
108
|
+
|
|
109
|
+
total = 1 # the pointer array's NULL terminator
|
|
110
|
+
for op in _walk(script.instructions):
|
|
111
|
+
total += 1 # this instruction's slot in the pointer array
|
|
112
|
+
if isinstance(op, markers):
|
|
113
|
+
total += MARKER_DWORDS
|
|
114
|
+
continue
|
|
115
|
+
size = OPCODE_OBJECT_DWORDS.get(name_of.get(type(op), ""))
|
|
116
|
+
if size is None:
|
|
117
|
+
return None
|
|
118
|
+
total += size
|
|
119
|
+
return total
|
tfbscript/binary.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""A small binary reader over an in-memory buffer."""
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from typing import BinaryIO
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BinaryReader:
|
|
8
|
+
def __init__(self, data: bytes, little_endian: bool = True):
|
|
9
|
+
self.data = memoryview(data)
|
|
10
|
+
self.offset = 0
|
|
11
|
+
self.endian = "<" if little_endian else ">"
|
|
12
|
+
|
|
13
|
+
def tell(self) -> int:
|
|
14
|
+
return self.offset
|
|
15
|
+
|
|
16
|
+
def seek(self, offset: int) -> None:
|
|
17
|
+
self.offset = offset
|
|
18
|
+
|
|
19
|
+
def skip(self, count: int) -> None:
|
|
20
|
+
self.offset += count
|
|
21
|
+
|
|
22
|
+
def size_remaining(self) -> int:
|
|
23
|
+
return len(self.data) - self.offset
|
|
24
|
+
|
|
25
|
+
def eof(self) -> bool:
|
|
26
|
+
return self.offset >= len(self.data)
|
|
27
|
+
|
|
28
|
+
def read(self, fmt: str):
|
|
29
|
+
"""Unpack a single value and advance the offset by its size."""
|
|
30
|
+
fmt = self.endian + fmt
|
|
31
|
+
value = struct.unpack_from(fmt, self.data, self.offset)[0]
|
|
32
|
+
self.offset += struct.calcsize(fmt)
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def read_bytes(self, count: int) -> bytes:
|
|
36
|
+
data = self.data[self.offset : self.offset + count].tobytes()
|
|
37
|
+
self.offset += count
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
def read_string(self, length: int, encoding: str = "latin1") -> str:
|
|
41
|
+
return self.read_bytes(length).decode(encoding)
|
|
42
|
+
|
|
43
|
+
def read_u8(self) -> int:
|
|
44
|
+
return self.read("B")
|
|
45
|
+
|
|
46
|
+
def read_u32(self) -> int:
|
|
47
|
+
return self.read("I")
|
|
48
|
+
|
|
49
|
+
def read_i16(self) -> int:
|
|
50
|
+
return self.read("h")
|
|
51
|
+
|
|
52
|
+
def read_i32(self) -> int:
|
|
53
|
+
return self.read("i")
|
|
54
|
+
|
|
55
|
+
def read_f32(self) -> float:
|
|
56
|
+
return self.read("f")
|
|
57
|
+
|
|
58
|
+
def read_rgba(self) -> tuple[int, int, int, int]:
|
|
59
|
+
return (self.read_u8(), self.read_u8(), self.read_u8(), self.read_u8())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def write_u8(f: BinaryIO, v: int):
|
|
63
|
+
f.write(struct.pack("<B", v))
|
|
64
|
+
|
|
65
|
+
def write_u16(f: BinaryIO, v: int):
|
|
66
|
+
f.write(struct.pack("<H", v))
|
|
67
|
+
|
|
68
|
+
def write_i16(f: BinaryIO, v: int):
|
|
69
|
+
f.write(struct.pack("<h", v))
|
|
70
|
+
|
|
71
|
+
def write_u32(f: BinaryIO, v: int):
|
|
72
|
+
f.write(struct.pack("<I", v))
|
|
73
|
+
|
|
74
|
+
def write_s32(f: BinaryIO, v: int):
|
|
75
|
+
f.write(struct.pack("<i", v))
|
|
76
|
+
|
|
77
|
+
def write_f32(f: BinaryIO, v: float):
|
|
78
|
+
f.write(struct.pack("<f", v))
|
|
79
|
+
|
|
80
|
+
def write_rgba(f: BinaryIO, v: tuple[int, int, int, int]):
|
|
81
|
+
f.write(struct.pack("<BBBB", *v))
|
|
82
|
+
|
|
83
|
+
def write_f16(f: BinaryIO, v: float):
|
|
84
|
+
f.write(struct.pack("<e", v))
|
tfbscript/debug.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def str_set() -> set[str]:
|
|
5
|
+
return set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DebugStore:
|
|
10
|
+
"""
|
|
11
|
+
Stores debug information while parsing a scirpt
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
unresolved_ops: set[str] = field(default_factory=str_set)
|
tfbscript/editor/app.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from tfbscript.ansi import set_colors_enabled
|
|
9
|
+
from tfbscript.script import ScriptFile
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from PySide6.QtCore import Qt
|
|
13
|
+
from PySide6.QtGui import QAction, QFont, QFontDatabase, QKeySequence
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QApplication,
|
|
16
|
+
QMainWindow,
|
|
17
|
+
QStyleFactory,
|
|
18
|
+
QTreeWidget,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
QWidget,
|
|
21
|
+
QMessageBox,
|
|
22
|
+
)
|
|
23
|
+
except ModuleNotFoundError:
|
|
24
|
+
print("WARN: PySide6 not installed, editor wont work")
|
|
25
|
+
|
|
26
|
+
from .fonts import register_bold_variant
|
|
27
|
+
from .search import SearchBar, SearchEntry, TreeSearch
|
|
28
|
+
from .style import build_palette, Win98TreeStyle
|
|
29
|
+
from .widgets import populate_tree
|
|
30
|
+
|
|
31
|
+
import yaml
|
|
32
|
+
|
|
33
|
+
os.environ.setdefault("QT_ENABLE_HIGHDPI_SCALING", "0")
|
|
34
|
+
os.environ.setdefault("QT_SCALE_FACTOR", "1")
|
|
35
|
+
|
|
36
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
37
|
+
|
|
38
|
+
set_colors_enabled(False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def editor_from_filepath(filepath: Path | str):
|
|
42
|
+
script = ScriptFile.from_path(filepath)
|
|
43
|
+
open_editor(script)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def open_editor(script: ScriptFile):
|
|
47
|
+
app = cast(QApplication, QApplication.instance()) or QApplication([])
|
|
48
|
+
|
|
49
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
50
|
+
|
|
51
|
+
window = QMainWindow()
|
|
52
|
+
window.setWindowTitle("TFBScript Editor")
|
|
53
|
+
window.resize(1200, 720)
|
|
54
|
+
|
|
55
|
+
app.setStyle(Win98TreeStyle(QStyleFactory.create("Fusion")))
|
|
56
|
+
|
|
57
|
+
QFontDatabase.addApplicationFont(
|
|
58
|
+
str(Path(SCRIPT_DIR, "fonts", "ms-sans-serif", "MS Sans Serif.ttf"))
|
|
59
|
+
)
|
|
60
|
+
register_bold_variant(
|
|
61
|
+
"MS Sans Serif",
|
|
62
|
+
Path(SCRIPT_DIR, "fonts", "ms-sans-serif-bold", "MS Sans Serif Bold.ttf"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not Path("EDITOR.yaml").exists():
|
|
66
|
+
cfg = {"appereance": {"font": "MS Sans Serif"}}
|
|
67
|
+
else:
|
|
68
|
+
with open("EDITOR.yaml", "r") as f:
|
|
69
|
+
cfg = yaml.safe_load(f)
|
|
70
|
+
|
|
71
|
+
font_name = cfg.get("appereance", {}).get("font", "MS Sans Serif")
|
|
72
|
+
|
|
73
|
+
font = QFont(font_name)
|
|
74
|
+
font.setPixelSize(13)
|
|
75
|
+
font.setBold(True)
|
|
76
|
+
app.setFont(font)
|
|
77
|
+
|
|
78
|
+
app.setPalette(build_palette())
|
|
79
|
+
|
|
80
|
+
def save() -> None:
|
|
81
|
+
print("SAVE SAVE SAVE")
|
|
82
|
+
if script._file_path is not None:
|
|
83
|
+
with open(
|
|
84
|
+
script._file_path,
|
|
85
|
+
"wb",
|
|
86
|
+
) as f:
|
|
87
|
+
script.write(f)
|
|
88
|
+
else:
|
|
89
|
+
QMessageBox.critical(None, "Save Failed", "FILE WAS OPENED WITHOUT A _file_path")
|
|
90
|
+
|
|
91
|
+
def expand_all() -> None:
|
|
92
|
+
tree.expandAll()
|
|
93
|
+
|
|
94
|
+
file_menu = window.menuBar().addMenu("File")
|
|
95
|
+
file_menu.addAction("Save").triggered.connect(save)
|
|
96
|
+
|
|
97
|
+
other_menu = window.menuBar().addMenu("Other")
|
|
98
|
+
|
|
99
|
+
expand_all_action = QAction("Expand All", other_menu)
|
|
100
|
+
|
|
101
|
+
expand_all_action.setShortcut(
|
|
102
|
+
QKeySequence(Qt.KeyboardModifier.ControlModifier | Qt.Key.Key_E)
|
|
103
|
+
)
|
|
104
|
+
expand_all_action.triggered.connect(expand_all)
|
|
105
|
+
|
|
106
|
+
other_menu.addAction(expand_all_action)
|
|
107
|
+
|
|
108
|
+
find_action = QAction("Find", other_menu)
|
|
109
|
+
find_action.setShortcut(QKeySequence.StandardKey.Find)
|
|
110
|
+
other_menu.addAction(find_action)
|
|
111
|
+
|
|
112
|
+
tree = QTreeWidget()
|
|
113
|
+
tree.setHeaderHidden(True)
|
|
114
|
+
tree.setSelectionMode(QTreeWidget.SelectionMode.NoSelection)
|
|
115
|
+
tree.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
116
|
+
tree.setRootIsDecorated(True)
|
|
117
|
+
tree.setItemsExpandable(True)
|
|
118
|
+
tree.setIndentation(40)
|
|
119
|
+
tree.itemClicked.connect(
|
|
120
|
+
lambda item, column: item.setExpanded(not item.isExpanded())
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
search_index: list[SearchEntry] = []
|
|
124
|
+
for opcode in script.instructions:
|
|
125
|
+
populate_tree(tree, opcode, search_index)
|
|
126
|
+
|
|
127
|
+
search_bar = SearchBar()
|
|
128
|
+
search = TreeSearch(tree, search_bar, search_index)
|
|
129
|
+
find_action.triggered.connect(search.open)
|
|
130
|
+
|
|
131
|
+
central = QWidget()
|
|
132
|
+
central_layout = QVBoxLayout(central)
|
|
133
|
+
central_layout.setContentsMargins(0, 0, 0, 0)
|
|
134
|
+
central_layout.setSpacing(0)
|
|
135
|
+
central_layout.addWidget(tree, 1)
|
|
136
|
+
central_layout.addWidget(search_bar)
|
|
137
|
+
|
|
138
|
+
window.setCentralWidget(central)
|
|
139
|
+
window.show()
|
|
140
|
+
|
|
141
|
+
app.exec()
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
The font file in this archive was created using Fontstruct the free, online
|
|
2
|
+
font-building tool.
|
|
3
|
+
This font was created by “lou”.
|
|
4
|
+
This font has a homepage where this archive and other versions may be found:
|
|
5
|
+
https://fontstruct.com/fontstructions/show/1384746
|
|
6
|
+
|
|
7
|
+
Try Fontstruct at http://fontstruct.com
|
|
8
|
+
It’s easy and it’s fun.
|
|
9
|
+
|
|
10
|
+
NOTE FOR FLASH USERS: Fontstruct fonts (fontstructions) are optimized for Flash.
|
|
11
|
+
If the font in this archive is a pixel font, it is best displayed at a font-size
|
|
12
|
+
of 11.
|
|
13
|
+
|
|
14
|
+
Fontstruct is sponsored by FontShop.
|
|
15
|
+
Visit them at https://fontshop.com
|
|
16
|
+
FontShop is the original independent font retailer. We’ve been around since
|
|
17
|
+
the dawn of digital type. Whether you need the right font or need to create the
|
|
18
|
+
right font from scratch, let our 26 years of experience work for you.
|
|
19
|
+
|
|
20
|
+
Fontstruct is copyright ©2017 Rob Meek
|
|
21
|
+
|
|
22
|
+
LEGAL NOTICE:
|
|
23
|
+
In using this font you must comply with the licensing terms described in the
|
|
24
|
+
file “license.txt” included with this archive.
|
|
25
|
+
If you redistribute the font file in this archive, it must be accompanied by all
|
|
26
|
+
the other files from this archive, including this one.
|
|
Binary file
|