PyLibMS 4.2.0__py3-none-any.whl → 4.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ class LMS_Flowchart:
16
16
  for node in entry_point.get_descendents():
17
17
  self._nodes[node.id] = node
18
18
 
19
+
19
20
  def __iter__(self) -> Iterator[LMS_BaseNode]:
20
21
  return iter(self._nodes.values())
21
22
 
@@ -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
@@ -27,15 +28,14 @@ class LMS_BaseNode:
27
28
  id: int | None,
28
29
  node_type: LMS_NodeType,
29
30
  parameter_type: LMS_NodeParameterType,
30
- parameter_value: LMS_NodeParameter | None,
31
- stream_next_id: int | None = None):
31
+ parameter_value: LMS_NodeParameter | None):
32
32
  self.id = id
33
33
  self._node_type = node_type
34
34
 
35
35
  self._parameter_type = parameter_type
36
36
  self._parameter_value = parameter_value
37
37
 
38
- self._stream_next_id: int | None = stream_next_id
38
+ self._stream_next_id: int | None = None
39
39
  self._next_node: LMS_BaseNode | None = None
40
40
 
41
41
  @property
@@ -76,14 +76,6 @@ class LMS_BaseNode:
76
76
  if not isinstance(value, self._parameter_type.builtin_type):
77
77
  raise TypeError(f"Wrong value provided! Expected {self._parameter_type.builtin_type} got {type(value)}")
78
78
 
79
- sliced_datatype = {
80
- LMS_NodeParameterType.PARAM_16_16: (LMS_DataType.UINT16, LMS_DataType.UINT16),
81
- LMS_NodeParameterType.PARAM_16_8_8: (LMS_DataType.UINT16, LMS_DataType.UINT8, LMS_DataType.UINT8),
82
- LMS_NodeParameterType.PARAM_8_8_16: (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT16),
83
- LMS_NodeParameterType.PARAM_8_8_8_8: (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT8,
84
- LMS_DataType.UINT8)
85
- }
86
-
87
79
  match self._parameter_type:
88
80
  case LMS_NodeParameterType.STRING:
89
81
  self._parameter_value = value
@@ -92,7 +84,8 @@ class LMS_BaseNode:
92
84
  self._parameter_value = value
93
85
  case _:
94
86
  for i, parameter in enumerate(value):
95
- verify_number_from_datatype(parameter, sliced_datatype[self._parameter_type][i])
87
+ verify_number_from_datatype(parameter,
88
+ self._parameter_type.sliced_datatype[self._parameter_type][i])
96
89
 
97
90
  self._parameter_value = value
98
91
 
@@ -100,8 +93,6 @@ class LMS_BaseNode:
100
93
  """Set the next node for the instance.
101
94
 
102
95
  :param node: the node object, None for no next node."""
103
- # Once next_node is properly set, stream_id is never accessed again
104
- # While it does become stale, there is no point to altering it afterward
105
96
  self._next_node = node
106
97
  return node
107
98
 
@@ -143,7 +134,6 @@ class LMS_MessageNode(LMS_BaseNode):
143
134
 
144
135
  def __init__(self,
145
136
  id: int | None,
146
- stream_next_node: int,
147
137
  msbt_index: int,
148
138
  label_index: int,
149
139
  msbt: MSBT | None = None
@@ -153,7 +143,6 @@ class LMS_MessageNode(LMS_BaseNode):
153
143
  LMS_NodeType.MESSAGE,
154
144
  LMS_NodeParameterType.NONE,
155
145
  None,
156
- stream_next_node
157
146
  )
158
147
 
159
148
  self.msbt_index = msbt_index
@@ -173,7 +162,7 @@ class LMS_MessageNode(LMS_BaseNode):
173
162
  @classmethod
174
163
  def new_msbt(cls, file_index: int, entry: MSBTEntry, msbt: MSBT):
