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/reference.py ADDED
@@ -0,0 +1,926 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING, BinaryIO, TypeVar, override
5
+
6
+ from tfbscript.ansi import builtin, keyword, variable, variable_global
7
+ from tfbscript.binary import write_u32
8
+ from tfbscript.string_table import StringTable, StringTableEntry
9
+
10
+ if TYPE_CHECKING:
11
+ from tfbscript.binary import BinaryReader
12
+ from tfbscript.opcodes.base import Opcode, ParserContext
13
+
14
+ _OpcodeT = TypeVar("_OpcodeT", bound="Opcode")
15
+
16
+ LOCAL_BASE = 0x3FC0D
17
+ BUILTIN_BASE = 0x3FFFA
18
+ NULL_REF = 0xFFFFFFFF
19
+
20
+ # References whose type resolution is in progress, to break cyclic
21
+ # builtin producer chains (see get_resolved_type).
22
+ _RESOLVING: set[int] = set()
23
+
24
+ # Fields `raw` is composed from: assigning any of them re-runs update_raw.
25
+ _RAW_INPUTS = frozenset(
26
+ {"index", "member", "scope", "sub", "kind", "builtin_kind", "slot"}
27
+ )
28
+
29
+
30
+ # fmt: off
31
+ class ReferenceType(Enum):
32
+ LOCAL = 0
33
+ GLOBAL = 1
34
+ BUILTIN = 2
35
+ NULL = 3
36
+
37
+ class BuiltinType(Enum):
38
+ SELF = 0x3FFFF
39
+ CONTROLLED = 0x3FFFE
40
+ EACH = 0x3FFFD # current in for each
41
+ SUBSET = 0x3FFFC # filtered list of any kind
42
+ MESSAGE_VALUE = 0x3FFFB
43
+ FOUND_VARIABLE = 0x3FFFA
44
+
45
+ BUILTIN_LABELS = { # type resolving implemented
46
+ BuiltinType.SELF : "my", # yes
47
+ BuiltinType.CONTROLLED : "[~controlled]", # yes
48
+ BuiltinType.EACH : "[~current in for each]", # no (see comment)
49
+ BuiltinType.SUBSET : "[~subset]", # yes
50
+ BuiltinType.MESSAGE_VALUE : "[~message value]", # yes
51
+ BuiltinType.FOUND_VARIABLE: "[~found variable]", # yes
52
+ }
53
+ # fmt: on
54
+
55
+ BINDINGS = {
56
+ "actor": {
57
+ 0x01: {"name": "health", "type": "value"},
58
+ 0x02: {"name": "waypoints", "type": "set", "of": "waypoint"},
59
+ 0x03: {"name": "origin", "type": "value"},
60
+ 0x04: {"name": "destination (point)", "type": "value"},
61
+ 0x05: {"name": "heading (OBSOLETE)", "type": "value"},
62
+ 0x06: {"name": "facing (OBSOLETE)", "type": "value"},
63
+ 0x07: {"name": "current speed", "type": "value"},
64
+ 0x08: {"name": "altitude", "type": "value"},
65
+ 0x09: {"name": "attach points", "type": "set", "of": "attachment"},
66
+ 0x0A: {"name": "activation range fade percent", "type": "value"},
67
+ 0x0B: {"name": "activation range", "type": "value"},
68
+ 0x0C: {"name": "deactivation range", "type": "value"},
69
+ 0x0D: {"name": "clones", "type": "set", "of": "actor"},
70
+ 0x0E: {"name": "ground top speed", "type": "value"},
71
+ 0x0F: {"name": "ground turn rate", "type": "value"},
72
+ 0x10: {"name": "ground acceleration", "type": "value"},
73
+ 0x11: {"name": "ground deceleration", "type": "value"},
74
+ 0x12: {"name": "move type", "type": "value"},
75
+ 0x13: {"name": "air top speed", "type": "value"},
76
+ 0x14: {"name": "air turn rate", "type": "value"},
77
+ 0x15: {"name": "air acceleration", "type": "value"},
78
+ 0x16: {"name": "air deceleration", "type": "value"},
79
+ 0x17: {"name": "jump delay", "type": "value"},
80
+ 0x18: {"name": "initial jump velocity", "type": "value"},
81
+ 0x19: {"name": "jump coast frames", "type": "value"},
82
+ 0x1A: {"name": "up top speed", "type": "value"},
83
+ 0x1B: {"name": "up acceleration", "type": "value"},
84
+ 0x1C: {"name": "down top speed", "type": "value"},
85
+ 0x1D: {"name": "down acceleration", "type": "value"},
86
+ 0x1E: {"name": "animation rate multiplier", "type": "value"},
87
+ 0x1F: {"name": "weight", "type": "value"},
88
+ 0x20: {"name": "bounce restitution", "type": "value"},
89
+ 0x21: {"name": "X offset", "type": "value"}, # Extend box X
90
+ 0x22: {"name": "Y offset", "type": "value"}, # Extend box Y
91
+ 0x23: {"name": "Z offset", "type": "value"}, # Extend box Z
92
+ 0x24: {"name": "W extend", "type": "value"}, # Extend box W extend
93
+ 0x25: {"name": "L extend", "type": "value"}, # Extend box L extend
94
+ 0x26: {"name": "H extend", "type": "value"}, # Extend box H extend
95
+ 0x27: {"name": "cone angle", "type": "value"},
96
+ 0x28: {"name": "cone length", "type": "value"},
97
+ 0x29: {"name": "cone sweep offset", "type": "value"},
98
+ 0x2A: {"name": "blip color", "type": "value", "of": "color"},
99
+ 0x2B: {"name": "cone color", "type": "value", "of": "color"},
100
+ 0x2C: {"name": "solid collision", "type": "value"},
101
+ 0x2D: {"name": "wall collision", "type": "value"},
102
+ 0x2E: {"name": "visible on radar", "type": "value"},
103
+ 0x2F: {"name": "wall climber", "type": "value"},
104
+ 0x30: {"name": "cut-scene ignore", "type": "value"},
105
+ 0x31: {"name": "ignore ground color", "type": "value"},
106
+ 0x32: {"name": "tilts with ground", "type": "value"},
107
+ 0x33: {"name": "no shadow", "type": "value"},
108
+ 0x34: {"name": "ID", "type": "value"},
109
+ 0x35: {"name": "mesh collider", "type": "value"},
110
+ 0x36: {"name": "triangles collide", "type": "value"},
111
+ 0x37: {"name": "tint color", "type": "value", "of": "color"},
112
+ 0x38: {"name": "turrets", "type": "set", "of": "turret"},
113
+ },
114
+ "camera": {
115
+ 0x01: {"name": "look from", "type": "value"},
116
+ 0x02: {"name": "look from horizontal offset", "type": "value"},
117
+ 0x03: {"name": "look from vertical offset", "type": "value"},
118
+ 0x04: {"name": "look from heading offset", "type": "value"},
119
+ 0x05: {"name": "look from heading relative", "type": "value"},
120
+ 0x06: {"name": "look at", "type": "value"},
121
+ 0x07: {"name": "look at horizontal offset", "type": "value"},
122
+ 0x08: {"name": "look at vertical offset", "type": "value"},
123
+ 0x09: {"name": "look at heading offset", "type": "value"},
124
+ 0x0A: {"name": "look at heading relative", "type": "value"},
125
+ 0x0B: {"name": "field of view", "type": "value"},
126
+ 0x0C: {"name": "laziness", "type": "value"},
127
+ 0x0D: {"name": "percent complete", "type": "value"},
128
+ 0x0E: {"name": "time remaining", "type": "value"},
129
+ 0x0F: {"name": "Vertical Follow At", "type": "value"},
130
+ 0x10: {"name": "Horizontal Follow", "type": "value"},
131
+ 0x11: {"name": "Current Facing", "type": "value"},
132
+ 0x12: {"name": "Vertical Follow From", "type": "value"},
133
+ 0x13: {"name": "pitch control", "type": "value"},
134
+ 0x14: {"name": "roll", "type": "value"},
135
+ },
136
+ "sprite": { # TODO: confirm labels
137
+ 0x01: {"name": "content", "type": "value"},
138
+ 0x02: {"name": "location", "type": "value", "of": "2D"},
139
+ 0x03: {"name": "scale", "type": "value", "of": "2D"},
140
+ 0x04: {"name": "rotation", "type": "value"},
141
+ 0x05: {"name": "tint color", "type": "value", "of": "color"},
142
+ 0x06: {"name": "justification", "type": "value"},
143
+ 0x07: {"name": "visible", "type": "value"},
144
+ 0x08: {"name": "clones", "type": "set", "of": "sprite"},
145
+ 0x09: {"name": "priority", "type": "value"}, # z-order!?
146
+ 0x0A: {"name": "pixel extends", "type": "value", "of": "2D"},
147
+ },
148
+ "sound": {
149
+ 0x01: {"name": "volume", "type": "value"},
150
+ 0x02: {"name": "pitch", "type": "value"},
151
+ 0x03: {"name": "elapsed time", "type": "value"},
152
+ 0x04: {"name": "remaining time", "type": "value"},
153
+ },
154
+ "2D": {
155
+ 0x01: {"name": "x", "type": "value"},
156
+ 0x02: {"name": "y", "type": "value"},
157
+ },
158
+ "color": {
159
+ 0x01: {"name": "r", "type": "value"},
160
+ 0x02: {"name": "g", "type": "value"},
161
+ 0x03: {"name": "b", "type": "value"},
162
+ 0x04: {"name": "a", "type": "value"},
163
+ },
164
+ "controller": {
165
+ 0x01: {"name": "circle button", "type": "value"},
166
+ 0x02: {"name": "triangle button", "type": "value"},
167
+ 0x03: {"name": "X button", "type": "value"},
168
+ 0x04: {"name": "square button", "type": "value"},
169
+ 0x05: {"name": "select button", "type": "value"},
170
+ 0x06: {"name": "start button", "type": "value"},
171
+ 0x07: {"name": "L1 button", "type": "value"},
172
+ 0x08: {"name": "R1 button", "type": "value"},
173
+ 0x09: {"name": "L2 button", "type": "value"},
174
+ 0x0A: {"name": "R2 button", "type": "value"},
175
+ 0x0B: {"name": "DPad left", "type": "value"},
176
+ 0x0C: {"name": "DPad right", "type": "value"},
177
+ 0x0D: {"name": "DPad up", "type": "value"},
178
+ 0x0E: {"name": "DPad down", "type": "value"},
179
+ 0x0F: {"name": "analog1 left", "type": "value"},
180
+ 0x10: {"name": "analog1 right", "type": "value"},
181
+ 0x11: {"name": "analog1 up", "type": "value"},
182
+ 0x12: {"name": "analog1 down", "type": "value"},
183
+ 0x13: {"name": "analog2 left", "type": "value"},
184
+ 0x14: {"name": "analog2 right", "type": "value"},
185
+ 0x15: {"name": "analog2 up", "type": "value"},
186
+ 0x16: {"name": "analog2 down", "type": "value"},
187
+ 0x17: {"name": "analog1 heading", "type": "value"},
188
+ 0x18: {"name": "analog1 magnitude", "type": "value"},
189
+ 0x19: {"name": "analog2 heading", "type": "value"},
190
+ 0x1A: {"name": "analog2 magnitude", "type": "value"},
191
+ 0x1B: {"name": "connection", "type": "value"},
192
+ },
193
+ "MOTION BLUR": {
194
+ 0x01: {"name": "amount", "type": "value"},
195
+ 0x02: {"name": "offset-x", "type": "value"},
196
+ 0x03: {"name": "offset-y", "type": "value"},
197
+ 0x04: {"name": "scale x-axis", "type": "value"},
198
+ 0x05: {"name": "scale y-axis", "type": "value"},
199
+ 0x06: {"name": "scale", "type": "value"},
200
+ 0x07: {"name": "angle", "type": "value"},
201
+ },
202
+ "animation mapping": {
203
+ 0x01: {"name": "ambient", "type": "value"},
204
+ 0x02: {"name": "slowest move", "type": "value"},
205
+ 0x03: {"name": "slow move", "type": "value"},
206
+ 0x04: {"name": "fast move", "type": "value"},
207
+ 0x05: {"name": "jump", "type": "value"},
208
+ 0x06: {"name": "fall", "type": "value"},
209
+ 0x07: {"name": "youch", "type": "value"},
210
+ 0x08: {"name": "slide", "type": "value"},
211
+ 0x09: {"name": "attack 1", "type": "value"},
212
+ 0x0A: {"name": "attack 2", "type": "value"},
213
+ 0x0B: {"name": "tumble", "type": "value"},
214
+ 0x0C: {"name": "extra 1", "type": "value"},
215
+ 0x0D: {"name": "extra 2", "type": "value"},
216
+ 0x0E: {"name": "extra 3", "type": "value"},
217
+ 0x0F: {"name": "extra 4", "type": "value"},
218
+ 0x10: {"name": "extra 5", "type": "value"},
219
+ 0x11: {"name": "extra 6", "type": "value"},
220
+ 0x12: {"name": "extra 7", "type": "value"},
221
+ 0x13: {"name": "extra 8", "type": "value"},
222
+ 0x14: {"name": "extra 9", "type": "value"},
223
+ 0x15: {"name": "extra 10", "type": "value"},
224
+ 0x16: {"name": "extra 11", "type": "value"},
225
+ 0x17: {"name": "extra 12", "type": "value"},
226
+ 0x18: {"name": "extra 13", "type": "value"},
227
+ 0x19: {"name": "extra 14", "type": "value"},
228
+ 0x1A: {"name": "extra 15", "type": "value"},
229
+ 0x1B: {"name": "extra 16", "type": "value"},
230
+ 0x1C: {"name": "extra 17", "type": "value"},
231
+ 0x1D: {"name": "extra 18", "type": "value"},
232
+ 0x1E: {"name": "extra 19", "type": "value"},
233
+ 0x1F: {"name": "extra 20", "type": "value"},
234
+ 0x20: {"name": "extra 21", "type": "value"},
235
+ 0x21: {"name": "extra 22", "type": "value"},
236
+ 0x22: {"name": "extra 23", "type": "value"},
237
+ 0x23: {"name": "extra 24", "type": "value"},
238
+ 0x24: {"name": "extra 25", "type": "value"},
239
+ 0x25: {"name": "extra 26", "type": "value"},
240
+ 0x26: {"name": "extra 27", "type": "value"},
241
+ 0x27: {"name": "extra 28", "type": "value"},
242
+ 0x28: {"name": "extra 29", "type": "value"},
243
+ 0x29: {"name": "extra 30", "type": "value"},
244
+ 0x2A: {"name": "extra 31", "type": "value"},
245
+ 0x2B: {"name": "extra 32", "type": "value"},
246
+ 0x2C: {"name": "left turn", "type": "value"},
247
+ 0x2D: {"name": "right turn", "type": "value"},
248
+ 0x2E: {"name": "jump land slow", "type": "value"},
249
+ 0x2F: {"name": "jump land running", "type": "value"},
250
+ 0x30: {"name": "jump land hard", "type": "value"},
251
+ 0x31: {"name": "stop from full speed", "type": "value"},
252
+ 0x32: {"name": "stop from slow speed", "type": "value"},
253
+ 0x33: {"name": "running 180 change", "type": "value"},
254
+ 0x34: {"name": "player 9", "type": "value"},
255
+ 0x35: {"name": "player 10", "type": "value"},
256
+ 0x36: {"name": "player 11", "type": "value"},
257
+ 0x37: {"name": "player 12", "type": "value"},
258
+ 0x38: {"name": "player 13", "type": "value"},
259
+ 0x39: {"name": "player 14", "type": "value"},
260
+ 0x3A: {"name": "lean left slow", "type": "value"},
261
+ 0x3B: {"name": "lean left medium", "type": "value"},
262
+ 0x3C: {"name": "lean left fast", "type": "value"},
263
+ 0x3D: {"name": "lean right slow", "type": "value"},
264
+ 0x3E: {"name": "lean right medium", "type": "value"},
265
+ 0x3F: {"name": "lean right fast", "type": "value"},
266
+ },
267
+ "particle": {
268
+ 0x01: {"name": "output rate", "type": "value"},
269
+ 0x02: {"name": "output rate spread", "type": "value"},
270
+ 0x03: {"name": "lifetime", "type": "value"},
271
+ 0x04: {"name": "lifetime spread", "type": "value"},
272
+ 0x05: {"name": "clones (list)", "type": "set", "of": "particle"},
273
+ 0x06: {"name": "activation range", "type": "value"},
274
+ 0x07: {"name": "deactivation range", "type": "value"},
275
+ 0x08: {"name": "primary start velocity", "type": "value"},
276
+ 0x09: {"name": "primary start velocity spread", "type": "value"},
277
+ 0x0A: {"name": "primary start gravity", "type": "value"},
278
+ 0x0B: {"name": "primary start drag", "type": "value"},
279
+ 0x0C: {"name": "primary start color", "type": "value", "of": "color"},
280
+ 0x0D: {"name": "primary start color spread", "type": "value", "of": "color"},
281
+ 0x0E: {"name": "primary start size", "type": "value"},
282
+ 0x0F: {"name": "primary start size spread", "type": "value"},
283
+ 0x10: {"name": "primary end velocity", "type": "value"},
284
+ 0x11: {"name": "primary end velocity spread", "type": "value"},
285
+ 0x12: {"name": "primary end gravity", "type": "value"},
286
+ 0x13: {"name": "primary end drag", "type": "value"},
287
+ 0x14: {"name": "primary end color", "type": "value", "of": "color"},
288
+ 0x15: {"name": "primary end color spread", "type": "value", "of": "color"},
289
+ 0x16: {"name": "primary end size", "type": "value"},
290
+ 0x17: {"name": "primary end size spread", "type": "value"},
291
+ 0x18: {"name": "emitter spread x", "type": "value"},
292
+ 0x19: {"name": "emitter spread y", "type": "value"},
293
+ 0x1A: {"name": "emitter spread z", "type": "value"},
294
+ 0x1B: {"name": "angular spread x", "type": "value"},
295
+ 0x1C: {"name": "angular spread y", "type": "value"},
296
+ 0x1D: {"name": "visual radius", "type": "value"},
297
+ 0x1E: {"name": "cut-scene ignore", "type": "value"},
298
+ },
299
+ "MATH OPS": {
300
+ 0x01: {"name": "Normalized Angle", "type": "value"},
301
+ },
302
+ "MemCard": {
303
+ 0x01: {"name": "Unplugged", "type": "value"},
304
+ 0x02: {"name": "Free Space", "type": "value"},
305
+ 0x03: {"name": "Status", "type": "value"},
306
+ 0x04: {"name": "Auto-Save", "type": "value"},
307
+ 0x05: {"name": "Auto-Load", "type": "value"},
308
+ 0x06: {"name": "Current Slot", "type": "value"},
309
+ 0x07: {"name": "Current Slot Size", "type": "value"},
310
+ 0x08: {"name": "Auto-Format", "type": "value"},
311
+ 0x09: {"name": "Auto-Delete", "type": "value"},
312
+ 0x0A: {"name": "Saved Language", "type": "value"},
313
+ 0x0B: {"name": "Current Slot Percent Complete", "type": "value"},
314
+ 0x0C: {"name": "Auto-Create", "type": "value"},
315
+ 0x0D: {"name": "Current Slot Game Time", "type": "value"},
316
+ 0x0E: {"name": "Changed State", "type": "value"},
317
+ },
318
+ "FOG": {
319
+ 0x01: {"name": "vertex fog distance", "type": "value"},
320
+ 0x02: {"name": "vertex fog color", "type": "value", "of": "color"},
321
+ 0x03: {"name": "pixel fog/DOF distance", "type": "value"},
322
+ 0x04: {"name": "pixel fog/DOF cutoff intensity", "type": "value"},
323
+ 0x05: {"name": "pixel fog toggle", "type": "value"},
324
+ 0x06: {"name": "DOF bluriness factor", "type": "value"},
325
+ 0x07: {"name": "pixel fog/DOF color", "type": "value", "of": "color"},
326
+ 0x08: {"name": "bloom toggle", "type": "value"},
327
+ 0x09: {"name": "bloom add factor", "type": "value"},
328
+ 0x0A: {"name": "bloom halo size", "type": "value"},
329
+ },
330
+ "rumble": {
331
+ 0x01: {"name": "low frequency", "type": "value"},
332
+ 0x02: {"name": "high frequency", "type": "value"},
333
+ 0x03: {"name": "duration", "type": "value"},
334
+ 0x04: {"name": "radius", "type": "value"},
335
+ 0x05: {"name": "attenuation", "type": "value"},
336
+ 0x06: {"name": "time remaining", "type": "value"},
337
+ },
338
+ }
339
+
340
+ SHOW_NAME_ONLY = True
341
+ TFBTOOL_MEMBER_FORMATTING = True
342
+
343
+ # Reference.scope -- which element a reference picks out of a set
344
+ # (0 = the set itself, no element picked).
345
+ SCOPE_LABELS = {
346
+ 1: "first",
347
+ 2: "last",
348
+ 3: "random",
349
+ }
350
+
351
+ # Types that have no members of their own. A `sub` on one of these picks a
352
+ # component out of it (e.g. the x of a sprite's location pair), which is
353
+ # itself a plain value.
354
+ SCALAR_TYPES = {"value", "angle", "color", "position"}
355
+
356
+
357
+ def member_field(type_name: str | None, index: int) -> dict[str, str] | None:
358
+ """The BINDINGS entry for field `index` of `type_name`, if it is known."""
359
+ bindings = BINDINGS.get(type_name) if type_name is not None else None
360
+ return bindings.get(index) if bindings is not None else None
361
+
362
+
363
+ @dataclass(frozen=True)
364
+ class ResolvedType:
365
+ """What a reference ultimately names: a type, and whether it is a whole
366
+ set of them or a single one."""
367
+
368
+ type: str | None # None when it could not be resolved
369
+ is_set: bool = False
370
+
371
+ @override
372
+ def __str__(self) -> str:
373
+ if self.type is None:
374
+ return "<unknown>"
375
+ return f"set::{self.type}" if self.is_set else self.type
376
+
377
+ def member_type(self, index: int) -> "ResolvedType":
378
+ """This type's field `index` -- the type `.member`/`.sub` selects."""
379
+ field = member_field(self.type, index)
380
+
381
+ if field is None:
382
+ if self.type in SCALAR_TYPES:
383
+ return ResolvedType("value")
384
+ return ResolvedType(None)
385
+
386
+ if field.get("type") == "set":
387
+ return ResolvedType(field.get("of"), is_set=True)
388
+
389
+ # A plain value is the end of the line, but some of them are really a
390
+ # colour or an x/y pair and `sub` picks one component out: "of" says
391
+ # which table names those components, so `tint color`'s sub 0x04
392
+ # resolves to the `a` of BINDINGS["color"].
393
+ return ResolvedType(field.get("of", field.get("type"))) # "value"
394
+
395
+
396
+ @dataclass
397
+ class Reference:
398
+ raw: int = 0x00 # the 32-bit little-endian value R, kept in sync by update_raw
399
+ index: int = 0 # R >> 14
400
+ member: int = 0 # (R >> 8) & 0x3F -- field within the target
401
+ scope: int = 0 # (R >> 6) & 3
402
+ sub: int = 0 # R & 0x3F
403
+
404
+ kind: ReferenceType = ReferenceType.NULL
405
+
406
+ builtin_kind: BuiltinType | None = (
407
+ None # resolved builtin kind (None for not builtin)
408
+ )
409
+
410
+ slot: int | None = None # table index used for the lookup (None for null/builtin)
411
+ entry: StringTableEntry | None = None # resolved entry (None for null/builtin)
412
+
413
+ context: "ParserContext | None" = None # script's parser context
414
+
415
+ @classmethod
416
+ def createSimple_local(
417
+ cls,
418
+ local_refs: StringTable,
419
+ slot: int,
420
+ ) -> "Reference":
421
+ index = slot + LOCAL_BASE
422
+
423
+ entry = local_refs.get(slot)
424
+
425
+ member = 0
426
+ scope = 0 # @1 @2 @3 (fist, last, random)
427
+ sub = 0
428
+
429
+ raw = (index << 14) | (member << 8) | (scope << 6) | sub
430
+
431
+ return cls(
432
+ raw=raw,
433
+ index=index,
434
+ member=0,
435
+ scope=0,
436
+ sub=0,
437
+ # --
438
+ kind=ReferenceType.LOCAL,
439
+ builtin_kind=None,
440
+ slot=slot,
441
+ entry=entry,
442
+ )
443
+
444
+ def __post_init__(self) -> None:
445
+ # Enable the __setattr__ hook only once every field exists, then compose
446
+ # `raw` from the values the caller passed.
447
+ object.__setattr__(self, "_raw_sync", True)
448
+ self.update_raw()
449
+
450
+ @override
451
+ def __setattr__(self, name: str, value: object) -> None:
452
+ # dataclass __init__ assigns fields one at a time, so the _raw_sync guard
453
+ # keeps us from composing a raw out of half-initialised state;
454
+ # __post_init__ does that once at the end.
455
+ syncing = name in _RAW_INPUTS and getattr(self, "_raw_sync", False)
456
+ previous = getattr(self, name) if syncing else None
457
+
458
+ super().__setattr__(name, value)
459
+
460
+ if syncing:
461
+ try:
462
+ self.update_raw()
463
+ except ValueError:
464
+ object.__setattr__(self, name, previous) # keep the object valid
465
+ raise
466
+
467
+ def update_raw(self) -> int:
468
+ """Recompose `raw` (and `index`) from the current field values.
469
+
470
+ `index` is derived from `kind` plus `slot`/`builtin_kind` whenever those
471
+ say what it should be, so moving a reference to another table slot is
472
+ enough to keep both `index` and `raw` right. Returns the new `raw`.
473
+ """
474
+ index = self.index
475
+
476
+ if self.kind == ReferenceType.NULL:
477
+ # A null reference is all-ones; that is what the bit-fields decode to.
478
+ index, member, scope, sub = 0x3FFFF, 0x3F, 3, 0x3F
479
+ else:
480
+ member, scope, sub = self.member, self.scope, self.sub
481
+
482
+ if self.kind == ReferenceType.BUILTIN and self.builtin_kind is not None:
483
+ index = self.builtin_kind.value
484
+ elif self.kind == ReferenceType.LOCAL and self.slot is not None:
485
+ index = LOCAL_BASE + self.slot
486
+ elif self.kind == ReferenceType.GLOBAL and self.slot is not None:
487
+ index = self.slot
488
+
489
+ for name, value, limit in (
490
+ ("index", index, 0x3FFFF),
491
+ ("member", member, 0x3F),
492
+ ("scope", scope, 0x3),
493
+ ("sub", sub, 0x3F),
494
+ ):
495
+ if not 0 <= value <= limit:
496
+ raise ValueError(
497
+ f"reference {name} {value:#x} does not fit its bit-field "
498
+ + f"(0..{limit:#x})"
499
+ )
500
+
501
+ raw = (index << 14) | (member << 8) | (scope << 6) | sub
502
+
503
+ # object.__setattr__, so writing these back does not re-enter update_raw.
504
+ object.__setattr__(self, "index", index)
505
+ object.__setattr__(self, "member", member)
506
+ object.__setattr__(self, "scope", scope)
507
+ object.__setattr__(self, "sub", sub)
508
+ object.__setattr__(self, "raw", raw)
509
+
510
+ return raw
511
+
512
+ @classmethod
513
+ def createSimple_global(
514
+ cls,
515
+ global_refs: StringTable,
516
+ slot: int,
517
+ ) -> "Reference":
518
+ index = slot
519
+
520
+ entry = global_refs.get(slot)
521
+
522
+ member = 0
523
+ scope = 0 # @1 @2 @3 (fist, last, random)
524
+ sub = 0
525
+
526
+ raw = (index << 14) | (member << 8) | (scope << 6) | sub
527
+
528
+ return cls(
529
+ raw=raw,
530
+ index=index,
531
+ member=0,
532
+ scope=0,
533
+ sub=0,
534
+ # --
535
+ kind=ReferenceType.GLOBAL,
536
+ builtin_kind=None,
537
+ slot=slot,
538
+ entry=entry,
539
+ )
540
+
541
+ @classmethod
542
+ def createSimple_builtin(
543
+ cls,
544
+ builtin_type: BuiltinType,
545
+ ) -> "Reference":
546
+ index = builtin_type.value
547
+
548
+ member = 0
549
+ scope = 0 # @1 @2 @3 (fist, last, random)
550
+ sub = 0
551
+
552
+ raw = (index << 14) | (member << 8) | (scope << 6) | sub
553
+
554
+ return cls(
555
+ raw=raw,
556
+ index=index,
557
+ member=0,
558
+ scope=0,
559
+ sub=0,
560
+ # --
561
+ kind=ReferenceType.BUILTIN,
562
+ builtin_kind=builtin_type,
563
+ slot=None,
564
+ entry=None,
565
+ )
566
+
567
+ @classmethod
568
+ def read(
569
+ cls,
570
+ reader: "BinaryReader",
571
+ global_refs: StringTable,
572
+ local_refs: StringTable,
573
+ context: "ParserContext | None" = None,
574
+ ) -> "Reference":
575
+ """Parse a 4-byte TFB-Script variable reference.
576
+
577
+ Args:
578
+ reader: BinaryReader positioned at the reference.
579
+ global_refs: the script's 2nd string table (globals) for name resolution.
580
+ local_refs: the script's 3rd string table (locals) for name resolution.
581
+ context: the script's parser context, used to resolve the type of
582
+ some builtins (e.g. "[~subset]") from prior opcodes.
583
+ Returns:
584
+ A Reference with the decoded bit-fields and a resolved `name`.
585
+ """
586
+ if reader.size_remaining() < 4:
587
+ raise ValueError("a reference is 4 bytes, not enough bytes remaining")
588
+
589
+ raw = reader.read_u32()
590
+ fields = {
591
+ "raw": raw,
592
+ "index": raw >> 0xE,
593
+ "member": (raw >> 0x8) & 0x3F, # member index (applied first)
594
+ "scope": (raw >> 0x6) & 3, # mode
595
+ "sub": raw & 0x3F, # member index (applied last)
596
+ }
597
+ index = fields["index"]
598
+
599
+ if raw == NULL_REF:
600
+ return cls(
601
+ **fields,
602
+ builtin_kind=None,
603
+ kind=ReferenceType.NULL,
604
+ slot=None,
605
+ entry=None,
606
+ context=context,
607
+ )
608
+
609
+ if index >= BUILTIN_BASE:
610
+ return cls(
611
+ **fields,
612
+ kind=ReferenceType.BUILTIN,
613
+ builtin_kind=BuiltinType(index),
614
+ slot=None,
615
+ entry=None,
616
+ context=context,
617
+ )
618
+
619
+ if index < LOCAL_BASE:
620
+ entry = global_refs.get(index)
621
+ return cls(
622
+ **fields,
623
+ kind=ReferenceType.GLOBAL,
624
+ builtin_kind=None,
625
+ slot=index,
626
+ entry=entry,
627
+ context=context,
628
+ )
629
+
630
+ slot = index - LOCAL_BASE
631
+ entry = local_refs.get(slot)
632
+ return cls(
633
+ **fields,
634
+ kind=ReferenceType.LOCAL,
635
+ builtin_kind=None,
636
+ slot=slot,
637
+ entry=entry,
638
+ context=context,
639
+ )
640
+
641
+ def write(self, f: BinaryIO) -> None:
642
+ if self.kind == ReferenceType.BUILTIN and self.builtin_kind is None:
643
+ raise ValueError("builtin reference has no builtin_kind")
644
+ if self.kind in (ReferenceType.GLOBAL, ReferenceType.LOCAL):
645
+ if self.slot is None:
646
+ raise ValueError(f"{self.kind.name.lower()} reference has no slot")
647
+ elif self.kind not in (ReferenceType.NULL, ReferenceType.BUILTIN):
648
+ raise ValueError(f"Reference has unknown kind {self.kind}")
649
+
650
+ write_u32(f, self.update_raw())
651
+
652
+ @override
653
+ def __str__(self) -> str:
654
+ if self.kind == ReferenceType.NULL: # Never seen a use of that ¯\_(ツ)_/¯
655
+ return "<null>"
656
+
657
+ if self.kind == ReferenceType.GLOBAL:
658
+ assert self.entry is not None
659
+ s = variable_global(
660
+ self.entry.name if SHOW_NAME_ONLY else self.entry.string
661
+ )
662
+ if self.entry.category == "set":
663
+ s = f" [{s}]"
664
+
665
+ elif self.kind == ReferenceType.LOCAL:
666
+ assert self.entry is not None
667
+ s = variable(self.entry.name if SHOW_NAME_ONLY else self.entry.string)
668
+ if self.entry.category == "set":
669
+ s = f" [{s}]"
670
+
671
+ elif self.kind == ReferenceType.BUILTIN:
672
+ assert (
673
+ self.builtin_kind is not None
674
+ ) # should never be None for BUILTIN kind
675
+
676
+ if TFBTOOL_MEMBER_FORMATTING and self.builtin_kind == BuiltinType.SELF:
677
+ if self.member or self.sub:
678
+ s = builtin("my")
679
+ else:
680
+ s = builtin("myself")
681
+ else:
682
+ s = builtin(BUILTIN_LABELS[self.builtin_kind])
683
+
684
+ else:
685
+ raise ValueError(f"Reference has unknown kind {self.kind}")
686
+
687
+ if self.member:
688
+ if self.builtin_kind == BuiltinType.MESSAGE_VALUE:
689
+ # message value will always resolve to "value" wich is not in the bindings, so we suppress the warning here because its intended behavior
690
+ s += self._get_member_string(suppressWarnings=True) or ""
691
+ else:
692
+ s += self._get_member_string() or ""
693
+
694
+ if self.scope:
695
+ # On a set, the scope picks one element out of it, e.g.
696
+ # "first in [players]". `member` above still selects the set
697
+ # itself, while `sub` below applies to the picked element. What it
698
+ # means on anything else is unknown, so leave those as a raw "@N".
699
+ if self._selects_from_set():
700
+ label = SCOPE_LABELS.get(self.scope, str(self.scope))
701
+ s = f"{keyword(f'{label} in')} {s.lstrip(' ')}"
702
+ else:
703
+ s += f"@{self.scope}"
704
+
705
+ if self.sub:
706
+ s += self._get_sub_string()
707
+
708
+ return s
709
+
710
+ def _get_sub_string(self) -> str:
711
+ """The component `sub` picks out, named when whatever it applies to has
712
+ a name for it -- the `a` of a tint colour, the `x` of a sprite's
713
+ location -- and the raw index when it does not."""
714
+ try:
715
+ applies_to = self.resolve_type(apply_sub=False).type
716
+ except ValueError:
717
+ applies_to = None # a builtin with no producer in reach
718
+
719
+ field = member_field(applies_to, self.sub)
720
+
721
+ if field is None or field.get("name") is None:
722
+ return f".sub[{self.sub:#04x}]"
723
+
724
+ return f".{field['name']}"
725
+
726
+ def _selects_from_set(self) -> bool:
727
+ """Whether what `scope` picks from is a set: the member it selects, or,
728
+ with no member, the reference's own target."""
729
+ if self.member:
730
+ my_field = member_field(self.get_resolved_type(), self.member)
731
+ return my_field is not None and my_field.get("type") == "set"
732
+
733
+ return self._target_is_set()
734
+
735
+ def _target_is_set(self) -> bool:
736
+ """Whether the reference's own target (before `member`) is a set."""
737
+ if self.kind == ReferenceType.BUILTIN:
738
+ return self.builtin_kind == BuiltinType.SUBSET
739
+
740
+ return self.entry is not None and self.entry.category == "set"
741
+
742
+ def _get_member_string(self, suppressWarnings=False) -> str | None:
743
+ my_type = self.get_resolved_type()
744
+
745
+ if my_type is not None:
746
+ my_bindings = BINDINGS.get(my_type)
747
+
748
+ if my_bindings is not None:
749
+ my_field = member_field(my_type, self.member)
750
+
751
+ if my_field is not None:
752
+ label = my_field.get("name")
753
+ type_ = my_field.get("type")
754
+
755
+ if TFBTOOL_MEMBER_FORMATTING:
756
+ if (
757
+ self.kind == ReferenceType.BUILTIN
758
+ and self.builtin_kind == BuiltinType.SELF
759
+ ):
760
+ if type_ == "set":
761
+ return f" [{label}]"
762
+ else:
763
+ return f" {label}"
764
+ else:
765
+ if type_ == "set":
766
+ return f"'s [{label}]"
767
+ else:
768
+ return f"'s {label}"
769
+
770
+ else:
771
+ if type_ == "set":
772
+ return f".[{label}]"
773
+ else:
774
+ return f".{label}"
775
+ else:
776
+ if not suppressWarnings:
777
+ print(
778
+ "WARN: cant resolve binding for",
779
+ my_type,
780
+ "member",
781
+ hex(self.member),
782
+ "entry",
783
+ self.entry,
784
+ file=sys.stderr,
785
+ )
786
+
787
+ else:
788
+ if not suppressWarnings:
789
+ print(
790
+ "WARN: cant resolve bindings for type",
791
+ '"' + my_type + '"',
792
+ "entry",
793
+ self.entry,
794
+ "field",
795
+ hex(self.member),
796
+ file=sys.stderr,
797
+ )
798
+
799
+ return f".field[{self.member:#04x}]"
800
+
801
+ def resolve_type(self, apply_sub: bool = True) -> ResolvedType:
802
+ """The type this reference *ultimately* names, following the whole
803
+ chain: target -> `member` -> `scope` -> `sub`.
804
+
805
+ e.g. "first in return point's [waypoints]" starts at the actor
806
+ variable "return point", takes its `waypoints` member (a set of
807
+ waypoints), then `scope` picks one out of it -> a single "waypoint".
808
+
809
+ `get_resolved_type` only gives the type of the target the chain starts
810
+ at ("actor" in that example), which is what a `member` index is looked
811
+ up against.
812
+
813
+ `apply_sub=False` stops before `sub`, for the ops where `sub` is not a
814
+ member selection: on a "find subset" set it names the field to filter
815
+ on, so "my [waypoints].sub[0x01]" is still a set of waypoints there.
816
+ """
817
+ resolved = ResolvedType(self.get_resolved_type(), self._target_is_set())
818
+
819
+ if self.member:
820
+ # NOTE: a member on a set variable applies to its elements, so the
821
+ # member's own kind is what we report -- same as `_selects_from_set`.
822
+ resolved = resolved.member_type(self.member)
823
+
824
+ if self.scope and resolved.is_set:
825
+ # The scope picks one element out of the set (first/last/random).
826
+ # On anything else its meaning is unknown, so leave the type alone.
827
+ resolved = ResolvedType(resolved.type)
828
+
829
+ if self.sub and apply_sub:
830
+ resolved = resolved.member_type(self.sub)
831
+
832
+ return resolved
833
+
834
+ def get_final_type(self, apply_sub: bool = True) -> str | None:
835
+ """The name of the type `resolve_type` lands on, e.g. "waypoint"."""
836
+ return self.resolve_type(apply_sub).type
837
+
838
+ def get_resolved_type(self) -> str | None:
839
+ """The type of the target this reference starts at, *before* `member`,
840
+ `scope` and `sub` are applied. Use `resolve_type` for the end of the
841
+ chain."""
842
+ # A builtin resolves through the op that produced it, and that op's own
843
+ # operand can be the same builtin again -- `control [~controlled]`
844
+ # nested inside `control [~controlled]` resolves via
845
+ # nearest_ancestor(OpControl), which hands back the same producer every
846
+ # time, so the chain never advances. Guard re-entry and report the type
847
+ # as unknown rather than recursing forever.
848
+ if id(self) in _RESOLVING:
849
+ return None
850
+
851
+ _RESOLVING.add(id(self))
852
+ try:
853
+ return self._get_resolved_type()
854
+ finally:
855
+ _RESOLVING.discard(id(self))
856
+
857
+ def _get_resolved_type(self) -> str | None:
858
+ if self.kind == ReferenceType.NULL:
859
+ return "null"
860
+
861
+ if self.kind == ReferenceType.BUILTIN:
862
+ if self.builtin_kind == BuiltinType.SELF:
863
+ return "actor"
864
+ elif self.builtin_kind == BuiltinType.SUBSET:
865
+ from tfbscript.opcodes import OpCheckFOV, OpFindSubset
866
+
867
+ producer = self._builtin_find_producer((OpCheckFOV, OpFindSubset))
868
+
869
+ if isinstance(producer, OpFindSubset):
870
+ return producer.set_ref.get_final_type(apply_sub=False)
871
+ else:
872
+ return producer.target_ref.get_final_type()
873
+
874
+ elif self.builtin_kind == BuiltinType.MESSAGE_VALUE:
875
+ return "value" # TODO: maybe we can resolve here??? seems kinda hard to do, since the message value is set by the sender and we don't have that context here
876
+
877
+ elif self.builtin_kind == BuiltinType.FOUND_VARIABLE:
878
+ # Not tested on real scripts cus couldnt find one
879
+ from tfbscript.opcodes import OpFindVariable
880
+
881
+ producer = self._builtin_find_producer(OpFindVariable)
882
+ return producer.var_ref.get_final_type()
883
+
884
+ elif self.builtin_kind == BuiltinType.EACH:
885
+ from tfbscript.opcodes import OpForEach
886
+
887
+ producer = self._builtin_find_producer(OpForEach)
888
+ return producer.set_ref.get_final_type()
889
+
890
+ elif self.builtin_kind == BuiltinType.CONTROLLED:
891
+ from tfbscript.opcodes import OpControl, OpSpawnActor
892
+
893
+ producer = self._builtin_find_producer((OpControl, OpSpawnActor))
894
+
895
+ if isinstance(producer, OpControl):
896
+ return producer.target.get_final_type()
897
+ else:
898
+ return producer.clone_ref.get_final_type()
899
+
900
+ else:
901
+ raise ValueError(
902
+ f"Reference is a builtin of kind {self.builtin_kind} but has no type resolution implemented"
903
+ )
904
+
905
+ if self.kind in (ReferenceType.GLOBAL, ReferenceType.LOCAL): # variable
906
+ if self.entry is not None:
907
+ return self.entry.type
908
+ else:
909
+ raise ValueError("Reference has no entry to resolve type")
910
+
911
+ return None # Should never reach here
912
+
913
+ def _builtin_find_producer(
914
+ self, opcode_types: type[_OpcodeT] | tuple[type[_OpcodeT], ...]
915
+ ):
916
+ if self.context is None:
917
+ raise ValueError(
918
+ f"Reference {self.builtin_kind} is a builtin (and a field on it is used) but has no context to resolve its type"
919
+ )
920
+ producer = self.context.nearest_ancestor(opcode_types)
921
+ if producer is None:
922
+ raise ValueError(
923
+ f"Reference {self.builtin_kind} is a builtin (and a field on it is used) but has no enclosing op to resolve its type"
924
+ )
925
+
926
+ return producer