PyLibMS 4.2.1__tar.gz → 4.2.3__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.1 → pylibms-4.2.3}/PKG-INFO +2 -2
  2. {pylibms-4.2.1 → pylibms-4.2.3}/PyLibMS.egg-info/PKG-INFO +2 -2
  3. {pylibms-4.2.1 → pylibms-4.2.3}/README.md +1 -1
  4. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/definitions/node.py +94 -11
  5. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/flw3.py +340 -340
  6. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/definitions/nodes.py +9 -1
  7. {pylibms-4.2.1 → pylibms-4.2.3}/pyproject.toml +1 -1
  8. {pylibms-4.2.1 → pylibms-4.2.3}/LICENSE +0 -0
  9. {pylibms-4.2.1 → pylibms-4.2.3}/MANIFEST.in +0 -0
  10. {pylibms-4.2.1 → pylibms-4.2.3}/PyLibMS.egg-info/SOURCES.txt +0 -0
  11. {pylibms-4.2.1 → pylibms-4.2.3}/PyLibMS.egg-info/dependency_links.txt +0 -0
  12. {pylibms-4.2.1 → pylibms-4.2.3}/PyLibMS.egg-info/requires.txt +0 -0
  13. {pylibms-4.2.1 → pylibms-4.2.3}/PyLibMS.egg-info/top_level.txt +0 -0
  14. {pylibms-4.2.1 → pylibms-4.2.3}/lms/__init__.py +0 -0
  15. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/__init__.py +0 -0
  16. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/field/__init__.py +0 -0
  17. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/field/io.py +0 -0
  18. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/field/lms_datatype.py +0 -0
  19. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/field/lms_field.py +0 -0
  20. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/lms_constants.py +0 -0
  21. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/lms_exceptions.py +0 -0
  22. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/lms_fileinfo.py +0 -0
  23. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/stream/fileinfo.py +0 -0
  24. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/stream/hashtable.py +0 -0
  25. {pylibms-4.2.1 → pylibms-4.2.3}/lms/common/stream/section.py +0 -0
  26. {pylibms-4.2.1 → pylibms-4.2.3}/lms/fileio/encoding.py +0 -0
  27. {pylibms-4.2.1 → pylibms-4.2.3}/lms/fileio/io.py +0 -0
  28. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/__init__.py +0 -0
  29. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/definitions/__init__.py +0 -0
  30. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/definitions/flowchart.py +0 -0
  31. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/definitions/lms_nodeexceptions.py +0 -0
  32. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/definitions/node_type.py +0 -0
  33. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/msbf.py +0 -0
  34. {pylibms-4.2.1 → pylibms-4.2.3}/lms/flowchart/msbfio.py +0 -0
  35. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/__init__.py +0 -0
  36. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/definitions/__init__.py +0 -0
  37. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/definitions/field/__init__.py +0 -0
  38. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/lms_messagetext.py +0 -0
  39. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/msbt.py +0 -0
  40. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/msbtentry.py +0 -0
  41. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/msbtio.py +0 -0
  42. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/section/__init__.py +0 -0
  43. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/section/atr1.py +0 -0
  44. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/section/nli1.py +0 -0
  45. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/section/tsy1.py +0 -0
  46. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/section/txt2.py +0 -0
  47. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/tag/__init__.py +0 -0
  48. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/tag/io/param_io.py +0 -0
  49. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/tag/io/tag_io.py +0 -0
  50. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/tag/lms_tag.py +0 -0
  51. {pylibms-4.2.1 → pylibms-4.2.3}/lms/message/tag/lms_tagexceptions.py +0 -0
  52. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/__init__.py +0 -0
  53. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/definitions/__init__.py +0 -0
  54. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/definitions/attribute.py +0 -0
  55. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/definitions/color.py +0 -0
  56. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/definitions/style.py +0 -0
  57. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/definitions/tag.py +0 -0
  58. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/msbp.py +0 -0
  59. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/msbpread.py +0 -0
  60. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/ali2.py +0 -0
  61. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/ati2.py +0 -0
  62. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/clr1.py +0 -0
  63. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/string.py +0 -0
  64. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/syl3.py +0 -0
  65. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/tag2.py +0 -0
  66. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/tgg2.py +0 -0
  67. {pylibms-4.2.1 → pylibms-4.2.3}/lms/project/section/tgp2.py +0 -0
  68. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/__init__.py +0 -0
  69. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/config.py +0 -0
  70. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/definitions/__init__.py +0 -0
  71. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/definitions/attribute.py +0 -0
  72. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/definitions/tags.py +0 -0
  73. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/definitions/value.py +0 -0
  74. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
  75. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
  76. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
  77. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
  78. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
  79. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
  80. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
  81. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
  82. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml +0 -0
  83. {pylibms-4.2.1 → pylibms-4.2.3}/lms/titleconfig/presets/Tomodachi Life NA-EU.yaml +0 -0
  84. {pylibms-4.2.1 → pylibms-4.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.2.1
3
+ Version: 4.2.3
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
@@ -107,7 +107,7 @@ pip install -e .
107
107
  ```
108
108
 
109
109
  # Credits & Sources
110
-
110
+ * [SS-Decomp](https://github.com/zeldaret/ss): Referenced some parts of their MSBF decomp when adding the format to [Nintendo-File-Formats](https://nintendo-formats.com) and this library.
111
111
  * [Nintendo-File-Formats](https://nintendo-formats.com) by Kinnay: For existing information on the MSBT and MSBP file
112
112
  formats.
113
113
  * [Trippixyz](https://github.com/Trippixyz): For helping me get started general decompilation of the formats and general
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.2.1
3
+ Version: 4.2.3
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
@@ -107,7 +107,7 @@ pip install -e .
107
107
  ```
108
108
 
109
109
  # Credits & Sources
110
-
110
+ * [SS-Decomp](https://github.com/zeldaret/ss): Referenced some parts of their MSBF decomp when adding the format to [Nintendo-File-Formats](https://nintendo-formats.com) and this library.
111
111
  * [Nintendo-File-Formats](https://nintendo-formats.com) by Kinnay: For existing information on the MSBT and MSBP file
112
112
  formats.
113
113
  * [Trippixyz](https://github.com/Trippixyz): For helping me get started general decompilation of the formats and general
@@ -86,7 +86,7 @@ pip install -e .
86
86
  ```
87
87
 
88
88
  # Credits & Sources
89
-
89
+ * [SS-Decomp](https://github.com/zeldaret/ss): Referenced some parts of their MSBF decomp when adding the format to [Nintendo-File-Formats](https://nintendo-formats.com) and this library.
90
90
  * [Nintendo-File-Formats](https://nintendo-formats.com) by Kinnay: For existing information on the MSBT and MSBP file
91
91
  formats.
92
92
  * [Trippixyz](https://github.com/Trippixyz): For helping me get started general decompilation of the formats and general
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import string
3
4
  from collections import deque
4
5
  from types import MappingProxyType
5
6
  from typing import Generator
@@ -156,18 +157,18 @@ class LMS_MessageNode(LMS_BaseNode):
156
157
  :param file_index: index of the MSBT file in a folder/archive.
157
158
  :param label_index: index of the label entry in that MSBT file.
158
159
  """
159
- return cls(None, None, file_index, label_index)
160
+ return cls(None, file_index, label_index)
160
161
 
161
162
  @classmethod
162
163
  def new_msbt(cls, file_index: int, entry: MSBTEntry, msbt: MSBT):
163
164
  """
164
- Creates a new LMS_MessageNode given a MSBT object.
165
+ Creates a new ``LMS_MessageNode`` given a MSBT object.
165
166
 
166
167
  :param file_index: index of the MSBT file in a folder/archive.
167
168
  :param entry: the MSBTEntry object to reference.
168
169
  :param msbt: the MSBT file.
169
170
  """
170
- return cls(None, None, file_index, msbt.entries.index(entry), msbt)
171
+ return cls(None, file_index, msbt.entries.index(entry), msbt)
171
172
 
172
173
  @property
173
174
  def msbt(self) -> MSBT | None:
@@ -270,6 +271,67 @@ class LMS_BranchNode(LMS_BaseNode):
270
271
  """The node branches."""
271
272
  return MappingProxyType(self._branches)
272
273
 
274
+ def get_case_options(self, compress_shared_references: bool = False) -> list[
275
+ tuple[tuple[int, ...], str, LMS_BaseNode | None]]:
276
+ """
277
+ Retrieves the cases from this node formatted with the proper messages.
278
+
279
+ If a definition is provided, and either ``case_format`` or ``case_options`` are provied, the resulting
280
+ message will utilize those values to format the message.
281
+
282
+ :param compress_shared_references: whether to compress shared references into one case.
283
+ """
284
+ result, case_map = [], {}
285
+
286
+ for case, branch in self._branches.items():
287
+ case_map.setdefault(branch, []).append(case)
288
+
289
+ if self.definition is not None:
290
+
291
+ if self.definition.case_options:
292
+ for case, branch in self._branches.items():
293
+ case_message = self.definition.case_options.get(case, f"Case {case}")
294
+
295
+ result.append(((case,), case_message, branch))
296
+
297
+ return result
298
+
299
+ if self.definition.case_format is not None:
300
+ if compress_shared_references:
301
+ for branch, cases in case_map.items():
302
+ case_message = self._format_case_message(merge_shared_cases(cases))
303
+ result.append((tuple(cases), case_message, branch))
304
+ else:
305
+ for case, branch in self._branches.items():
306
+ case_message = self._format_case_message(case)
307
+ result.append(((case,), case_message, branch))
308
+
309
+ return result
310
+
311
+ if compress_shared_references:
312
+ for branch, cases in case_map.items():
313
+ case_message = f"Case {cases[0]}" if len(cases) == 1 else f"Cases {merge_shared_cases(cases)}"
314
+ result.append((tuple(cases), case_message, branch))
315
+ else:
316
+ for case, branch in self._branches.items():
317
+ case_message = f"Case {case}"
318
+ result.append(((case,), case_message, branch))
319
+
320
+ return result
321
+
322
+ def _format_case_message(self, case_prefix: str):
323
+ prefix = string.Template(self._definition.case_format)
324
+
325
+ # Unpack dict for formatting $case and $param formats
326
+ # Allows inserting parameter values dynamically into the case message
327
+ string_map = {}
328
+ for param_definition in self.definition.parameter_definitions:
329
+ string_map[param_definition.name] = self.parameter_value[
330
+ param_definition.name].value
331
+
332
+ string_map["case"] = case_prefix
333
+ return prefix.substitute(**string_map)
334
+
273
335
  def add_branch(self, node: LMS_BaseNode | None) -> None:
274
336
  """
275
337
  Adds a branch to the node.
@@ -339,7 +401,7 @@ class LMS_EventNode(LMS_BaseNode):
339
401
  id: int,
340
402
  parameter_type: LMS_NodeParameterType,
341
403
  parameter_value: LMS_FieldMap | int | str | tuple[int, ...],
342
- action_id: int,
404
+ event_id: int,
343
405
  definition: NodeDefinition | None = None,
344
406
  ):
345
407
  super().__init__(
@@ -348,7 +410,7 @@ class LMS_EventNode(LMS_BaseNode):
348
410
  parameter_type,
349
411
  parameter_value)
350
412
 
351
- self._action_id = action_id
413
+ self._event_id = event_id
352
414
  self._definition = definition
353
415
 
354
416
  def __repr__(self):
@@ -357,17 +419,17 @@ class LMS_EventNode(LMS_BaseNode):
357
419
  return f"{self._definition.name}({ {field.name: field.value for field in self._parameter_value} } {self.id}"
358
420
 
359
421
  @classmethod
360
- def new(cls, parameter_type: LMS_NodeParameterType, parameter_value: int | str | tuple[int, ...], action_id: int):
422
+ def new(cls, parameter_type: LMS_NodeParameterType, parameter_value: int | str | tuple[int, ...], event_id: int):
361
423
  """
362
424
  Instantiates a new event node.
363
425
 
364
426
  :param parameter_type: the parameter type.
365
427
  :param parameter_value: the parameter value.
366
- :param action_id: the action_id of the node.
428
+ :param event_id: the event_id of the node.
367
429
 
368
430
  """
369
431
  verify_parameter_structure(parameter_value, parameter_type)
370
- return cls(None, parameter_type, parameter_value, action_id)
432
+ return cls(None, parameter_type, parameter_value, event_id)
371
433
 
372
434
  @classmethod
373
435
  def new_from_definition(cls,
@@ -397,9 +459,9 @@ class LMS_EventNode(LMS_BaseNode):
397
459
  return self._definition.name
398
460
 
399
461
  @property
400
- def action_id(self) -> int:
462
+ def event_id(self) -> int:
401
463
  """The identifier to determine which action to run."""
402
- return self._action_id
464
+ return self._event_id
403
465
 
404
466
 
405
467
  class LMS_EntryNode(LMS_BaseNode):
@@ -443,7 +505,7 @@ class LMS_JumpNode(LMS_BaseNode):
443
505
  :param next_entry_point: the next entry point.
444
506
  :param unknown_short0a: unknown short value. -1 if there is no value.
445
507
  """
446
- node = cls(None, next_entry_point.id, unknown_short0a)
508
+ node = cls(None, unknown_short0a)
447
509
  node.next_flowchart = next_entry_point
448
510
  return node
449
511
 
@@ -474,3 +536,24 @@ def verify_parameter_structure(value: int | tuple[int, ...] | str, parameter_typ
474
536
  if isinstance(value, tuple) and len(value) != parameter_type.value_count:
475
537
  raise node_exceptions.LMS_NodeMissingParameterValueError(
476
538
  f"Parameter type {parameter_type} expects {parameter_type.value_count} parameter values, got {len(value)}!")
539
+
540
+
541
+ def merge_shared_cases(cases: list[int]) -> str:
542
+ cases = sorted(cases)
543
+
544
+ result = []
545
+ start = cases[0]
546
+ end = cases[0]
547
+
548
+ for case in cases[1:]:
549
+ if case == end + 1:
550
+ end = case
551
+ continue
552
+
553
+ result.append(str(start) if start == end else f"{start}-{end}")
554
+
555
+ start = case
556
+ end = case
557
+
558
+ result.append(str(start) if start == end else f"{start}-{end}")
559
+ return ", ".join(result)
@@ -1,340 +1,340 @@
1
- from lms.common.field.io import read_field, write_field
2
- from lms.common.field.lms_datatype import LMS_DataType
3
- from lms.common.field.lms_field import LMS_FieldMap, LMS_Field
4
- from lms.common.lms_exceptions import LMS_Error
5
- from lms.fileio.io import FileReader, FileWriter
6
- from lms.flowchart.definitions.node import LMS_MessageNode, LMS_EntryNode, \
7
- LMS_BranchNode, \
8
- LMS_EventNode, LMS_JumpNode, LMS_BaseNode, LMS_NodeParameter
9
- from lms.flowchart.definitions.node_type import LMS_NodeType, LMS_NodeParameterType
10
- from lms.message.msbt import MSBT
11
- from lms.titleconfig.definitions.nodes import NodeConfig, NodeDefinition
12
-
13
- NO_NEXT_NODE = 0xFFFF
14
- FLW3_HEADER_SIZE = 16
15
- NODE_SIZE = 16
16
-
17
- VARIABLE_WIDTH_DATATYPES = (LMS_DataType.BOOL, LMS_DataType.LIST)
18
-
19
-
20
- def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None, ) -> tuple[
21
- int, dict[int, LMS_EntryNode]]:
22
- section_start = reader.tell()
23
- node_count = reader.read_uint16()
24
- branch_table_id_count = reader.read_uint16()
25
-
26
- # Huge padding section skip
27
- reader.skip(12)
28
-
29
- nodes: list[LMS_BaseNode] = []
30
- entry_nodes: list[LMS_EntryNode] = []
31
-
32
- next_id_map: dict[LMS_BaseNode, int | None] = {}
33
- branch_metadata: dict[LMS_BranchNode, dict] = {}
34
-
35
- for i in range(node_count):
36
- node_data = reader.tell() + 8
37
- node_type = LMS_NodeType(reader.read_uint8())
38
- parameter_type = LMS_NodeParameterType(reader.read_int8())
39
-
40
- reader.skip(2)
41
-
42
- parameter_offset = reader.tell()
43
- match node_type:
44
- case LMS_NodeType.MESSAGE:
45
- reader.skip(4)
46
-
47
- stream_next_id = read_next_node_id(reader)
48
- file_index = reader.read_uint16()
49
- message_index = reader.read_uint16()
50
- node = LMS_MessageNode(i, file_index, message_index, msbt)
51
- reader.skip(2)
52
- case LMS_NodeType.BRANCH:
53
- # There is no next node id as the next node is determined by the branches
54
- # Skip parameter value alongside that
55
- reader.skip(2 + 4)
56
-
57
- condition_id = reader.read_uint16()
58
- case_count = reader.read_uint16()
59
- table_index = reader.read_uint16()
60
- end = reader.tell()
61
-
62
- reader.seek(parameter_offset)
63
- definition = get_config_definition(config, LMS_NodeType.BRANCH, parameter_type, condition_id)
64
- parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
65
-
66
- node = LMS_BranchNode(i, parameter_type, parameter_value, condition_id, definition)
67
-
68
- # Save the metadata for the branch node so that the branches itself can be set later on
69
- branch_metadata[node] = case_count, table_index
70
- reader.seek(end)
71
- case LMS_NodeType.EVENT:
72
- reader.skip(4)
73
-
74
- stream_next_id = read_next_node_id(reader)
75
- event_id = reader.read_uint16()
76
- end = reader.tell() + 4
77
-
78
- reader.seek(parameter_offset)
79
-
80
- definition = get_config_definition(config, LMS_NodeType.EVENT, parameter_type, event_id)
81
- parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
82
-
83
- node = LMS_EventNode(i, parameter_type, parameter_value, event_id, definition)
84
- reader.seek(end)
85
- case LMS_NodeType.ENTRY:
86
- reader.seek(node_data)
87
- stream_next_id = read_next_node_id(reader)
88
- node = LMS_EntryNode(i)
89
- entry_nodes.append(node)
90
- reader.skip(6)
91
- case LMS_NodeType.JUMP:
92
- reader.seek(node_data)
93
- stream_next_id = read_next_node_id(reader)
94
- unknown_short0a = reader.read_uint16()
95
- node = LMS_JumpNode(i, unknown_short0a)
96
- reader.skip(4)
97
-
98
- next_id_map[node] = stream_next_id
99
- nodes.append(node)
100
-
101
- node_map: dict[int, LMS_BaseNode] = {node.id: node for node in nodes}
102
- branch_ids = [read_next_node_id(reader) for _ in range(branch_table_id_count)]
103
-
104
- for node in nodes:
105
- next_node_id = next_id_map[node]
106
-
107
- if isinstance(node, LMS_BranchNode):
108
- case_count, starting_index = branch_metadata[node]
109
-
110
- # Slice the portion of IDs linked to this branch node and add the node objects themselves
111
- for branch_id in branch_ids[starting_index:starting_index + case_count]:
112
- if branch_id is None:
113
- node.add_branch(None)
114
- else:
115
- node.add_branch(node_map[branch_id])
116
-
117
- continue
118
- elif isinstance(node, LMS_JumpNode):
119
- node.next_flowchart = node_map[next_node_id]
120
- continue
121
-
122
- if next_node_id is None:
123
- continue
124
-
125
- node.set_next_node(node_map[next_node_id])
126
-
127
- return node_count, entry_nodes
128
-
129
-
130
- def read_next_node_id(reader: FileReader) -> int | None:
131
- return None if (id := reader.read_uint16()) == NO_NEXT_NODE else id
132
-
133
-
134
- def get_config_definition(config: NodeConfig,
135
- node_type: LMS_NodeType.BRANCH | LMS_NodeType.EVENT,
136
- parameter_type: LMS_NodeParameterType,
137
- id: int) -> NodeDefinition:
138
- return None if config is None else config.get_definition(id, node_type, parameter_type)
139
-
140
-
141
- def evaluate_node_parameter(reader: FileReader,
142
- section_start: int,
143
- parameter_type: LMS_NodeParameterType,
144
- definition: NodeDefinition | None) -> LMS_NodeParameter:
145
- # How functions may utilize node parameters vary, such that they may cast values as they wish.
146
- # By default, PyLibMS interprets the parameter types using signed integers for simplicity.
147
- # If the user believes a better interpretation fits, then they may utilize a node configuration
148
- # and cast individual parameters to any LMS_Datatype.
149
- # This is my understanding of how node parameters work, but updating may be necessary as more research surfaces.
150
- # -
151
- # It also noted that by default LIST types are allocated a single byte for its index, it is unknown if
152
- # some games may utilize a whole 4 byte stack as an index and/or 2 bytes depending on the parameter type.
153
- # For now, by PyLibMS will read a single byte for its index until proven otherwise.
154
- if definition is not None:
155
- result = {}
156
-
157
- if parameter_type is LMS_NodeParameterType.STRING:
158
- param_definition = definition.parameter_definitions[0]
159
- value = reader.read_string_offset(section_start)
160
-
161
- return LMS_FieldMap({
162
- param_definition.name: LMS_Field(value, param_definition)
163
- })
164
-
165
- for i, param_definition in enumerate(definition.parameter_definitions):
166
- stream_map = {1: reader.read_uint8, 2: reader.read_uint16, 4: reader.read_uint32}
167
- result[param_definition.name] = read_field(reader, param_definition,
168
- stream_map[parameter_type.sliced_datatype[i].stream_size])
169
-
170
- return LMS_FieldMap(result)
171
- else:
172
- # Default parameter reading
173
- match parameter_type:
174
- case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
175
- return reader.read_uint32()
176
- case LMS_NodeParameterType.PARAM_16_16:
177
- return reader.read_uint16(), reader.read_uint16()
178
- case LMS_NodeParameterType.PARAM_16_8_8:
179
- return reader.read_uint16(), reader.read_uint8(), reader.read_uint8()
180
- case LMS_NodeParameterType.PARAM_8_8_16:
181
- return reader.read_uint8(), reader.read_uint8(), reader.read_uint16()
182
- case LMS_NodeParameterType.PARAM_8_8_8_8:
183
- return reader.read_uint8(), reader.read_uint8(), reader.read_uint8(), reader.read_uint8()
184
- case LMS_NodeParameterType.STRING:
185
- return reader.read_string_offset(section_start)
186
- case _:
187
- raise LMS_Error(f"Invalid parameter type of '{parameter_type}'!")
188
-
189
-
190
- def write_flw3(writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[LMS_BaseNode, int]) -> None:
191
- node_count = len(nodes)
192
-
193
- branch_table: list[int] = []
194
- string_table: list[str] = []
195
- string_offsets: list[int] = []
196
-
197
- writer.write_uint16(node_count)
198
- branch_id_offset = writer.tell()
199
-
200
- # Placeholder value for branch table count
201
- writer.write_uint16(0)
202
-
203
- writer.write_bytes(b'\x00' * 12)
204
- for node in nodes:
205
- writer.write_int8(node.type)
206
- writer.write_int8(node.parameter_type)
207
-
208
- match node:
209
- case LMS_MessageNode():
210
- writer.write_bytes(b"\x00" * 2)
211
- writer.write_bytes(b"\x00" * 4)
212
-
213
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
214
-
215
- writer.write_uint16(node.msbt_index)
216
- writer.write_uint16(node.label_index)
217
-
218
- writer.write_bytes(b"\x00\x00")
219
- case LMS_BranchNode():
220
- writer.write_bytes(b"\x00\x00")
221
-
222
- if node.parameter_type is LMS_NodeParameterType.STRING:
223
- string_offsets.append(writer.tell())
224
-
225
- if node.definition is not None:
226
- parameter_definition = node.definition.parameter_definitions[0]
227
- field = node.parameter_value[parameter_definition.name]
228
- string_table.append(field.value)
229
- else:
230
- string_table.append(node.parameter_value)
231
-
232
- writer.write_uint32(0)
233
- else:
234
- write_node_parameter(writer, node.parameter_value, node.parameter_type)
235
-
236
- starting_index = len(branch_table)
237
- for branch in node.branches.values():
238
- if branch is None:
239
- branch_table.append(NO_NEXT_NODE)
240
- else:
241
- branch_table.append(stream_ids[branch])
242
-
243
- writer.write_uint16(NO_NEXT_NODE)
244
-
245
- writer.write_uint16(node.condition_id)
246
- writer.write_uint16(node.case_count)
247
- writer.write_uint16(starting_index)
248
- case LMS_EventNode():
249
- writer.write_bytes(b"\x00\x00")
250
-
251
- if node.parameter_type is LMS_NodeParameterType.STRING:
252
- string_offsets.append(writer.tell())
253
-
254
- if isinstance(node.parameter_value, LMS_FieldMap):
255
- parameter_definition = node.definition.parameter_definitions[0]
256
- field = node.parameter_value[parameter_definition.name]
257
- string_table.append(field.value)
258
- else:
259
- string_table.append(node.parameter_value)
260
-
261
- writer.write_uint32(0)
262
- else:
263
- write_node_parameter(writer, node.parameter_value, node.parameter_type)
264
-
265
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
266
- writer.write_uint16(node.action_id)
267
-
268
- writer.write_bytes(b"\x00" * 4)
269
- case LMS_EntryNode():
270
- writer.write_bytes(b"\x00" * 2)
271
- writer.write_bytes(b"\x00" * 4)
272
-
273
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
274
- writer.skip(6)
275
- case LMS_JumpNode():
276
- writer.write_bytes(b"\x00" * 2)
277
- writer.write_bytes(b"\x00" * 4)
278
-
279
- write_next_node_id(writer, stream_ids[node.next_flowchart])
280
- writer.write_uint16(node.unknown_short0a)
281
- writer.skip(4)
282
-
283
- for node_id in branch_table:
284
- writer.write_uint16(node_id)
285
-
286
- string_offset = FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
287
-
288
- for offset, string in zip(string_offsets, string_table):
289
- write_offset = writer.tell()
290
-
291
- writer.seek(offset)
292
- writer.write_uint32(string_offset)
293
-
294
- writer.seek(write_offset)
295
- writer.write_encoded_string(string)
296
-
297
- string_offset += len(string) * writer.encoding.width + len(writer.encoding.terminator)
298
-
299
- end = writer.tell()
300
- writer.seek(branch_id_offset)
301
- writer.write_uint16(len(branch_table))
302
- writer.seek(end)
303
-
304
-
305
- def write_node_parameter(writer: FileWriter,
306
- value: LMS_NodeParameter,
307
- parameter_type: LMS_NodeParameterType) -> None:
308
- if isinstance(value, LMS_FieldMap):
309
- for i, field in enumerate(value):
310
- # In MSBF files there could be a case where naturally one bit datatypes take up 2/4 bits.
311
- if field.datatype in VARIABLE_WIDTH_DATATYPES:
312
- stream_map = {1: writer.write_uint8, 2: writer.write_uint16, 3: writer.write_uint32}
313
- write_field(writer, field, stream_map[parameter_type.sliced_datatype[i].stream_size])
314
- else:
315
- write_field(writer, field)
316
- return
317
-
318
- match parameter_type:
319
- case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
320
- writer.write_uint32(value)
321
- case LMS_NodeParameterType.PARAM_16_16:
322
- writer.write_uint16(value[0])
323
- writer.write_uint16(value[1])
324
- case LMS_NodeParameterType.PARAM_16_8_8:
325
- writer.write_uint16(value[0])
326
- writer.write_uint8(value[1])
327
- writer.write_uint8(value[2])
328
- case LMS_NodeParameterType.PARAM_8_8_16:
329
- writer.write_uint8(value[0])
330
- writer.write_uint8(value[1])
331
- writer.write_uint16(value[2])
332
- case LMS_NodeParameterType.PARAM_8_8_8_8:
333
- writer.write_uint8(value[0])
334
- writer.write_uint8(value[1])
335
- writer.write_uint8(value[2])
336
- writer.write_uint8(value[3])
337
-
338
-
339
- def write_next_node_id(writer: FileWriter, id: int | None) -> None:
340
- writer.write_uint16(NO_NEXT_NODE if id is None else id)
1
+ from lms.common.field.io import read_field, write_field
2
+ from lms.common.field.lms_datatype import LMS_DataType
3
+ from lms.common.field.lms_field import LMS_FieldMap, LMS_Field
4
+ from lms.common.lms_exceptions import LMS_Error
5
+ from lms.fileio.io import FileReader, FileWriter
6
+ from lms.flowchart.definitions.node import LMS_MessageNode, LMS_EntryNode, \
7
+ LMS_BranchNode, \
8
+ LMS_EventNode, LMS_JumpNode, LMS_BaseNode, LMS_NodeParameter
9
+ from lms.flowchart.definitions.node_type import LMS_NodeType, LMS_NodeParameterType
10
+ from lms.message.msbt import MSBT
11
+ from lms.titleconfig.definitions.nodes import NodeConfig, NodeDefinition
12
+
13
+ NO_NEXT_NODE = 0xFFFF
14
+ FLW3_HEADER_SIZE = 16
15
+ NODE_SIZE = 16
16
+
17
+ VARIABLE_WIDTH_DATATYPES = (LMS_DataType.BOOL, LMS_DataType.LIST)
18
+
19
+
20
+ def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None, ) -> tuple[
21
+ int, dict[int, LMS_EntryNode]]:
22
+ section_start = reader.tell()
23
+ node_count = reader.read_uint16()
24
+ branch_table_id_count = reader.read_uint16()
25
+
26
+ # Huge padding section skip
27
+ reader.skip(12)
28
+
29
+ nodes: list[LMS_BaseNode] = []
30
+ entry_nodes: list[LMS_EntryNode] = []
31
+
32
+ next_id_map: dict[LMS_BaseNode, int | None] = {}
33
+ branch_metadata: dict[LMS_BranchNode, dict] = {}
34
+
35
+ for i in range(node_count):
36
+ node_data = reader.tell() + 8
37
+ node_type = LMS_NodeType(reader.read_uint8())
38
+ parameter_type = LMS_NodeParameterType(reader.read_int8())
39
+
40
+ reader.skip(2)
41
+
42
+ parameter_offset = reader.tell()
43
+ match node_type:
44
+ case LMS_NodeType.MESSAGE:
45
+ reader.skip(4)
46
+
47
+ stream_next_id = read_next_node_id(reader)
48
+ file_index = reader.read_uint16()
49
+ message_index = reader.read_uint16()
50
+ node = LMS_MessageNode(i, file_index, message_index, msbt)
51
+ reader.skip(2)
52
+ case LMS_NodeType.BRANCH:
53
+ # There is no next node id as the next node is determined by the branches
54
+ # Skip parameter value alongside that
55
+ reader.skip(2 + 4)
56
+
57
+ condition_id = reader.read_uint16()
58
+ case_count = reader.read_uint16()
59
+ table_index = reader.read_uint16()
60
+ end = reader.tell()
61
+
62
+ reader.seek(parameter_offset)
63
+ definition = get_config_definition(config, LMS_NodeType.BRANCH, parameter_type, condition_id)
64
+ parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
65
+
66
+ node = LMS_BranchNode(i, parameter_type, parameter_value, condition_id, definition)
67
+
68
+ # Save the metadata for the branch node so that the branches itself can be set later on
69
+ branch_metadata[node] = case_count, table_index
70
+ reader.seek(end)
71
+ case LMS_NodeType.EVENT:
72
+ reader.skip(4)
73
+
74
+ stream_next_id = read_next_node_id(reader)
75
+ event_id = reader.read_uint16()
76
+ end = reader.tell() + 4
77
+
78
+ reader.seek(parameter_offset)
79
+
80
+ definition = get_config_definition(config, LMS_NodeType.EVENT, parameter_type, event_id)
81
+ parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
82
+
83
+ node = LMS_EventNode(i, parameter_type, parameter_value, event_id, definition)
84
+ reader.seek(end)
85
+ case LMS_NodeType.ENTRY:
86
+ reader.seek(node_data)
87
+ stream_next_id = read_next_node_id(reader)
88
+ node = LMS_EntryNode(i)
89
+ entry_nodes.append(node)
90
+ reader.skip(6)
91
+ case LMS_NodeType.JUMP:
92
+ reader.seek(node_data)
93
+ stream_next_id = read_next_node_id(reader)
94
+ unknown_short0a = reader.read_uint16()
95
+ node = LMS_JumpNode(i, unknown_short0a)
96
+ reader.skip(4)
97
+
98
+ next_id_map[node] = stream_next_id
99
+ nodes.append(node)
100
+
101
+ node_map: dict[int, LMS_BaseNode] = {node.id: node for node in nodes}
102
+ branch_ids = [read_next_node_id(reader) for _ in range(branch_table_id_count)]
103
+
104
+ for node in nodes:
105
+ next_node_id = next_id_map[node]
106
+
107
+ if isinstance(node, LMS_BranchNode):
108
+ case_count, starting_index = branch_metadata[node]
109
+
110
+ # Slice the portion of IDs linked to this branch node and add the node objects themselves
111
+ for branch_id in branch_ids[starting_index:starting_index + case_count]:
112
+ if branch_id is None:
113
+ node.add_branch(None)
114
+ else:
115
+ node.add_branch(node_map[branch_id])
116
+
117
+ continue
118
+ elif isinstance(node, LMS_JumpNode):
119
+ node.next_flowchart = node_map[next_node_id]
120
+ continue
121
+
122
+ if next_node_id is None:
123
+ continue
124
+
125
+ node.set_next_node(node_map[next_node_id])
126
+
127
+ return node_count, entry_nodes
128
+
129
+
130
+ def read_next_node_id(reader: FileReader) -> int | None:
131
+ return None if (id := reader.read_uint16()) == NO_NEXT_NODE else id
132
+
133
+
134
+ def get_config_definition(config: NodeConfig,
135
+ node_type: LMS_NodeType.BRANCH | LMS_NodeType.EVENT,
136
+ parameter_type: LMS_NodeParameterType,
137
+ id: int) -> NodeDefinition:
138
+ return None if config is None else config.get_definition(id, node_type, parameter_type)
139
+
140
+
141
+ def evaluate_node_parameter(reader: FileReader,
142
+ section_start: int,
143
+ parameter_type: LMS_NodeParameterType,
144
+ definition: NodeDefinition | None) -> LMS_NodeParameter:
145
+ # How functions may utilize node parameters vary, such that they may cast values as they wish.
146
+ # By default, PyLibMS interprets the parameter types using signed integers for simplicity.
147
+ # If the user believes a better interpretation fits, then they may utilize a node configuration
148
+ # and cast individual parameters to any LMS_Datatype.
149
+ # This is my understanding of how node parameters work, but updating may be necessary as more research surfaces.
150
+ # -
151
+ # It also noted that by default LIST types are allocated a single byte for its index, it is unknown if
152
+ # some games may utilize a whole 4 byte stack as an index and/or 2 bytes depending on the parameter type.
153
+ # For now, by PyLibMS will read a single byte for its index until proven otherwise.
154
+ if definition is not None:
155
+ result = {}
156
+
157
+ if parameter_type is LMS_NodeParameterType.STRING:
158
+ param_definition = definition.parameter_definitions[0]
159
+ value = reader.read_string_offset(section_start)
160
+
161
+ return LMS_FieldMap({
162
+ param_definition.name: LMS_Field(value, param_definition)
163
+ })
164
+
165
+ for i, param_definition in enumerate(definition.parameter_definitions):
166
+ stream_map = {1: reader.read_uint8, 2: reader.read_uint16, 4: reader.read_uint32}
167
+ result[param_definition.name] = read_field(reader, param_definition,
168
+ stream_map[parameter_type.sliced_datatype[i].stream_size])
169
+
170
+ return LMS_FieldMap(result)
171
+ else:
172
+ # Default parameter reading
173
+ match parameter_type:
174
+ case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
175
+ return reader.read_uint32()
176
+ case LMS_NodeParameterType.PARAM_16_16:
177
+ return reader.read_uint16(), reader.read_uint16()
178
+ case LMS_NodeParameterType.PARAM_16_8_8:
179
+ return reader.read_uint16(), reader.read_uint8(), reader.read_uint8()
180
+ case LMS_NodeParameterType.PARAM_8_8_16:
181
+ return reader.read_uint8(), reader.read_uint8(), reader.read_uint16()
182
+ case LMS_NodeParameterType.PARAM_8_8_8_8:
183
+ return reader.read_uint8(), reader.read_uint8(), reader.read_uint8(), reader.read_uint8()
184
+ case LMS_NodeParameterType.STRING:
185
+ return reader.read_string_offset(section_start)
186
+ case _:
187
+ raise LMS_Error(f"Invalid parameter type of '{parameter_type}'!")
188
+
189
+
190
+ def write_flw3(writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[LMS_BaseNode, int]) -> None:
191
+ node_count = len(nodes)
192
+
193
+ branch_table: list[int] = []
194
+ string_table: list[str] = []
195
+ string_offsets: list[int] = []
196
+
197
+ writer.write_uint16(node_count)
198
+ branch_id_offset = writer.tell()
199
+
200
+ # Placeholder value for branch table count
201
+ writer.write_uint16(0)
202
+
203
+ writer.write_bytes(b'\x00' * 12)
204
+ for node in nodes:
205
+ writer.write_int8(node.type)
206
+ writer.write_int8(node.parameter_type)
207
+
208
+ match node:
209
+ case LMS_MessageNode():
210
+ writer.write_bytes(b"\x00" * 2)
211
+ writer.write_bytes(b"\x00" * 4)
212
+
213
+ write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
214
+
215
+ writer.write_uint16(node.msbt_index)
216
+ writer.write_uint16(node.label_index)
217
+
218
+ writer.write_bytes(b"\x00\x00")
219
+ case LMS_BranchNode():
220
+ writer.write_bytes(b"\x00\x00")
221
+
222
+ if node.parameter_type is LMS_NodeParameterType.STRING:
223
+ string_offsets.append(writer.tell())
224
+
225
+ if node.definition is not None:
226
+ parameter_definition = node.definition.parameter_definitions[0]
227
+ field = node.parameter_value[parameter_definition.name]
228
+ string_table.append(field.value)
229
+ else:
230
+ string_table.append(node.parameter_value)
231
+
232
+ writer.write_uint32(0)
233
+ else:
234
+ write_node_parameter(writer, node.parameter_value, node.parameter_type)
235
+
236
+ starting_index = len(branch_table)
237
+ for branch in node.branches.values():
238
+ if branch is None:
239
+ branch_table.append(NO_NEXT_NODE)
240
+ else:
241
+ branch_table.append(stream_ids[branch])
242
+
243
+ writer.write_uint16(NO_NEXT_NODE)
244
+
245
+ writer.write_uint16(node.condition_id)
246
+ writer.write_uint16(node.case_count)
247
+ writer.write_uint16(starting_index)
248
+ case LMS_EventNode():
249
+ writer.write_bytes(b"\x00\x00")
250
+
251
+ if node.parameter_type is LMS_NodeParameterType.STRING:
252
+ string_offsets.append(writer.tell())
253
+
254
+ if isinstance(node.parameter_value, LMS_FieldMap):
255
+ parameter_definition = node.definition.parameter_definitions[0]
256
+ field = node.parameter_value[parameter_definition.name]
257
+ string_table.append(field.value)
258
+ else:
259
+ string_table.append(node.parameter_value)
260
+
261
+ writer.write_uint32(0)
262
+ else:
263
+ write_node_parameter(writer, node.parameter_value, node.parameter_type)
264
+
265
+ write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
266
+ writer.write_uint16(node.event_id)
267
+
268
+ writer.write_bytes(b"\x00" * 4)
269
+ case LMS_EntryNode():
270
+ writer.write_bytes(b"\x00" * 2)
271
+ writer.write_bytes(b"\x00" * 4)
272
+
273
+ write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
274
+ writer.skip(6)
275
+ case LMS_JumpNode():
276
+ writer.write_bytes(b"\x00" * 2)
277
+ writer.write_bytes(b"\x00" * 4)
278
+
279
+ write_next_node_id(writer, stream_ids[node.next_flowchart])
280
+ writer.write_uint16(node.unknown_short0a)
281
+ writer.skip(4)
282
+
283
+ for node_id in branch_table:
284
+ writer.write_uint16(node_id)
285
+
286
+ string_offset = FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
287
+
288
+ for offset, string in zip(string_offsets, string_table):
289
+ write_offset = writer.tell()
290
+
291
+ writer.seek(offset)
292
+ writer.write_uint32(string_offset)
293
+
294
+ writer.seek(write_offset)
295
+ writer.write_encoded_string(string)
296
+
297
+ string_offset += len(string) * writer.encoding.width + len(writer.encoding.terminator)
298
+
299
+ end = writer.tell()
300
+ writer.seek(branch_id_offset)
301
+ writer.write_uint16(len(branch_table))
302
+ writer.seek(end)
303
+
304
+
305
+ def write_node_parameter(writer: FileWriter,
306
+ value: LMS_NodeParameter,
307
+ parameter_type: LMS_NodeParameterType) -> None:
308
+ if isinstance(value, LMS_FieldMap):
309
+ for i, field in enumerate(value):
310
+ # In MSBF files there could be a case where naturally one bit datatypes take up 2/4 bits.
311
+ if field.datatype in VARIABLE_WIDTH_DATATYPES:
312
+ stream_map = {1: writer.write_uint8, 2: writer.write_uint16, 3: writer.write_uint32}
313
+ write_field(writer, field, stream_map[parameter_type.sliced_datatype[i].stream_size])
314
+ else:
315
+ write_field(writer, field)
316
+ return
317
+
318
+ match parameter_type:
319
+ case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
320
+ writer.write_uint32(value)
321
+ case LMS_NodeParameterType.PARAM_16_16:
322
+ writer.write_uint16(value[0])
323
+ writer.write_uint16(value[1])
324
+ case LMS_NodeParameterType.PARAM_16_8_8:
325
+ writer.write_uint16(value[0])
326
+ writer.write_uint8(value[1])
327
+ writer.write_uint8(value[2])
328
+ case LMS_NodeParameterType.PARAM_8_8_16:
329
+ writer.write_uint8(value[0])
330
+ writer.write_uint8(value[1])
331
+ writer.write_uint16(value[2])
332
+ case LMS_NodeParameterType.PARAM_8_8_8_8:
333
+ writer.write_uint8(value[0])
334
+ writer.write_uint8(value[1])
335
+ writer.write_uint8(value[2])
336
+ writer.write_uint8(value[3])
337
+
338
+
339
+ def write_next_node_id(writer: FileWriter, id: int | None) -> None:
340
+ writer.write_uint16(NO_NEXT_NODE if id is None else id)
@@ -56,6 +56,7 @@ class NodeDefinition:
56
56
  parameter_type: LMS_NodeParameterType
57
57
  parameter_definitions: tuple[ValueDefinition, ...]
58
58
  case_format: str | None
59
+ case_options: dict[int, str]
59
60
  next_node_dependency: bool = False
60
61
 
61
62
  @classmethod
@@ -95,7 +96,13 @@ class NodeDefinition:
95
96
  converted_parameters.append(definition)
96
97
 
97
98
  # case_format is only for branch nodesa
98
- if (case_format := data.get("case_format", None)) is not None and type == LMS_NodeType.EVENT:
99
+ if "case_format" in data and "case_options" in data:
100
+ raise ValueError("There may only be one of case_format and options in the definition.")
101
+
102
+ if (case_options := data.get("case_options", None)) is not None and node_type == LMS_NodeType.EVENT:
103
+ raise ValueError("There may only be options for branch nodes!")
104
+
105
+ if (case_format := data.get("case_format", None)) is not None and node_type == LMS_NodeType.EVENT:
99
106
  raise ValueError("There may only be case_format for branch nodes!")
100
107
 
101
108
  return NodeDefinition(
@@ -106,5 +113,6 @@ class NodeDefinition:
106
113
  parameter_type=parameter_type,
107
114
  parameter_definitions=converted_parameters,
108
115
  case_format=case_format,
116
+ case_options=case_options,
109
117
  next_node_dependency=data.get("next_node_dependency", False)
110
118
  )
@@ -7,7 +7,7 @@ include = ["lms*"]
7
7
 
8
8
  [project]
9
9
  name = "PyLibMS"
10
- version = "4.2.1"
10
+ version = "4.2.3"
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"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes