PyLibMS 4.2.5__tar.gz → 4.3.1__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.
Files changed (84) hide show
  1. {pylibms-4.2.5 → pylibms-4.3.1}/PKG-INFO +1 -1
  2. {pylibms-4.2.5 → pylibms-4.3.1}/PyLibMS.egg-info/PKG-INFO +1 -1
  3. {pylibms-4.2.5 → pylibms-4.3.1}/PyLibMS.egg-info/SOURCES.txt +1 -2
  4. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/definitions/flowchart.py +3 -11
  5. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/definitions/node.py +2 -2
  6. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/definitions/node_type.py +13 -13
  7. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/flw3.py +30 -31
  8. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/msbf.py +75 -13
  9. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/msbfio.py +10 -20
  10. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/config.py +27 -12
  11. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/definitions/nodes.py +6 -10
  12. {pylibms-4.2.5 → pylibms-4.3.1}/pyproject.toml +1 -1
  13. pylibms-4.2.5/lms/titleconfig/presets/Tomodachi Life NA-EU.yaml +0 -7506
  14. {pylibms-4.2.5 → pylibms-4.3.1}/LICENSE +0 -0
  15. {pylibms-4.2.5 → pylibms-4.3.1}/MANIFEST.in +0 -0
  16. {pylibms-4.2.5 → pylibms-4.3.1}/PyLibMS.egg-info/dependency_links.txt +0 -0
  17. {pylibms-4.2.5 → pylibms-4.3.1}/PyLibMS.egg-info/requires.txt +0 -0
  18. {pylibms-4.2.5 → pylibms-4.3.1}/PyLibMS.egg-info/top_level.txt +0 -0
  19. {pylibms-4.2.5 → pylibms-4.3.1}/README.md +0 -0
  20. {pylibms-4.2.5 → pylibms-4.3.1}/lms/__init__.py +0 -0
  21. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/__init__.py +0 -0
  22. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/field/__init__.py +0 -0
  23. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/field/io.py +0 -0
  24. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/field/lms_datatype.py +0 -0
  25. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/field/lms_field.py +0 -0
  26. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/lms_constants.py +0 -0
  27. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/lms_exceptions.py +0 -0
  28. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/lms_fileinfo.py +0 -0
  29. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/stream/fileinfo.py +0 -0
  30. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/stream/hashtable.py +0 -0
  31. {pylibms-4.2.5 → pylibms-4.3.1}/lms/common/stream/section.py +0 -0
  32. {pylibms-4.2.5 → pylibms-4.3.1}/lms/fileio/encoding.py +0 -0
  33. {pylibms-4.2.5 → pylibms-4.3.1}/lms/fileio/io.py +0 -0
  34. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/__init__.py +0 -0
  35. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/definitions/__init__.py +0 -0
  36. {pylibms-4.2.5 → pylibms-4.3.1}/lms/flowchart/definitions/lms_nodeexceptions.py +0 -0
  37. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/__init__.py +0 -0
  38. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/definitions/__init__.py +0 -0
  39. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/definitions/field/__init__.py +0 -0
  40. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/lms_messagetext.py +0 -0
  41. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/msbt.py +0 -0
  42. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/msbtentry.py +0 -0
  43. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/msbtio.py +0 -0
  44. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/section/__init__.py +0 -0
  45. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/section/atr1.py +0 -0
  46. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/section/nli1.py +0 -0
  47. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/section/tsy1.py +0 -0
  48. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/section/txt2.py +0 -0
  49. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/tag/__init__.py +0 -0
  50. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/tag/io/param_io.py +0 -0
  51. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/tag/io/tag_io.py +0 -0
  52. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/tag/lms_tag.py +0 -0
  53. {pylibms-4.2.5 → pylibms-4.3.1}/lms/message/tag/lms_tagexceptions.py +0 -0
  54. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/__init__.py +0 -0
  55. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/definitions/__init__.py +0 -0
  56. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/definitions/attribute.py +0 -0
  57. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/definitions/color.py +0 -0
  58. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/definitions/style.py +0 -0
  59. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/definitions/tag.py +0 -0
  60. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/msbp.py +0 -0
  61. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/msbpread.py +0 -0
  62. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/ali2.py +0 -0
  63. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/ati2.py +0 -0
  64. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/clr1.py +0 -0
  65. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/string.py +0 -0
  66. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/syl3.py +0 -0
  67. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/tag2.py +0 -0
  68. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/tgg2.py +0 -0
  69. {pylibms-4.2.5 → pylibms-4.3.1}/lms/project/section/tgp2.py +0 -0
  70. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/__init__.py +0 -0
  71. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/definitions/__init__.py +0 -0
  72. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/definitions/attribute.py +0 -0
  73. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/definitions/tags.py +0 -0
  74. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/definitions/value.py +0 -0
  75. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
  76. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
  77. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
  78. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
  79. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
  80. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
  81. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
  82. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
  83. {pylibms-4.2.5 → pylibms-4.3.1}/lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml +0 -0
  84. {pylibms-4.2.5 → pylibms-4.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.2.5
3
+ Version: 4.3.1
4
4
  Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and MSBF.
5
5
  Author: AbdyyEee
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.2.5
3
+ Version: 4.3.1
4
4
  Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and MSBF.
5
5
  Author: AbdyyEee
6
6
  License-Expression: MIT
@@ -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 not isinstance(node, LMS_BaseNode):
56
- raise TypeError(
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 delete_node(self, node_id: int) -> None:
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
- NO_NEXT_NODE = -1
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 = 1
25
- BRANCH = 2
26
- EVENT = 3
27
- ENTRY = 4
28
- JUMP = 5
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 = 0
48
- PARAM_16_16 = 1
49
- PARAM_16_8_8 = 2
50
- PARAM_8_8_16 = 3
51
- PARAM_8_8_8_8 = 4
52
- STRING = 5
53
- PARAM_32_1 = 6
54
- NONE = -1
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 = 0xFF
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, dict] = {}
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.read_int8())
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 : starting_index + case_count]:
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 node_count, entry_nodes
147
+ return entry_nodes
149
148
 
