PyLibMS 4.0.3__tar.gz → 4.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {pylibms-4.0.3 → pylibms-4.1.0}/PKG-INFO +1 -1
  2. {pylibms-4.0.3 → pylibms-4.1.0}/PyLibMS.egg-info/PKG-INFO +1 -1
  3. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/lms_exceptions.py +3 -0
  4. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/stream/fileinfo.py +4 -0
  5. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/definitions/flowchart.py +0 -1
  6. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/definitions/node.py +2 -12
  7. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/flw3.py +27 -27
  8. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/msbf.py +24 -7
  9. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/msbt.py +2 -1
  10. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/config.py +96 -24
  11. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/definitions/nodes.py +5 -5
  12. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Tomodachi Life NA-EU.yaml +3 -21
  13. {pylibms-4.0.3 → pylibms-4.1.0}/pyproject.toml +1 -1
  14. {pylibms-4.0.3 → pylibms-4.1.0}/LICENSE +0 -0
  15. {pylibms-4.0.3 → pylibms-4.1.0}/MANIFEST.in +0 -0
  16. {pylibms-4.0.3 → pylibms-4.1.0}/PyLibMS.egg-info/SOURCES.txt +0 -0
  17. {pylibms-4.0.3 → pylibms-4.1.0}/PyLibMS.egg-info/dependency_links.txt +0 -0
  18. {pylibms-4.0.3 → pylibms-4.1.0}/PyLibMS.egg-info/requires.txt +0 -0
  19. {pylibms-4.0.3 → pylibms-4.1.0}/PyLibMS.egg-info/top_level.txt +0 -0
  20. {pylibms-4.0.3 → pylibms-4.1.0}/README.md +0 -0
  21. {pylibms-4.0.3 → pylibms-4.1.0}/lms/__init__.py +0 -0
  22. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/__init__.py +0 -0
  23. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/field/__init__.py +0 -0
  24. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/field/io.py +0 -0
  25. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/field/lms_datatype.py +0 -0
  26. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/field/lms_field.py +0 -0
  27. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/lms_constants.py +0 -0
  28. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/lms_fileinfo.py +0 -0
  29. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/stream/hashtable.py +0 -0
  30. {pylibms-4.0.3 → pylibms-4.1.0}/lms/common/stream/section.py +0 -0
  31. {pylibms-4.0.3 → pylibms-4.1.0}/lms/fileio/encoding.py +0 -0
  32. {pylibms-4.0.3 → pylibms-4.1.0}/lms/fileio/io.py +0 -0
  33. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/__init__.py +0 -0
  34. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/definitions/__init__.py +0 -0
  35. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/definitions/lms_nodeexceptions.py +0 -0
  36. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/definitions/node_type.py +0 -0
  37. {pylibms-4.0.3 → pylibms-4.1.0}/lms/flowchart/msbfio.py +0 -0
  38. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/__init__.py +0 -0
  39. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/definitions/__init__.py +0 -0
  40. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/definitions/field/__init__.py +0 -0
  41. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/lms_messagetext.py +0 -0
  42. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/msbtentry.py +0 -0
  43. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/msbtio.py +0 -0
  44. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/section/__init__.py +0 -0
  45. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/section/atr1.py +0 -0
  46. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/section/nli1.py +0 -0
  47. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/section/tsy1.py +0 -0
  48. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/section/txt2.py +0 -0
  49. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/tag/__init__.py +0 -0
  50. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/tag/io/param_io.py +0 -0
  51. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/tag/io/tag_io.py +0 -0
  52. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/tag/lms_tag.py +0 -0
  53. {pylibms-4.0.3 → pylibms-4.1.0}/lms/message/tag/lms_tagexceptions.py +0 -0
  54. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/__init__.py +0 -0
  55. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/definitions/__init__.py +0 -0
  56. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/definitions/attribute.py +0 -0
  57. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/definitions/color.py +0 -0
  58. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/definitions/style.py +0 -0
  59. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/definitions/tag.py +0 -0
  60. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/msbp.py +0 -0
  61. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/msbpread.py +0 -0
  62. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/ali2.py +0 -0
  63. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/ati2.py +0 -0
  64. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/clr1.py +0 -0
  65. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/string.py +0 -0
  66. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/syl3.py +0 -0
  67. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/tag2.py +0 -0
  68. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/tgg2.py +0 -0
  69. {pylibms-4.0.3 → pylibms-4.1.0}/lms/project/section/tgp2.py +0 -0
  70. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/__init__.py +0 -0
  71. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/definitions/__init__.py +0 -0
  72. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/definitions/attribute.py +0 -0
  73. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/definitions/tags.py +0 -0
  74. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/definitions/value.py +0 -0
  75. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
  76. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
  77. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
  78. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
  79. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
  80. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
  81. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
  82. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
  83. {pylibms-4.0.3 → pylibms-4.1.0}/lms/titleconfig/presets/Tomodachi Life Living The Dream.yaml +0 -0
  84. {pylibms-4.0.3 → pylibms-4.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 4.0.3
3
+ Version: 4.1.0
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.0.3
3
+ Version: 4.1.0
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
@@ -8,3 +8,6 @@ class LMS_MisalignedSizeError(Exception): ...
8
8
 
9
9
 
10
10
  class LMS_UnsupportedSectionError(Exception): ...
11
+
12
+
13
+ class LMS_UnsupportedFileVersionError(Exception): ...
@@ -23,6 +23,10 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
23
23
  reader.encoding = encoding
24
24
 
25
25
  version = reader.read_uint8()
26
+
27
+ if version < 3:
28
+ raise lms_exceptions.LMS_UnsupportedFileVersionError("Only version 3+ files are supported!")
29
+
26
30
  section_count = reader.read_uint16()
27
31
 
28
32
  reader.skip(2)
@@ -53,7 +53,6 @@ class LMS_Flowchart:
53
53
  if node in self._nodes.values():
54
54
  raise ValueError(f"Node of ID '{node.id}' is already registered to this flowchart!")
55
55
 
56
-
57
56
  node.id = self._id_generator()
58
57
  self._nodes[node.id] = node
59
58
 
@@ -122,8 +122,8 @@ class LMS_MessageNode(LMS_BaseNode):
122
122
  stream_next_node
123
123
  )
