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
@@ -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