PyLibMS 3.1.7__tar.gz → 3.2.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 (71) hide show
  1. {pylibms-3.1.7 → pylibms-3.2.0}/PKG-INFO +1 -1
  2. {pylibms-3.1.7 → pylibms-3.2.0}/PyLibMS.egg-info/PKG-INFO +1 -1
  3. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/lms_datatype.py +36 -6
  4. {pylibms-3.1.7 → pylibms-3.2.0}/lms/fileio/io.py +7 -7
  5. pylibms-3.2.0/lms/message/definitions/field/io.py +60 -0
  6. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/definitions/field/lms_field.py +14 -11
  7. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/definitions/lms_messagetext.py +23 -25
  8. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/msbt.py +34 -12
  9. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/msbtentry.py +9 -7
  10. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/msbtio.py +36 -35
  11. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/section/atr1.py +7 -4
  12. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/section/txt2.py +1 -1
  13. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/tag/io/param_io.py +11 -11
  14. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/tag/io/tag_io.py +12 -8
  15. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/tag/lms_tag.py +3 -3
  16. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/definitions/tag.py +3 -3
  17. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/msbp.py +6 -2
  18. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/msbpread.py +18 -24
  19. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/ali2.py +1 -1
  20. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/string.py +1 -1
  21. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/tag2.py +1 -1
  22. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/tgg2.py +3 -3
  23. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/tgp2.py +2 -2
  24. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/config.py +2 -2
  25. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/definitions/tags.py +8 -6
  26. {pylibms-3.1.7 → pylibms-3.2.0}/pyproject.toml +1 -1
  27. pylibms-3.1.7/lms/message/definitions/field/io.py +0 -59
  28. {pylibms-3.1.7 → pylibms-3.2.0}/LICENSE +0 -0
  29. {pylibms-3.1.7 → pylibms-3.2.0}/MANIFEST.in +0 -0
  30. {pylibms-3.1.7 → pylibms-3.2.0}/PyLibMS.egg-info/SOURCES.txt +0 -0
  31. {pylibms-3.1.7 → pylibms-3.2.0}/PyLibMS.egg-info/dependency_links.txt +0 -0
  32. {pylibms-3.1.7 → pylibms-3.2.0}/PyLibMS.egg-info/requires.txt +0 -0
  33. {pylibms-3.1.7 → pylibms-3.2.0}/PyLibMS.egg-info/top_level.txt +0 -0
  34. {pylibms-3.1.7 → pylibms-3.2.0}/README.md +0 -0
  35. {pylibms-3.1.7 → pylibms-3.2.0}/lms/__init__.py +0 -0
  36. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/__init__.py +0 -0
  37. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/lms_exceptions.py +0 -0
  38. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/lms_fileinfo.py +0 -0
  39. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/stream/fileinfo.py +0 -0
  40. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/stream/hashtable.py +0 -0
  41. {pylibms-3.1.7 → pylibms-3.2.0}/lms/common/stream/section.py +0 -0
  42. {pylibms-3.1.7 → pylibms-3.2.0}/lms/fileio/encoding.py +0 -0
  43. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/__init__.py +0 -0
  44. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/definitions/__init__.py +0 -0
  45. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/definitions/field/__init__.py +0 -0
  46. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/section/__init__.py +0 -0
  47. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/section/tsy1.py +0 -0
  48. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/tag/__init__.py +0 -0
  49. {pylibms-3.1.7 → pylibms-3.2.0}/lms/message/tag/lms_tagexceptions.py +0 -0
  50. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/__init__.py +0 -0
  51. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/definitions/__init__.py +0 -0
  52. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/definitions/attribute.py +0 -0
  53. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/definitions/color.py +0 -0
  54. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/definitions/style.py +0 -0
  55. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/ati2.py +0 -0
  56. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/clr1.py +0 -0
  57. {pylibms-3.1.7 → pylibms-3.2.0}/lms/project/section/syl3.py +0 -0
  58. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/__init__.py +0 -0
  59. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/definitions/__init__.py +0 -0
  60. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/definitions/attribute.py +0 -0
  61. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/definitions/value.py +0 -0
  62. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
  63. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
  64. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
  65. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
  66. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
  67. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
  68. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
  69. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
  70. {pylibms-3.1.7 → pylibms-3.2.0}/lms/titleconfig/presets/Tomodachi Life.yaml +0 -0
  71. {pylibms-3.1.7 → pylibms-3.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.1.7
3
+ Version: 3.2.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: Copyright 2025 AbdyyEee
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.1.7
3
+ Version: 3.2.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: Copyright 2025 AbdyyEee
@@ -1,5 +1,35 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
- from typing import Type
4
+ from typing import Type, TypeGuard
5
+
6
+
7
+ def is_number_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[int | float]:
8
+ return datatype in (
9
+ LMS_DataType.UINT8,
10
+ LMS_DataType.UINT16,
11
+ LMS_DataType.UINT32,
12
+ LMS_DataType.INT8,
13
+ LMS_DataType.INT16,
14
+ LMS_DataType.INT32,
15
+ LMS_DataType.FLOAT32,
16
+ ) and isinstance(value, (int, float))
17
+
18
+
19
+ def is_list_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[str]:
20
+ return datatype is LMS_DataType.LIST and isinstance(value, str)
21
+
22
+
23
+ def is_bool_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[bool]:
24
+ return datatype is LMS_DataType.BOOL and isinstance(value, bool)
25
+
26
+
27
+ def is_bytes_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[bytes]:
28
+ return datatype is LMS_DataType.BYTES and isinstance(value, bytes)
29
+
30
+
31
+ def is_string_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[str]:
32
+ return datatype is LMS_DataType.STRING and isinstance(value, str)
3
33
 
4
34
 
5
35
  class LMS_DataType(Enum):
@@ -17,16 +47,16 @@ class LMS_DataType(Enum):
17
47
 
18
48
  # Unknown 16 bit type (value of 6) has yet to be documented
19
49
  # Might be some sort of 2 byte integer, float, or may be an array.
20
- # We wont ever know cause no game (yet that has been found) has utilized this type
50
+ # We won't ever know cause no game (yet that has been found) has utilized this type
21
51
  # Thanks Nintendo.
22
52
  ...
23
53
 
24
54
  STRING = 8
25
55
  LIST = 9
26
56
 
27
- # These types are not offical, but allow for abstraction from the value of the actual type
28
- # As an example, BOOL can be utilized for UInt8 values that act like a bool
29
- # Byte types can also be used for when the type/value is unknown or if there is extra data in the tag.
57
+ # Interface types
58
+ # These types act as an abstraction for a real LMS_Datatype
59
+ # The actual value isn't important since they are not real types, but are instantiated from a config
30
60
  BOOL = "bool"
31
61
  BYTES = "byte"
32
62
 
@@ -81,7 +111,7 @@ class LMS_DataType(Enum):
81
111
 
82
112
  @classmethod
83
113
  def from_string(cls, string: str):
84
- """Creates a LMS_Datatype enum value from it's string representation"""
114
+ """Creates an enum value from its string representation"""
85
115
  member = string.upper()
86
116
  if member in cls.__members__:
87
117
  return cls[member]
@@ -93,7 +93,7 @@ class FileReader:
93
93
  def read_string_len(self, length: int) -> str:
94
94
  return self._stream.read(length).decode("UTF-8")
95
95
 
96
- def read_str_variable_encoding(self):
96
+ def read_encoded_string(self):
97
97
  message = b""
98
98
  while (
99
99
  raw_char := self.read_bytes(self.encoding.width)
@@ -101,7 +101,7 @@ class FileReader:
101
101
  message += raw_char
102
102
  return message.decode(self.encoding.to_string_format(self.is_big_endian))
103
103
 
104
- def read_len_string_variable_encoding(self):
104
+ def read_len_string_encoded(self):
105
105
  self.align(self.encoding.width)
106
106
  length = self.read_uint16()
107
107
  return self.read_bytes(length).decode(
@@ -138,7 +138,7 @@ class FileWriter:
138
138
  return self.data.tell()
139
139
 
140
140
  def write_alignment(self, data: bytes, alignment: int) -> None:
141
- self.write_bytes(data * self.align(self.tell(), alignment))
141
+ self.write_bytes(data * self._align(self.tell(), alignment))
142
142
 
143
143
  def write_uint16_array(self, array: list[int]) -> None:
144
144
  for number in array:
@@ -168,18 +168,18 @@ class FileWriter:
168
168
  def write_string(self, string: str):
169
169
  self.write_bytes(string.encode("UTF-8"))
170
170
 
171
- def write_len_variable_encoding_string(self, string: str) -> None:
171
+ def write_len_encoded_string(self, string: str) -> None:
172
172
  self.write_uint16(len(string) * self.encoding.width)
173
- self.write_variable_encoding_string(string, False)
173
+ self.write_encoded_string(string, False)
174
174
 
175
- def write_variable_encoding_string(self, string: str, terminate: bool = True):
175
+ def write_encoded_string(self, string: str, terminate: bool = True):
176
176
  self.write_bytes(
177
177
  string.encode(self.encoding.to_string_format(self.is_big_endian))
178
178
  )
179
179
  if terminate:
180
180
  self.write_bytes(b"\x00" * self.encoding.width)
181
181
 
182
- def align(self, number: int, alignment: int) -> int:
182
+ def _align(self, number: int, alignment: int) -> int:
183
183
  return (-number % alignment + alignment) % alignment
184
184
 
185
185
  def _get_datatype(self, name: str) -> str:
@@ -0,0 +1,60 @@
1
+ from typing import Callable
2
+
3
+ from lms.common.lms_datatype import (LMS_DataType, is_bool_datatype,
4
+ is_bytes_datatype, is_list_datatype,
5
+ is_number_datatype)
6
+ from lms.fileio.io import FileReader, FileWriter
7
+ from lms.message.definitions.field.lms_field import LMS_Field
8
+ from lms.titleconfig.definitions.value import ValueDefinition
9
+
10
+
11
+ def read_field(reader: FileReader, definition: ValueDefinition) -> LMS_Field:
12
+ # String is excluded as their reading varies between tags and attributes
13
+ match definition.datatype:
14
+ case LMS_DataType.UINT8:
15
+ value = reader.read_uint8()
16
+ case LMS_DataType.UINT16:
17
+ value = reader.read_uint16()
18
+ case LMS_DataType.UINT32:
19
+ value = reader.read_uint32()
20
+ case LMS_DataType.INT8:
21
+ value = reader.read_int8()
22
+ case LMS_DataType.INT16:
23
+ value = reader.read_int16()
24
+ case LMS_DataType.INT32:
25
+ value = reader.read_int32()
26
+ case LMS_DataType.FLOAT32:
27
+ value = reader.read_float32()
28
+ case LMS_DataType.LIST:
29
+ index = reader.read_uint8()
30
+ value = definition.list_items[index]
31
+ case LMS_DataType.BOOL:
32
+ value = bool(reader.read_uint8())
33
+ case LMS_DataType.BYTES:
34
+ value = reader.read_bytes(1)
35
+
36
+ return LMS_Field(value, definition) # type: ignore
37
+
38
+
39
+ def write_field(writer: FileWriter, field: LMS_Field) -> None:
40
+ if is_number_datatype(field.value, field.datatype):
41
+ write_functions: dict[LMS_DataType, Callable] = {
42
+ LMS_DataType.UINT8: writer.write_uint8,
43
+ LMS_DataType.INT8: writer.write_int8,
44
+ LMS_DataType.UINT16: writer.write_uint16,
45
+ LMS_DataType.INT16: writer.write_int16,
46
+ LMS_DataType.UINT32: writer.write_uint32,
47
+ LMS_DataType.INT32: writer.write_int32,
48
+ LMS_DataType.FLOAT32: writer.write_float32,
49
+ }
50
+ write_functions[field.datatype](field.value)
51
+ return
52
+
53
+ if is_list_datatype(field.value, field.datatype):
54
+ writer.write_uint8(field.list_items.index(field.value))
55
+ elif is_bytes_datatype(field.value, field.datatype):
56
+ writer.write_bytes(field.value)
57
+ elif is_bool_datatype(field.value, field.datatype):
58
+ writer.write_uint8(bool(field.value))
59
+ else:
60
+ raise ValueError(f"Unsupported datatype: {field.datatype}")
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterator
4
- from dataclasses import dataclass, field
5
- from typing import cast
4
+ from dataclasses import dataclass
6
5
 
7
6
  from lms.common.lms_datatype import LMS_DataType
8
7
  from lms.titleconfig.definitions.value import ValueDefinition
@@ -17,7 +16,8 @@ type FieldValue = int | str | float | bool | bytes
17
16
  @dataclass(frozen=True)
18
17
  class LMS_FieldMap:
19
18
  """
20
- A wrapper for a dictionary of LMS_Field objects for controlled access, validation and abstraction from the dictionary object.
19
+ A wrapper for a dictionary of LMS_Field objects for controlled access, validation
20
+ and abstraction from the dict object.
21
21
  """
22
22
 
23
23
  fields: dict[str, LMS_Field]
@@ -91,8 +91,8 @@ class LMS_Field:
91
91
 
92
92
  def __repr__(self):
93
93
  if self.datatype is LMS_DataType.LIST:
94
- return f"LMS_Field(value={self._value}, list_items={self.list_items})"
95
- return f"LMS_Field(value={self._value}, type={self.datatype.name})"
94
+ return f"LMS_Field(value={self._value!r}, list_items={self.list_items!r})"
95
+ return f"LMS_Field(value={self._value!r}, type={self.datatype!r})"
96
96
 
97
97
  @property
98
98
  def name(self) -> str:
@@ -147,7 +147,7 @@ def _verify_value(
147
147
  else:
148
148
  return
149
149
  case LMS_DataType.FLOAT32 if isinstance(value, float):
150
- _verify_range(value, FLOAT_MIN, FLOAT_MAX, definition)
150
+ _verify_number_is_in_range(value, FLOAT_MIN, FLOAT_MAX, definition)
151
151
  return
152
152
  case _ if isinstance(value, int):
153
153
  bits = datatype.stream_size * 8
@@ -157,7 +157,7 @@ def _verify_value(
157
157
  else:
158
158
  min_value, max_value = 0, (2**bits) - 1
159
159
 
160
- _verify_range(value, min_value, max_value, definition)
160
+ _verify_number_is_in_range(value, min_value, max_value, definition)
161
161
  return
162
162
 
163
163
  raise TypeError(
@@ -165,10 +165,13 @@ def _verify_value(
165
165
  )
166
166
 
167
167
 
168
- def _verify_range(
169
- value: int | float, min: int | float, max: int | float, definition: ValueDefinition
168
+ def _verify_number_is_in_range(
169
+ value: int | float,
170
+ min_value: int | float,
171
+ max_value: int | float,
172
+ definition: ValueDefinition,
170
173
  ):
171
- if not min <= value <= max:
174
+ if not min_value <= value <= max_value:
172
175
  raise ValueError(
173
- f"The value '{value}' of type '{definition.datatype}' provided for field '{definition.name}' is out of range of ({min}, {max})"
176
+ f"The value '{value}' of type '{definition.datatype}' provided for field '{definition.name}' is out of range of ({min_value}, {max_value})"
174
177
  )
@@ -9,7 +9,7 @@ from lms.titleconfig.config import TagConfig
9
9
  class LMS_MessageText:
10
10
  """Class that represents a message text entry."""
11
11
 
12
- TAG_FORMAT = re.compile(r"(\[[^\]]+\])")
12
+ TAG_FORMAT = re.compile(r"(\[[^]]+])")
13
13
 
14
14
  def __init__(
15
15
  self,
@@ -38,7 +38,7 @@ class LMS_MessageText:
38
38
  return "".join(result)
39
39
 
40
40
  @text.setter
41
- def text(self, string: str):
41
+ def text(self, string: str) -> None:
42
42
  self._set_parts(string)
43
43
 
44
44
  @property
@@ -47,19 +47,20 @@ class LMS_MessageText:
47
47
  return [part for part in self._parts if is_tag(part)]
48
48
 
49
49
  def append_encoded_tag(
50
- self, group_id: int, tag_index: int, is_closing: bool = False, *parameters: str
50
+ self, group_id: int, tag_index: int, *parameters: str, is_closing: bool = False
51
51
  ) -> LMS_EncodedTag:
52
52
  """
53
53
  Appends an encoded tag to the current message and returns that tag.
54
54
 
55
- :param group: the group name or index.
56
- :param tag: the group tag or index:
57
- :param parameters: a list of hex strings.
55
+ :param group_id: the group index.
56
+ :param tag_index: the index of the tag in the group.
57
+ :param parameters: args of hex strings.
58
+ :param is_closing: whether the tag is closing or not.
58
59
 
59
- ## Usage
60
- ```
61
- message.append_encoded_tag(1, 2, "01", "00", "00", "CD")
62
- ```
60
+ Example
61
+ ------
62
+ >>> message = LMS_MessageText("Text")
63
+ >>> message.append_encoded_tag(0, 3, "00", "23", "43", "32")
63
64
  """
64
65
  if is_closing:
65
66
  tag = LMS_EncodedTag(group_id, tag_index, is_closing=True)
@@ -79,35 +80,32 @@ class LMS_MessageText:
79
80
  **parameters: int | str | float | bool | bytes,
80
81
  ) -> LMS_DecodedTag:
81
82
  """
82
- Appends an decoded tag to the current message and returns that tag.
83
+ Appends a decoded tag to the current message and returns that tag.
83
84
 
84
85
  :param group_name: the group name.
85
86
  :param tag_name: the tag name.:
87
+ :param is_closing: whether the tag is closing or not.
86
88
  :param parameters: keyword arguments of parameters mapped to their value.
87
89
 
88
- ## Usage
89
- ```
90
- message.append_decoded_tag("Mii", "Nickname", buffer=1, type="Voice", conversion="None")
91
- ```
90
+ Example
91
+ -------
92
+ >>> message = LMS_MessageText("Text")
93
+ >>> message.append_decoded_tag("Mii", "Nickname", buffer=1, type="Voice", conversion="None")
92
94
  """
93
95
  if self._tag_config is None:
94
96
  raise ValueError("A TitleConfig is required to append decoded tags.")
95
97
 
96
98
  definition = self._tag_config.get_definition_by_names(group_name, tag_name)
97
99
 
98
- param_map = None
99
-
100
100
  if not parameters:
101
101
  tag = LMS_DecodedTag(definition)
102
- self._parts.append(tag)
103
- return tag
104
-
105
- param_map = LMS_FieldMap.from_dict(parameters, definition.parameters)
106
-
107
- if is_closing:
108
- tag = LMS_DecodedTag(definition, is_closing=True)
109
102
  else:
110
- tag = LMS_DecodedTag(definition, param_map)
103
+ param_map = LMS_FieldMap.from_dict(parameters, definition.parameters)
104
+
105
+ if is_closing:
106
+ tag = LMS_DecodedTag(definition, is_closing=True)
107
+ else:
108
+ tag = LMS_DecodedTag(definition, param_map)
111
109
 
112
110
  self._parts.append(tag)
113
111
  return tag
@@ -1,21 +1,29 @@
1
1
  from typing import Any
2
2
 
3
3
  from lms.common.lms_fileinfo import LMS_FileInfo
4
- from lms.message.definitions.field.lms_field import FieldValue, LMS_FieldMap
5
- from lms.message.definitions.lms_messagetext import LMS_MessageText
6
4
  from lms.message.msbtentry import MSBTEntry
7
5
  from lms.titleconfig.definitions.attribute import AttributeConfig
8
6
  from lms.titleconfig.definitions.tags import TagConfig
9
7
 
10
8
 
11
9
  class MSBT:
12
- """A class that represents a MSBT file.
10
+ """
11
+ A class that represents a MSBT file.
13
12
 
14
- https://nintendo-formats.com/libs/lms/msbt.html."""
13
+ https://nintendo-formats.com/libs/lms/msbt.html.
14
+ """
15
+
16
+ MAGIC = "MsgStdBn"
17
+
18
+ # While 101 is the default slot count for LBL1 sections in a MSBT
19
+ # The value may be overridden at the instance level as some games alter the value
20
+ DEFAULT_SLOT_COUNT = 101
15
21
 
16
22
  def __init__(
17
23
  self,
18
24
  info: LMS_FileInfo | None = None,
25
+ section_list: list[str] | None = None,
26
+ _unsupported_section_map: dict[str, bytes] | None = None,
19
27
  attribute_config: AttributeConfig | None = None,
20
28
  tag_config: TagConfig | None = None,
21
29
  ):
@@ -26,17 +34,15 @@ class MSBT:
26
34
 
27
35
  self.size_per_attribute = 0
28
36
 
29
- # 101 is default for almost all games. However the value can be overriden by some games (i.e Echos of Wisdom).
30
- # Due to this, the slot count is set dynamically when LBL1 is read.
31
- self.slot_count = 101
37
+ self.slot_count = MSBT.DEFAULT_SLOT_COUNT
32
38
 
33
- self.attr_string_table: bytes | None = None
34
39
  self.uses_encoded_attributes = True
40
+ self.attr_string_table: bytes | None = None
35
41
 
36
- self.unsupported_sections: dict[str, bytes] = {}
42
+ self._unsupported_section_map = _unsupported_section_map or {}
37
43
 
38
44
  # Store the section list so that the order of any and all sections is preserved when writing
39
- self._section_list: list[str] = ["LBL1"]
45
+ self._section_list: list[str] = section_list or ["LBL1"]
40
46
 
41
47
  self._attribute_config = attribute_config
42
48
  self._tag_config = tag_config
@@ -54,9 +60,14 @@ class MSBT:
54
60
  """The list of sections with order preserved."""
55
61
  return tuple(self._section_list)
56
62
 
63
+ @property
64
+ def unsupported_sections(self) -> tuple[str, ...]:
65
+ """The list of unsupported sections."""
66
+ return tuple(self._unsupported_section_map.keys())
67
+
57
68
  @property
58
69
  def has_attributes(self) -> bool:
59
- """If the msbt contains attributs."""
70
+ """If the msbt contains attributes."""
60
71
  return self.section_exists("ATR1")
61
72
 
62
73
  @property
@@ -79,7 +90,7 @@ class MSBT:
79
90
  """
80
91
  Retrieves an entry given its name.
81
92
 
82
- :param name: the label name for the entry.
93
+ :param label: the label name for the entry.
83
94
  """
84
95
  if label not in self._label_map:
85
96
  raise KeyError(f"The label '{label}' does not exist!")
@@ -139,3 +150,14 @@ class MSBT:
139
150
  :param name: the name of the section.
140
151
  """
141
152
  return name in self._section_list
153
+
154
+ def get_unsupported_section_data(self, name: str) -> bytes:
155
+ """
156
+ Retrieves the raw data of an unsupported section.
157
+
158
+ :param name: the name of the section.
159
+ """
160
+ if name not in self._unsupported_section_map:
161
+ raise KeyError(f"The section '{name}' does not exist in the MSBT!")
162
+
163
+ return self._unsupported_section_map[name]
@@ -29,7 +29,8 @@ class MSBTEntry:
29
29
 
30
30
  if attribute is not None and not isinstance(attribute, (LMS_FieldMap, bytes)):
31
31
  raise TypeError(
32
- f"An invalid type was provided for attribute in entry '{name}'. Expected dict or bytes got {type(attribute)},"
32
+ f"An invalid type was provided for attribute in entry '{name}'. "
33
+ f"Expected LMS_FieldMap or bytes, got {type(attribute)}"
33
34
  )
34
35
 
35
36
  self._attribute = attribute
@@ -47,9 +48,10 @@ class MSBTEntry:
47
48
 
48
49
  def to_dict(self) -> dict:
49
50
  """Converts the MSBTEntry instance into a dictionary object."""
50
- result = {}
51
- result["name"] = self.name
52
- result["message"] = "" if self.message is None else self.message.text
51
+ result: dict[str, int | str | dict | None] = {
52
+ "name": self.name,
53
+ "message": "" if self.message is None else self.message.text,
54
+ }
53
55
 
54
56
  if self._attribute is not None:
55
57
  if isinstance(self._attribute, bytes):
@@ -64,7 +66,7 @@ class MSBTEntry:
64
66
  def from_dict(
65
67
  cls,
66
68
  data: dict,
67
- attribute_conifg: AttributeConfig | None = None,
69
+ attribute_config: AttributeConfig | None = None,
68
70
  tag_config: TagConfig | None = None,
69
71
  ):
70
72
  """
@@ -86,12 +88,12 @@ class MSBTEntry:
86
88
  raise TypeError("Invalid attribute type provided!")
87
89
 
88
90
  if isinstance(attribute, dict):
89
- if attribute_conifg is None:
91
+ if attribute_config is None:
90
92
  raise TypeError(
91
93
  "A valid attribute config must be provided for decoded attributes!"
92
94
  )
93
95
  attribute = LMS_FieldMap.from_dict(
94
- attribute, attribute_conifg.definitions
96
+ attribute, attribute_config.definitions
95
97
  )
96
98
  else:
97
99
  attribute = bytes.fromhex(attribute)
@@ -26,16 +26,14 @@ def read_msbt_path(
26
26
  """
27
27
  Reads and retrieves a MSBT file from a given path.
28
28
 
29
- :param stream: a stream object.
29
+ :param file_path: the path to the MSBT file.
30
30
  :param attribute_config: the attribute config to use for decoding attributes.
31
31
  :param tag_config: the tag config to use for decoding tags.
32
32
  :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
33
33
 
34
- ## Usage
35
- ```
36
- msbt: MSBT = read_msbt_path("path/to/file.msbt")
37
- ...
38
- ```
34
+ Example
35
+ ---------
36
+ >>> msbt = read_msbt_path("path/to/file.msbt")
39
37
  """
40
38
  with open(file_path, "rb") as stream:
41
39
  return read_msbt(
@@ -56,44 +54,50 @@ def read_msbt(
56
54
  """
57
55
  Reads and retrieves a MSBT file from a specified stream.
58
56
 
59
- :param stream: an `IOBase`, `BytesIO`, `memoryview`, or `bytes` object.
57
+ :param stream: an ``IOBase``, ``BytesIO``, ``memoryview``, or ``bytes`` object.
60
58
  :param attribute_config: the attribute config to use for decoding attributes.
61
59
  :param tag_config: the tag config to use for decoding tags.
62
60
  :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
63
61
 
64
- ## Usage
65
- ```
66
- msbt: MSBT = read_msbt(stream)
67
- ...
68
- ```
62
+ Example
63
+ ---------
64
+ >>> msbt = read_msbt(stream)
69
65
  """
70
66
  reader = FileReader(stream)
71
- file_info = read_file_info(reader, "MsgStdBn")
67
+ file_info = read_file_info(reader, MSBT.MAGIC)
72
68
 
73
- file = MSBT(file_info, attribute_config, tag_config)
69
+ section_list = []
70
+ unsupported_sections = {}
71
+ slot_count = MSBT.DEFAULT_SLOT_COUNT
74
72
 
75
- if attribute_config is not None:
76
- file.uses_encoded_attributes = False
73
+ messages = atr1_data = style_indexes = None
77
74
 
78
- atr1_data = style_indexes = None
75
+ labels: dict[int, str] = {}
79
76
  for magic, size in read_section_data(reader, file_info.section_count):
80
77
  match magic:
81
78
  case "LBL1":
82
79
  labels, slot_count = read_labels(reader)
83
- file.slot_count = slot_count
84
80
  case "ATR1":
85
81
  atr1_data = read_atr1(reader, attribute_config, size)
86
- file.size_per_attribute = atr1_data.size_per_attribute
87
- file.attr_string_table = atr1_data.string_table
88
82
  case "TXT2":
89
83
  messages = read_txt2(reader, tag_config, suppress_tag_errors)
90
84
  case "TSY1":
91
85
  style_indexes = read_tsy1(reader, len(labels))
92
86
  case _:
93
- file.unsupported_sections[magic] = reader.read_bytes(size)
87
+ unsupported_sections[magic] = reader.read_bytes(size)
94
88
 
95
- if not file.section_exists(magic):
96
- file._section_list.append(magic)
89
+ if magic not in section_list:
90
+ section_list.append(magic)
91
+
92
+ file = MSBT(
93
+ file_info, section_list, unsupported_sections, attribute_config, tag_config
94
+ )
95
+ file.slot_count = slot_count
96
+
97
+ file.uses_encoded_attributes = attribute_config is None
98
+ if atr1_data is not None:
99
+ file.size_per_attribute = atr1_data.size_per_attribute
100
+ file.attr_string_table = atr1_data.string_table
97
101
 
98
102
  for i, label in labels.items():
99
103
  text = None if messages is None else messages[i]
@@ -111,13 +115,11 @@ def write_msbt_path(file_path: str, file: MSBT) -> None:
111
115
  Writes a MSBT file to a given file path.
112
116
 
113
117
  :param file_path: the path to write the file to.
114
- :param msbt: the MSBT file object.
118
+ :param file: the MSBT file object.
115
119
 
116
- ## Usage
117
- ```
118
- write_msbt_path("path/to/file.msbt", msbt)
119
- ...
120
- ```
120
+ Example
121
+ -------
122
+ >>> write_msbt_path("path/to/file.msbt", msbt)
121
123
  """
122
124
  with open(file_path, "wb") as stream:
123
125
  data = write_msbt(file)
@@ -129,10 +131,9 @@ def write_msbt(file: MSBT) -> bytes:
129
131
 
130
132
  :param file: a MSBT object.
131
133
 
132
- ## Usage
133
- ```
134
- data = write_msbt(msbt)
135
- ```
134
+ Example
135
+ -------
136
+ >>> data = write_msbt(msbt)
136
137
  """
137
138
  if not isinstance(file, MSBT):
138
139
  raise LMS_Exceptions.LMS_Error(
@@ -140,7 +141,7 @@ def write_msbt(file: MSBT) -> bytes:
140
141
  )
141
142
 
142
143
  writer = FileWriter(file.info.encoding)
143
- write_file_info(writer, "MsgStdBn", file.info)
144
+ write_file_info(writer, MSBT.MAGIC, file.info)
144
145
 
145
146
  for section in file.section_list:
146
147
  match section:
@@ -174,7 +175,7 @@ def write_msbt(file: MSBT) -> bytes:
174
175
  write_section(writer, "TSY1", write_tsy1, style_indexes)
175
176
  case _:
176
177
  write_unsupported_section(
177
- writer, section, file.unsupported_sections[section]
178
+ writer, section, file.get_unsupported_section_data(section)
178
179
  )
179
180
 
180
181
  writer.seek(0x12)
@@ -3,8 +3,11 @@ from dataclasses import dataclass
3
3
  from lms.common.lms_datatype import LMS_DataType
4
4
  from lms.fileio.io import FileReader, FileWriter
5
5
  from lms.message.definitions.field.io import read_field, write_field
6
- from lms.message.definitions.field.lms_field import (LMS_DataType, LMS_Field,
7
- LMS_FieldMap)
6
+ from lms.message.definitions.field.lms_field import (
7
+ LMS_DataType,
8
+ LMS_Field,
9
+ LMS_FieldMap,
10
+ )
8
11
  from lms.titleconfig.definitions.attribute import AttributeConfig
9
12
 
10
13
 
@@ -54,7 +57,7 @@ def read_decoded_atr1(reader: FileReader, config: AttributeConfig) -> ATR1Data:
54
57
  if definition.datatype is LMS_DataType.STRING:
55
58
  last = reader.tell() + 4
56
59
  reader.seek(section_start + reader.read_uint32())
57
- value = LMS_Field(reader.read_str_variable_encoding(), definition)
60
+ value = LMS_Field(reader.read_encoded_string(), definition)
58
61
  reader.seek(last)
59
62
  else:
60
63
  value = read_field(reader, definition)
@@ -109,4 +112,4 @@ def write_decoded_atr1(
109
112
  write_field(writer, field)
110
113
 
111
114
  for string in string_table:
112
- writer.write_variable_encoding_string(string)
115
+ writer.write_encoded_string(string)