124
124
 
125
- self._msbt_index = msbt_index
126
- self._entry_index = label_index
125
+ self.msbt_index = msbt_index
126
+ self.label_index = label_index
127
127
  self._msbt = msbt
128
128
 
129
129
  @classmethod
@@ -147,16 +147,6 @@ class LMS_MessageNode(LMS_BaseNode):
147
147
  """
148
148
  return cls(None, None, file_index, msbt.entries.index(entry), msbt)
149
149
 
150
- @property
151
- def msbt_index(self) -> int:
152
- """Index of the MSBT file the node references in a folder/archive."""
153
- return self._msbt_index
154
-
155
- @property
156
- def label_index(self) -> int:
157
- """Index of the MSBT entry the node references in the file."""
158
- return self._entry_index
159
-
160
150
  @property
161
151
  def msbt(self) -> MSBT | None:
162
152
  """MSBT instance the node references. Optional, and must be set with `set_msbt` or by providing a MSBT file on reading."""
@@ -9,7 +9,7 @@ from lms.flowchart.definitions.node_type import LMS_NodeType, LMS_NodeParameterT
9
9
  from lms.message.msbt import MSBT
10
10
  from lms.titleconfig.definitions.nodes import NodeConfig, NodeDefinition
11
11
 
12
- NO_NEXT_NODE = -1
12
+ NO_NEXT_NODE = 0xFFFF
13
13
  FLW3_HEADER_SIZE = 16
14
14
  NODE_SIZE = 16
15
15
 
@@ -50,7 +50,7 @@ def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None,
50
50
  reader.skip(2 + 4)
51
51
 
52
52
  condition_id = reader.read_uint16()
53
- case_count = reader.read_int16()
53
+ case_count = reader.read_uint16()
54
54
  table_index = reader.read_uint16()
55
55
  end = reader.tell()
56
56
 
@@ -86,7 +86,7 @@ def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None,
86
86
  case LMS_NodeType.JUMP:
87
87
  reader.seek(node_data)
88
88
  next_flowchart_id = read_next_node_id(reader)
89
- unknown_short0a = reader.read_int16()
89
+ unknown_short0a = reader.read_uint16()
90
90
  node = LMS_JumpNode(i, next_flowchart_id, unknown_short0a)
91
91
  reader.skip(4)
92
92
 
@@ -121,7 +121,7 @@ def read_flw3(reader: FileReader, config: NodeConfig | None, msbt: MSBT | None,
121
121
 
122
122
 
123
123
  def read_next_node_id(reader: FileReader) -> int | None:
124
- return None if (id := reader.read_int16()) == NO_NEXT_NODE else id
124
+ return None if (id := reader.read_uint16()) == NO_NEXT_NODE else id
125
125
 
126
126
 
127
127
  def get_config_definition(config: NodeConfig,
@@ -163,15 +163,15 @@ def evaluate_node_parameter(reader: FileReader,
163
163
  # Default parameter reading
164
164
  match parameter_type:
165
165
  case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
166
- return reader.read_int32()
166
+ return reader.read_uint32()
167
167
  case LMS_NodeParameterType.PARAM_16_16:
168
- return reader.read_int16(), reader.read_int16()
168
+ return reader.read_uint16(), reader.read_uint16()
169
169
  case LMS_NodeParameterType.PARAM_16_8_8:
170
- return reader.read_int16(), reader.read_int8(), reader.read_int8()
170
+ return reader.read_uint16(), reader.read_uint8(), reader.read_uint8()
171
171
  case LMS_NodeParameterType.PARAM_8_8_16:
172
- return reader.read_int8(), reader.read_int8(), reader.read_int16()
172
+ return reader.read_uint8(), reader.read_uint8(), reader.read_uint16()
173
173
  case LMS_NodeParameterType.PARAM_8_8_8_8:
174
- return reader.read_int8(), reader.read_int8(), reader.read_int8(), reader.read_int8()
174
+ return reader.read_uint8(), reader.read_uint8(), reader.read_uint8(), reader.read_uint8()
175
175
  case LMS_NodeParameterType.STRING:
176
176
  return reader.read_string_offset(section_start)
177
177
  case _:
@@ -227,11 +227,11 @@ def write_flw3(writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[L
227
227
  starting_index = len(branch_table)
228
228
  for branch in node.branches.values():
229
229
  if branch is None:
230
- branch_table.append(-1)
230
+ branch_table.append(NO_NEXT_NODE)
231
231
  else:
232
232
  branch_table.append(stream_ids[branch])
233
233
 
234
- writer.write_int16(NO_NEXT_NODE)
234
+ writer.write_uint16(NO_NEXT_NODE)
235
235
 
236
236
  writer.write_uint16(node.condition_id)
237
237
  writer.write_uint16(node.case_count)
@@ -268,11 +268,11 @@ def write_flw3(writer: FileWriter, nodes: list[LMS_BaseNode], stream_ids: dict[L
268
268
  writer.write_bytes(b"\x00" * 4)
269
269
 
270
270
  write_next_node_id(writer, stream_ids[node.next_flowchart])
271
- writer.write_int16(node.unknown_short0a)
271
+ writer.write_uint16(node.unknown_short0a)
272
272
  writer.skip(4)
273
273
 
274
274
  for node_id in branch_table:
275
- writer.write_int16(node_id)
275
+ writer.write_uint16(node_id)
276
276
 
277
277
  string_offset = FLW3_HEADER_SIZE + (NODE_SIZE * node_count) + (2 * len(branch_table))
278
278
 
@@ -303,24 +303,24 @@ def write_node_parameter(writer: FileWriter,
303
303
 
304
304
  match parameter_type:
305
305
  case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
306
- writer.write_int32(value)
306
+ writer.write_uint32(value)
307
307
  case LMS_NodeParameterType.PARAM_16_16:
308
- writer.write_int16(value[0])
309
- writer.write_int16(value[1])
308
+ writer.write_uint16(value[0])
309
+ writer.write_uint16(value[1])
310
310
  case LMS_NodeParameterType.PARAM_16_8_8:
311
- writer.write_int16(value[0])
312
- writer.write_int8(value[1])
313
- writer.write_int8(value[2])
311
+ writer.write_uint16(value[0])
312
+ writer.write_uint8(value[1])
313
+ writer.write_uint8(value[2])
314
314
  case LMS_NodeParameterType.PARAM_8_8_16:
315
- writer.write_int8(value[0])
316
- writer.write_int8(value[1])
317
- writer.write_int16(value[2])
315
+ writer.write_uint8(value[0])
316
+ writer.write_uint8(value[1])
317
+ writer.write_uint16(value[2])
318
318
  case LMS_NodeParameterType.PARAM_8_8_8_8:
319
- writer.write_int8(value[0])
320
- writer.write_int8(value[1])
321
- writer.write_int8(value[2])
322
- writer.write_int8(value[3])
319
+ writer.write_uint8(value[0])
320
+ writer.write_uint8(value[1])
321
+ writer.write_uint8(value[2])
322
+ writer.write_uint8(value[3])
323
323
 
324
324
 
325
325
  def write_next_node_id(writer: FileWriter, id: int | None) -> None:
326
- writer.write_int16(NO_NEXT_NODE if id is None else id)
326
+ writer.write_uint16(NO_NEXT_NODE if id is None else id)
@@ -1,6 +1,7 @@
1
1
  from types import MappingProxyType
2
2
 
3
3
  from lms.common.lms_fileinfo import LMS_FileInfo
4
+ from lms.fileio.encoding import FileEncoding
4
5
  from lms.flowchart.definitions.flowchart import LMS_Flowchart
5
6
  from lms.flowchart.definitions.node import LMS_EntryNode
6
7
 
@@ -24,18 +25,34 @@ class MSBF:
24
25
  DEFAULT_SLOT_COUNT = 59
25
26
 
26
27
  def __init__(self, info: LMS_FileInfo | None = None,
27
- flowcharts: dict[str, LMS_Flowchart] = None):
28
+ flowcharts: list[LMS_Flowchart] = None):
28
29
  self._info = info if info is not None else LMS_FileInfo()
29
- self._flowcharts: dict[str, LMS_Flowchart] = flowcharts or {}
30
-
30
+ self._flowcharts = flowcharts or []
31
31
  self._global_node_id = 0
32
32
 
33
33
  def __iter__(self):
34
- return iter(self._flowcharts.values())
34
+ return iter(self._flowcharts)
35
35
 
36
36
  def __len__(self):
37
37
  return len(self._flowcharts)
38
38
 
39
+ @classmethod
40
+ def new(cls,
41
+ is_big_endian: bool = False,
42
+ encoding: FileEncoding = FileEncoding.UTF16,
43
+ version: int = 3,
44
+ section_count: int = 2):
45
+ """
46
+ Create a new MSBF instance.
47
+
48
+ :param is_big_endian: if the file is big endian.
49
+ :param encoding: the file encoding.
50
+ :param version: the file version.
51
+ :param section_count: the number of sections.
52
+
53
+ """
54
+ return MSBF(LMS_FileInfo(is_big_endian, encoding, version, section_count))
55
+
39
56
  @property
40
57
  def info(self) -> LMS_FileInfo:
41
58
  """The file info for the MSBT instance."""
@@ -44,7 +61,7 @@ class MSBF:
44
61
  @property
45
62
  def flowcharts(self) -> MappingProxyType[str, LMS_Flowchart]:
46
63
  """The flowcharts of the MSBF instance."""
47
- return MappingProxyType(self._flowcharts)
64
+ return MappingProxyType({flowchart.name: flowchart for flowchart in self._flowcharts})
48
65
 
49
66
  @property
50
67
  def global_node_id(self) -> int:
@@ -67,14 +84,14 @@ class MSBF:
67
84
 
68
85
  :param flowchart_name: The name of the new flowchart.
69
86
  """
