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,321 @@
1
+ # pyright: basic
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from typing import override
6
+
7
+ from tfbscript.opcodes.base import InstructionFlags, Opcode
8
+
9
+ from PySide6.QtCore import Qt, Signal
10
+ from PySide6.QtWidgets import (
11
+ QDialog,
12
+ QDialogButtonBox,
13
+ QHBoxLayout,
14
+ QLabel,
15
+ QMenu,
16
+ QPushButton,
17
+ QRadioButton,
18
+ QTreeWidget,
19
+ QTreeWidgetItem,
20
+ QVBoxLayout,
21
+ QWidget,
22
+ )
23
+
24
+ from .search import ROW_PADDING, SearchEntry, row_text
25
+ from .style import Win98TreeStyle, tfb_colored_box
26
+
27
+
28
+ def clear_layout(layout: QHBoxLayout):
29
+ while layout.count():
30
+ item = layout.takeAt(0)
31
+ if item is None:
32
+ continue
33
+ widget = item.widget()
34
+ if widget is not None:
35
+ widget.deleteLater()
36
+
37
+ def text_to_tb(label: QWidget):
38
+ fm = label.fontMetrics()
39
+ label.setFixedHeight(fm.height())
40
+
41
+ class RHSDialog(QDialog):
42
+ def __init__(self, rhs=None, parent=None):
43
+ super().__init__(parent)
44
+
45
+ self.rhs = rhs
46
+
47
+ self.setWindowTitle("RHS Builder")
48
+
49
+ layout = QVBoxLayout(self)
50
+
51
+ self.option1 = QRadioButton("Integer")
52
+ self.option2 = QRadioButton("Float")
53
+ self.option3 = QRadioButton("Color")
54
+ self.option4 = QRadioButton("(X,Y) Position")
55
+ self.option5 = QRadioButton("Reference")
56
+ self.option6 = QRadioButton("Expression (Combination)")
57
+
58
+ layout.addWidget(self.option1)
59
+ layout.addWidget(self.option2)
60
+ layout.addWidget(self.option3)
61
+ layout.addWidget(self.option4)
62
+ layout.addWidget(self.option5)
63
+ layout.addWidget(self.option6)
64
+
65
+ # Select the existing RHS kind
66
+ if rhs is not None:
67
+ options = {
68
+ "int": self.option1,
69
+ "float": self.option2,
70
+ "color": self.option3,
71
+ "pair": self.option4,
72
+ "reference": self.option5,
73
+ "expression": self.option6,
74
+ }
75
+
76
+ if button := options.get(rhs.kind):
77
+ button.setChecked(True)
78
+
79
+ buttons = QDialogButtonBox(
80
+ standardButtons=(
81
+ QDialogButtonBox.StandardButton.Ok
82
+ | QDialogButtonBox.StandardButton.Cancel
83
+ )
84
+ )
85
+
86
+ buttons.accepted.connect(self.accept)
87
+ buttons.rejected.connect(self.reject)
88
+
89
+ layout.addWidget(buttons)
90
+
91
+ class RHSButton(QPushButton):
92
+ doubleClicked = Signal()
93
+
94
+ def __init__(self, rhs, parent=None):
95
+ super().__init__(parent)
96
+ self.rhs = rhs
97
+
98
+ def mouseDoubleClickEvent(self, event):
99
+ dialog = RHSDialog(self.rhs, self.window())
100
+
101
+ if dialog.exec() == QDialog.DialogCode.Accepted:
102
+ self.rhs = dialog.rhs
103
+
104
+ super().mouseDoubleClickEvent(event)
105
+
106
+ def opcode_row(opcode: Opcode, layout: QHBoxLayout, on_change: Callable[[], None]):
107
+ fields = opcode.editor_repr().get("fields", [])
108
+
109
+ for _field in fields:
110
+ f_type = _field.get("type")
111
+
112
+ if f_type == "op-label":
113
+ label = QLabel(_field.get("value", ""))
114
+ tfb_colored_box(label, "#0000AA")
115
+ text_to_tb(label)
116
+ layout.addWidget(label)
117
+
118
+ elif f_type == "behavior-label":
119
+ label = QLabel(_field.get("value", ""))
120
+ tfb_colored_box(label, "#00AA31")
121
+ text_to_tb(label)
122
+ layout.addWidget(label)
123
+
124
+ elif f_type == "block-label":
125
+ label = QLabel(_field.get("value", ""))
126
+ label.setStyleSheet("font-weight: bold;")
127
+ text_to_tb(label)
128
+ layout.addWidget(label)
129
+
130
+ elif f_type == "label":
131
+ label = QLabel(_field.get("content", ""))
132
+ label.setStyleSheet("font-weight: bold;")
133
+ text_to_tb(label)
134
+ layout.addWidget(label)
135
+
136
+ elif f_type == "string":
137
+ label = QLabel(_field.get("value", ""))
138
+ label.setStyleSheet("color: #8A8880;")
139
+ text_to_tb(label)
140
+ layout.addWidget(label)
141
+
142
+ elif f_type == "rhs":
143
+ selected = _field.get("rhs")
144
+ attr_name = _field.get("name")
145
+
146
+ button = RHSButton(selected, str(selected))
147
+ text_to_tb(button)
148
+ button.setFlat(True)
149
+ button.setStyleSheet(
150
+ "QPushButton { padding: 2px 4px; margin: 0px; border: none; background-color: #616161; color: #FFFFFF; }"
151
+ " QPushButton::menu-indicator { image: none; width: 0px; height: 0px; }"
152
+ )
153
+ layout.addWidget(button)
154
+
155
+
156
+ elif f_type == "ref":
157
+ content = str(_field.get("ref", None))
158
+ label = QLabel(content)
159
+ tfb_colored_box(label, "#616161")
160
+ text_to_tb(label)
161
+ layout.addWidget(label)
162
+
163
+ elif f_type == "flow":
164
+ content = str(_field.get("value", None))
165
+ label = QLabel(content)
166
+ tfb_colored_box(label, "#616161")
167
+ text_to_tb(label)
168
+ layout.addWidget(label)
169
+
170
+ elif f_type == "enum":
171
+ selected = _field.get("entry", _field.get("value", None))
172
+ attr_name = _field.get("name")
173
+
174
+ button = QPushButton(str(selected))
175
+ text_to_tb(button)
176
+ button.setFlat(True)
177
+ button.setStyleSheet(
178
+ "QPushButton { padding: 2px 4px; margin: 0px; border: none; background-color: #616161; color: #FFFFFF; }"
179
+ " QPushButton::menu-indicator { image: none; width: 0px; height: 0px; }"
180
+ )
181
+ layout.addWidget(button)
182
+ menu = QMenu(button)
183
+ menu.setStyleSheet(
184
+ "QMenu { background-color: #3c3c3c; color: #e0e0e0; border: 1px solid #555; padding: 2px; }"
185
+ " QMenu::item { padding: 4px 16px; }"
186
+ " QMenu::item:selected { background-color: #5a5a5a; color: #ffffff; }"
187
+ )
188
+
189
+ def select_entry(entry: object, attr_name: str | None = attr_name) -> None:
190
+ if attr_name is not None:
191
+ setattr(opcode, attr_name, entry)
192
+ on_change()
193
+
194
+ for entry in selected.__class__:
195
+ action = menu.addAction(str(entry))
196
+ action.triggered.connect(
197
+ lambda checked=False, entry=entry: select_entry(entry)
198
+ )
199
+
200
+ button.setMenu(menu)
201
+
202
+ else:
203
+ raise NotImplementedError(f"Unsupported field type: {f_type}")
204
+
205
+ @dataclass
206
+ class FlowControlOpcode(Opcode):
207
+ parent_op_idx: int = field(default=-1)
208
+ op_flags: InstructionFlags = field(default_factory=InstructionFlags)
209
+
210
+ @override
211
+ def editor_repr(self) -> dict:
212
+ return {
213
+ "fields": [
214
+ {"type": "label", "content": "flow"},
215
+ {"type": "label", "content": self.op_flags.flow_control_str()} if self.parent_op_idx == 0xFF else {"type": "flow", "value": self.op_flags.flow_control_str()},
216
+ ]
217
+ }
218
+
219
+
220
+ @dataclass
221
+ class GroupOpcode(Opcode):
222
+ """Pseudo-opcode heading a labelled branch of another opcode's body, e.g.
223
+ the "true" / "else" halves of an if/else. `scope` holds the opcodes that
224
+ must stay open while its body renders (see ParserContext.open_opcodes)."""
225
+
226
+ label: str = field(default="")
227
+ scope: list[Opcode] = field(default_factory=list)
228
+
229
+ @override
230
+ def editor_repr(self) -> dict:
231
+ return {
232
+ "fields": [{"type": "label", "content": self.label}],
233
+ "hasBody": True,
234
+ "scope": self.scope,
235
+ }
236
+
237
+
238
+ def populate_tree(
239
+ parent: QTreeWidget | QTreeWidgetItem,
240
+ opcode: Opcode,
241
+ search_index: list[SearchEntry] | None = None,
242
+ ):
243
+ item = QTreeWidgetItem(parent)
244
+ item.setExpanded(False)
245
+
246
+ tree = item.treeWidget()
247
+
248
+ editor_repr = opcode.editor_repr()
249
+ # An opcode may render its row from another opcode ("row"), e.g. if/else
250
+ # shows its condition, so edits land on the opcode the fields came from.
251
+ row_opcode = editor_repr.get("row", opcode)
252
+
253
+ container = QWidget()
254
+ outer_layout = QHBoxLayout(container)
255
+ outer_layout.setContentsMargins(Win98TreeStyle.CONTENT_OFFSET, 0, 0, 2)
256
+ outer_layout.setSpacing(0)
257
+ outer_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
258
+
259
+ # The fields live in their own widget so a row highlight can hug the
260
+ # content instead of stretching across the full width of the tree. Its
261
+ # margin is the padding around a highlight, and is reserved unconditionally
262
+ # so switching the highlight on and off never nudges the row's contents.
263
+ content = QWidget()
264
+ layout = QHBoxLayout(content)
265
+ layout.setContentsMargins(
266
+ ROW_PADDING, ROW_PADDING, ROW_PADDING, ROW_PADDING
267
+ )
268
+ layout.setSpacing(4)
269
+ layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
270
+
271
+ outer_layout.addWidget(content)
272
+ outer_layout.addStretch()
273
+
274
+ entry = SearchEntry(item=item, content=content)
275
+ if search_index is not None:
276
+ search_index.append(entry)
277
+
278
+ def rerender_row() -> None:
279
+ clear_layout(layout)
280
+ opcode_row(row_opcode, layout, rerender_row)
281
+ entry.set_text(row_text(layout))
282
+
283
+ rerender_row()
284
+
285
+ tree.setItemWidget(item, 0, container)
286
+ context = opcode.context
287
+ scope = [opcode, *editor_repr.get("scope", [])]
288
+ if context is not None:
289
+ context.open_opcodes.extend(scope)
290
+
291
+ try:
292
+ groups = editor_repr.get("groups")
293
+ if groups is not None:
294
+ # Each group becomes a labelled node owning part of the body; the
295
+ # flow-control row is emitted per group, not for the opcode itself.
296
+ for group in groups:
297
+ populate_tree(
298
+ item,
299
+ GroupOpcode(
300
+ opcode_index=opcode.opcode_index,
301
+ flags=opcode.flags,
302
+ context=context,
303
+ children=list(group.get("children", [])),
304
+ label=group.get("label", ""),
305
+ scope=list(group.get("scope", [])),
306
+ ),
307
+ search_index,
308
+ )
309
+ elif opcode.children or editor_repr.get("hasBody", False):
310
+ for child in opcode.children:
311
+ populate_tree(item, child, search_index)
312
+ populate_tree(
313
+ item,
314
+ FlowControlOpcode(
315
+ parent_op_idx=opcode.opcode_index, op_flags=opcode.flags
316
+ ),
317
+ search_index,
318
+ )
319
+ finally:
320
+ if context is not None:
321
+ del context.open_opcodes[-len(scope) :]
@@ -0,0 +1,104 @@
1
+ """Opcode implementations, one module per opcode.
2
+
3
+ Importing this package registers every implemented opcode in OPCODE_REGISTRY
4
+ (via the @opcode decorator); unimplemented opcodes fall back to the generic
5
+ Opcode class, which keeps the raw payload.
6
+ """
7
+
8
+ from tfbscript.opcodes.base import (
9
+ OPCODE_REGISTRY,
10
+ InstructionFlags,
11
+ Opcode,
12
+ ParserContext,
13
+ )
14
+ from tfbscript.opcodes.block import BlockOpcode
15
+ from tfbscript.opcodes.enums import RelOp
16
+ from tfbscript.opcodes.op_behavior_implementation import OpBehaviorImplementation
17
+ from tfbscript.opcodes.op_change_membership import OpChangeMembership
18
+ from tfbscript.opcodes.op_check_fov import OpCheckFOV
19
+ from tfbscript.opcodes.op_check_membership import OpCheckMembership
20
+ from tfbscript.opcodes.op_check_message import OpCheckMessage
21
+ from tfbscript.opcodes.op_check_reference import OpCheckReference
22
+ from tfbscript.opcodes.op_check_value import OpCheckValue
23
+ from tfbscript.opcodes.op_comment import OpComment
24
+ from tfbscript.opcodes.op_control import OpControl
25
+ from tfbscript.opcodes.op_create_variable import OpCreateVariable
26
+ from tfbscript.opcodes.op_cut_scene import OpCutScene
27
+ from tfbscript.opcodes.op_dec_value import OpDecValue
28
+ from tfbscript.opcodes.op_displace import OpDisplace
29
+ from tfbscript.opcodes.op_find_subset import OpFindSubset
30
+ from tfbscript.opcodes.op_find_variable import OpFindVariable
31
+ from tfbscript.opcodes.op_for_each import OpForEach
32
+ from tfbscript.opcodes.op_if_else import OpIfElse
33
+ from tfbscript.opcodes.op_inc_value import OpIncValue
34
+ from tfbscript.opcodes.op_loop_value import OpLoopValue
35
+ from tfbscript.opcodes.op_move_from import OpMoveFrom
36
+ from tfbscript.opcodes.op_move_to import OpMoveTo
37
+ from tfbscript.opcodes.op_play_animation import OpPlayAnimation
38
+ from tfbscript.opcodes.op_play_sound import OpPlaySound
39
+ from tfbscript.opcodes.op_prescript import OpPrescript
40
+ from tfbscript.opcodes.op_print import OpPrint
41
+ from tfbscript.opcodes.op_remove import OpRemove
42
+ from tfbscript.opcodes.op_reset import OpReset
43
+ from tfbscript.opcodes.op_run_as_player import OpRunAsPlayer
44
+ from tfbscript.opcodes.op_send_message import OpSendMessage
45
+ from tfbscript.opcodes.op_set_behavior import OpSetBehavior
46
+ from tfbscript.opcodes.op_set_reference import OpSetReference
47
+ from tfbscript.opcodes.op_set_value import OpSetValue
48
+ from tfbscript.opcodes.op_shutdown import OpShutdown
49
+ from tfbscript.opcodes.op_slide_value import OpSlideValue
50
+ from tfbscript.opcodes.op_spawn_actor import OpSpawnActor
51
+ from tfbscript.opcodes.op_startup import OpStartup
52
+ from tfbscript.opcodes.op_stop_sound import OpStopSound
53
+ from tfbscript.opcodes.op_teleport_to import OpTeleportTo
54
+ from tfbscript.opcodes.op_turn_to import OpTurnTo
55
+ from tfbscript.opcodes.op_use_camera import OpUseCamera
56
+
57
+ __all__ = [
58
+ "OPCODE_REGISTRY",
59
+ "BlockOpcode",
60
+ "InstructionFlags",
61
+ "OpBehaviorImplementation",
62
+ "OpChangeMembership",
63
+ "OpCheckFOV",
64
+ "OpCheckMembership",
65
+ "OpCheckMessage",
66
+ "OpCheckReference",
67
+ "OpCheckValue",
68
+ "OpComment",
69
+ "OpControl",
70
+ "OpCreateVariable",
71
+ "OpCutScene",
72
+ "OpDecValue",
73
+ "OpDisplace",
74
+ "OpFindSubset",
75
+ "OpFindVariable",
76
+ "OpForEach",
77
+ "OpIfElse",
78
+ "OpIncValue",
79
+ "OpLoopValue",
80
+ "OpMoveFrom",
81
+ "OpMoveTo",
82
+ "OpPlayAnimation",
83
+ "OpPlaySound",
84
+ "OpPrescript",
85
+ "OpPrint",
86
+ "OpRemove",
87
+ "OpReset",
88
+ "OpRunAsPlayer",
89
+ "OpSendMessage",
90
+ "OpSetBehavior",
91
+ "OpSetReference",
92
+ "OpSetValue",
93
+ "OpShutdown",
94
+ "OpSlideValue",
95
+ "OpSpawnActor",
96
+ "OpStartup",
97
+ "OpStopSound",
98
+ "OpTeleportTo",
99
+ "OpTurnTo",
100
+ "OpUseCamera",
101
+ "Opcode",
102
+ "ParserContext",
103
+ "RelOp",
104
+ ]