175
164
  """
176
- Creates a new LMS_MessageNode given a MSBT object.
165
+ Creates a new ``LMS_MessageNode`` given a MSBT object.
177
166
 
178
167
  :param file_index: index of the MSBT file in a folder/archive.
179
168
  :param entry: the MSBTEntry object to reference.
@@ -211,13 +200,20 @@ class LMS_BranchNode(LMS_BaseNode):
211
200
  LMS_NodeType.BRANCH,
212
201
  parameter_type,
213
202
  parameter_value,
214
- NO_NEXT_NODE
215
203
  )
216
204
 
217
205
  self._condition_id = condition_id
218
206
  self._branches: dict[int, LMS_BaseNode | None] = {}
219
207
  self._definition = definition
220
208
 
209
+ def __repr__(self):
210
+ if self._definition is None:
211
+ return super().__repr__()
212
+ return f"{self._definition.name}({ {field.name: field.value for field in self._parameter_value} } {self.id}"
213
+
214
+ def set_next_node(self, node: LMS_BaseNode | None) -> LMS_BaseNode | None:
215
+ raise NotImplementedError("Use function set_branch_case to alter the flow of a branch node!")
216
+
221
217
  @classmethod
222
218
  def new(cls,
223
219
  parameter_type: LMS_NodeParameterType,
@@ -275,6 +271,67 @@ class LMS_BranchNode(LMS_BaseNode):
275
271
  """The node branches."""
276
272
  return MappingProxyType(self._branches)
277
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
+
278
335
  def add_branch(self, node: LMS_BaseNode | None) -> None:
279
336
  """
280
337
  Adds a branch to the node.
@@ -345,31 +402,34 @@ class LMS_EventNode(LMS_BaseNode):
345
402
  parameter_type: LMS_NodeParameterType,
346
403
  parameter_value: LMS_FieldMap | int | str | tuple[int, ...],
347
404
  action_id: int,
348
- stream_next_id: int | None = None,
349
405
  definition: NodeDefinition | None = None,
350
406
  ):
351
407
  super().__init__(
352
408
  id,
353
409
  LMS_NodeType.EVENT,
354
410
  parameter_type,
355
- parameter_value,
356
- stream_next_id)
411
+ parameter_value)
357
412
 
358
- self._action_id = action_id
413
+ self._event_id = action_id
359
414
  self._definition = definition
360
415
 
416
+ def __repr__(self):
417
+ if self._definition is None:
418
+ return super().__repr__()
419
+ return f"{self._definition.name}({ {field.name: field.value for field in self._parameter_value} } {self.id}"
420
+
361
421
  @classmethod
362
- 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):
363
423
  """
364
424
  Instantiates a new event node.
365
425
 
366
426
  :param parameter_type: the parameter type.
367
427
  :param parameter_value: the parameter value.
368
- :param action_id: the action_id of the node.
428
+ :param event_id: the event_id of the node.
369
429
 
370
430
  """
371
431
  verify_parameter_structure(parameter_value, parameter_type)
372
- return cls(None, parameter_type, parameter_value, action_id)
432
+ return cls(None, parameter_type, parameter_value, event_id)
373
433
 
374
434
  @classmethod
375
435
  def new_from_definition(cls,
@@ -399,9 +459,9 @@ class LMS_EventNode(LMS_BaseNode):
399
459
  return self._definition.name
400
460
 
401
461
  @property
402
- def action_id(self) -> int:
462
+ def event_id(self) -> int:
403
463
  """The identifier to determine which action to run."""
404
- return self._action_id
464
+ return self._event_id
405
465
 
406
466
 
407
467
  class LMS_EntryNode(LMS_BaseNode):
@@ -409,13 +469,12 @@ class LMS_EntryNode(LMS_BaseNode):
409
469
  Class that represents an entry node. Node that is at the start of a flowchart.
410
470
  """
411
471
 
412
- def __init__(self, id: int, stream_next_id=None, flowchart_name: str = ""):
472
+ def __init__(self, id: int, flowchart_name: str = ""):
413
473
  super().__init__(
414
474
  id,
415
475
  LMS_NodeType.ENTRY,
416
476
  LMS_NodeParameterType.NONE,
417
- None,
418
- stream_next_id)
477
+ None)
419
478
 
420
479
  self.flowchart_name = flowchart_name
421
480
 
@@ -425,13 +484,12 @@ class LMS_JumpNode(LMS_BaseNode):
425
484
  Class that represents a jump node. Node that jumps to another flowchart.
426
485
  """
427
486
 
428
- def __init__(self, id: int, next_flowchart_id: int, unknown_short0a: int):
487
+ def __init__(self, id: int, unknown_short0a: int):
429
488
  super().__init__(
430
489
  id,
431
490
  LMS_NodeType.JUMP,
432
491
  LMS_NodeParameterType.NONE,
433
- None,
434
- next_flowchart_id)
492
+ None)
435
493
 