70
- if flowchart_name in self._flowcharts:
87
+ if flowchart_name in self.flowcharts:
71
88
  raise KeyError(f"Flowchart with name '{flowchart_name}' already exists!")
72
89
 
73
90
  if entry_point is None:
74
91
  entry_point = LMS_EntryNode(self.generate_next_id(), flowchart_name=flowchart_name)
75
92
 
76
93
  flowchart = LMS_Flowchart(entry_point, self.generate_next_id)
77
- self._flowcharts[flowchart_name] = flowchart
94
+ self._flowcharts.append(flowchart)
78
95
  return flowchart
79
96
 
80
97
  def delete_flowchart(self, name: str):
@@ -65,7 +65,8 @@ class MSBT:
65
65
  encoding: FileEncoding = FileEncoding.UTF16,
66
66
  version: int = 3,
67
67
  section_count: int = 2):
68
- """Create a new MSBT instance.
68
+ """
69
+ Create a new MSBT instance.
69
70
 
70
71
  :param uses_nli1: flag to determine if to use nli1 section for labels.
71
72
  :param attribute_config: the attribute config object
@@ -1,5 +1,9 @@
1
- from importlib import resources
1
+ import hashlib
2
+ import os.path
3
+ import pathlib
4
+ from typing import Literal, get_args
2
5
 