150
149
 
151
150
  def read_next_node_id(reader: FileReader) -> int | None:
152
- return None if (id := reader.read_uint16()) == NO_NEXT_NODE else id
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
- config: NodeConfig,
157
- node_type: LMS_NodeType.BRANCH | LMS_NodeType.EVENT,
158
- parameter_type: LMS_NodeParameterType,
159
- id: int,
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(id, node_type, parameter_type)
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
- reader: FileReader,
168
- section_start: int,
169
- parameter_type: LMS_NodeParameterType,
170
- definition: NodeDefinition | None,
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
- writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[LMS_BaseNode, int]
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.write_int8(node.type)
245
- writer.write_int8(node.parameter_type)
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
- if node.definition is not None:
268
- parameter_definition = node.definition.parameter_definitions[0]
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
- FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
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
- writer: FileWriter, value: LMS_NodeParameter, parameter_type: LMS_NodeParameterType
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, id: int | None) -> None:
404
- writer.write_uint16(NO_NEXT_NODE if id is None else id)
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 set_global_id_count(self, value: int) -> None:
73
- self._global_node_id = value
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
- def generate_next_id(self) -> int:
88
+ :param node: The node to register.
89
+ :param flowchart: optional argument that registers the node to that flowchart.
76
90
  """
77
- Lazy generation for the next ID of a node. IDs are normalized properly when the msbf file is being written.
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
- next_id = self._global_node_id
80
- self._global_node_id += 1
81
- return next_id
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 | None = None
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.generate_next_id)
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
- file_path: str,
23
- config: NodeConfig | None = None,
24
- msbt: MSBT | None = None,
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
- stream: BinaryIO | bytes,
44
- node_config: NodeConfig | None = None,
45
- msbt: MSBT | None = None,
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
- node_count, entry_nodes = read_flw3(reader, node_config, msbt)
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
- "Badge Arcade",
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
- self,
46
- game: str | None,
47
- attribute_config_map: dict[str, AttributeConfig] | None = None,
48
- tag_config: TagConfig | None = None,
49
- node_config: NodeConfig | None = None,
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
- case_options := data.get("case_options", "")
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
- case_format := data.get("case_format", "")
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=data["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.2.5"
10
+ version = "4.3.1"
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"