436
494
  self.next_flowchart: LMS_EntryNode | None = None
437
495
 
@@ -478,3 +536,24 @@ def verify_parameter_structure(value: int | tuple[int, ...] | str, parameter_typ
478
536
  if isinstance(value, tuple) and len(value) != parameter_type.value_count:
479
537
  raise node_exceptions.LMS_NodeMissingParameterValueError(
480
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)
@@ -54,21 +54,21 @@ class LMS_NodeParameterType(IntEnum):
54
54
  NONE = -1
55
55
 
56
56
  @property
57
- def sliced_datatype(self):
58
- """The parameter type split into the raw datatypes. Not valid for PARAM_32_0, PARAM_32_1, and STRING."""
57
+ def sliced_datatype(self) -> tuple[LMS_DataType, ...]:
58
+ """The parameter type split into the raw datatypes. Not valid for STRING."""
59
59
  if self in [
60
60
  LMS_NodeParameterType.NONE,
61
- LMS_NodeParameterType.PARAM_32_0,
62
- LMS_NodeParameterType.PARAM_32_1,
63
61
  LMS_NodeParameterType.STRING]:
64
62
  raise TypeError(f"There is no sliced datatypes for {self} parameter types!")
65
63
 
66
64
  return {
65
+ LMS_NodeParameterType.PARAM_32_0: (LMS_DataType.UINT32,),
67
66
  LMS_NodeParameterType.PARAM_16_16: (LMS_DataType.UINT16, LMS_DataType.UINT16),
68
67
  LMS_NodeParameterType.PARAM_16_8_8: (LMS_DataType.UINT16, LMS_DataType.UINT8, LMS_DataType.UINT8),
69
68
  LMS_NodeParameterType.PARAM_8_8_16: (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT16),
70
69
  LMS_NodeParameterType.PARAM_8_8_8_8: (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT8,
71
- LMS_DataType.UINT8)
70
+ LMS_DataType.UINT8),
71
+ LMS_NodeParameterType.PARAM_32_1: (LMS_DataType.UINT32,)
72
72
  }[self]
73
73
 
74
74
  @property
