PyLibMS 4.2.5__tar.gz → 4.3.0__tar.gz
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.
- {pylibms-4.2.5 → pylibms-4.3.0}/PKG-INFO +1 -1
- {pylibms-4.2.5 → pylibms-4.3.0}/PyLibMS.egg-info/PKG-INFO +1 -1
- {pylibms-4.2.5 → pylibms-4.3.0}/PyLibMS.egg-info/SOURCES.txt +1 -2
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/definitions/flowchart.py +3 -11
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/definitions/node.py +2 -2
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/definitions/node_type.py +13 -13
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/flw3.py +30 -31
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/msbf.py +75 -13
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/msbfio.py +10 -20
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/config.py +27 -12
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/definitions/nodes.py +6 -10
- {pylibms-4.2.5 → pylibms-4.3.0}/pyproject.toml +1 -1
- pylibms-4.2.5/lms/titleconfig/presets/Tomodachi Life NA-EU.yaml +0 -7506
- {pylibms-4.2.5 → pylibms-4.3.0}/LICENSE +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/MANIFEST.in +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/PyLibMS.egg-info/dependency_links.txt +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/PyLibMS.egg-info/requires.txt +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/PyLibMS.egg-info/top_level.txt +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/README.md +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/field/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/field/io.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/field/lms_datatype.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/field/lms_field.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/lms_constants.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/lms_exceptions.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/lms_fileinfo.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/stream/fileinfo.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/stream/hashtable.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/common/stream/section.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/fileio/encoding.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/fileio/io.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/definitions/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/flowchart/definitions/lms_nodeexceptions.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/definitions/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/definitions/field/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/lms_messagetext.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/msbt.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/msbtentry.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/msbtio.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/section/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/section/atr1.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/section/nli1.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/section/tsy1.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/section/txt2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/tag/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/tag/io/param_io.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/tag/io/tag_io.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/tag/lms_tag.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/message/tag/lms_tagexceptions.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/definitions/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/definitions/attribute.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/definitions/color.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/definitions/style.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/definitions/tag.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/msbp.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/msbpread.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/ali2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/ati2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/clr1.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/string.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/syl3.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/tag2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/tgg2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/project/section/tgp2.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/definitions/__init__.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/definitions/attribute.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/definitions/tags.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/definitions/value.py +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml +0 -0
- {pylibms-4.2.5 → pylibms-4.3.0}/setup.cfg +0 -0
|
@@ -78,5 +78,4 @@ lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml
|
|
|
78
78
|
lms/titleconfig/presets/Super Mario Odyssey.yaml
|
|
79
79
|
lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml
|
|
80
80
|
lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml
|
|
81
|
-
lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml
|
|
82
|
-
lms/titleconfig/presets/Tomodachi Life NA-EU.yaml
|
|
81
|
+
lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml
|
|
@@ -52,20 +52,12 @@ class LMS_Flowchart:
|
|
|
52
52
|
|
|
53
53
|
:param node: the node to add.
|
|
54
54
|
"""
|
|
55
|
-
if
|
|
56
|
-
raise
|
|
57
|
-
f"Node type '{type(node).__name__}' is not a child of LMS_BaseNode!"
|
|
58
|
-
)
|
|
55
|
+
if node.id is None:
|
|
56
|
+
raise ValueError("Node must be registered to an MSBF file before a flowchart!")
|
|
59
57
|
|
|
60
|
-
if node in self._nodes.values():
|
|
61
|
-
raise ValueError(
|
|
62
|
-
f"Node of ID '{node.id}' is already registered to this flowchart!"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
node.id = self._id_generator()
|
|
66
58
|
self._nodes[node.id] = node
|
|
67
59
|
|
|
68
|
-
def
|
|
60
|
+
def deregister_node(self, node_id: int) -> None:
|
|
69
61
|
"""
|
|
70
62
|
Deletes a node from the flowchart.
|
|
71
63
|
|
|
@@ -16,7 +16,7 @@ from lms.titleconfig.definitions.nodes import NodeDefinition
|
|
|
16
16
|
|
|
17
17
|
type LMS_NodeParameter = LMS_FieldMap | int | str | tuple[int, ...]
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
NO_VALUE = 0xFFFF
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class LMS_BaseNode:
|
|
@@ -531,7 +531,7 @@ class LMS_JumpNode(LMS_BaseNode):
|
|
|
531
531
|
self.unknown_short0a = unknown_short0a
|
|
532
532
|
|
|
533
533
|
@classmethod
|
|
534
|
-
def new(cls, next_entry_point: LMS_EntryNode, unknown_short0a: int):
|
|
534
|
+
def new(cls, next_entry_point: LMS_EntryNode | None = None, unknown_short0a: int = NO_VALUE):
|
|
535
535
|
"""
|
|
536
536
|
Instantiates a new jump node.
|
|
537
537
|
|
|
@@ -21,11 +21,11 @@ class LMS_NodeType(IntEnum):
|
|
|
21
21
|
See https://nintendo-formats.com/libs/lms/msbf.html#node-types for more information.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
MESSAGE =
|
|
25
|
-
BRANCH =
|
|
26
|
-
EVENT =
|
|
27
|
-
ENTRY =
|
|
28
|
-
JUMP =
|
|
24
|
+
MESSAGE = 0x1
|
|
25
|
+
BRANCH = 0x2
|
|
26
|
+
EVENT = 0x3
|
|
27
|
+
ENTRY = 0x4
|
|
28
|
+
JUMP = 0x5
|
|
29
29
|
|
|
30
30
|
@classmethod
|
|
31
31
|
def from_string(cls, string: str):
|
|
@@ -44,14 +44,14 @@ class LMS_NodeParameterType(IntEnum):
|
|
|
44
44
|
See https://nintendo-formats.com/libs/lms/msbf.html#parameter-types for more information.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
-
PARAM_32_0 =
|
|
48
|
-
PARAM_16_16 =
|
|
49
|
-
PARAM_16_8_8 =
|
|
50
|
-
PARAM_8_8_16 =
|
|
51
|
-
PARAM_8_8_8_8 =
|
|
52
|
-
STRING =
|
|
53
|
-
PARAM_32_1 =
|
|
54
|
-
NONE =
|
|
47
|
+
PARAM_32_0 = 0x0
|
|
48
|
+
PARAM_16_16 = 0x1
|
|
49
|
+
PARAM_16_8_8 = 0x2
|
|
50
|
+
PARAM_8_8_16 = 0x3
|
|
51
|
+
PARAM_8_8_8_8 = 0x4
|
|
52
|
+
STRING = 0x5
|
|
53
|
+
PARAM_32_1 = 0x6
|
|
54
|
+
NONE = 0xFFFF
|
|
55
55
|
|
|
56
56
|
@property
|
|
57
57
|
def sliced_datatype(self) -> tuple[LMS_DataType, ...]:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
1
3
|
from lms.common.field.io import read_field, write_field
|
|
2
4
|
from lms.common.field.lms_datatype import LMS_DataType
|
|
3
5
|
from lms.common.field.lms_field import LMS_FieldMap, LMS_Field
|
|
@@ -23,11 +25,7 @@ NODE_SIZE = 16
|
|
|
23
25
|
VARIABLE_WIDTH_DATATYPES = (LMS_DataType.BOOL, LMS_DataType.LIST)
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
def read_flw3(
|
|
27
|
-
reader: FileReader,
|
|
28
|
-
config: NodeConfig | None,
|
|
29
|
-
msbt: MSBT | None,
|
|
30
|
-
) -> tuple[int, dict[int, LMS_EntryNode]]:
|
|
28
|
+
def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None, ) -> list[LMS_EntryNode]:
|
|
31
29
|
section_start = reader.tell()
|
|
32
30
|
node_count = reader.read_uint16()
|
|
33
31
|
branch_table_id_count = reader.read_uint16()
|
|
@@ -39,12 +37,13 @@ def read_flw3(
|
|
|
39
37
|
entry_nodes: list[LMS_EntryNode] = []
|
|
40
38
|
|
|
41
39
|
next_id_map: dict[LMS_BaseNode, int | None] = {}
|
|
42
|
-
branch_metadata: dict[LMS_BranchNode,
|
|
40
|
+
branch_metadata: dict[LMS_BranchNode, tuple] = {}
|
|
43
41
|
|
|
42
|
+
stream_next_id = None
|
|
44
43
|
for i in range(node_count):
|
|
45
44
|
node_data = reader.tell() + 8
|
|
46
45
|
node_type = LMS_NodeType(reader.read_uint8())
|
|
47
|
-
parameter_type = LMS_NodeParameterType(reader.
|
|
46
|
+
parameter_type = LMS_NodeParameterType(reader.read_uint8())
|
|
48
47
|
|
|
49
48
|
reader.skip(2)
|
|
50
49
|
|
|
@@ -119,7 +118,7 @@ def read_flw3(
|
|
|
119
118
|
next_id_map[node] = stream_next_id
|
|
120
119
|
nodes.append(node)
|
|
121
120
|
|
|
122
|
-
node_map: dict[int, LMS_BaseNode] = {node.id: node for node in nodes}
|
|
121
|
+
node_map: dict[int, LMS_BaseNode | None] = {node.id: node for node in nodes}
|
|
123
122
|
branch_ids = [read_next_node_id(reader) for _ in range(branch_table_id_count)]
|
|
124
123
|
|
|
125
124
|
for node in nodes:
|
|
@@ -129,7 +128,7 @@ def read_flw3(
|
|
|
129
128
|
case_count, starting_index = branch_metadata[node]
|
|
130
129
|
|
|
131
130
|
# Slice the portion of IDs linked to this branch node and add the node objects themselves
|
|
132
|
-
for branch_id in branch_ids[starting_index
|
|
131
|
+
for branch_id in branch_ids[starting_index: starting_index + case_count]:
|
|
133
132
|
if branch_id is None:
|
|
134
133
|
node.add_branch(None)
|
|
135
134
|
else:
|
|
@@ -145,29 +144,29 @@ def read_flw3(
|
|
|
145
144
|
|
|
146
145
|
node.set_next_node(node_map[next_node_id])
|
|
147
146
|
|
|
148
|
-
return
|
|
147
|
+
return entry_nodes
|
|
149
148
|
|
|
150
149
|
|
|
151
150
|
def read_next_node_id(reader: FileReader) -> int | None:
|
|
152
|
-
return None if (
|
|
151
|
+
return None if (node_id := reader.read_uint16()) == NO_NEXT_NODE else node_id
|
|
153
152
|
|
|
154
153
|
|
|
155
154
|
def get_config_definition(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
config: NodeConfig | None,
|
|
156
|
+
node_type: Literal[LMS_NodeType.BRANCH, LMS_NodeType.EVENT],
|
|
157
|
+
parameter_type: LMS_NodeParameterType,
|
|
158
|
+
identifier: int,
|
|
160
159
|
) -> NodeDefinition:
|
|
161
160
|
return (
|
|
162
|
-
None if config is None else config.get_definition(
|
|
161
|
+
None if config is None else config.get_definition(identifier, node_type, parameter_type)
|
|
163
162
|
)
|
|
164
163
|
|
|
165
164
|
|
|
166
165
|
def evaluate_node_parameter(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
reader: FileReader,
|
|
167
|
+
section_start: int,
|
|
168
|
+
parameter_type: LMS_NodeParameterType,
|
|
169
|
+
definition: NodeDefinition | None,
|
|
171
170
|
) -> LMS_NodeParameter:
|
|
172
171
|
# How functions may utilize node parameters vary, such that they may cast values as they wish.
|
|
173
172
|
# By default, PyLibMS interprets the parameter types using signed integers for simplicity.
|
|
@@ -186,7 +185,7 @@ def evaluate_node_parameter(
|
|
|
186
185
|
)
|
|
187
186
|
|
|
188
187
|
for i, param_definition in enumerate(definition.parameter_definitions):
|
|
189
|
-
# By default VARIABLE_WIDTH_DATATYPES are allocated a single byte for its index.
|
|
188
|
+
# By default, VARIABLE_WIDTH_DATATYPES are allocated a single byte for its index.
|
|
190
189
|
# Some games may utilize a whole 4 byte stack as an index and/or 2 bytes depending on the parameter type.
|
|
191
190
|
stream_map = {
|
|
192
191
|
1: reader.read_uint8,
|
|
@@ -225,7 +224,7 @@ def evaluate_node_parameter(
|
|
|
225
224
|
|
|
226
225
|
|
|
227
226
|
def write_flw3(
|
|
228
|
-
|
|
227
|
+
writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[LMS_BaseNode, int]
|
|
229
228
|
) -> None:
|
|
230
229
|
node_count = len(nodes)
|
|
231
230
|
|
|
@@ -241,8 +240,8 @@ def write_flw3(
|
|
|
241
240
|
|
|
242
241
|
writer.write_bytes(b"\x00" * 12)
|
|
243
242
|
for node in nodes:
|
|
244
|
-
writer.
|
|
245
|
-
writer.
|
|
243
|
+
writer.write_uint8(node.type)
|
|
244
|
+
writer.write_uint8(node.parameter_type)
|
|
246
245
|
|
|
247
246
|
match node:
|
|
248
247
|
case LMS_MessageNode():
|
|
@@ -264,8 +263,9 @@ def write_flw3(
|
|
|
264
263
|
if node.parameter_type is LMS_NodeParameterType.STRING:
|
|
265
264
|
string_offsets.append(writer.tell())
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
definition = node.definition
|
|
267
|
+
if definition is not None:
|
|
268
|
+
parameter_definition = definition.parameter_definitions[0]
|
|
269
269
|
field = node.parameter_value[parameter_definition.name]
|
|
270
270
|
string_table.append(field.value)
|
|
271
271
|
else:
|
|
@@ -336,7 +336,7 @@ def write_flw3(
|
|
|
336
336
|
writer.write_uint16(node_id)
|
|
337
337
|
|
|
338
338
|
string_offset = (
|
|
339
|
-
|
|
339
|
+
FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
|
|
340
340
|
)
|
|
341
341
|
|
|
342
342
|
for offset, string in zip(string_offsets, string_table):
|
|
@@ -359,11 +359,10 @@ def write_flw3(
|
|
|
359
359
|
|
|
360
360
|
|
|
361
361
|
def write_node_parameter(
|
|
362
|
-
|
|
362
|
+
writer: FileWriter, value: LMS_NodeParameter, parameter_type: LMS_NodeParameterType
|
|
363
363
|
) -> None:
|
|
364
364
|
if isinstance(value, LMS_FieldMap):
|
|
365
365
|
for i, field in enumerate(value):
|
|
366
|
-
# In MSBF files there could be a case where naturally one bit datatypes take up 2/4 bits.
|
|
367
366
|
if field.datatype in VARIABLE_WIDTH_DATATYPES:
|
|
368
367
|
stream_map = {
|
|
369
368
|
1: writer.write_uint8,
|
|
@@ -400,5 +399,5 @@ def write_node_parameter(
|
|
|
400
399
|
writer.write_uint8(value[3])
|
|
401
400
|
|
|
402
401
|
|
|
403
|
-
def write_next_node_id(writer: FileWriter,
|
|
404
|
-
writer.write_uint16(NO_NEXT_NODE if
|
|
402
|
+
def write_next_node_id(writer: FileWriter, node_id: int | None) -> None:
|
|
403
|
+
writer.write_uint16(NO_NEXT_NODE if node_id is None else node_id)
|
|
@@ -3,7 +3,7 @@ from types import MappingProxyType
|
|
|
3
3
|
from lms.common.lms_fileinfo import LMS_FileInfo
|
|
4
4
|
from lms.fileio.encoding import FileEncoding
|
|
5
5
|
from lms.flowchart.definitions.flowchart import LMS_Flowchart
|
|
6
|
-
from lms.flowchart.definitions.node import LMS_EntryNode, LMS_JumpNode, LMS_BranchNode
|
|
6
|
+
from lms.flowchart.definitions.node import LMS_EntryNode, LMS_JumpNode, LMS_BranchNode, LMS_BaseNode
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MSBF:
|
|
@@ -29,7 +29,9 @@ class MSBF:
|
|
|
29
29
|
self, info: LMS_FileInfo | None = None, flowcharts: list[LMS_Flowchart] = None
|
|
30
30
|
):
|
|
31
31
|
self._info = info if info is not None else LMS_FileInfo()
|
|
32
|
+
self._shared_references: dict[int, LMS_BaseNode] = {}
|
|
32
33
|
self._flowcharts = flowcharts or []
|
|
34
|
+
self._nodes: dict[int, LMS_BaseNode] = {}
|
|
33
35
|
self._global_node_id = 0
|
|
34
36
|
|
|
35
37
|
def __iter__(self):
|
|
@@ -62,6 +64,16 @@ class MSBF:
|
|
|
62
64
|
"""The file info for the MSBT instance."""
|
|
63
65
|
return self._info
|
|
64
66
|
|
|
67
|
+
@property
|
|
68
|
+
def nodes(self) -> MappingProxyType[int, LMS_BaseNode]:
|
|
69
|
+
"""Global node map for the file."""
|
|
70
|
+
return MappingProxyType(self._nodes)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def shared_references(self) -> MappingProxyType[int, LMS_BaseNode]:
|
|
74
|
+
"""Shared references of nodes between flowcharts."""
|
|
75
|
+
return self._shared_references
|
|
76
|
+
|
|
65
77
|
@property
|
|
66
78
|
def flowcharts(self) -> MappingProxyType[str, LMS_Flowchart]:
|
|
67
79
|
"""The flowcharts of the MSBF instance."""
|
|
@@ -69,35 +81,77 @@ class MSBF:
|
|
|
69
81
|
{flowchart.name: flowchart for flowchart in self._flowcharts}
|
|
70
82
|
)
|
|
71
83
|
|
|
72
|
-
def
|
|
73
|
-
|
|
84
|
+
def register_node(self, node: LMS_BaseNode, flowchart: LMS_Flowchart = None) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Registers a node to the MSBF instance.
|
|
74
87
|
|
|
75
|
-
|
|
88
|
+
:param node: The node to register.
|
|
89
|
+
:param flowchart: optional argument that registers the node to that flowchart.
|
|
76
90
|
"""
|
|
77
|
-
|
|
91
|
+
if not isinstance(node, LMS_BaseNode):
|
|
92
|
+
raise TypeError(
|
|
93
|
+
f"Node type '{type(node).__name__}' is not a child of LMS_BaseNode!"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if node.id in self._nodes:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Node of ID '{node.id}' is already registered to this flowchart!"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
node.id = self._id_generator()
|
|
102
|
+
self._nodes[node.id] = node
|
|
103
|
+
|
|
104
|
+
if flowchart is not None:
|
|
105
|
+
flowchart.register_node(node)
|
|
106
|
+
|
|
107
|
+
def destroy_node(self, node_id: int) -> None:
|
|
78
108
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
109
|
+
Destroys a node and all its references in every flowchart.
|
|
110
|
+
|
|
111
|
+
:param node_id: the id of the node to delete.
|
|
112
|
+
"""
|
|
113
|
+
if node_id not in self._nodes:
|
|
114
|
+
raise KeyError(f"Node id '{node_id}' does not exist in this flowchart!")
|
|
115
|
+
|
|
116
|
+
deleted_node = self._nodes[node_id]
|
|
117
|
+
|
|
118
|
+
if isinstance(deleted_node, LMS_EntryNode):
|
|
119
|
+
raise ValueError("Entry nodes cannot be deleted!")
|
|
120
|
+
|
|
121
|
+
for flowchart in self._flowcharts:
|
|
122
|
+
if node_id in flowchart.nodes:
|
|
123
|
+
flowchart.deregister_node(node_id)
|
|
124
|
+
|
|
125
|
+
del self._nodes[node_id]
|
|
82
126
|
|
|
83
127
|
def add_flowchart(
|
|
84
|
-
self, flowchart_name: str, entry_point: LMS_EntryNode
|
|
128
|
+
self, flowchart_name: str, entry_point: LMS_EntryNode = None
|
|
85
129
|
) -> LMS_Flowchart:
|
|
86
130
|
"""
|
|
87
131
|
Add a flowchart to the MSBF instance.
|
|
88
132
|
|
|
89
133
|
:param flowchart_name: The name of the new flowchart.
|
|
134
|
+
:param entry_point: The entry point of the new flowchart. If not provided, the method creates one.
|
|
90
135
|
"""
|
|
91
136
|
if flowchart_name in self.flowcharts:
|
|
92
137
|
raise KeyError(f"Flowchart with name '{flowchart_name}' already exists!")
|
|
93
138
|
|
|
139
|
+
# Node IDs may not be sequential after user additions/deletions, so we track the absolute max value of
|
|
140
|
+
# all nodes and store it so that _generate_next_id can set any IDs added via the flowchart correctly.
|
|
141
|
+
self._global_node_id = max((node.id for flowchart in self for node in flowchart), default=1) + 1
|
|
142
|
+
|
|
94
143
|
if entry_point is None:
|
|
95
|
-
entry_point = LMS_EntryNode(
|
|
96
|
-
self.generate_next_id(), flowchart_name=flowchart_name
|
|
97
|
-
)
|
|
144
|
+
entry_point = LMS_EntryNode(self._generate_next_id(), flowchart_name)
|
|
98
145
|
|
|
99
|
-
flowchart = LMS_Flowchart(entry_point, self.
|
|
146
|
+
flowchart = LMS_Flowchart(entry_point, self._generate_next_id)
|
|
100
147
|
self._flowcharts.append(flowchart)
|
|
148
|
+
|
|
149
|
+
for flowchart in self._flowcharts:
|
|
150
|
+
for node_id, node in flowchart.nodes.items():
|
|
151
|
+
self._shared_references.setdefault(node.id, set()).add(flowchart)
|
|
152
|
+
self._nodes[node_id] = node
|
|
153
|
+
|
|
154
|
+
self._nodes = dict(sorted(self._nodes.items()))
|
|
101
155
|
return flowchart
|
|
102
156
|
|
|
103
157
|
def delete_flowchart(self, name: str):
|
|
@@ -124,3 +178,11 @@ class MSBF:
|
|
|
124
178
|
|
|
125
179
|
if is_valid_jump(node):
|
|
126
180
|
node.next_flowchart = None
|
|
181
|
+
|
|
182
|
+
def _generate_next_id(self) -> int:
|
|
183
|
+
"""
|
|
184
|
+
Lazy generation for the next ID of a node. IDs are normalized properly when the msbf file is being written.
|
|
185
|
+
"""
|
|
186
|
+
next_id = self._global_node_id
|
|
187
|
+
self._global_node_id += 1
|
|
188
|
+
return next_id
|
|
@@ -9,7 +9,6 @@ from lms.common.stream.fileinfo import read_file_info, write_file_info, write_fi
|
|
|
9
9
|
from lms.common.stream.hashtable import read_labels, write_labels
|
|
10
10
|
from lms.common.stream.section import read_section_data, write_section
|
|
11
11
|
from lms.fileio.io import FileReader, FileWriter
|
|
12
|
-
from lms.flowchart.definitions.node import LMS_BaseNode
|
|
13
12
|
from lms.flowchart.flw3 import read_flw3, write_flw3
|
|
14
13
|
from lms.flowchart.msbf import MSBF
|
|
15
14
|
from lms.message.msbt import MSBT
|
|
@@ -19,9 +18,9 @@ __all__ = ("read_msbf", "read_msbf_path", "write_msbf", "write_msbf_path")
|
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def read_msbf_path(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
file_path: str,
|
|
22
|
+
config: NodeConfig | None = None,
|
|
23
|
+
msbt: MSBT | None = None,
|
|
25
24
|
) -> MSBF:
|
|
26
25
|
"""
|
|
27
26
|
Reads a MSBF file from a path.
|
|
@@ -40,14 +39,15 @@ def read_msbf_path(
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def read_msbf(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
stream: BinaryIO | bytes,
|
|
43
|
+
config: NodeConfig | None = None,
|
|
44
|
+
msbt: MSBT | None = None,
|
|
46
45
|
) -> MSBF:
|
|
47
46
|
"""
|
|
48
47
|
Reads a MSBF file from a stream.
|
|
49
48
|
|
|
50
49
|
:param stream: Stream to read the file from.
|
|
50
|
+
:param config: the node configuration to use.
|
|
51
51
|
:param msbt: MSBT object for decoding message nodes.
|
|
52
52
|
|
|
53
53
|
=====
|
|
@@ -63,15 +63,13 @@ def read_msbf(
|
|
|
63
63
|
for magic, size in read_section_data(reader, file_info.section_count):
|
|
64
64
|
match magic:
|
|
65
65
|
case "FLW3":
|
|
66
|
-
|
|
66
|
+
entry_nodes = read_flw3(reader, config, msbt)
|
|
67
67
|
case "FEN1":
|
|
68
68
|
labels, _ = read_labels(reader)
|
|
69
69
|
case "REF1":
|
|
70
70
|
raise LMS_UnsupportedSectionError("""REF1 was found in the MSBF file! Please report this file as an issue
|
|
71
71
|
in the PyLibMS repository https://github.com/AbdyyEee/PylibMS""")
|
|
72
72
|
|
|
73
|
-
msbf.set_global_id_count(node_count)
|
|
74
|
-
|
|
75
73
|
for i, label in enumerate(labels.values()):
|
|
76
74
|
entry_nodes[i].flowchart_name = label
|
|
77
75
|
msbf.add_flowchart(label, entry_nodes[i])
|
|
@@ -113,17 +111,9 @@ def write_msbf(file: MSBF) -> bytes:
|
|
|
113
111
|
writer = FileWriter(file.info.encoding)
|
|
114
112
|
write_file_info(writer, MSBF.MAGIC, file.info)
|
|
115
113
|
|
|
116
|
-
nodes: set[LMS_BaseNode] = set()
|
|
117
|
-
|
|
118
|
-
for flowchart in file:
|
|
119
|
-
for node in flowchart.nodes.values():
|
|
120
|
-
nodes.add(node)
|
|
121
|
-
|
|
122
|
-
nodes = sorted(list(nodes), key=lambda node: node.id)
|
|
123
|
-
|
|
124
114
|
# Real IDs that must be sequential will be written to the stream
|
|
125
115
|
# Saved here so ids aren't mutated in the current state
|
|
126
|
-
stream_ids = {node: stream_id for stream_id, node in enumerate(nodes)}
|
|
116
|
+
stream_ids = {node: stream_id for stream_id, node in enumerate(file.nodes.values())}
|
|
127
117
|
|
|
128
118
|
for chart in file:
|
|
129
119
|
if amount := len(chart.get_dangling_nodes()):
|
|
@@ -135,7 +125,7 @@ def write_msbf(file: MSBF) -> bytes:
|
|
|
135
125
|
flowchart.name: stream_ids[flowchart.entry_point] for flowchart in file
|
|
136
126
|
}
|
|
137
127
|
|
|
138
|
-
write_section(writer, "FLW3", write_flw3, nodes, stream_ids)
|
|
128
|
+
write_section(writer, "FLW3", write_flw3, list(file.nodes.values()), stream_ids)
|
|
139
129
|
write_section(
|
|
140
130
|
writer,
|
|
141
131
|
"FEN1",
|
|
@@ -29,24 +29,39 @@ class TitleConfig:
|
|
|
29
29
|
NODE_KEY = "node_definitions"
|
|
30
30
|
|
|
31
31
|
GAME_PRESET = Literal[
|
|
32
|
-
"
|
|
32
|
+
"Animal Crossing: City Folk",
|
|
33
|
+
"Animal Crossing: New Horizons",
|
|
34
|
+
"Animal Crossing: New Leaf",
|
|
35
|
+
"The Legend of Zelda: Echoes of Wisdom"
|
|
36
|
+
"Metroid Prime 4: Beyond"
|
|
37
|
+
"Mario vs. Donkey Kong"
|
|
38
|
+
"Paper Mario: Color Splash"
|
|
39
|
+
"Paper Mario: Sticker Star"
|
|
40
|
+
"Paper Mario: The Origami King"
|
|
41
|
+
"Paper Mario: The Thousand-Year Door (Nintendo Switch)"
|
|
42
|
+
"Super Mario 3D Land"
|
|
43
|
+
"Super Mario 3D World + Bowser's Fury"
|
|
44
|
+
"Super Mario Odyssey"
|
|
45
|
+
"Splatoon 3"
|
|
46
|
+
"The Legend of Zelda: Link's Awakening (Nintendo Switch)"
|
|
47
|
+
"The Legend of Zelda a Link Between Worlds",
|
|
48
|
+
"The Legend of Zelda: Tri Force Heroes"
|
|
49
|
+
"Tomodachi Life (NA-EU)",
|
|
50
|
+
"Tomodachi Life (JP)",
|
|
51
|
+
"Tomodachi Life: Living the Dream",
|
|
52
|
+
"The Legend of Zelda: Tears of the Kingdom"
|
|
53
|
+
"Wii Sports Club"
|
|
33
54
|
"Brain Age Concentration Training",
|
|
34
55
|
"Kirby Planet Robobot",
|
|
35
56
|
"Super Mario Odyssey",
|
|
36
|
-
"Super Mario 3D Land",
|
|
37
|
-
"Super Mario 3D World + Bowsers Fury",
|
|
38
|
-
"The Legend of Zelda a Link Between Worlds",
|
|
39
|
-
"The Legend of Zelda Echos of Wisdom",
|
|
40
|
-
"Tomodachi Life Living The Dream",
|
|
41
|
-
"Tomodachi Life NA-EU",
|
|
42
57
|
]
|
|
43
58
|
|
|
44
59
|
def __init__(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
self,
|
|
61
|
+
game: str | None,
|
|
62
|
+
attribute_config_map: dict[str, AttributeConfig] | None = None,
|
|
63
|
+
tag_config: TagConfig | None = None,
|
|
64
|
+
node_config: NodeConfig | None = None,
|
|
50
65
|
):
|
|
51
66
|
self._game = game
|
|
52
67
|
self._attribute_config_map = attribute_config_map
|
|
@@ -68,7 +68,7 @@ class NodeDefinition:
|
|
|
68
68
|
@classmethod
|
|
69
69
|
def from_dict(cls, identifier: int, node_type: LMS_NodeType, data: dict) -> NodeDefinition | None:
|
|
70
70
|
|
|
71
|
-
description = data.get("description", "")
|
|
71
|
+
name, description = data["name"], data.get("description", "")
|
|
72
72
|
converted_parameters: list[ValueDefinition] = []
|
|
73
73
|
parameter_type = LMS_NodeParameterType.from_string(data["parameter_type"])
|
|
74
74
|
|
|
@@ -128,18 +128,14 @@ class NodeDefinition:
|
|
|
128
128
|
"There may only be one of case_format and options in the definition."
|
|
129
129
|
)
|
|
130
130
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
) is not None and node_type == LMS_NodeType.EVENT:
|
|
134
|
-
raise ValueError("There may only be options for branch nodes!")
|
|
131
|
+
if (case_options := data.get("case_options", "")) and node_type is LMS_NodeType.EVENT:
|
|
132
|
+
raise ValueError(f"There may only be options for branch nodes for definition '{name}'")
|
|
135
133
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
) is not None and node_type == LMS_NodeType.EVENT:
|
|
139
|
-
raise ValueError("There may only be case_format for branch nodes!")
|
|
134
|
+
if (case_format := data.get("case_format", "")) and node_type == LMS_NodeType.EVENT:
|
|
135
|
+
raise ValueError(f"There may only be case_format for branch nodes, '{name}' is an event definition!")
|
|
140
136
|
|
|
141
137
|
return NodeDefinition(
|
|
142
|
-
name=
|
|
138
|
+
name=name,
|
|
143
139
|
id=identifier,
|
|
144
140
|
description=data.get("description", ""),
|
|
145
141
|
type=node_type,
|
|
@@ -7,7 +7,7 @@ include = ["lms*"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "PyLibMS"
|
|
10
|
-
version = "4.
|
|
10
|
+
version = "4.3.0"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
description = "Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and MSBF."
|
|
13
13
|
readme = "README.md"
|