PyLibMS 3.2.6__tar.gz → 3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {pylibms-3.2.6 → pylibms-3.3}/PKG-INFO +2 -2
  2. {pylibms-3.2.6 → pylibms-3.3}/PyLibMS.egg-info/PKG-INFO +2 -2
  3. {pylibms-3.2.6 → pylibms-3.3}/PyLibMS.egg-info/SOURCES.txt +1 -0
  4. {pylibms-3.2.6 → pylibms-3.3}/lms/common/stream/fileinfo.py +2 -1
  5. {pylibms-3.2.6 → pylibms-3.3}/lms/common/stream/section.py +6 -6
  6. {pylibms-3.2.6 → pylibms-3.3}/lms/fileio/encoding.py +1 -1
  7. {pylibms-3.2.6 → pylibms-3.3}/lms/fileio/io.py +2 -2
  8. {pylibms-3.2.6 → pylibms-3.3}/lms/message/definitions/field/lms_field.py +7 -8
  9. {pylibms-3.2.6 → pylibms-3.3}/lms/message/definitions/lms_messagetext.py +47 -26
  10. {pylibms-3.2.6 → pylibms-3.3}/lms/message/msbt.py +34 -22
  11. {pylibms-3.2.6 → pylibms-3.3}/lms/message/msbtentry.py +10 -10
  12. {pylibms-3.2.6 → pylibms-3.3}/lms/message/msbtio.py +33 -20
  13. {pylibms-3.2.6 → pylibms-3.3}/lms/message/section/atr1.py +7 -8
  14. pylibms-3.3/lms/message/section/nli1.py +22 -0
  15. {pylibms-3.2.6 → pylibms-3.3}/lms/message/section/txt2.py +1 -1
  16. {pylibms-3.2.6 → pylibms-3.3}/lms/message/tag/io/param_io.py +6 -9
  17. {pylibms-3.2.6 → pylibms-3.3}/lms/message/tag/io/tag_io.py +10 -10
  18. {pylibms-3.2.6 → pylibms-3.3}/lms/message/tag/lms_tag.py +24 -32
  19. pylibms-3.3/lms/message/tag/lms_tagexceptions.py +10 -0
  20. {pylibms-3.2.6 → pylibms-3.3}/lms/project/definitions/tag.py +16 -18
  21. {pylibms-3.2.6 → pylibms-3.3}/lms/project/msbp.py +7 -7
  22. {pylibms-3.2.6 → pylibms-3.3}/lms/project/msbpread.py +7 -5
  23. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/config.py +4 -4
  24. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/definitions/tags.py +1 -1
  25. pylibms-3.3/lms/titleconfig/presets/Badge Arcade.yaml +430 -0
  26. pylibms-3.3/lms/titleconfig/presets/Brain Age Concentration Training.yaml +132 -0
  27. pylibms-3.3/lms/titleconfig/presets/Kirby Planet Robobot.yaml +209 -0
  28. pylibms-3.3/lms/titleconfig/presets/Super Mario 3D Land.yaml +367 -0
  29. pylibms-3.3/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +200 -0
  30. pylibms-3.3/lms/titleconfig/presets/Super Mario Odyssey.yaml +1961 -0
  31. pylibms-3.3/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +110 -0
  32. pylibms-3.3/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +383 -0
  33. pylibms-3.3/lms/titleconfig/presets/Tomodachi Life.yaml +7410 -0
  34. {pylibms-3.2.6 → pylibms-3.3}/pyproject.toml +2 -2
  35. pylibms-3.2.6/lms/message/tag/lms_tagexceptions.py +0 -7
  36. pylibms-3.2.6/lms/titleconfig/presets/Badge Arcade.yaml +0 -430
  37. pylibms-3.2.6/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -132
  38. pylibms-3.2.6/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -209
  39. pylibms-3.2.6/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -367
  40. pylibms-3.2.6/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -200
  41. pylibms-3.2.6/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -1961
  42. pylibms-3.2.6/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -110
  43. pylibms-3.2.6/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -383
  44. pylibms-3.2.6/lms/titleconfig/presets/Tomodachi Life.yaml +0 -7410
  45. {pylibms-3.2.6 → pylibms-3.3}/LICENSE +0 -0
  46. {pylibms-3.2.6 → pylibms-3.3}/MANIFEST.in +0 -0
  47. {pylibms-3.2.6 → pylibms-3.3}/PyLibMS.egg-info/dependency_links.txt +0 -0
  48. {pylibms-3.2.6 → pylibms-3.3}/PyLibMS.egg-info/requires.txt +0 -0
  49. {pylibms-3.2.6 → pylibms-3.3}/PyLibMS.egg-info/top_level.txt +0 -0
  50. {pylibms-3.2.6 → pylibms-3.3}/README.md +0 -0
  51. {pylibms-3.2.6 → pylibms-3.3}/lms/__init__.py +0 -0
  52. {pylibms-3.2.6 → pylibms-3.3}/lms/common/__init__.py +0 -0
  53. {pylibms-3.2.6 → pylibms-3.3}/lms/common/lms_datatype.py +0 -0
  54. {pylibms-3.2.6 → pylibms-3.3}/lms/common/lms_exceptions.py +0 -0
  55. {pylibms-3.2.6 → pylibms-3.3}/lms/common/lms_fileinfo.py +0 -0
  56. {pylibms-3.2.6 → pylibms-3.3}/lms/common/stream/hashtable.py +0 -0
  57. {pylibms-3.2.6 → pylibms-3.3}/lms/message/__init__.py +0 -0
  58. {pylibms-3.2.6 → pylibms-3.3}/lms/message/definitions/__init__.py +0 -0
  59. {pylibms-3.2.6 → pylibms-3.3}/lms/message/definitions/field/__init__.py +0 -0
  60. {pylibms-3.2.6 → pylibms-3.3}/lms/message/definitions/field/io.py +0 -0
  61. {pylibms-3.2.6 → pylibms-3.3}/lms/message/section/__init__.py +0 -0
  62. {pylibms-3.2.6 → pylibms-3.3}/lms/message/section/tsy1.py +0 -0
  63. {pylibms-3.2.6 → pylibms-3.3}/lms/message/tag/__init__.py +0 -0
  64. {pylibms-3.2.6 → pylibms-3.3}/lms/project/__init__.py +0 -0
  65. {pylibms-3.2.6 → pylibms-3.3}/lms/project/definitions/__init__.py +0 -0
  66. {pylibms-3.2.6 → pylibms-3.3}/lms/project/definitions/attribute.py +0 -0
  67. {pylibms-3.2.6 → pylibms-3.3}/lms/project/definitions/color.py +0 -0
  68. {pylibms-3.2.6 → pylibms-3.3}/lms/project/definitions/style.py +0 -0
  69. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/ali2.py +0 -0
  70. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/ati2.py +0 -0
  71. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/clr1.py +0 -0
  72. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/string.py +0 -0
  73. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/syl3.py +0 -0
  74. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/tag2.py +0 -0
  75. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/tgg2.py +0 -0
  76. {pylibms-3.2.6 → pylibms-3.3}/lms/project/section/tgp2.py +0 -0
  77. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/__init__.py +0 -0
  78. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/definitions/__init__.py +0 -0
  79. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/definitions/attribute.py +0 -0
  80. {pylibms-3.2.6 → pylibms-3.3}/lms/titleconfig/definitions/value.py +0 -0
  81. {pylibms-3.2.6 → pylibms-3.3}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.2.6