lms/flowchart/flw3.py CHANGED
@@ -1,336 +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
- branch_metadata: dict[LMS_BranchNode, dict] = {}
32
-
33
- for i in range(node_count):
34
- node_data = reader.tell() + 8
35
- node_type = LMS_NodeType(reader.read_uint8())
36
- parameter_type = LMS_NodeParameterType(reader.read_int8())
37
-
38
- reader.skip(2)
39
-
40
- parameter_offset = reader.tell()
41
- match node_type:
42
- case LMS_NodeType.MESSAGE:
43
- reader.skip(4)
44
-
45
- stream_next_id = read_next_node_id(reader)
46
- file_index = reader.read_uint16()
47
- message_index = reader.read_uint16()
48
- node = LMS_MessageNode(i, stream_next_id, file_index, message_index, msbt)
49
- reader.skip(2)
50
- case LMS_NodeType.BRANCH:
51
- # There is no next node id as the next node is determined by the branches
52
- # Skip parameter value alongside that
53
- reader.skip(2 + 4)
54
-
55
- condition_id = reader.read_uint16()
56
- case_count = reader.read_uint16()
57
- table_index = reader.read_uint16()
58
- end = reader.tell()
59
-
60
- reader.seek(parameter_offset)
61
- definition = get_config_definition(config, LMS_NodeType.BRANCH, parameter_type, condition_id)
62
- parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
63
-
64
- node = LMS_BranchNode(i, parameter_type, parameter_value, condition_id, definition)
65
-
66
- # Save the metadata for the branch node so that the branches itself can be set later on
67
- branch_metadata[node] = case_count, table_index
68
- reader.seek(end)
69
- case LMS_NodeType.EVENT:
70
- reader.skip(4)
71
-
72
- stream_next_id = read_next_node_id(reader)
73
- event_id = reader.read_uint16()
74
- end = reader.tell() + 4
75
-
76
- reader.seek(parameter_offset)
77
-
78
- definition = get_config_definition(config, LMS_NodeType.EVENT, parameter_type, event_id)
79
- parameter_value = evaluate_node_parameter(reader, section_start, parameter_type, definition)
80
-
81
- node = LMS_EventNode(i, parameter_type, parameter_value, event_id, stream_next_id, definition)
82
- reader.seek(end)
83
- case LMS_NodeType.ENTRY:
84
- reader.seek(node_data)
85
- stream_next_id = read_next_node_id(reader)
86
- node = LMS_EntryNode(i, stream_next_id)
87
- entry_nodes.append(node)
88
- reader.skip(6)
89
- case LMS_NodeType.JUMP:
90
- reader.seek(node_data)
91
- next_flowchart_id = read_next_node_id(reader)
92
- unknown_short0a = reader.read_uint16()
93
- node = LMS_JumpNode(i, next_flowchart_id, unknown_short0a)
94
- reader.skip(4)
95
-
96
- nodes.append(node)
97
-
98
- node_map: dict[int, LMS_BaseNode] = {node.id: node for node in nodes}
99
- branch_ids = [read_next_node_id(reader) for _ in range(branch_table_id_count)]
100
-
101
- for node in nodes:
102
-
103
- if isinstance(node, LMS_BranchNode):
104
- case_count, starting_index = branch_metadata[node]
105
-
106
- # Slice the portion of IDs linked to this branch node and add the node objects themselves
107
- for branch_id in branch_ids[starting_index:starting_index + case_count]:
108
- if branch_id is None:
109
- node.add_branch(None)
110
- else:
111
- node.add_branch(node_map[branch_id])
112
-
113
- continue
114
- elif isinstance(node, LMS_JumpNode):
115
- node.next_flowchart = node_map[node.next_node_id]
116
- continue
117
-
118
- if node.next_node_id is None:
119
- continue
120
-
121
- node.set_next_node(node_map[node.next_node_id])
122
-
123
- return node_count, entry_nodes
124
-
125
-
126
- def read_next_node_id(reader: FileReader) -> int | None:
127
- return None if (id := reader.read_uint16()) == NO_NEXT_NODE else id
128
-
129
-
130
- def get_config_definition(config: NodeConfig,
131
- node_type: LMS_NodeType.BRANCH | LMS_NodeType.EVENT,
132
- parameter_type: LMS_NodeParameterType,
133
- id: int) -> NodeDefinition:
134
- return None if config is None else config.get_definition(id, node_type, parameter_type)
135
-
136
-
137
- def evaluate_node_parameter(reader: FileReader,
138
- section_start: int,
139
- parameter_type: LMS_NodeParameterType,
140
- definition: NodeDefinition | None) -> LMS_NodeParameter:
141
- # How functions may utilize node parameters vary, such that they may cast values as they wish.
142
- # By default, PyLibMS interprets the parameter types using signed integers for simplicity.
143
- # If the user believes a better interpretation fits, then they may utilize a node configuration
144
- # and cast individual parameters to any LMS_Datatype.
145
- # This is my understanding of how node parameters work, but updating may be necessary as more research surfaces.
146
- # -
147
- # It also noted that by default LIST types are allocated a single byte for its index, it is unknown if
148
- # some games may utilize a whole 4 byte stack as an index and/or 2 bytes depending on the parameter type.
149
- # For now, by PyLibMS will read a single byte for its index until proven otherwise.
150
- if definition is not None:
151
- result = {}
152
-
153
- if parameter_type is LMS_NodeParameterType.STRING:
154
- param_definition = definition.parameter_definitions[0]
155
- value = reader.read_string_offset(section_start)
156
-
157
- return LMS_FieldMap({
158
- param_definition.name: LMS_Field(value, param_definition)
159
- })
160
-
161
- for i, param_definition in enumerate(definition.parameter_definitions):
162
- stream_map = {1: reader.read_uint8, 2: reader.read_uint16, 3: reader.read_uint32}
163
- result[param_definition.name] = read_field(reader, param_definition,
164
- stream_map[parameter_type.sliced_datatype[i].stream_size])
165
-
166
- return LMS_FieldMap(result)
167
- else:
168
- # Default parameter reading
169
- match parameter_type:
170
- case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
171
- return reader.read_uint32()
172
- case LMS_NodeParameterType.PARAM_16_16:
173
- return reader.read_uint16(), reader.read_uint16()
174
- case LMS_NodeParameterType.PARAM_16_8_8:
175
- return reader.read_uint16(), reader.read_uint8(), reader.read_uint8()
176
- case LMS_NodeParameterType.PARAM_8_8_16:
177
- return reader.read_uint8(), reader.read_uint8(), reader.read_uint16()
178
- case LMS_NodeParameterType.PARAM_8_8_8_8:
179
- return reader.read_uint8(), reader.read_uint8(), reader.read_uint8(), reader.read_uint8()
180
- case LMS_NodeParameterType.STRING:
181
- return reader.read_string_offset(section_start)
182
- case _:
183
- raise LMS_Error(f"Invalid parameter type of '{parameter_type}'!")
184
-
185
-
186
- def write_flw3(writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[LMS_BaseNode, int]) -> None:
187
- node_count = len(nodes)
188
-
189
- branch_table: list[int] = []
190
- string_table: list[str] = []
191
- string_offsets: list[int] = []
192
-
193
- writer.write_uint16(node_count)
194
- branch_id_offset = writer.tell()
195
-
196
- # Placeholder value for branch table count
197
- writer.write_uint16(0)
198
-
199
- writer.write_bytes(b'\x00' * 12)
200
- for node in nodes:
201
- writer.write_int8(node.type)
202
- writer.write_int8(node.parameter_type)
203
-
204
- match node:
205
- case LMS_MessageNode():
206
- writer.write_bytes(b"\x00" * 2)
207
- writer.write_bytes(b"\x00" * 4)
208
-
209
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
210
-
211
- writer.write_uint16(node.msbt_index)
212
- writer.write_uint16(node.label_index)
213
-
214
- writer.write_bytes(b"\x00\x00")
215
- case LMS_BranchNode():
216
- writer.write_bytes(b"\x00\x00")
217
-
218
- if node.parameter_type is LMS_NodeParameterType.STRING:
219
- string_offsets.append(writer.tell())
220
-
221
- if node.definition is not None:
222
- parameter_definition = node.definition.parameter_definitions[0]
223
- field = node.parameter_value[parameter_definition.name]
224
- string_table.append(field.value)
225
- else:
226
- string_table.append(node.parameter_value)
227
-
228
- writer.write_uint32(0)
229
- else:
230
- write_node_parameter(writer, node.parameter_value, node.parameter_type)
231
-
232
- starting_index = len(branch_table)
233
- for branch in node.branches.values():
234
- if branch is None:
235
- branch_table.append(NO_NEXT_NODE)
236
- else:
237
- branch_table.append(stream_ids[branch])
238
-
239
- writer.write_uint16(NO_NEXT_NODE)
240
-
241
- writer.write_uint16(node.condition_id)
242
- writer.write_uint16(node.case_count)
243
- writer.write_uint16(starting_index)
244
- case LMS_EventNode():
245
- writer.write_bytes(b"\x00\x00")
246
-
247
- if node.parameter_type is LMS_NodeParameterType.STRING:
248
- string_offsets.append(writer.tell())
249
-
250
- if isinstance(node.parameter_value, LMS_FieldMap):
251
- parameter_definition = node.definition.parameter_definitions[0]
252
- field = node.parameter_value[parameter_definition.name]
253
- string_table.append(field.value)
254
- else:
255
- string_table.append(node.parameter_value)
256
-
257
- writer.write_uint32(0)
258
- else:
259
- write_node_parameter(writer, node.parameter_value, node.parameter_type)
260
-
261
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
262
- writer.write_uint16(node.action_id)
263
-
264
- writer.write_bytes(b"\x00" * 4)
265
- case LMS_EntryNode():
266
- writer.write_bytes(b"\x00" * 2)
267
- writer.write_bytes(b"\x00" * 4)
268
-
269
- write_next_node_id(writer, None if node.next_node is None else stream_ids[node.next_node])
270
- writer.skip(6)
271
- case LMS_JumpNode():
272
- writer.write_bytes(b"\x00" * 2)
273
- writer.write_bytes(b"\x00" * 4)
274
-
275
- write_next_node_id(writer, stream_ids[node.next_flowchart])
276
- writer.write_uint16(node.unknown_short0a)
277
- writer.skip(4)
278
-
279
- for node_id in branch_table:
280
- writer.write_uint16(node_id)
281
-
282
- string_offset = FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
283
-
284
- for offset, string in zip(string_offsets, string_table):
285
- write_offset = writer.tell()
286
-
287
- writer.seek(offset)
288
- writer.write_uint32(string_offset)
289
-
290
- writer.seek(write_offset)
291
- writer.write_encoded_string(string)
292
-
293
- string_offset += len(string) * writer.encoding.width + len(writer.encoding.terminator)
294
-
295
- end = writer.tell()
296
- writer.seek(branch_id_offset)
297
- writer.write_uint16(len(branch_table))
298
- writer.seek(end)
299
-
300
-
301
- def write_node_parameter(writer: FileWriter,
302
- value: LMS_NodeParameter,
303
- parameter_type: LMS_NodeParameterType) -> None:
304
- if isinstance(value, LMS_FieldMap):
305
- for i, field in enumerate(value):
306
- # In MSBF files there could be a case where naturally one bit datatypes take up 2/4 bits.
307
- if field.datatype in VARIABLE_WIDTH_DATATYPES:
308
- stream_map = {1: writer.write_uint8, 2: writer.write_uint16, 3: writer.write_uint32}
309
- write_field(writer, field, stream_map[parameter_type.sliced_datatype[i].stream_size])
310
- else:
311
- write_field(writer, field)
312
- return
313
-
314
- match parameter_type:
315
- case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
316
- writer.write_uint32(value)
317
- case LMS_NodeParameterType.PARAM_16_16:
318
- writer.write_uint16(value[0])
319
- writer.write_uint16(value[1])
320
- case LMS_NodeParameterType.PARAM_16_8_8:
321
- writer.write_uint16(value[0])
322
- writer.write_uint8(value[1])
323
- writer.write_uint8(value[2])
324
- case LMS_NodeParameterType.PARAM_8_8_16:
325
- writer.write_uint8(value[0])
326
- writer.write_uint8(value[1])
327
- writer.write_uint16(value[2])
328
- case LMS_NodeParameterType.PARAM_8_8_8_8:
329
- writer.write_uint8(value[0])
330
- writer.write_uint8(value[1])
331
- writer.write_uint8(value[2])
332
- writer.write_uint8(value[3])
333
-
334
-
335
- def write_next_node_id(writer: FileWriter, id: int | None) -> None:
336
- 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,6 +96,12 @@ class NodeDefinition:
95
96
  converted_parameters.append(definition)