6
+ import requests
3
7
  import yaml
4
8
 
5
9
  from lms.common.field.lms_datatype import LMS_DataType
@@ -10,6 +14,8 @@ from lms.titleconfig.definitions.nodes import NodeConfig, NodeDefinition
10
14
  from lms.titleconfig.definitions.tags import TagConfig, TagDefinition
11
15
  from lms.titleconfig.definitions.value import ValueDefinition
12
16
 
17
+ PRESETS_URL = "https://api.github.com/repos/AbdyyEee/PyLibMS/contents/lms/titleconfig/presets"
18
+
13
19
 
14
20
  class TitleConfig:
15
21
  """
@@ -20,9 +26,17 @@ class TitleConfig:
20
26
  ATTR_KEY = "attribute_definitions"
21
27
  NODE_KEY = "node_definitions"
22
28
 
23
- PRESET_LIST = [
24
- file.name.removesuffix(".yaml")
25
- for file in resources.files("lms.titleconfig.presets").iterdir()
29
+ GAME_PRESET = Literal[
30
+ "Badge Arcade",
31
+ "Brain Age Concentration Training",
32
+ "Kirby Planet Robobot",
33
+ "Super Mario Odyssey",
34
+ "Super Mario 3D Land",
35
+ "Super Mario 3D World + Bowsers Fury",
36
+ "The Legend of Zelda a Link Between Worlds",
37
+ "The Legend of Zelda Echos of Wisdom",
38
+ "Tomodachi Life Living The Dream",
39
+ "Tomodachi Life NA-EU",
26
40
  ]
27
41
 
28
42
  def __init__(
@@ -42,6 +56,84 @@ class TitleConfig:
42
56
  """The name of the game for the titleconfig."""
43
57
  return self._game
44
58
 
59
+ @classmethod
60
+ def get_preset_list(cls) -> tuple[str, ...]:
61
+ """Get the current preset list."""
62
+ return get_args(cls.GAME_PRESET)
63
+
64
+ @classmethod
65
+ def preset_has_update(cls, game: GAME_PRESET) -> bool:
66
+ """
67
+ Checks whether a preset has an available update.
68
+
69
+ :param game: the game preset.
70
+ """
71
+ preset_list = cls._request_preset_list()
72
+
73
+ if f"{game}.yaml" not in os.listdir("presets"):
74
+ raise FileNotFoundError(f"Preset '{game}.yaml' was not found!")
75
+
76
+ path = os.path.join("presets", f"{game}.yaml")
77
+
78
+ with open(path, "rb") as f:
79
+ data = f.read().replace(b"\r\n", b"\n")
80
+
81
+ # Blob calculation https://git-scm.com/book/en/v2/Git-Internals-Git-Objects.html
82
+ local_hash = hashlib.sha1(f"blob {len(data)}\0".encode() + data).hexdigest()
83
+
84
+ return local_hash != preset_list[game.lower()]["sha"]
85
+
86
+ @classmethod
87
+ def download_preset(cls, game: GAME_PRESET):
88
+ """
89
+ Fetches a preset from the repository and loads it to ./presets
90
+
91
+ :param game: the game preset.
92
+
93
+ List of presets:
94
+
95
+ https://github.com/AbdyyEee/PylibMS/tree/main/lms/titleconfig/presets
96
+ """
97
+
98
+ preset_list = cls._request_preset_list()
99
+
100
+ if game.lower() not in preset_list:
101
+ raise FileNotFoundError(f"Preset '{game}' not found.")
102
+
103
+ raw = cls._request_preset_file(game, preset_list)
104
+
105
+ if "presets" not in os.path.join(os.getcwd(), "presets", f"{game}.yaml"):
106
+ pathlib.Path("presets").mkdir()
107
+
108
+ path = os.path.join("presets", f"{game}.yaml")
109
+ with open(path, "wb") as f:
110
+ f.write(raw.content)
111
+
112
+ @classmethod
113
+ def _request_preset_list(cls) -> dict[str, dict]:
114
+ result = {}
115
+
116
+ try:
117
+ folder_response = requests.get(PRESETS_URL, timeout=10)
118
+ folder_response.raise_for_status()
119
+ except requests.exceptions.RequestException as e:
120
+ raise RuntimeError("An error occurred fetching the preset list!") from e
121
+
122
+ for preset in folder_response.json():
123
+ result[os.path.basename(preset["path"]).lower().removesuffix(".yaml")] = preset
124
+
125
+ return result
126
+
127
+ @classmethod
128
+ def _request_preset_file(cls, game: str, preset_list: dict) -> requests.Response:
129
+ try:
130
+ raw_response = requests.get(preset_list[game.lower()]["download_url"], timeout=10)
131
+ raw_response.raise_for_status()
132
+ except requests.exceptions.RequestException as e:
133
+ raise RuntimeError(f"An error occurred fetching the file data for preset '{game}'") from e
134
+
135
+ return raw_response
136
+
45
137
  @property
46
138
  def tag_config(self) -> TagConfig | None:
47
139
  """The loaded tag config instance."""
@@ -75,26 +167,6 @@ class TitleConfig:
75
167
  """The loaded node configuration."""
76
168
  return self._node_config
77
169
 
78
- @classmethod
79
- def load_preset(cls, game: str):
80
- """
81
- Loads an existing preset from a game.
82
-
83
- :param game: the game preset.
84
-
85
- The list of presets are found with `TitleConfig.preset_list`.
86
- """
87
- # TODO: Load presets via requests instead of hard-coded files in the library
88
- preset_map = {preset.lower(): preset for preset in cls.PRESET_LIST}
89
-
90
- if game.lower() not in preset_map:
91
- raise FileNotFoundError(f"Preset '{game}' not found.")
92
-
93
- actual_name = preset_map[game.lower()]
94
-
95
- with resources.open_text("lms.titleconfig.presets", f"{actual_name}.yaml") as f:
96
- return cls.load_config(f.read())
97
-
98
170
  @classmethod
99
171
  def load_file(cls, file_path: str):
100
172
  """
