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.
Files changed (72) hide show
  1. tfbscript/__init__.py +21 -0
  2. tfbscript/__main__.py +52 -0
  3. tfbscript/ansi.py +203 -0
  4. tfbscript/arena.py +119 -0
  5. tfbscript/binary.py +84 -0
  6. tfbscript/debug.py +14 -0
  7. tfbscript/editor/__init__.py +3 -0
  8. tfbscript/editor/app.py +141 -0
  9. tfbscript/editor/fonts/ms-sans-serif/MS Sans Serif.ttf +0 -0
  10. tfbscript/editor/fonts/ms-sans-serif/license.txt +4 -0
  11. tfbscript/editor/fonts/ms-sans-serif/readme.txt +26 -0
  12. tfbscript/editor/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
  13. tfbscript/editor/fonts/ms-sans-serif-bold/license.txt +4 -0
  14. tfbscript/editor/fonts/ms-sans-serif-bold/readme.txt +26 -0
  15. tfbscript/editor/fonts.py +98 -0
  16. tfbscript/editor/search.py +200 -0
  17. tfbscript/editor/style.py +141 -0
  18. tfbscript/editor/widgets.py +321 -0
  19. tfbscript/opcodes/__init__.py +104 -0
  20. tfbscript/opcodes/base.py +362 -0
  21. tfbscript/opcodes/block.py +34 -0
  22. tfbscript/opcodes/enums.py +219 -0
  23. tfbscript/opcodes/op_behavior_implementation.py +39 -0
  24. tfbscript/opcodes/op_change_membership.py +62 -0
  25. tfbscript/opcodes/op_check_fov.py +73 -0
  26. tfbscript/opcodes/op_check_membership.py +41 -0
  27. tfbscript/opcodes/op_check_message.py +79 -0
  28. tfbscript/opcodes/op_check_reference.py +61 -0
  29. tfbscript/opcodes/op_check_value.py +66 -0
  30. tfbscript/opcodes/op_comment.py +43 -0
  31. tfbscript/opcodes/op_control.py +80 -0
  32. tfbscript/opcodes/op_create_variable.py +42 -0
  33. tfbscript/opcodes/op_cut_scene.py +44 -0
  34. tfbscript/opcodes/op_dec_value.py +43 -0
  35. tfbscript/opcodes/op_displace.py +111 -0
  36. tfbscript/opcodes/op_find_subset.py +49 -0
  37. tfbscript/opcodes/op_find_variable.py +28 -0
  38. tfbscript/opcodes/op_for_each.py +65 -0
  39. tfbscript/opcodes/op_if_else.py +105 -0
  40. tfbscript/opcodes/op_inc_value.py +43 -0
  41. tfbscript/opcodes/op_loop_value.py +28 -0
  42. tfbscript/opcodes/op_move_from.py +47 -0
  43. tfbscript/opcodes/op_move_to.py +99 -0
  44. tfbscript/opcodes/op_play_animation.py +46 -0
  45. tfbscript/opcodes/op_play_sound.py +42 -0
  46. tfbscript/opcodes/op_prescript.py +26 -0
  47. tfbscript/opcodes/op_print.py +53 -0
  48. tfbscript/opcodes/op_remove.py +43 -0
  49. tfbscript/opcodes/op_reset.py +42 -0
  50. tfbscript/opcodes/op_run_as_player.py +26 -0
  51. tfbscript/opcodes/op_send_message.py +53 -0
  52. tfbscript/opcodes/op_set_behavior.py +41 -0
  53. tfbscript/opcodes/op_set_reference.py +52 -0
  54. tfbscript/opcodes/op_set_value.py +52 -0
  55. tfbscript/opcodes/op_shutdown.py +26 -0
  56. tfbscript/opcodes/op_slide_value.py +81 -0
  57. tfbscript/opcodes/op_spawn_actor.py +96 -0
  58. tfbscript/opcodes/op_startup.py +26 -0
  59. tfbscript/opcodes/op_stop_sound.py +26 -0
  60. tfbscript/opcodes/op_teleport_to.py +47 -0
  61. tfbscript/opcodes/op_turn_to.py +33 -0
  62. tfbscript/opcodes/op_use_camera.py +68 -0
  63. tfbscript/payload.py +40 -0
  64. tfbscript/reference.py +926 -0
  65. tfbscript/rhs.py +326 -0
  66. tfbscript/script.py +160 -0
  67. tfbscript/string_table.py +101 -0
  68. tfbscript-0.1.0.dist-info/METADATA +116 -0
  69. tfbscript-0.1.0.dist-info/RECORD +72 -0
  70. tfbscript-0.1.0.dist-info/WHEEL +4 -0
  71. tfbscript-0.1.0.dist-info/entry_points.txt +2 -0
  72. 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)
@@ -0,0 +1,3 @@
1
+ from tfbscript.editor.app import open_editor, editor_from_filepath
2
+
3
+ __all__ = ["open_editor", "editor_from_filepath"]
@@ -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()
@@ -0,0 +1,4 @@
1
+ The FontStruction “MS Sans Serif”
2
+ (https://fontstruct.com/fontstructions/show/1384746) by “lou” is licensed
3
+ under a Creative Commons Attribution Share Alike license
4
+ (http://creativecommons.org/licenses/by-sa/3.0/).
@@ -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.
@@ -0,0 +1,4 @@
1
+ The FontStruction “MS Sans Serif Bold”
2
+ (https://fontstruct.com/fontstructions/show/1384862) by “lou” is licensed
3
+ under a Creative Commons Attribution Share Alike license
4
+ (http://creativecommons.org/licenses/by-sa/3.0/).