96
97
 
97
98
  # case_format is only for branch nodesa
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 (options := data.get("case_options", None)) is not None and type == LMS_NodeType.EVENT:
103
+ raise ValueError("There may only be options for branch nodes!")
104
+
98
105
  if (case_format := data.get("case_format", None)) is not None and type == LMS_NodeType.EVENT:
99
106
  raise ValueError("There may only be case_format for branch nodes!")
100
107
 
@@ -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=options,
109
117
  next_node_dependency=data.get("next_node_dependency", False)
110
118
  )
@@ -8,7 +8,7 @@ node_definitions:
8
8
  - name: exp
9
9
  - id: 1
10
10
  name: SwitchVariable
11
- case_format: "var == {case}"
11
+ case_format: "var == $case"
12
12
  parameter_type: string
13
13
  parameters:
14
14
  - name: var
@@ -29,6 +29,11 @@ node_definitions:
29
29
  parameter_type: string
30
30
  parameters:
31
31
  - name: var
32
+ - id: 82
33
+ name: SetFlag
34
+ parameter_type: p32_1
35
+ parameters:
36
+ - name: flag
32
37
  - id: 90
33
38
  name: LoadSEGroup
34
39
  parameter_type: string
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.2.0
3
+ Version: 4.2.2
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
@@ -13,14 +13,14 @@ lms/common/stream/section.py,sha256=WAqqj4FlUS8puB0iAnjzwNRtJ0YdHVK_DSrj051TZwU,
13
13
  lms/fileio/encoding.py,sha256=8OmDxpVLu613TH6xb47Szb3bo7gqNeCh2xJjnNI3Ds0,1283