@@ -69,16 +69,16 @@ class NodeDefinition:
69
69
  if datatype_from_dict is None:
70
70
  match parameter_type:
71
71
  case LMS_NodeParameterType.PARAM_32_0 | LMS_NodeParameterType.PARAM_32_1:
72
- datatype_from_dict = LMS_DataType.INT32
72
+ datatype_from_dict = LMS_DataType.UINT32
73
73
  case LMS_NodeParameterType.PARAM_16_16:
74
- datatype_from_dict = (LMS_DataType.INT16, LMS_DataType.INT16)[i]
74
+ datatype_from_dict = (LMS_DataType.UINT16, LMS_DataType.UINT16)[i]
75
75
  case LMS_NodeParameterType.PARAM_16_8_8:
76
- datatype_from_dict = (LMS_DataType.INT16, LMS_DataType.INT8, LMS_DataType.INT8)[i]
76
+ datatype_from_dict = (LMS_DataType.UINT16, LMS_DataType.UINT8, LMS_DataType.UINT8)[i]
77
77
  case LMS_NodeParameterType.PARAM_8_8_16:
78
- datatype_from_dict = (LMS_DataType.INT8, LMS_DataType.INT8, LMS_DataType.INT16)[i]
78
+ datatype_from_dict = (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT16)[i]
79
79
  case LMS_NodeParameterType.PARAM_8_8_8_8:
