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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Instruction flags, parser context, opcode registry and the Opcode base class."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Self, TypeVar
|
|
8
|
+
|
|
9
|
+
from tfbscript.ansi import flow_control, keyword
|
|
10
|
+
from tfbscript.binary import write_u32, write_u8
|
|
11
|
+
from tfbscript.debug import DebugStore
|
|
12
|
+
from tfbscript.payload import PayloadReader
|
|
13
|
+
from tfbscript.string_table import StringTable
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from tfbscript.binary import BinaryReader
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class InstructionFlags:
|
|
21
|
+
"""The 32-bit flags word that follow s every opcode index."""
|
|
22
|
+
|
|
23
|
+
flow_control: int = (
|
|
24
|
+
0 # bits 0-2 -- flow-control value returned by tfb_walk_children (FUN_00431130)
|
|
25
|
+
)
|
|
26
|
+
# 0: return - skip to end of script (block)
|
|
27
|
+
# 1: continue - proceed normally
|
|
28
|
+
# 2,3,4...: break - skip the rest of N-1 enclosing levels, then resume
|
|
29
|
+
# normally one level further up.
|
|
30
|
+
reserved_low: int = 0 # bits 3-5
|
|
31
|
+
no_handler: bool = False # bit 6 -- no handler bound (loader skips construction) (aka disabled at runtime)
|
|
32
|
+
runtime_scratch: bool = (
|
|
33
|
+
False # bit 7 -- runtime-only scratch flag (always 0 on disk)
|
|
34
|
+
)
|
|
35
|
+
reserved_high: int = 0
|
|
36
|
+
descendant_span: int = 0 # bits 11-31 -- 21-bit descendant span
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def decode(value: int) -> "InstructionFlags":
|
|
40
|
+
return InstructionFlags(
|
|
41
|
+
flow_control=value & 0x7,
|
|
42
|
+
reserved_low=(value >> 3) & 0x7,
|
|
43
|
+
no_handler=bool(value & 0x40),
|
|
44
|
+
runtime_scratch=bool(value & 0x80),
|
|
45
|
+
reserved_high=(value >> 8) & 0x7,
|
|
46
|
+
descendant_span=(value >> 11) & 0x1FFFFF,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def encode(self) -> int:
|
|
50
|
+
value = self.flow_control & 0x7
|
|
51
|
+
value |= (self.reserved_low & 0x7) << 3
|
|
52
|
+
if self.no_handler:
|
|
53
|
+
value |= 0x40
|
|
54
|
+
if self.runtime_scratch:
|
|
55
|
+
value |= 0x80
|
|
56
|
+
value |= (self.reserved_high & 0x7) << 8
|
|
57
|
+
value |= (self.descendant_span & 0x1FFFFF) << 11
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
def flow_control_str(self) -> str:
|
|
61
|
+
if self.flow_control == 0:
|
|
62
|
+
return "end" # return / stop
|
|
63
|
+
if self.flow_control == 1:
|
|
64
|
+
return "continue"
|
|
65
|
+
return f"break {self.flow_control - 1}"
|
|
66
|
+
|
|
67
|
+
def print(self) -> None:
|
|
68
|
+
print(
|
|
69
|
+
"Flags: "
|
|
70
|
+
+ f"flow_control={self.flow_control_str()}, "
|
|
71
|
+
+ f"reserved_low={hex(self.reserved_low)} ({self.reserved_low}), "
|
|
72
|
+
+ f"no_handler={self.no_handler}, "
|
|
73
|
+
+ f"runtime_scratch={self.runtime_scratch}, "
|
|
74
|
+
+ f"reserved_high={hex(self.reserved_high)} ({self.reserved_high}), "
|
|
75
|
+
+ f"descendant_span={self.descendant_span}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_OpcodeT = TypeVar("_OpcodeT", bound="Opcode")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ParserContext:
|
|
84
|
+
"""Shared state while reading a script's instruction stream."""
|
|
85
|
+
|
|
86
|
+
opcode_table: StringTable
|
|
87
|
+
global_refs: StringTable
|
|
88
|
+
local_refs: StringTable
|
|
89
|
+
|
|
90
|
+
control_block_counter: int = 0
|
|
91
|
+
|
|
92
|
+
# Stack of opcodes whose body is currently being walked (outermost first),
|
|
93
|
+
# maintained by every traversal that descends into children: Opcode.read,
|
|
94
|
+
# Opcode.write, Opcode.print_tree and the editor's populate_tree. Lets a
|
|
95
|
+
# descendant resolve state from its nearest enclosing opcode of a given
|
|
96
|
+
# type, e.g. the "[~subset]" builtin resolving its type from the nearest
|
|
97
|
+
# enclosing "find subset" op. Some opcodes need this while parsing or
|
|
98
|
+
# writing their own payload (see OpControl), not only while rendering
|
|
99
|
+
# source, so the stack has to be kept during those traversals too.
|
|
100
|
+
open_opcodes: list["Opcode"] = field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
def nearest_ancestor(
|
|
103
|
+
self, opcode_types: type[_OpcodeT] | tuple[type[_OpcodeT], ...]
|
|
104
|
+
) -> _OpcodeT | None:
|
|
105
|
+
"""The innermost currently-open opcode that is an instance of `opcode_types`
|
|
106
|
+
(a single opcode class, or a tuple of classes to match any of)."""
|
|
107
|
+
for op in reversed(self.open_opcodes):
|
|
108
|
+
if isinstance(op, opcode_types):
|
|
109
|
+
return op
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Opcode-table name (e.g. "set value") -> Opcode subclass. Populated by the
|
|
114
|
+
# @opcode decorator; importing tfbscript.opcodes registers all implementations.
|
|
115
|
+
OPCODE_REGISTRY: dict[str, type["Opcode"]] = {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def opcode(
|
|
119
|
+
name: str, description: str = ""
|
|
120
|
+
) -> Callable[[type[_OpcodeT]], type[_OpcodeT]]:
|
|
121
|
+
"""Class decorator that registers an Opcode subclass under its table name."""
|
|
122
|
+
|
|
123
|
+
def register(cls: type[_OpcodeT]) -> type[_OpcodeT]:
|
|
124
|
+
OPCODE_REGISTRY[name] = cls
|
|
125
|
+
return cls
|
|
126
|
+
|
|
127
|
+
return register
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class Opcode:
|
|
132
|
+
"""One instruction; also the generic fallback for unimplemented opcodes."""
|
|
133
|
+
|
|
134
|
+
opcode_index: int = field(default=0)
|
|
135
|
+
flags: InstructionFlags = field(default_factory=InstructionFlags)
|
|
136
|
+
raw_payload: bytes = b""
|
|
137
|
+
context: ParserContext | None = field(default=None)
|
|
138
|
+
|
|
139
|
+
# The next {flags.descendant_span} instructions in the stream, re-nested.
|
|
140
|
+
children: list["Opcode"] = field(default_factory=list)
|
|
141
|
+
|
|
142
|
+
def total_span(self) -> int:
|
|
143
|
+
"""Total flattened count of this instruction plus all of its descendants
|
|
144
|
+
(children, grandchildren, ...), matching the on-disk descendant_span encoding.
|
|
145
|
+
"""
|
|
146
|
+
return 1 + sum(child.total_span() for child in self.children)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def read(
|
|
150
|
+
cls,
|
|
151
|
+
reader: "BinaryReader",
|
|
152
|
+
context: ParserContext,
|
|
153
|
+
debug_store: DebugStore | None = None,
|
|
154
|
+
debugOptions: dict[str, bool | int] | None = None,
|
|
155
|
+
) -> "Opcode":
|
|
156
|
+
"""Read one instruction (and its re-nested descendants) from the stream."""
|
|
157
|
+
from tfbscript.opcodes.op_behavior_implementation import (
|
|
158
|
+
OpBehaviorImplementation,
|
|
159
|
+
)
|
|
160
|
+
from tfbscript.opcodes.op_prescript import OpPrescript
|
|
161
|
+
from tfbscript.opcodes.op_shutdown import OpShutdown
|
|
162
|
+
from tfbscript.opcodes.op_startup import OpStartup
|
|
163
|
+
|
|
164
|
+
if debugOptions is None:
|
|
165
|
+
debugOptions = {}
|
|
166
|
+
|
|
167
|
+
opcode_index = reader.read_u8()
|
|
168
|
+
flags = InstructionFlags.decode(reader.read_u32())
|
|
169
|
+
payload_size = reader.read_u8()
|
|
170
|
+
|
|
171
|
+
behavior_entry = None
|
|
172
|
+
|
|
173
|
+
if opcode_index == 0xFF: # Control Blocks
|
|
174
|
+
opcode_name = "control block"
|
|
175
|
+
control_blocks: list[type[Opcode]] = [OpPrescript, OpStartup, OpShutdown]
|
|
176
|
+
if context.control_block_counter < len(control_blocks):
|
|
177
|
+
opcode_class = control_blocks[context.control_block_counter]
|
|
178
|
+
else:
|
|
179
|
+
# abc::behavior, test::behavior, etc...
|
|
180
|
+
behaviors = context.local_refs.all_with_type("behavior")
|
|
181
|
+
behavior_entry = behaviors[context.control_block_counter - 3]
|
|
182
|
+
opcode_class = OpBehaviorImplementation
|
|
183
|
+
|
|
184
|
+
context.control_block_counter += 1
|
|
185
|
+
|
|
186
|
+
else: # Normal opcodes
|
|
187
|
+
entry = context.opcode_table.get(opcode_index)
|
|
188
|
+
if entry is None:
|
|
189
|
+
raise ValueError(f"Invalid opcode index: {opcode_index}")
|
|
190
|
+
|
|
191
|
+
opcode_name = entry.name # for example "print::op-code" -> "print"
|
|
192
|
+
opcode_class = OPCODE_REGISTRY.get(opcode_name)
|
|
193
|
+
if opcode_class is None:
|
|
194
|
+
print(
|
|
195
|
+
f"Unknown opcode name: {opcode_name}, falling back to generic Opcode",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
if debug_store is not None:
|
|
199
|
+
debug_store.unresolved_ops.add(opcode_name)
|
|
200
|
+
opcode_class = Opcode
|
|
201
|
+
|
|
202
|
+
raw_payload = reader.read_bytes(payload_size)
|
|
203
|
+
payload_reader = PayloadReader(
|
|
204
|
+
raw_payload,
|
|
205
|
+
context.global_refs,
|
|
206
|
+
context.local_refs,
|
|
207
|
+
little_endian=True,
|
|
208
|
+
context=context,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
instruction = opcode_class.parse_payload(payload_reader)
|
|
213
|
+
|
|
214
|
+
if payload_reader.size_remaining() > 0 and opcode_class is not Opcode:
|
|
215
|
+
print(
|
|
216
|
+
f"Warning: {opcode_class.__name__} did not consume all payload bytes "
|
|
217
|
+
+ f"(opcode index {opcode_index}, name {opcode_name if opcode_name else 'control block'}): "
|
|
218
|
+
+ f"{payload_reader.size_remaining()} bytes remaining, "
|
|
219
|
+
+ f"{payload_reader.offset} bytes read "
|
|
220
|
+
+ f"-> {payload_reader.offset}/{len(payload_reader.data)} bytes read",
|
|
221
|
+
file=sys.stderr,
|
|
222
|
+
)
|
|
223
|
+
throwOnPayloadRemaining = debugOptions.get(
|
|
224
|
+
"throwOnPayloadRemaining", False
|
|
225
|
+
)
|
|
226
|
+
if throwOnPayloadRemaining:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"Opcode {opcode_class.__name__} did not consume all payload bytes "
|
|
229
|
+
+ f"(opcode index {opcode_index}, name {opcode_name if opcode_name else 'control block'}): "
|
|
230
|
+
+ f"{payload_reader.size_remaining()} bytes remaining, "
|
|
231
|
+
+ f"{payload_reader.offset} bytes read "
|
|
232
|
+
+ f"-> {payload_reader.offset}/{len(payload_reader.data)} bytes read"
|
|
233
|
+
)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(
|
|
236
|
+
f"Error parsing payload for opcode {opcode_class.__name__} "
|
|
237
|
+
+ f"(index {opcode_index}, name {opcode_name if opcode_name else 'control block'}): {e}",
|
|
238
|
+
file=sys.stderr,
|
|
239
|
+
)
|
|
240
|
+
instruction = Opcode()
|
|
241
|
+
raise
|
|
242
|
+
|
|
243
|
+
instruction.opcode_index = opcode_index
|
|
244
|
+
instruction.flags = flags
|
|
245
|
+
instruction.raw_payload = raw_payload
|
|
246
|
+
instruction.context = context
|
|
247
|
+
|
|
248
|
+
if isinstance(instruction, OpBehaviorImplementation):
|
|
249
|
+
instruction.behavior_entry = behavior_entry
|
|
250
|
+
|
|
251
|
+
# Stay "open" while the body is parsed so a descendant resolving a
|
|
252
|
+
# builtin reference (e.g. "[~controlled]") can find this op.
|
|
253
|
+
context.open_opcodes.append(instruction)
|
|
254
|
+
try:
|
|
255
|
+
descendants_read = 0
|
|
256
|
+
while descendants_read < flags.descendant_span:
|
|
257
|
+
child = Opcode.read(reader, context, debug_store=debug_store)
|
|
258
|
+
instruction.children.append(child)
|
|
259
|
+
descendants_read += child.total_span()
|
|
260
|
+
finally:
|
|
261
|
+
context.open_opcodes.pop()
|
|
262
|
+
|
|
263
|
+
return instruction
|
|
264
|
+
|
|
265
|
+
def write(self, f: BinaryIO) -> None:
|
|
266
|
+
"""Write the ScriptFile to a binary file."""
|
|
267
|
+
write_u8(f, self.opcode_index)
|
|
268
|
+
|
|
269
|
+
self.flags.descendant_span = sum(child.total_span() for child in self.children)
|
|
270
|
+
write_u32(f, self.flags.encode())
|
|
271
|
+
|
|
272
|
+
payload_buf = io.BytesIO()
|
|
273
|
+
self.write_payload(payload_buf)
|
|
274
|
+
|
|
275
|
+
write_u8(f, len(payload_buf.getvalue()))
|
|
276
|
+
f.write(payload_buf.getvalue())
|
|
277
|
+
|
|
278
|
+
if self.context is not None:
|
|
279
|
+
self.context.open_opcodes.append(self)
|
|
280
|
+
try:
|
|
281
|
+
for child in self.children:
|
|
282
|
+
child.write(f)
|
|
283
|
+
finally:
|
|
284
|
+
if self.context is not None:
|
|
285
|
+
self.context.open_opcodes.pop()
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def parse_payload(cls, reader: PayloadReader) -> Self:
|
|
289
|
+
"""Parse this opcode's payload. Override in subclasses; the base
|
|
290
|
+
implementation is the generic fallback and parses nothing."""
|
|
291
|
+
reader.read_bytes(reader.size_remaining()) # consume all remaining bytes
|
|
292
|
+
if cls is not Opcode:
|
|
293
|
+
print(
|
|
294
|
+
f"Warning: parse_payload not implemented for {cls.__name__}. "
|
|
295
|
+
+ "Returning empty instance.",
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
return cls()
|
|
299
|
+
|
|
300
|
+
def write_payload(self, f: BinaryIO) -> None:
|
|
301
|
+
f.write(self.raw_payload) # default implementation just writes the raw payload
|
|
302
|
+
if self.__class__ is not Opcode:
|
|
303
|
+
print(
|
|
304
|
+
f"Warning: write_payload not implemented for {self.__class__.__name__}. Changes might have been discarded.",
|
|
305
|
+
file=sys.stderr,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def source_line(self, inline: bool = False) -> str:
|
|
309
|
+
"""One line of decompiled pseudo-source for this instruction.
|
|
310
|
+
|
|
311
|
+
Override in subclasses; the base implementation dumps the raw payload.
|
|
312
|
+
`inline` renders the instruction as an embeddable expression (used by
|
|
313
|
+
if/else conditions) instead of a statement.
|
|
314
|
+
"""
|
|
315
|
+
payload = self.raw_payload.hex() if self.raw_payload else "-"
|
|
316
|
+
name = ""
|
|
317
|
+
if self.context is not None:
|
|
318
|
+
name = f"{self.context.opcode_table.entries[self.opcode_index].string}, "
|
|
319
|
+
return f"{name}{type(self).__name__}, payload: {payload}"
|
|
320
|
+
|
|
321
|
+
def print_tree(self, indent: int = 0) -> None:
|
|
322
|
+
"""Print this instruction and its children as indented pseudo-source."""
|
|
323
|
+
has_children = len(self.children) > 0
|
|
324
|
+
children_indent = indent + 1
|
|
325
|
+
|
|
326
|
+
print(f"{' ' * indent}{self.source_line()} {'{' if has_children else ''}")
|
|
327
|
+
|
|
328
|
+
if has_children:
|
|
329
|
+
if self.context is not None:
|
|
330
|
+
self.context.open_opcodes.append(self)
|
|
331
|
+
try:
|
|
332
|
+
for child in self.children:
|
|
333
|
+
child.print_tree(children_indent)
|
|
334
|
+
finally:
|
|
335
|
+
if self.context is not None:
|
|
336
|
+
self.context.open_opcodes.pop()
|
|
337
|
+
|
|
338
|
+
if self.flags.flow_control != 1: # is not continue
|
|
339
|
+
print(
|
|
340
|
+
f"{' ' * children_indent}{keyword('flow ')}{flow_control(self.flags.flow_control_str())}"
|
|
341
|
+
)
|
|
342
|
+
print(f"{' ' * indent}{'}'}")
|
|
343
|
+
|
|
344
|
+
def editor_repr(self) -> dict[str, Any]:
|
|
345
|
+
"""Return a JSON-serializable representation of this opcode for the editor.
|
|
346
|
+
|
|
347
|
+
Override in subclasses to provide a custom representation.
|
|
348
|
+
Base Implementation returns one filed with class name as op-name
|
|
349
|
+
"""
|
|
350
|
+
return {
|
|
351
|
+
"fields": [
|
|
352
|
+
{
|
|
353
|
+
"name": "op-name",
|
|
354
|
+
"type": "op-label",
|
|
355
|
+
"value": self.__class__.__name__,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"type": "label",
|
|
359
|
+
"content": self.source_line(),
|
|
360
|
+
},
|
|
361
|
+
]
|
|
362
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared base for the four 0xFF control blocks that partition a script.
|
|
2
|
+
|
|
3
|
+
They carry no payload; which block an 0xFF opcode is depends only on how many
|
|
4
|
+
0xFF opcodes came before it (see ParserContext.control_block_counter).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import override
|
|
9
|
+
|
|
10
|
+
from tfbscript.opcodes.base import Opcode
|
|
11
|
+
from tfbscript.payload import PayloadReader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class BlockOpcode(Opcode):
|
|
16
|
+
"""A named top-level block: prints as `[ NAME ]` ... `[ NAME: END ]`."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
@override
|
|
20
|
+
def parse_payload(cls, reader: PayloadReader) -> "BlockOpcode":
|
|
21
|
+
return cls()
|
|
22
|
+
|
|
23
|
+
def block_name(self) -> str:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
def source_line(self, inline: bool = False) -> str:
|
|
28
|
+
return f"[ {self.block_name()} ]"
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
def print_tree(self, indent: int = 0) -> None:
|
|
32
|
+
print()
|
|
33
|
+
super().print_tree(indent)
|
|
34
|
+
print(f"{' ' * indent}[ {self.block_name()}: END ]\n")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Enums shared by opcode payloads."""
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# fmt: off
|
|
8
|
+
class RelOp(IntEnum):
|
|
9
|
+
"""Relational operator for OpCheckValue / OpFindSubset."""
|
|
10
|
+
|
|
11
|
+
LessOrEq = 0 # <=
|
|
12
|
+
Eq = 1 # ==
|
|
13
|
+
GreatOrEq = 2 # >=
|
|
14
|
+
Less = 3 # <
|
|
15
|
+
Great = 4 # >
|
|
16
|
+
NotEq = 5 # !=
|
|
17
|
+
|
|
18
|
+
def symbol(self) -> str:
|
|
19
|
+
return ("<=", "==", ">=", "<", ">", "!=")[self.value]
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return str(self.symbol())
|
|
24
|
+
|
|
25
|
+
class ControlRequirement(IntEnum):
|
|
26
|
+
"""u8 in OpControl payload that specifies how the control block is executed."""
|
|
27
|
+
|
|
28
|
+
Suspend = 0
|
|
29
|
+
|
|
30
|
+
Allow = 1
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return ("suspend script", "allow script")[self.value]
|
|
35
|
+
|
|
36
|
+
class CutsceneCommand(IntEnum):
|
|
37
|
+
BeginAlt = 0 # start (allow skipping) - in retail same effect as start because skipping is implmenented via tfbscript
|
|
38
|
+
|
|
39
|
+
Begin = 1 # start
|
|
40
|
+
|
|
41
|
+
End = 2 # end
|
|
42
|
+
|
|
43
|
+
Pause = 3 # pause
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return {0: "start (allow skipping)", 1: "start", 2: "end", 3: "pause"}[self.value]
|
|
48
|
+
|
|
49
|
+
class SetDirection(IntEnum):
|
|
50
|
+
"""Direction for how to traverse a set"""
|
|
51
|
+
|
|
52
|
+
forward = 0
|
|
53
|
+
backward = 1
|
|
54
|
+
randomly = 2
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return ("forward", "backward", "randomly")[self.value]
|
|
59
|
+
|
|
60
|
+
class CombineMode(IntEnum):
|
|
61
|
+
"""Displacement combine mode for OpDisplace."""
|
|
62
|
+
|
|
63
|
+
relative = 0
|
|
64
|
+
absolute = 1
|
|
65
|
+
local = 2
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return ("relative", "absolute", "local")[self.value]
|
|
70
|
+
|
|
71
|
+
class MembershipTest(IntEnum):
|
|
72
|
+
"""Membership test for OpCheckMembership."""
|
|
73
|
+
|
|
74
|
+
includes = 0 # set_a includes element_a
|
|
75
|
+
excludes = 1 # set_a excludes element_a
|
|
76
|
+
intersects_with = 2 # set_a intersects with_b (set_a has at least one element in common with set_b)
|
|
77
|
+
includes_all = 3 # set_a all in set_b
|
|
78
|
+
|
|
79
|
+
def symbol(self) -> str:
|
|
80
|
+
# assumes format is "set_a <membership_test> element_a" or "set_a <membership_test> set_b"
|
|
81
|
+
return ("in", "not in", "intersects with", "all in")[self.value]
|
|
82
|
+
|
|
83
|
+
class MembershipCombiner(IntEnum):
|
|
84
|
+
"""Membership combiner for OpChangeMembership."""
|
|
85
|
+
|
|
86
|
+
include = 0 # add a single thing?
|
|
87
|
+
exclude = 1 # remove a single thing?
|
|
88
|
+
intersect_with = 2
|
|
89
|
+
be_replaced_by = 3
|
|
90
|
+
add = 4 # concat sets together?
|
|
91
|
+
exclude_all = 5 # remove all elements that are also in set b?
|
|
92
|
+
|
|
93
|
+
def symbol(self) -> str:
|
|
94
|
+
# assumes format is "set_a.<membership_combiner>(element_a)" or "set_a.<membership_combiner>(set_b)"
|
|
95
|
+
return ("include", "remove", "intersectWith", "replaceWith", "add", "removeAll")[self.value]
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def __str__(self) -> str:
|
|
99
|
+
return ("include", "remove", "intersectWith", "replaceWith", "add", "removeAll")[self.value]
|
|
100
|
+
|
|
101
|
+
class AnimationMapping(IntEnum):
|
|
102
|
+
"""Animation mapping for ops that play an animation."""
|
|
103
|
+
|
|
104
|
+
NO_ANIMATION = 0x00
|
|
105
|
+
ambient = 0x01
|
|
106
|
+
slowest_move = 0x02
|
|
107
|
+
slow_move = 0x03
|
|
108
|
+
fast_move = 0x04
|
|
109
|
+
jump = 0x05
|
|
110
|
+
fall = 0x06
|
|
111
|
+
youch = 0x07
|
|
112
|
+
slide = 0x08
|
|
113
|
+
attack_1 = 0x09
|
|
114
|
+
attack_2 = 0x0A
|
|
115
|
+
tumble = 0x0B
|
|
116
|
+
extra_1 = 0x0C
|
|
117
|
+
extra_2 = 0x0D
|
|
118
|
+
extra_3 = 0x0E
|
|
119
|
+
extra_4 = 0x0F
|
|
120
|
+
extra_5 = 0x10
|
|
121
|
+
extra_6 = 0x11
|
|
122
|
+
extra_7 = 0x12
|
|
123
|
+
extra_8 = 0x13
|
|
124
|
+
extra_9 = 0x14
|
|
125
|
+
extra_10 = 0x15
|
|
126
|
+
extra_11 = 0x16
|
|
127
|
+
extra_12 = 0x17
|
|
128
|
+
extra_13 = 0x18
|
|
129
|
+
extra_14 = 0x19
|
|
130
|
+
extra_15 = 0x1A
|
|
131
|
+
extra_16 = 0x1B
|
|
132
|
+
extra_17 = 0x1C
|
|
133
|
+
extra_18 = 0x1D
|
|
134
|
+
extra_19 = 0x1E
|
|
135
|
+
extra_20 = 0x1F
|
|
136
|
+
extra_21 = 0x20
|
|
137
|
+
extra_22 = 0x21
|
|
138
|
+
extra_23 = 0x22
|
|
139
|
+
extra_24 = 0x23
|
|
140
|
+
extra_25 = 0x24
|
|
141
|
+
extra_26 = 0x25
|
|
142
|
+
extra_27 = 0x26
|
|
143
|
+
extra_28 = 0x27
|
|
144
|
+
extra_29 = 0x28
|
|
145
|
+
extra_30 = 0x29
|
|
146
|
+
extra_31 = 0x2A
|
|
147
|
+
extra_32 = 0x2B
|
|
148
|
+
left_turn = 0x2C
|
|
149
|
+
right_turn = 0x2D
|
|
150
|
+
jump_land_slow = 0x2E
|
|
151
|
+
jump_land_running = 0x2F
|
|
152
|
+
jump_land_hard = 0x30
|
|
153
|
+
stop_from_full_speed = 0x31
|
|
154
|
+
stop_from_slow_speed = 0x32
|
|
155
|
+
running_180_change = 0x33
|
|
156
|
+
player_9 = 0x34
|
|
157
|
+
player_10 = 0x35
|
|
158
|
+
player_11 = 0x36
|
|
159
|
+
player_12 = 0x37
|
|
160
|
+
player_13 = 0x38
|
|
161
|
+
player_14 = 0x39
|
|
162
|
+
lean_left_slow = 0x3A
|
|
163
|
+
lean_left_medium = 0x3B
|
|
164
|
+
lean_left_fast = 0x3C
|
|
165
|
+
lean_right_slow = 0x3D
|
|
166
|
+
lean_right_medium = 0x3E
|
|
167
|
+
lean_right_fast = 0x3F
|
|
168
|
+
|
|
169
|
+
idle_cycle_1_A = 0x40
|
|
170
|
+
idle_cycle_1_B = 0x41
|
|
171
|
+
idle_cycle_1_C = 0x42
|
|
172
|
+
idle_trans_1_to_2 = 0x43
|
|
173
|
+
idle_trans_1_to_3 = 0x44
|
|
174
|
+
idle_cycle_2_A = 0x45
|
|
175
|
+
idle_cycle_2_B = 0x46
|
|
176
|
+
idle_cycle_2_C = 0x47
|
|
177
|
+
idle_trans_2_to_3 = 0x48
|
|
178
|
+
idle_trans_2_to_1 = 0x49
|
|
179
|
+
idle_cycle_3_A = 0x4A
|
|
180
|
+
idle_cycle_3_B = 0x4B
|
|
181
|
+
idle_cycle_3_C = 0x4C
|
|
182
|
+
idle_trans_3_to_1 = 0x4D
|
|
183
|
+
idle_trans_3_to_2 = 0x4E
|
|
184
|
+
|
|
185
|
+
@override
|
|
186
|
+
def __str__(self) -> str:
|
|
187
|
+
return self.name.replace("_", " ")
|
|
188
|
+
|
|
189
|
+
class CheckFOVMode(IntEnum):
|
|
190
|
+
"""Mode for OpCheckFOV."""
|
|
191
|
+
|
|
192
|
+
ignore_obstructions = 0
|
|
193
|
+
|
|
194
|
+
consider_obstructions = 1
|
|
195
|
+
|
|
196
|
+
@override
|
|
197
|
+
def __str__(self) -> str:
|
|
198
|
+
return ("ignore obstructions", "consider obstructions")[self.value]
|
|
199
|
+
|
|
200
|
+
class CamTransitionInMode(IntEnum):
|
|
201
|
+
pan = 0
|
|
202
|
+
fade_in = 1
|
|
203
|
+
iris_in = 2
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def __str__(self) -> str:
|
|
207
|
+
return ("pan", "fade in", "iris in")[self.value]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class CamTransitionOutMode(IntEnum):
|
|
211
|
+
no_transition = 0
|
|
212
|
+
fade_out = 1
|
|
213
|
+
iris_out = 2
|
|
214
|
+
|
|
215
|
+
@override
|
|
216
|
+
def __str__(self) -> str:
|
|
217
|
+
return ("no transition", "fade out", "iris out")[self.value]
|
|
218
|
+
|
|
219
|
+
# fmt: on
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, BinaryIO, override
|
|
3
|
+
|
|
4
|
+
from tfbscript.opcodes.block import BlockOpcode
|
|
5
|
+
from tfbscript.string_table import StringTableEntry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class OpBehaviorImplementation(BlockOpcode):
|
|
10
|
+
# The behavior's entry in the local ref table, resolved by Opcode.read;
|
|
11
|
+
# used for printing.
|
|
12
|
+
behavior_entry: StringTableEntry | None = None
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
def block_name(self) -> str:
|
|
16
|
+
name = self.behavior_entry.name if self.behavior_entry else "ERR Failed to resolve"
|
|
17
|
+
return f"Behavior {name}"
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
def source_line(self, inline: bool = False) -> str:
|
|
21
|
+
name = self.behavior_entry.name if self.behavior_entry else "ERR Failed to resolve"
|
|
22
|
+
return f"[ Behavior: {name} ]"
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def editor_repr(self) -> dict[str, Any]:
|
|
26
|
+
name = self.behavior_entry.name if self.behavior_entry else "ERR Failed to resolve"
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
"fields": [
|
|
30
|
+
{
|
|
31
|
+
"type": "behavior-label",
|
|
32
|
+
"value": name,
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def write_payload(self, f: BinaryIO) -> None:
|
|
39
|
+
pass # Behavior implementation has no payload
|