14
14
  lms/fileio/io.py,sha256=ATr1s_m420KngK8HYYz5dSFkR8EhLJbk7U8-l_8xSss,6636
15
15
  lms/flowchart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- lms/flowchart/flw3.py,sha256=eeNWEvmLc0JJHNHFUI9qwqaGcZ2VTnA_joPwuIn_NdA,14225
16
+ lms/flowchart/flw3.py,sha256=JFQfmppKAUMcQvO6fKCRPpx335r_xcUV92OrrnFiBQU,14617
17
17
  lms/flowchart/msbf.py,sha256=-IehJxdWqZwNz50p_Z4oELUTMJHmKZs-d7Awx7e7sew,3196
18
18
  lms/flowchart/msbfio.py,sha256=KrR5rUZ_3NK6UfwfPhbrFFFg9LeGuXGE6CRVqlXB75o,4366
19
19
  lms/flowchart/definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- lms/flowchart/definitions/flowchart.py,sha256=NvHgD0gXggdyibuU8wi9Dh8ID32AyFOTICGG1LDJsZI,3553
20
+ lms/flowchart/definitions/flowchart.py,sha256=H0DZSLboWvPvDHf7J-G_2Krnv-sE7eQ0ZIJOpGZYJs4,3555
21
21
  lms/flowchart/definitions/lms_nodeexceptions.py,sha256=hlcqW6ERMVyAf2nqxaiSyFdXDfwthFADK79ggS_B3GU,181