80
80
  datatype_from_dict = \
81
- (LMS_DataType.INT8, LMS_DataType.INT8, LMS_DataType.INT8, LMS_DataType.INT8)[i]
81
+ (LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT8, LMS_DataType.UINT8)[i]
82
82
  case LMS_NodeParameterType.STRING:
83
83
  datatype_from_dict = LMS_DataType.STRING
84
84
  definition = ValueDefinition(parameter["name"], data.get("description", ""), datatype_from_dict,
@@ -3,61 +3,43 @@ node_definitions:
3
3
  branch:
4
4
  - id: 0
5
5
  name: EvaluateExpression
6
+ description: ''
6
7
  parameter_type: string
7
8
  parameters:
8
9
  - name: exp
9
- description: ''
10
10
  - id: 1
11
11
  name: SwitchVariable
12
- description: ''
13
12
  parameter_type: string
14
13
  parameters:
15
14
  - name: var
16
- description: ''
17
15
  event:
18
16
  - id: 3
19
17
  name: SetVariable
20
- description: ''
21
18
  parameter_type: string
22
19
  parameters:
23
20
  - name: expression
24
- description: ''
25
21
  - id: 43
26
22
  name: SetMiiPosition
27
- description: ''
28
23
  parameter_type: string
29
24
  parameters:
30
25
  - name: pos
31
- description: ''
32
- - id: 48
33
- name: SetupMessage
34
- description: ''
35
- parameter_type: p16_16
36
- parameters:
37
- - name: val1
38
- - name: val2
39
26
  - id: 58
40
27
  name: SetupButtonPrompt
41
- description: ''
42
28
  next_node_dependency: true
43
29
  parameter_type: string
44
30
  parameters:
45
31
  - name: var
46
- description: ''
47
32
  - id: 90
48
33
  name: LoadSEGroup
49
- description: ''
50
34
  parameter_type: string
51
35
  parameters:
52
36
  - name: group
53
37
  description: ''
54
38
  - id: 96
55
39
  name: PlaySE
56
- description: ''
57
40
  parameter_type: string
58
41
  parameters:
59
42
  - name: name
60
- description: ''
61
43
  tag_definitions:
62
44
  groups:
63
45
  0: System
@@ -96,7 +78,7 @@ tag_definitions:
96
78
  description: 'Alters the size of the text.'
97
79
  parameters:
98
80
  - name: percent
99
- description: 'The size as a percentage'
81
+ description: 'The size as a percentage.'
100
82
  datatype: uint16
101
83
  - name: Color
102
84
  group_id: 0
@@ -7466,4 +7448,4 @@ attribute_definitions:
7466
7448
  - Relation
7467
7449
  - name: SolvedCount
7468
7450
  description: ''
7469
- datatype: uint8
7451
+ datatype: uint8
@@ -7,7 +7,7 @@ include = ["lms*"]
7
7
 
8
8
  [project]
9
9
  name = "PyLibMS"
10
- version = "4.0.3"
10
+ version = "4.1.0"
11
11
  requires-python = ">=3.12"
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