4
- Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and MSBF.
3
+ Version: 3.3
4
+ Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and soon to be MSBF.
5
5
  Author: AbdyyEee
6
6
  License: Copyright 2025 AbdyyEee
7
7
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.2.6
4
- Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and MSBF.
3
+ Version: 3.3
4
+ Summary: Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and soon to be MSBF.
5
5
  Author: AbdyyEee
6
6
  License: Copyright 2025 AbdyyEee
7
7
 
@@ -28,6 +28,7 @@ lms/message/definitions/field/io.py
28
28
  lms/message/definitions/field/lms_field.py
29
29
  lms/message/section/__init__.py
30
30
  lms/message/section/atr1.py
31
+ lms/message/section/nli1.py
31
32
  lms/message/section/tsy1.py
32
33
  lms/message/section/txt2.py
33
34
  lms/message/tag/__init__.py
@@ -13,7 +13,8 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
13
13
 
14
14
  if magic != expected_magic:
15
15
  raise lms_exceptions.LMS_UnexpectedMagicError(
16
- f"Invalid magic!' Expected {expected_magic}', got '{magic}'."
16
+ f"""Invalid magic!' Expected {expected_magic}', got '{magic}'.
17
+ This file may not a valid LMS format or the wrong reading function was utilized."""
17
18
  )
18
19
 