22
- lms/flowchart/definitions/node.py,sha256=j0RVgo3meT7prai1BklbodMxShbLko5fq_bgFBYPctE,17088
23
- lms/flowchart/definitions/node_type.py,sha256=eFXH5ymUZlgfHG6nefb7OiPEmsoQqKmF6lKKSxZv2Q0,4065
22
+ lms/flowchart/definitions/node.py,sha256=aSYnvUHEcf7Bb0u23YbeC93H0DNpE3ZHgaBakC-uQno,19897
23
+ lms/flowchart/definitions/node_type.py,sha256=W6_whMPwTxC11YxV8x8YMAxHT9Psq8_XOITiUPwRCRg,4113
24
24
  lms/message/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  lms/message/lms_messagetext.py,sha256=A6iXkumfhD_r55BnJAdWEWmYKiM-2iJre0v7oU3JcZs,5971
26
26
  lms/message/msbt.py,sha256=WPtTDQQn1_n8uNny9YWBh5HsY-TW8YgAF2Hv5G-PBZM,7804
@@ -58,7 +58,7 @@ lms/titleconfig/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  lms/titleconfig/config.py,sha256=SDryeNLLRKDEaDL6ffqmHE_Q0EB3g7jznKP8FGM9sEk,12107
59
59
  lms/titleconfig/definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  lms/titleconfig/definitions/attribute.py,sha256=1Wcnn9FFicUHVWRtnx1S7LP-hmtE2MxIvtgncpsWX5c,299
