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,26 @@
1
+ The font file in this archive was created using Fontstruct the free, online
2
+ font-building tool.
3
+ This font was created by “lou”.
4
+ This font has a homepage where this archive and other versions may be found:
5
+ https://fontstruct.com/fontstructions/show/1384862
6
+
7
+ Try Fontstruct at http://fontstruct.com
8
+ It’s easy and it’s fun.
9
+
10
+ NOTE FOR FLASH USERS: Fontstruct fonts (fontstructions) are optimized for Flash.
11
+ If the font in this archive is a pixel font, it is best displayed at a font-size
12
+ of 11.
13
+
14
+ Fontstruct is sponsored by FontShop.
15
+ Visit them at https://fontshop.com
16
+ FontShop is the original independent font retailer. We’ve been around since
17
+ the dawn of digital type. Whether you need the right font or need to create the
18
+ right font from scratch, let our 26 years of experience work for you.
19
+
20
+ Fontstruct is copyright ©2017 Rob Meek
21
+
22
+ LEGAL NOTICE:
23
+ In using this font you must comply with the licensing terms described in the
24
+ file “license.txt” included with this archive.
25
+ If you redistribute the font file in this archive, it must be accompanied by all
26
+ the other files from this archive, including this one.
@@ -0,0 +1,98 @@
1
+ # pyright: basic
2
+
3
+ import io
4
+ from pathlib import Path
5
+
6
+ from fontTools.ttLib import TTFont
7
+
8
+ from PySide6.QtGui import QFont, QFontDatabase
9
+
10
+
11
+ def load_font(
12
+ path: str | Path,
13
+ size: int = 11,
14
+ weight: QFont.Weight = QFont.Weight.Normal,
15
+ italic: bool = False,
16
+ aliased: bool = True,
17
+ ) -> QFont:
18
+ """
19
+ Load a custom font from file, rendered GDI-style (grid-fitted, no AA).
20
+
21
+ Args:
22
+ path: Path to the font file (.ttf, .otf, etc.), or a Qt resource path
23
+ size: Size in PIXELS (not points) — 11px == XP's default 8pt Tahoma
24
+ weight: Font weight (e.g. QFont.Weight.Normal, QFont.Weight.Bold)
25
+ italic: Whether the font should be italic
26
+ aliased: True for crisp XP "standard" text, False for soft ClearType-ish text
27
+
28
+ Returns:
29
+ The QFont instance.
30
+
31
+ Raises:
32
+ ValueError: if the font file could not be loaded.
33
+ """
34
+ font_id = QFontDatabase.addApplicationFont(str(path))
35
+
36
+ if font_id == -1:
37
+ raise ValueError(f"Failed to load font from: {path}")
38
+
39
+ families = QFontDatabase.applicationFontFamilies(font_id)
40
+ if not families:
41
+ raise ValueError(f"No font families found in: {path}")
42
+
43
+ font = QFont(families[0])
44
+ font.setPixelSize(size) # pixel size, so DPI can't rescale us into mush
45
+ font.setWeight(weight)
46
+ font.setItalic(italic)
47
+
48
+ if aliased:
49
+ # Hard pixel edges + snap stems to the pixel grid == comctl32 look
50
+ font.setStyleStrategy(
51
+ QFont.StyleStrategy(
52
+ QFont.StyleStrategy.NoAntialias | QFont.StyleStrategy.PreferMatch
53
+ )
54
+ )
55
+ font.setHintingPreference(QFont.HintingPreference.PreferFullHinting)
56
+ else:
57
+ # Soft, slightly smeared — closer to ClearType on a non-LCD display
58
+ font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
59
+ font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
60
+
61
+ return font
62
+
63
+
64
+ def register_bold_variant(regular_family: str, bold_path: str | Path) -> None:
65
+ """
66
+ Load `bold_path` as the Bold weight of `regular_family`.
67
+
68
+ The bold TTF's own name/OS2 tables declare it as an unrelated Regular-weight
69
+ family (e.g. "MS Sans Serif Bold" @ weight 400), so Qt registers it as a
70
+ separate font family and `font-weight: bold` on the regular family just
71
+ synthesizes a fake bold instead of using the real hinted glyphs. Patch the
72
+ tables in memory so Qt sees it as `regular_family`'s Bold style.
73
+ """
74
+ font = TTFont(str(bold_path))
75
+
76
+ name = font["name"]
77
+ for name_id, value in (
78
+ (1, regular_family),
79
+ (2, "Bold"),
80
+ (4, f"{regular_family} Bold"),
81
+ (16, regular_family),
82
+ (17, "Bold"),
83
+ ):
84
+ name.setName(value, name_id, 3, 1, 0x409)
85
+ name.setName(value, name_id, 1, 0, 0)
86
+
87
+ os2 = font["OS/2"]
88
+ os2.usWeightClass = 700 # pyright: ignore[reportAttributeAccessIssue]
89
+ os2.fsSelection = ( # pyright: ignore[reportAttributeAccessIssue]
90
+ os2.fsSelection & ~0x40 # pyright: ignore[reportAttributeAccessIssue]
91
+ ) | 0x20 # clear REGULAR, set BOLD
92
+ font["head"].macStyle |= 0x1 # pyright: ignore[reportAttributeAccessIssue]
93
+
94
+ buf = io.BytesIO()
95
+ font.save(buf)
96
+
97
+ if QFontDatabase.addApplicationFontFromData(buf.getvalue()) == -1:
98
+ raise ValueError(f"Failed to register bold variant from: {bold_path}")
@@ -0,0 +1,200 @@
1
+ # pyright: basic
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QKeySequence, QShortcut
7
+ from PySide6.QtWidgets import (
8
+ QAbstractItemView,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QLineEdit,
12
+ QPushButton,
13
+ QTreeWidget,
14
+ QTreeWidgetItem,
15
+ QWidget,
16
+ )
17
+
18
+ ROW_OBJECT_NAME = "tfbRow"
19
+
20
+ MATCH_BG = "#FFF2A8"
21
+ CURRENT_MATCH_BG = "#FFC63F"
22
+
23
+ # Space kept around a row's fields so a highlight doesn't hug the text
24
+ ROW_PADDING = 3
25
+
26
+
27
+ def row_text(layout: QHBoxLayout) -> str:
28
+ """Flattens a rendered row into one searchable string.
29
+
30
+ Reads the already-built widgets rather than the opcode itself: rendering a
31
+ reference needs the enclosing-op stack, which only exists while the tree is
32
+ being populated.
33
+ """
34
+ parts: list[str] = []
35
+ for i in range(layout.count()):
36
+ item = layout.itemAt(i)
37
+ if item is None:
38
+ continue
39
+ widget = item.widget()
40
+ if isinstance(widget, (QLabel, QPushButton)):
41
+ parts.append(widget.text())
42
+ return " ".join(parts)
43
+
44
+
45
+ @dataclass
46
+ class SearchEntry:
47
+ item: QTreeWidgetItem
48
+ content: QWidget
49
+ _text: str = field(default="", repr=False)
50
+
51
+ def text(self) -> str:
52
+ return self._text
53
+
54
+ def set_text(self, text: str) -> None:
55
+ self._text = text.lower()
56
+
57
+
58
+ class SearchBar(QWidget):
59
+ def __init__(self, parent: QWidget | None = None) -> None:
60
+ super().__init__(parent)
61
+
62
+ layout = QHBoxLayout(self)
63
+ layout.setContentsMargins(6, 3, 6, 3)
64
+ layout.setSpacing(4)
65
+
66
+ self.edit = QLineEdit()
67
+ self.edit.setPlaceholderText("Find")
68
+ self.edit.setClearButtonEnabled(False)
69
+
70
+ self.status = QLabel("")
71
+ self.status.setMinimumWidth(90)
72
+ self.status.setAlignment(
73
+ Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
74
+ )
75
+
76
+ self.prev_button = QPushButton("Previous")
77
+ self.next_button = QPushButton("Next")
78
+ self.close_button = QPushButton("Close")
79
+
80
+ layout.addWidget(QLabel("Find:"))
81
+ layout.addWidget(self.edit, 1)
82
+ layout.addWidget(self.status)
83
+ layout.addWidget(self.prev_button)
84
+ layout.addWidget(self.next_button)
85
+ layout.addWidget(self.close_button)
86
+
87
+
88
+ class TreeSearch:
89
+ """Incremental find over the rows of the opcode tree.
90
+
91
+ Rows are custom item widgets, so matching runs against the flattened text of
92
+ each opcode and the hit is shown by tinting the row's container widget.
93
+ """
94
+
95
+ def __init__(self, tree: QTreeWidget, bar: SearchBar, entries: list[SearchEntry]):
96
+ self.tree = tree
97
+ self.bar = bar
98
+ self.entries = entries
99
+ self.matches: list[int] = []
100
+ self.current = -1
101
+
102
+ for entry in entries:
103
+ entry.content.setObjectName(ROW_OBJECT_NAME)
104
+ entry.content.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
105
+
106
+ bar.edit.textChanged.connect(self._on_query_changed)
107
+ bar.edit.returnPressed.connect(self.next_match)
108
+ bar.next_button.clicked.connect(self.next_match)
109
+ bar.prev_button.clicked.connect(self.previous_match)
110
+ bar.close_button.clicked.connect(self.close)
111
+
112
+ shift_return = QShortcut(
113
+ QKeySequence(Qt.KeyboardModifier.ShiftModifier | Qt.Key.Key_Return), bar.edit
114
+ )
115
+ shift_return.setContext(Qt.ShortcutContext.WidgetShortcut)
116
+ shift_return.activated.connect(self.previous_match)
117
+
118
+ escape = QShortcut(QKeySequence(Qt.Key.Key_Escape), bar)
119
+ escape.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
120
+ escape.activated.connect(self.close)
121
+
122
+ bar.hide()
123
+
124
+ def open(self) -> None:
125
+ self.bar.show()
126
+ self.bar.edit.setFocus()
127
+ self.bar.edit.selectAll()
128
+ self._search(self.bar.edit.text())
129
+
130
+ def close(self) -> None:
131
+ self._clear_highlights()
132
+ self.matches = []
133
+ self.current = -1
134
+ self.bar.hide()
135
+
136
+ def next_match(self) -> None:
137
+ self._step(1)
138
+
139
+ def previous_match(self) -> None:
140
+ self._step(-1)
141
+
142
+ def _on_query_changed(self, query: str) -> None:
143
+ self._search(query)
144
+
145
+ def _search(self, query: str) -> None:
146
+ self._clear_highlights()
147
+
148
+ needle = query.strip().lower()
149
+ if not needle:
150
+ self.matches = []
151
+ self.current = -1
152
+ self.bar.status.setText("")
153
+ return
154
+
155
+ self.matches = [
156
+ i for i, entry in enumerate(self.entries) if needle in entry.text()
157
+ ]
158
+ self.current = 0 if self.matches else -1
159
+ self._apply_highlights()
160
+ self._reveal_current()
161
+ self._update_status()
162
+
163
+ def _step(self, delta: int) -> None:
164
+ if not self.matches:
165
+ return
166
+ self.current = (self.current + delta) % len(self.matches)
167
+ self._apply_highlights()
168
+ self._reveal_current()
169
+ self._update_status()
170
+
171
+ def _clear_highlights(self) -> None:
172
+ for index in self.matches:
173
+ self.entries[index].content.setStyleSheet("")
174
+
175
+ def _apply_highlights(self) -> None:
176
+ for position, index in enumerate(self.matches):
177
+ color = CURRENT_MATCH_BG if position == self.current else MATCH_BG
178
+ self.entries[index].content.setStyleSheet(
179
+ f"#{ROW_OBJECT_NAME} {{ background-color: {color}; }}"
180
+ )
181
+
182
+ def _reveal_current(self) -> None:
183
+ if self.current < 0:
184
+ return
185
+ item = self.entries[self.matches[self.current]].item
186
+
187
+ parent = item.parent()
188
+ while parent is not None:
189
+ parent.setExpanded(True)
190
+ parent = parent.parent()
191
+
192
+ self.tree.scrollToItem(
193
+ item, QAbstractItemView.ScrollHint.PositionAtCenter
194
+ )
195
+
196
+ def _update_status(self) -> None:
197
+ if not self.matches:
198
+ self.bar.status.setText("No results")
199
+ else:
200
+ self.bar.status.setText(f"{self.current + 1} of {len(self.matches)}")
@@ -0,0 +1,141 @@
1
+ # pyright: basic
2
+
3
+ from PySide6.QtCore import Qt, QRect
4
+ from PySide6.QtGui import QColor, QPainter, QPalette, QPen
5
+ from PySide6.QtWidgets import QProxyStyle, QStyle, QStyleOption, QWidget
6
+
7
+ BG = QColor("#DCDCDC")
8
+ TEXT = QColor("#101010")
9
+
10
+
11
+ def tfb_colored_box(element: QWidget, color: str):
12
+ element.setStyleSheet(
13
+ f"background-color: {color}; color: #FFFFFF; padding: 0px 4px;"
14
+ )
15
+
16
+
17
+ def build_palette() -> QPalette:
18
+ palette = QPalette()
19
+ palette.setColor(QPalette.ColorRole.Window, BG)
20
+ palette.setColor(QPalette.ColorRole.WindowText, TEXT)
21
+ palette.setColor(QPalette.ColorRole.Base, BG)
22
+ palette.setColor(QPalette.ColorRole.AlternateBase, BG.lighter(105))
23
+ palette.setColor(QPalette.ColorRole.ToolTipBase, BG)
24
+ palette.setColor(QPalette.ColorRole.ToolTipText, TEXT)
25
+ palette.setColor(QPalette.ColorRole.Text, TEXT)
26
+ palette.setColor(QPalette.ColorRole.Button, BG)
27
+ palette.setColor(QPalette.ColorRole.ButtonText, TEXT)
28
+ palette.setColor(QPalette.ColorRole.BrightText, QColor("red"))
29
+ palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
30
+ palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
31
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("white"))
32
+
33
+ # Disabled state, so grayed-out buttons/labels don't go invisible
34
+ palette.setColor(
35
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor("#707070")
36
+ )
37
+ palette.setColor(
38
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor("#707070")
39
+ )
40
+ palette.setColor(
41
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor("#707070")
42
+ )
43
+
44
+ return palette
45
+
46
+
47
+ class Win98TreeStyle(QProxyStyle):
48
+ """Redraws QTreeView branch connectors as classic Win98/comctl32-style
49
+ dotted lines and a dotted-bordered +/- box, instead of Fusion's arrows."""
50
+
51
+ LINE_COLOR = QColor("#97A1AD")
52
+ BOX_SIZE = 11
53
+
54
+ CONTENT_OFFSET = 0
55
+
56
+ BOX_INSET = 20
57
+
58
+ def drawPrimitive(
59
+ self,
60
+ element: QStyle.PrimitiveElement,
61
+ option: QStyleOption,
62
+ painter: QPainter,
63
+ widget: QWidget | None = None,
64
+ ) -> None:
65
+ if element == QStyle.PrimitiveElement.PE_IndicatorBranch:
66
+ self._draw_branch(option, painter)
67
+ return
68
+ super().drawPrimitive(element, option, painter, widget)
69
+
70
+ def _draw_branch(self, option: QStyleOption, painter: QPainter) -> None:
71
+ rect = option.rect
72
+ mid_x = max(rect.left(), rect.right() - self.BOX_INSET)
73
+ mid_y = rect.center().y()
74
+ state = option.state
75
+
76
+ has_children = bool(state & QStyle.StateFlag.State_Children)
77
+ has_sibling = bool(state & QStyle.StateFlag.State_Sibling)
78
+ is_item = bool(state & QStyle.StateFlag.State_Item)
79
+ is_open = bool(state & QStyle.StateFlag.State_Open)
80
+
81
+ painter.save()
82
+ pen = QPen(self.LINE_COLOR)
83
+ pen.setWidth(1)
84
+ pen.setStyle(Qt.PenStyle.DotLine)
85
+ painter.setPen(pen)
86
+
87
+ if has_sibling:
88
+ painter.drawLine(
89
+ mid_x + self.CONTENT_OFFSET,
90
+ rect.top(),
91
+ mid_x + self.CONTENT_OFFSET,
92
+ rect.bottom(),
93
+ )
94
+ elif is_item:
95
+ painter.drawLine(
96
+ mid_x + self.CONTENT_OFFSET,
97
+ rect.top(),
98
+ mid_x + self.CONTENT_OFFSET,
99
+ mid_y,
100
+ )
101
+
102
+ # Horizontal line
103
+ if is_item:
104
+ painter.drawLine(
105
+ mid_x + self.CONTENT_OFFSET,
106
+ mid_y,
107
+ rect.right() + self.CONTENT_OFFSET,
108
+ mid_y,
109
+ )
110
+
111
+ painter.restore()
112
+
113
+ if has_children:
114
+ box_size = max(7, min(self.BOX_SIZE, rect.width() - 2, rect.height() - 2))
115
+ if box_size % 2 == 0:
116
+ box_size -= 1
117
+ half = box_size // 2
118
+
119
+ box_x = mid_x - half
120
+ box_y = mid_y - half
121
+ box = QRect(box_x, box_y, box_size, box_size)
122
+
123
+ painter.save()
124
+ painter.setPen(QPen(QColor("#788897"))) # BOX BORDER COLOR
125
+ painter.setBrush(QColor("#DCDCDC")) # BOX BG COLOR
126
+ painter.drawRect(box)
127
+
128
+ painter.setPen(QPen(QColor("#3F3F3F"))) # SIGN COLOR
129
+
130
+ # + or - sign
131
+ sign_x = mid_x + self.CONTENT_OFFSET
132
+ painter.drawLine(box.left() + 2, box_y + half, box.right() - 2, box_y + half) # -
133
+ if not is_open: # | (| + - = +)
134
+ painter.drawLine(
135
+ sign_x,
136
+ box.top() + 2,
137
+ sign_x,
138
+ box.bottom() - 2,
139
+ )
140
+
141
+ painter.restore()