19
20
  is_big_endian = reader.read_bytes(2) == BIG_ENDIAN_BOM
@@ -4,7 +4,7 @@ from lms.fileio.io import FileReader, FileWriter
4
4
 
5
5
 
6
6
  def read_section_data(
7
- reader: FileReader, section_count: int
7
+ reader: FileReader, section_count: int
8
8
  ) -> Generator[tuple[str, int], Any, None]:
9
9
  reader.seek(0x20)
10
10
  for _ in range(section_count):
@@ -21,11 +21,11 @@ def read_section_data(
21
21
 
22
22
 
23
23
  def write_section(
24
- writer: FileWriter,
25
- magic: str,
26
- section_call: Callable,
27
- data: list[Any],
28
- *write_arguments: Any,
24
+ writer: FileWriter,
25
+ magic: str,
26
+ section_call: Callable,
27
+ data: list[Any],
28
+ *write_arguments: Any,
29
29
  ) -> None:
30
30
  writer.write_string(magic)
31
31
  size_offset = writer.tell()
@@ -10,7 +10,7 @@ class FileEncoding(IntEnum):
10
10
  UTF32 = 0x02
11
11
 
12
12
  def to_string_format(
13
- self, is_big_endian: bool = False
13
+ self, is_big_endian: bool = False
14
14
  ) -> Literal["UTF-8", "UTF-16-BE", "UTF-16-LE", "UTF-32-BE", "UTF-32-LE"]:
15
15
  """Converts the FileEncoding to string format."""
16
16
  match self:
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import struct
4
- from io import BytesIO, IOBase
5
4
  from typing import BinaryIO, Generator
6
5
 
6
+ from io import BytesIO, IOBase
7
7
  from lms.fileio.encoding import FileEncoding
8
8
 
9
9
  STRUCT_TYPES = {
@@ -96,7 +96,7 @@ class FileReader:
96
96
  def read_encoded_string(self):
97
97
  message = b""
98
98
  while (
99
- raw_char := self.read_bytes(self.encoding.width)
99
+ raw_char := self.read_bytes(self.encoding.width)
100
100
  ) != self.encoding.terminator:
101
101
  message += raw_char
102
102
  return message.decode(self.encoding.to_string_format(self.is_big_endian))
@@ -9,7 +9,6 @@ from lms.titleconfig.definitions.value import ValueDefinition
9
9
  FLOAT_MIN = 1.17549435e-38
10
10
  FLOAT_MAX = 3.4028235e38
11
11
 
12
-
13
12
  type FieldValue = int | str | float | bool | bytes
14
13
 
15
14
 
@@ -83,7 +82,7 @@ class LMS_Field:
83
82
  """
84
83
 
85
84
  def __init__(
86
- self, value: int | str | float | bytes | bool, definition: ValueDefinition
85
+ self, value: int | str | float | bytes | bool, definition: ValueDefinition
87
86
  ):
88
87
  _verify_value(value, definition)
89
88
  self._definition = definition
@@ -126,7 +125,7 @@ class LMS_Field:
126
125
 
127
126
 
128
127
  def _verify_value(
129
- value: int | str | float | bytes | bool, definition: ValueDefinition
128
+ value: int | str | float | bytes | bool, definition: ValueDefinition
130
129
  ) -> None:
131
130
  datatype = definition.datatype
132
131
 
@@ -155,7 +154,7 @@ def _verify_value(
155
154
  max_value = 2 ** (bits - 1)
156
155
  min_value = -max_value
157
156
  else:
158
- min_value, max_value = 0, (2**bits) - 1
157
+ min_value, max_value = 0, (2 ** bits) - 1
159
158
 
160
159
  _verify_number_is_in_range(value, min_value, max_value, definition)
161
160
  return
@@ -166,10 +165,10 @@ def _verify_value(
166
165
 
167
166
 
168
167
  def _verify_number_is_in_range(
169
- value: int | float,
170
- min_value: int | float,
171
- max_value: int | float,
172
- definition: ValueDefinition,
168
+ value: int | float,
169
+ min_value: int | float,
170
+ max_value: int | float,
171
+ definition: ValueDefinition,
173
172
  ):
174
173
  if not min_value <= value <= max_value:
175
174
  raise ValueError(
@@ -3,6 +3,7 @@ import re
3
3
  from lms.message.definitions.field.lms_field import LMS_FieldMap
4
4
  from lms.message.tag.lms_tag import (LMS_ControlTag, LMS_DecodedTag,
5
5
  LMS_EncodedTag, is_tag)
6
+ from lms.message.tag.lms_tagexceptions import LMS_TagForbiddenParametersError
6
7
  from lms.titleconfig.config import TagConfig
7
8
 
8
9
 
@@ -12,9 +13,9 @@ class LMS_MessageText:
12
13
  TAG_FORMAT = re.compile(r"(\[[^]]+])")
13
14
 
14
15
  def __init__(
15
- self,
16
- message: str | list[str | LMS_ControlTag],
17
- tag_config: TagConfig | None = None,
16
+ self,
17
+ message: str | list[str | LMS_ControlTag],
18
+ tag_config: TagConfig | None = None,
18
19
  ):
19
20
  self._tag_config = tag_config
20
21
 
@@ -59,22 +60,26 @@ class LMS_MessageText:
59
60
  return positions
60
61
 
61
62
  def append_encoded_tag(
62
- self, group_id: int, tag_index: int, *parameters: str, is_closing: bool = False
63
+ self, group_id: int, tag_index: int, *parameters: int, is_closing: bool = False
63
64
  ) -> LMS_EncodedTag:
64
65
  """
65
66
  Appends an encoded tag to the current message and returns that tag.
66
67
 
67
68
  :param group_id: the group index.
68
69
  :param tag_index: the index of the tag in the group.
69
- :param parameters: args of hex strings.
70
+ :param parameters: any amount of integer parameters.
70
71
  :param is_closing: whether the tag is closing or not.
71
72
 
72
- Example
73
- ------
73
+ =====
74
+ Usage
75
+ =====
74
76
  >>> message = LMS_MessageText("Text")
75
- >>> message.append_encoded_tag(0, 3, "00", "23", "43", "32")
77
+ >>> message.append_encoded_tag(0, 3, 0x00, 0x23, 0x43, 0x32)
76
78
  """
77
79
  if is_closing:
80
+ if parameters:
81
+ raise LMS_TagForbiddenParametersError("There may not be parameters for closing tags!")
82
+
78
83
  tag = LMS_EncodedTag(group_id, tag_index, is_closing=True)
79
84
  else:
80
85
  tag = LMS_EncodedTag(
@@ -85,22 +90,23 @@ class LMS_MessageText:
85
90
  return tag
86
91
 
87
92
  def append_decoded_tag(
88
- self,
89
- group_name: str,
90
- tag_name: str,
91
- is_closing: bool = False,
92
- **parameters: int | str | float | bool | bytes,
93
+ self,
94
+ group_name: str,
95
+ tag_name: str,
96
+ is_closing: bool = False,
97
+ **parameters: int | str | float | bool | bytes,
93
98
  ) -> LMS_DecodedTag:
94
99
  """
95
100
  Appends a decoded tag to the current message and returns that tag.
96
101
 
97
102
  :param group_name: the group name.
98
- :param tag_name: the tag name.:
103
+ :param tag_name: the tag name.
99
104
  :param is_closing: whether the tag is closing or not.
100
105
  :param parameters: keyword arguments of parameters mapped to their value.
101
106
 
102
- Example
103
- -------
107
+ =====
108
+ Usage
109
+ =====
104
110
  >>> message = LMS_MessageText("Text")
105
111
  >>> message.append_decoded_tag("Mii", "Nickname", buffer=1, type="Voice", conversion="None")
106
112
  """
@@ -109,15 +115,19 @@ class LMS_MessageText:
109
115
 
110
116
  definition = self._tag_config.get_definition_by_names(group_name, tag_name)
111
117
 
112
- if not parameters:
113
- tag = LMS_DecodedTag(definition)
114
- else:
115
- param_map = LMS_FieldMap.from_dict(parameters, definition.parameters)
118
+ if is_closing:
119
+ if parameters:
120
+ raise LMS_TagForbiddenParametersError("There may not be parameters for closing tags!")
116
121
 
117
- if is_closing:
118
- tag = LMS_DecodedTag(definition, is_closing=True)
119
- else:
120
- tag = LMS_DecodedTag(definition, param_map)
122
+ tag = LMS_DecodedTag(definition, is_closing=True)
123
+ self._parts.append(tag)
124
+ return tag
125
+
126
+ if parameters:
127
+ param_map = LMS_FieldMap.from_dict(parameters, definition.parameters)
128
+ tag = LMS_DecodedTag(definition, param_map)
129
+ else:
130
+ tag = LMS_DecodedTag(definition)
121
131
 
122
132
  self._parts.append(tag)
123
133
  return tag
@@ -127,15 +137,26 @@ class LMS_MessageText:
127
137
  Appends a tag to the current message given a string.
128
138
 
129
139
  :param tag: the tag string.
140
+
141
+ If the tag provided is an encoded tag and the amount of parameters is an odd number, padding
142
+ will automatically be appended.
143
+
144
+ =====
145
+ Usage
146
+ =====
147
+ >>> tag = message.append_tag_string('[1:3 00-00-00-CD]')
148
+ >>> tag = message.append_tag_string('[/2:3]')
149
+ >>> tag = message.append_tag_string('[Reference:SpecialProduct buffer="0" type="Name" sp="Plural"]']
150
+ >>> tag = message.append_tag_string('[/Edward:ToLowerRange]')
130
151
  """
131
152
  if re.fullmatch(LMS_DecodedTag.TAG_FORMAT, tag):
132
153
  if self._tag_config is None:
133
- raise ValueError("TagConfig is required to append decoded tags.")
154
+ raise ValueError("TagConfig is required to append decoded tags!")
134
155
  tag_obj = LMS_DecodedTag.from_string(tag, self._tag_config)
135
156
  elif re.fullmatch(LMS_EncodedTag.TAG_FORMAT, tag):
136
157
  tag_obj = LMS_EncodedTag.from_string(tag)
137
158
  else:
138
- raise ValueError(f"Invalid format for tag '{tag}'.")
159
+ raise ValueError(f"Invalid format in tag '{tag}'.")
139
160
 
140
161
  self._parts.append(tag_obj)
141
162
  return tag_obj
@@ -1,30 +1,32 @@
1
1
  from lms.common.lms_fileinfo import LMS_FileInfo
2
+ from lms.fileio.encoding import FileEncoding
2
3
  from lms.message.msbtentry import MSBTEntry
3
4
  from lms.titleconfig.definitions.attribute import AttributeConfig
4
5
  from lms.titleconfig.definitions.tags import TagConfig
5
- from lms.fileio.encoding import FileEncoding
6
6
 
7
7
 
8
8
  class MSBT:
9
9
  """
10
10
  A class that represents a MSBT file.
11
11
 
12
- https://nintendo-formats.com/libs/lms/msbt.html.
12
+ https://nintendo-formats.com/libs/lms/msbt.html
13
13
  """
14
14
 
15
15
  MAGIC = "MsgStdBn"
16
16
 
17
- # While 101 is the default slot count for LBL1 sections in a MSBT
18
- # The value may be overridden at the instance level as some games alter the value
19
17
  DEFAULT_SLOT_COUNT = 101
20
18
 
19
+ ATR1_INDEX = 1
20
+ TXT2_INDEX = 2
21
+ TSY_INDEX = 3
22
+
21
23
  def __init__(
22
- self,
23
- info: LMS_FileInfo | None = None,
24
- section_list: list[str] | None = None,
25
- unsupported_section_map: dict[str, bytes] | None = None,
26
- attribute_config: AttributeConfig | None = None,
27
- tag_config: TagConfig | None = None,
24
+ self,
25
+ info: LMS_FileInfo | None = None,
26
+ section_list: list[str] | None = None,
27
+ unsupported_section_map: dict[str, bytes] | None = None,
28
+ attribute_config: AttributeConfig | None = None,
29
+ tag_config: TagConfig | None = None,
28
30
  ):
29
31
  self._info = info if info is not None else LMS_FileInfo()
30
32
 
@@ -54,14 +56,19 @@ class MSBT:
54
56
  encoding: FileEncoding = FileEncoding.UTF16,
55
57
  version: int = 3,
56
58
  section_count: int = 2):
57
- """Creates a new MSBT instance.
59
+ """Create a new MSBT instance.
58
60
 
59
- :param attribute_config: The attribute config object
60
- :param tag_config: The tag config object
61
+ :param attribute_config: the attribute config object
62
+ :param tag_config: the tag config object
61
63
  :param is_big_endian: if the file is big endian.
62
64
  :param encoding: the file encoding.
63
65
  :param version: the file version.
64
66
  :param section_count: the number of sections.
67
+
68
+ ========
69
+ Examples
70
+ ========
71
+ See https://github.com/AbdyyEee/PylibMS/wiki/MSBT#creating-a-msbt
65
72
  """
66
73
  return MSBT(info=LMS_FileInfo(is_big_endian, encoding, version, section_count),
67
74
  attribute_config=attribute_config, tag_config=tag_config)
@@ -94,12 +101,12 @@ class MSBT:
94
101
 
95
102
  @property
96
103
  def has_attributes(self) -> bool:
97
- """If the msbt contains attributes."""
104
+ """If the MSBT contains attributes."""
98
105
  return self.section_exists("ATR1")
99
106
 
100
107
  @property
101
- def has_style_indexes(self) -> bool:
102
- """If the msbt contains style indexes."""
108
+ def has_styles(self) -> bool:
109
+ """If the MSBT contains style indexes."""
103
110
  return self.section_exists("TSY1")
104
111
 
105
112
  def get_entry_by_index(self, index: int) -> MSBTEntry:
@@ -133,18 +140,23 @@ class MSBT:
133
140
  if entry.name in self._label_map:
134
141
  raise KeyError(f"The label '{entry.name}' already exists!")
135
142
 
143
+ # The implementation of ensuring section orders are maintained are done by constant indexes.
144
+ # In most MSBT files, it goes as LBL1 -> TXT2 -> ATR1 -> TSY1.
145
+ # If add_entry is utilized on a new MSBT instance, then these indexes ensure the order is maintained.
146
+ # While technically, undocumented sections (i.e. ATO1) can prefix TXT2 and other sections,
147
+ # we do not need to account for that scenario as they aren't supported by the library
148
+
136
149
  if self.section_exists("ATR1"):
137
150
  if entry.attribute is None:
138
151
  raise ValueError(
139
152
  f"Entry '{entry.name}' can't be added with no attributes when attributes already exist!"
140
153
  )
141
154
  elif entry.attribute is not None:
142
- self._section_list.insert(1, "ATR1")
155
+ self._section_list.insert(self.ATR1_INDEX, "ATR1")
143
156
  self._info.section_count += 1
144
157
 
145
- # TXT2 will always exist so insert it at all times
146
158
  if not self.section_exists("TXT2"):
147
- self._section_list.insert(2, "TXT2")
159
+ self._section_list.insert(self.TXT2_INDEX, "TXT2")
148
160
 
149
161
  if self.section_exists("TSY1"):
150
162
  if entry.style_index is None:
@@ -152,7 +164,7 @@ class MSBT:
152
164
  f"Entry '{entry.name}' can't be added with no style index when styles already exist!"
153
165
  )
154
166
  elif entry.style_index is not None:
155
- self._section_list.insert(3, "TSY1")
167
+ self._section_list.insert(self.TSY_INDEX, "TSY1")
156
168
  self._info.section_count += 1
157
169
 
158
170
  self._entries.append(entry)
@@ -160,7 +172,7 @@ class MSBT:
160
172
 
161
173
  def delete_entry(self, entry: MSBTEntry) -> None:
162
174
  """
163
- Removes an entry from the MSBT instance.
175
+ Deletes an entry from the MSBT instance.
164
176
 
165
177
  :param entry: the MSBTEntry object to remove.
166
178
  """
@@ -172,7 +184,7 @@ class MSBT:
172
184
 
173
185
  def section_exists(self, name: str) -> bool:
174
186
  """
175
- Determines if a section exists in the current MSBT.
187
+ Determines if a section exists in the MSBT instance.
176
188
 
177
189
  :param name: the name of the section.
178
190
  """
@@ -8,12 +8,12 @@ class MSBTEntry:
8
8
  """A class that represents an entry in a MSBT file."""
9
9
 
10
10
  def __init__(
11
- self,
12
- name: str,
13
- *,
14
- message: LMS_MessageText | str | None = "",
15
- attribute: LMS_FieldMap | bytes | None = None,
16
- style_index: int | None = None,
11
+ self,
12
+ name: str,
13
+ *,
14
+ message: LMS_MessageText | str | None = "",
15
+ attribute: LMS_FieldMap | bytes | None = None,
16
+ style_index: int | None = None,
17
17
  ):
18
18
  self.name = name
19
19
 
@@ -64,10 +64,10 @@ class MSBTEntry:
64
64
 
65
65
  @classmethod
66
66
  def from_dict(
67
- cls,
68
- data: dict,
69
- attribute_config: AttributeConfig | None = None,
70
- tag_config: TagConfig | None = None,
67
+ cls,
68
+ data: dict,
69
+ attribute_config: AttributeConfig | None = None,
70
+ tag_config: TagConfig | None = None,
71
71
  ):
72
72
  """
73
73
  Creates a MSBTEntry from a dictionary object.
@@ -9,6 +9,7 @@ from lms.message.msbt import MSBT
9
9
  from lms.message.msbtentry import MSBTEntry
10
10
  from lms.message.section.atr1 import (read_atr1, write_decoded_atr1,
11
11
  write_encoded_atr1)
12
+ from lms.message.section.nli1 import read_nli1, write_nli1
12
13
  from lms.message.section.tsy1 import read_tsy1, write_tsy1
13
14
  from lms.message.section.txt2 import read_txt2, write_txt2
14
15
  from lms.titleconfig.config import AttributeConfig, TagConfig
@@ -17,11 +18,11 @@ __all__ = ["read_msbt", "read_msbt_path", "write_msbt", "write_msbt_path"]
17
18
 
18
19
 
19
20
  def read_msbt_path(
20
- file_path: str,
21
- *,
22
- attribute_config: AttributeConfig | None = None,
23
- tag_config: TagConfig | None = None,
24
- suppress_tag_errors: bool = False,
21
+ file_path: str,
22
+ *,
23
+ attribute_config: AttributeConfig | None = None,
24
+ tag_config: TagConfig | None = None,
25
+ suppress_tag_errors: bool = False,
25
26
  ) -> MSBT:
26
27
  """
27
28
  Reads and retrieves a MSBT file from a given path.
@@ -31,8 +32,9 @@ def read_msbt_path(
31
32
  :param tag_config: the tag config to use for decoding tags.
32
33
  :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
33
34
 
34
- Example
35
- ---------
35
+ =====
36
+ Usage
37
+ =====
36
38
  >>> msbt = read_msbt_path("path/to/file.msbt")
37
39
  """
38
40
  with open(file_path, "rb") as stream:
@@ -45,11 +47,11 @@ def read_msbt_path(
45
47
 
46
48
 
47
49
  def read_msbt(
48
- stream: BinaryIO | bytes,
49
- *,
50
- attribute_config: AttributeConfig | None = None,
51
- tag_config: TagConfig | None = None,
52
- suppress_tag_errors: bool = False,
50
+ stream: BinaryIO | bytes,
51
+ *,
52
+ attribute_config: AttributeConfig | None = None,
53
+ tag_config: TagConfig | None = None,
54
+ suppress_tag_errors: bool = False,
53
55
  ) -> MSBT:
54
56
  """
55
57
  Reads and retrieves a MSBT file from a specified stream.
@@ -59,15 +61,19 @@ def read_msbt(
59
61
  :param tag_config: the tag config to use for decoding tags.
60
62
  :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
61
63
 
62
- Example
63
- ---------
64
- >>> msbt = read_msbt(stream)
64
+ =====
65
+ Usage
66
+ =====
67
+ >>> msbt = read_msbt_path("path/to/file.msbt")
65
68
  """
66
69
  reader = FileReader(stream)
67
70
  file_info = read_file_info(reader, MSBT.MAGIC)
68
71
 
69
72
  section_list = []
70
73
  unsupported_sections = {}
74
+
75
+ # While 101 is the default slot count for LBL1 sections in a MSBT
76
+ # The value may be overridden at the instance level... not sure why it varies though...
71
77
  slot_count = MSBT.DEFAULT_SLOT_COUNT
72
78
 
73
79
  messages = atr1_data = style_indexes = None
@@ -77,6 +83,8 @@ def read_msbt(
77
83
  match magic:
78
84
  case "LBL1":
79
85
  labels, slot_count = read_labels(reader)
86
+ case "NLI1":
87
+ labels = read_nli1(reader)
80
88
  case "ATR1":
81
89
  atr1_data = read_atr1(reader, attribute_config, size)
82
90
  case "TXT2":
@@ -112,13 +120,14 @@ def read_msbt(
112
120
 
113
121
  def write_msbt_path(file_path: str, file: MSBT) -> None:
114
122
  """
115
- Writes a MSBT file to a given file path.
123
+ Writes a MSBT file to a given file path. If the target path does not exist, it will be created.
116
124
 
117
125
  :param file_path: the path to write the file to.
118
126
  :param file: the MSBT file object.
119
127
 
120
- Example
121
- -------
128
+ =====
129
+ Usage
130
+ =====
122
131
  >>> write_msbt_path("path/to/file.msbt", msbt)
123
132
  """
124
133
  with open(file_path, "wb") as stream:
@@ -131,8 +140,9 @@ def write_msbt(file: MSBT) -> bytes:
131
140
 
132
141
  :param file: a MSBT object.
133
142
 
134
- Example
135
- -------
143
+ =====
144
+ Usage
145
+ =====
136
146
  >>> data = write_msbt(msbt)
137
147
  """
138
148
  if not isinstance(file, MSBT):
@@ -148,6 +158,9 @@ def write_msbt(file: MSBT) -> bytes:
148
158
  case "LBL1":
149
159
  labels = [entry.name for entry in file]
150
160
  write_section(writer, "LBL1", write_labels, labels, file.slot_count)
161
+ case "NLI1":
162
+ labels = [entry.name for entry in file]
163
+ write_section(writer, "NLI1", write_nli1, labels)
151
164
  case "ATR1":
152
165
  attributes = [entry.attribute for entry in file]
153
166
  if file.uses_encoded_attributes:
@@ -1,6 +1,5 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from lms.common.lms_datatype import LMS_DataType
4
3
  from lms.fileio.io import FileReader, FileWriter
5
4
  from lms.message.definitions.field.io import read_field, write_field
6
5
  from lms.message.definitions.field.lms_field import (LMS_DataType, LMS_Field,
@@ -16,7 +15,7 @@ class ATR1Data:
16
15
 
17
16
 
18
17
  def read_atr1(
19
- reader: FileReader, config: AttributeConfig | None, section_size: int
18
+ reader: FileReader, config: AttributeConfig | None, section_size: int
20
19
  ) -> ATR1Data:
21
20
  if config is None:
22
21
  return read_encoded_atr1(reader, section_size)
@@ -72,11 +71,11 @@ def read_decoded_atr1(reader: FileReader, config: AttributeConfig) -> ATR1Data:
72
71
 
73
72
 
74
73
  def write_encoded_atr1(
75
- writer: FileWriter,
76
- attributes: list[bytes],
77
- size_per_attribute: int,
78
- string_table: bytes | None,
79
- ):
74
+ writer: FileWriter,
75
+ attributes: list[bytes],
76
+ size_per_attribute: int,
77
+ string_table: bytes | None,
78
+ ) -> None:
80
79
  writer.write_uint32(len(attributes))
81
80
 
82
81
  if attributes:
@@ -92,7 +91,7 @@ def write_encoded_atr1(
92
91
 
93
92
 
94
93
  def write_decoded_atr1(
95
- writer: FileWriter, attributes: list[LMS_FieldMap], size_per_attribute: int
94
+ writer: FileWriter, attributes: list[LMS_FieldMap], size_per_attribute: int
96
95
  ) -> None:
97
96
  writer.write_uint32(len(attributes))
98
97
  writer.write_uint32(size_per_attribute)
@@ -0,0 +1,22 @@
1
+ from lms.fileio.io import FileReader, FileWriter
2
+
3
+
4
+ def read_nli1(reader: FileReader) -> dict[int, str]:
5
+ entry_count = reader.read_uint32()
6
+
7
+ labels = {}
8
+ for _ in range(entry_count):
9
+ label = str(reader.read_uint32())
10
+ index = reader.read_uint32()
11
+ labels[index] = label
12
+
13
+ return labels
14
+
15
+
16
+ def write_nli1(writer: FileWriter, labels: list[str]) -> None:
17
+ label_count = len(labels)
18
+ writer.write_uint32(label_count)
19
+
20
+ for i in range(label_count):
21
+ writer.write_uint32(int(labels[i]))
22
+ writer.write_uint32(i)
@@ -6,7 +6,7 @@ from lms.titleconfig.definitions.tags import TagConfig
6
6
 
7
7
 
8
8
  def read_txt2(
9
- reader: FileReader, config: TagConfig | None, suppress_tag_errors: bool
9
+ reader: FileReader, config: TagConfig | None, suppress_tag_errors: bool
10
10
  ) -> list[LMS_MessageText]:
11
11
  encoding = reader.encoding
12
12