61
- lms/titleconfig/definitions/nodes.py,sha256=hs-22FGgSEc7qvvtrnnQo5xIHneHIweOrp4Lm7q1vs8,4692
61
+ lms/titleconfig/definitions/nodes.py,sha256=bzo6InVCDkRQykrXFVdihACU2bGOqsADeSD0gDJimXc,5105
62
62
  lms/titleconfig/definitions/tags.py,sha256=84aM-QgPFxonllkzRwTvPR76CvV_LJpypFv4ABNsJ7o,2916
63
63
  lms/titleconfig/definitions/value.py,sha256=_Py26KzhQ93v83k7k6_ywO0U1WTWFUzyodtQdWjBxSI,576
64
64
  lms/titleconfig/presets/Badge Arcade.yaml,sha256=OotJblQ210dNPiwBpdAI1qwbEYCRQn4jDI7boUSrCao,11854
@@ -70,9 +70,9 @@ lms/titleconfig/presets/Super Mario Odyssey.yaml,sha256=mV4BsLPzupEPy2RmPUxXlz0U
70
70
  lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml,sha256=6AoSNRNORjMzMBp2ais_ujRdLLeOe7FB0YlozZJNvHg,2878
71
71
  lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml,sha256=IY8wPIWMA4ElnN_dtV455beBucntxEBeeJXxUUIrnuw,9785
72
72
  lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml,sha256=hJNb0_KdUAZrztyphaTeaZJl_os7HpmdglLgly_eerU,82382
73
- lms/titleconfig/presets/Tomodachi Life NA-EU.yaml,sha256=FaMRfqDWNkoVB8gsloNSG4bIlUPYREKqSHOnd_qQoqk,188257
74
- pylibms-4.2.0.dist-info/licenses/LICENSE,sha256=IGTA9rMb-XpTMPJOzC6z0rRxSzysj3DlV5IQyDd06Xo,1061
75
- pylibms-4.2.0.dist-info/METADATA,sha256=kt6aWylAjZKo3TkUUComwJserMazsc9SeaBz-c56oKs,4021
76
- pylibms-4.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
77
- pylibms-4.2.0.dist-info/top_level.txt,sha256=SWQqb3sjP5r5Ls1Ol4E_PIbsv1sggK4DCkkLEzXNRTU,4
78
- pylibms-4.2.0.dist-info/RECORD,,
73
+ lms/titleconfig/presets/Tomodachi Life NA-EU.yaml,sha256=0W9JayobbFCieqDufnedpjRMbOWR2nOdzR-YbJmxs4M,188361
74
+ pylibms-4.2.2.dist-info/licenses/LICENSE,sha256=IGTA9rMb-XpTMPJOzC6z0rRxSzysj3DlV5IQyDd06Xo,1061
75
+ pylibms-4.2.2.dist-info/METADATA,sha256=bYI_CFMKZVa2QSmqCfQugtlmIXgw50G4ALdIbSTEoRo,4208
76
+ pylibms-4.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
77
+ pylibms-4.2.2.dist-info/top_level.txt,sha256=SWQqb3sjP5r5Ls1Ol4E_PIbsv1sggK4DCkkLEzXNRTU,4
78
+ pylibms-4.2.2.dist-info/RECORD,,