PyLibMS 5.0.4__tar.gz → 5.0.5__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 (76) hide show
  1. {pylibms-5.0.4 → pylibms-5.0.5}/PKG-INFO +1 -1
  2. {pylibms-5.0.4 → pylibms-5.0.5}/PyLibMS.egg-info/PKG-INFO +1 -1
  3. {pylibms-5.0.4 → pylibms-5.0.5}/PyLibMS.egg-info/SOURCES.txt +3 -0
  4. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/lms_fileinfo.py +2 -1
  5. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/stream/fileinfo.py +14 -9
  6. {pylibms-5.0.4 → pylibms-5.0.5}/lms/fileio/encoding.py +5 -7
  7. pylibms-5.0.5/lms/fileio/endian.py +19 -0
  8. {pylibms-5.0.4 → pylibms-5.0.5}/lms/fileio/io.py +10 -9
  9. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/msbf.py +4 -3
  10. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/msbfio.py +1 -0
  11. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/lms_messagetext.py +1 -0
  12. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/msbt.py +22 -7
  13. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/msbtentry.py +11 -11
  14. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/msbtio.py +21 -15
  15. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/section/txt2.py +3 -3
  16. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/tag/io/tag_io.py +5 -4
  17. pylibms-5.0.5/lms/message/umsbt.py +91 -0
  18. pylibms-5.0.5/lms/message/umsbtio.py +183 -0
  19. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/msbpread.py +1 -0
  20. {pylibms-5.0.4 → pylibms-5.0.5}/pyproject.toml +1 -1
  21. {pylibms-5.0.4 → pylibms-5.0.5}/LICENSE +0 -0
  22. {pylibms-5.0.4 → pylibms-5.0.5}/MANIFEST.in +0 -0
  23. {pylibms-5.0.4 → pylibms-5.0.5}/PyLibMS.egg-info/dependency_links.txt +0 -0
  24. {pylibms-5.0.4 → pylibms-5.0.5}/PyLibMS.egg-info/requires.txt +0 -0
  25. {pylibms-5.0.4 → pylibms-5.0.5}/PyLibMS.egg-info/top_level.txt +0 -0
  26. {pylibms-5.0.4 → pylibms-5.0.5}/README.md +0 -0
  27. {pylibms-5.0.4 → pylibms-5.0.5}/lms/__init__.py +0 -0
  28. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/__init__.py +0 -0
  29. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/field/__init__.py +0 -0
  30. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/field/io.py +0 -0
  31. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/field/lms_datatype.py +0 -0
  32. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/field/lms_field.py +0 -0
  33. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/lms_constants.py +0 -0
  34. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/lms_exceptions.py +0 -0
  35. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/stream/hashtable.py +0 -0
  36. {pylibms-5.0.4 → pylibms-5.0.5}/lms/common/stream/section.py +0 -0
  37. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/__init__.py +0 -0
  38. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/definitions/__init__.py +0 -0
  39. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/definitions/lms_nodeexceptions.py +0 -0
  40. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/definitions/node.py +0 -0
  41. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/definitions/node_type.py +0 -0
  42. {pylibms-5.0.4 → pylibms-5.0.5}/lms/flowchart/flw3.py +0 -0
  43. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/__init__.py +0 -0
  44. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/definitions/__init__.py +0 -0
  45. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/section/__init__.py +0 -0
  46. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/section/atr1.py +0 -0
  47. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/section/nli1.py +0 -0
  48. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/section/tsy1.py +0 -0
  49. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/tag/__init__.py +0 -0
  50. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/tag/io/param_io.py +0 -0
  51. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/tag/lms_tag.py +0 -0
  52. {pylibms-5.0.4 → pylibms-5.0.5}/lms/message/tag/lms_tagexceptions.py +0 -0
  53. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/__init__.py +0 -0
  54. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/definitions/__init__.py +0 -0
  55. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/definitions/attribute.py +0 -0
  56. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/definitions/color.py +0 -0
  57. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/definitions/style.py +0 -0
  58. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/definitions/tag.py +0 -0
  59. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/msbp.py +0 -0
  60. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/ali2.py +0 -0
  61. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/ati2.py +0 -0
  62. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/clr1.py +0 -0
  63. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/string.py +0 -0
  64. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/syl3.py +0 -0
  65. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/tag2.py +0 -0
  66. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/tgg2.py +0 -0
  67. {pylibms-5.0.4 → pylibms-5.0.5}/lms/project/section/tgp2.py +0 -0
  68. {pylibms-5.0.4 → pylibms-5.0.5}/lms/py.typed +0 -0
  69. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/__init__.py +0 -0
  70. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/config.py +0 -0
  71. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/definitions/__init__.py +0 -0
  72. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/definitions/attribute.py +0 -0
  73. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/definitions/field.py +0 -0
  74. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/definitions/nodes.py +0 -0
  75. {pylibms-5.0.4 → pylibms-5.0.5}/lms/titleconfig/definitions/tags.py +0 -0
  76. {pylibms-5.0.4 → pylibms-5.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 5.0.4
3
+ Version: 5.0.5
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: 5.0.4
3
+ Version: 5.0.5
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
@@ -21,6 +21,7 @@ lms/common/stream/fileinfo.py
21
21
  lms/common/stream/hashtable.py
22
22
  lms/common/stream/section.py
23
23
  lms/fileio/encoding.py
24
+ lms/fileio/endian.py
24
25
  lms/fileio/io.py
25
26
  lms/flowchart/__init__.py
26
27
  lms/flowchart/flw3.py
@@ -35,6 +36,8 @@ lms/message/lms_messagetext.py
35
36
  lms/message/msbt.py
36
37
  lms/message/msbtentry.py
37
38
  lms/message/msbtio.py
39
+ lms/message/umsbt.py
40
+ lms/message/umsbtio.py
38
41
  lms/message/definitions/__init__.py
39
42
  lms/message/section/__init__.py
40
43
  lms/message/section/atr1.py
@@ -1,11 +1,12 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  from lms.fileio.encoding import FileEncoding
4
+ from lms.fileio.io import FileEndian
4
5
 
5
6
 
6
7
  @dataclass
7
8
  class LMS_FileInfo:
8
- is_big_endian: bool = False
9
+ endian: FileEndian = FileEndian.LITTLE
9
10
  encoding: FileEncoding = FileEncoding.UTF16
10
11
  version: int = 3
11
12
  section_count: int = 2
@@ -1,12 +1,12 @@
1
1
  from lms.common import lms_exceptions
2
2
  from lms.common.lms_constants import (
3
- BIG_ENDIAN_BOM,
4
3
  SECTION_DATA_START,
5
- LITTLE_ENDIAN_BOM,
6
4
  SIZE_OFFSET, LMS_MINIMUM_VERSION,
5
+ LITTLE_ENDIAN_BOM, BIG_ENDIAN_BOM,
7
6
  )
8
7
  from lms.common.lms_fileinfo import LMS_FileInfo
9
8
  from lms.fileio.encoding import FileEncoding
9
+ from lms.fileio.endian import FileEndian
10
10
  from lms.fileio.io import FileReader, FileWriter
11
11
 
12
12
 
@@ -19,9 +19,16 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
19
19
  This file may not a valid LMS format or the wrong reading function was utilized."""
20
20
  )
21
21
 
22
- is_big_endian = reader.read_bytes(2) == BIG_ENDIAN_BOM
23
- reader.is_big_endian = is_big_endian
22
+ bom = reader.read_bytes(2)
24
23
 
24
+ if bom == LITTLE_ENDIAN_BOM:
25
+ endian = FileEndian.LITTLE
26
+ elif bom == BIG_ENDIAN_BOM:
27
+ endian = FileEndian.BIG
28
+ else:
29
+ raise ValueError(f"Invalid bom found in file '{bom}'!")
30
+
31
+ reader.endian = endian
25
32
  reader.skip(2)
26
33
 
27
34
  encoding = FileEncoding(reader.read_uint8())
@@ -48,7 +55,7 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
48
55
  reader.seek(SECTION_DATA_START)
49
56
 
50
57
  return LMS_FileInfo(
51
- is_big_endian,
58
+ endian,
52
59
  encoding,
53
60
  version,
54
61
  section_count,
@@ -56,13 +63,11 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
56
63
 
57
64
 
58
65
  def write_file_info(writer: FileWriter, magic: str, file_info: LMS_FileInfo) -> None:
59
- writer.is_big_endian = file_info.is_big_endian
66
+ writer.endian = file_info.endian
60
67
  writer.encoding = file_info.encoding
61
68
 
62
69
  writer.write_string(magic)
63
- writer.write_bytes(
64
- LITTLE_ENDIAN_BOM if not file_info.is_big_endian else BIG_ENDIAN_BOM
65
- )
70
+ writer.write_bytes(file_info.endian.byte_order_mark)
66
71
  writer.write_bytes(b"\x00\x00")
67
72
 
68
73
  writer.write_uint8(file_info.encoding.value)
@@ -1,6 +1,8 @@
1
1
  from enum import IntEnum
2
2
  from typing import Literal
3
3
 
4
+ from lms.fileio.endian import FileEndian
5
+
4
6
 
5
7
  class FileEncoding(IntEnum):
6
8
  """An enum that represents a file encoding."""
@@ -10,20 +12,16 @@ class FileEncoding(IntEnum):
10
12
  UTF32 = 0x02
11
13
 
12
14
  def to_string_format(
13
- self, is_big_endian: bool = False
15
+ self, endian: FileEndian = FileEndian.LITTLE
14
16
  ) -> Literal["UTF-8", "UTF-16-BE", "UTF-16-LE", "UTF-32-BE", "UTF-32-LE"]:
15
17
  """Converts the FileEncoding to string format."""
16
18
  match self:
17
19
  case FileEncoding.UTF8:
18
20
  return "UTF-8"
19
21
  case FileEncoding.UTF16:
20
- if is_big_endian:
21
- return "UTF-16-BE"
22
- return "UTF-16-LE"
22
+ return "UTF-16-BE" if endian is FileEndian.BIG else "UTF-16-LE"
23
23
  case FileEncoding.UTF32:
24
- if is_big_endian:
25
- return "UTF-32-BE"
26
- return "UTF-32-LE"
24
+ return "UTF-32-BE" if endian is FileEndian.BIG else "UTF-32-LE"
27
25
 
28
26
  @property
29
27
  def width(self) -> Literal[1, 2, 4]:
@@ -0,0 +1,19 @@
1
+ from enum import Enum
2
+ from typing import Literal
3
+
4
+
5
+ class FileEndian(Enum):
6
+ """An enum that represents a file endianness."""
7
+
8
+ LITTLE = 0
9
+ BIG = 1
10
+
11
+ @property
12
+ def string_name(self) -> Literal["little", "big"]:
13
+ """Name of the endian as a string."""
14
+ return self._name_.lower()
15
+
16
+ @property
17
+ def byte_order_mark(self) -> Literal[b"\xFF\xFE", b"\xFE\xFF"]:
18
+ """The BOM of the endian."""
19
+ return b"\xFF\xFE" if self is FileEndian.LITTLE else b"\xFE\xFF"
@@ -6,6 +6,7 @@ from typing import BinaryIO, Generator, Type
6
6
 
7
7
  from io import BytesIO, IOBase, TextIOBase
8
8
  from lms.fileio.encoding import FileEncoding
9
+ from lms.fileio.endian import FileEndian
9
10
 
10
11
  PADDING_BYTE = b"\x00"
11
12
 
@@ -31,7 +32,7 @@ class StructBETypes(StrEnum):
31
32
 
32
33
 
33
34
  class FileReader:
34
- def __init__(self, data: BinaryIO | bytes | bytearray | memoryview, big_endian: bool = False):
35
+ def __init__(self, data: BinaryIO | bytes | bytearray | memoryview, endian: FileEndian = FileEndian.LITTLE) -> None:
35
36
  self._stream: BinaryIO = None
36
37
 
37
38
  match data:
@@ -43,11 +44,11 @@ class FileReader:
43
44
  raise TypeError("The stream provided is not valid!")
44
45
 
45
46
  self.encoding = FileEncoding.UTF8
46
- self.is_big_endian = big_endian
47
+ self.endian = endian
47
48
 
48
49
  @property
49
50
  def datatypes(self) -> Type[StructLETypes | StructBETypes]:
50
- return StructLETypes if not self.is_big_endian else StructBETypes
51
+ return StructLETypes if self.endian == FileEndian.LITTLE else StructBETypes
51
52
 
52
53
  def tell(self) -> int:
53
54
  return self._stream.tell()
@@ -110,25 +111,25 @@ class FileReader:
110
111
  string = b""
111
112
  while (raw_char := self.read_bytes(self.encoding.width)) != self.encoding.terminator:
112
113
  string += raw_char
113
- return string.decode(self.encoding.to_string_format(self.is_big_endian))
114
+ return string.decode(self.encoding.to_string_format(self.endian))
114
115
 
115
116
  def read_len_string_encoded(self):
116
117
  self.align(self.encoding.width)
117
118
  length = self.read_uint16()
118
119
  return self.read_bytes(length).decode(
119
- self.encoding.to_string_format(self.is_big_endian)
120
+ self.encoding.to_string_format(self.endian)
120
121
  )
121
122
 
122
123
 
123
124
  class FileWriter:
124
- def __init__(self, encoding: FileEncoding):
125
+ def __init__(self, encoding: FileEncoding = FileEncoding.UTF8, endian: FileEndian = FileEndian.LITTLE) -> None:
125
126
  self.data = BytesIO(b"")
126
127
  self.encoding = encoding
127
- self.is_big_endian = False
128
+ self.endian = endian
128
129
 
129
130
  @property
130
131
  def datatypes(self) -> Type[StructLETypes | StructBETypes]:
131
- return StructLETypes if not self.is_big_endian else StructBETypes
132
+ return StructLETypes if self.endian == FileEndian.LITTLE else StructBETypes
132
133
 
133
134
  def skip(self, length: int) -> None:
134
135
  self.data.seek(length, 1)
@@ -186,7 +187,7 @@ class FileWriter:
186
187
 
187
188
  def write_encoded_string(self, string: str, terminate: bool = True) -> None:
188
189
  self.write_bytes(
189
- string.encode(self.encoding.to_string_format(self.is_big_endian))
190
+ string.encode(self.encoding.to_string_format(self.endian))
190
191
  )
191
192
  if terminate:
192
193
  self.write_bytes(b"\x00" * self.encoding.width)
@@ -3,6 +3,7 @@ from typing import Mapping, Iterator
3
3
 
4
4
  from lms.common.lms_fileinfo import LMS_FileInfo
5
5
  from lms.fileio.encoding import FileEncoding
6
+ from lms.fileio.endian import FileEndian
6
7
  from lms.flowchart.definitions.node import LMS_EntryNode, LMS_JumpNode, LMS_BranchNode, LMS_BaseNode
7
8
 
8
9
 
@@ -52,7 +53,7 @@ class MSBF:
52
53
  @classmethod
53
54
  def new(
54
55
  cls,
55
- is_big_endian: bool = False,
56
+ endian: FileEndian = FileEndian.LITTLE,
56
57
  encoding: FileEncoding = FileEncoding.UTF16,
57
58
  version: int = 3,
58
59
  section_count: int = 2,
@@ -60,13 +61,13 @@ class MSBF:
60
61
  """
61
62
  Create a new MSBF instance.
62
63
 
63
- :param is_big_endian: if the file is big endian.
64
+ :param endian: the endian of the file.
64
65
  :param encoding: the file encoding.
65
66
  :param version: the file version.
66
67
  :param section_count: the number of sections.
67
68
 
68
69
  """
69
- return MSBF(LMS_FileInfo(is_big_endian, encoding, version, section_count))
70
+ return MSBF(LMS_FileInfo(endian, encoding, version, section_count))
70
71
 
71
72
  @property
72
73
  def info(self) -> LMS_FileInfo:
@@ -63,6 +63,7 @@ def read_msbf(
63
63
  >>> msbf = read_msbf(stream)
64
64
  """
65
65
  reader = FileReader(stream)
66
+
66
67
  file_info = read_file_info(reader, MSBF.MAGIC)
67
68
 
68
69
  if msbt is not None and msbt_files:
@@ -177,6 +177,7 @@ class LMS_MessageText:
177
177
 
178
178
  def _set_segments(self, text: str) -> None:
179
179
  self._segments = []
180
+
180
181
  for part in self.TAG_FORMAT.split(text):
181
182
  if bool(re.match(self.TAG_FORMAT, part)):
182
183
  self.append_tag_string(part)
@@ -1,8 +1,10 @@
1
- from typing import overload
1
+ from types import MappingProxyType
2
+ from typing import overload, Mapping
2
3
 
3
4
  from lms.common.field.lms_field import LMS_FieldMap
4
5
  from lms.common.lms_fileinfo import LMS_FileInfo
5
6
  from lms.fileio.encoding import FileEncoding
7
+ from lms.fileio.endian import FileEndian
6
8
  from lms.message.lms_messagetext import LMS_MessageText
7
9
  from lms.message.msbtentry import MSBTEntry
8
10
  from lms.titleconfig.definitions.attribute import AttributeConfig
@@ -56,9 +58,7 @@ class MSBT:
56
58
  self._unsupported_section_map = unsupported_section_map or {}
57
59
 
58
60
  # Store the section list so that the order of any and all sections is preserved when writing
59
- self._section_list: list[str] = section_list or [
60
- "LBL1" if not uses_nli1 else "NLI1"
61
- ]
61
+ self._section_list: list[str] = section_list
62
62
 
63
63
  self._attribute_config = attribute_config
64
64
  self._tag_config = tag_config
@@ -91,7 +91,7 @@ class MSBT:
91
91
  uses_nli1: bool = False,
92
92
  attribute_config: AttributeConfig | None = None,
93
93
  tag_config: TagConfig | None = None,
94
- is_big_endian: bool = False,
94
+ endian: FileEndian = FileEndian.LITTLE,
95
95
  encoding: FileEncoding = FileEncoding.UTF16,
96
96
  version: int = 3,
97
97
  section_count: int = 2,
@@ -102,7 +102,7 @@ class MSBT:
102
102
  :param uses_nli1: flag to determine if to use nli1 section for labels.
103
103
  :param attribute_config: the attribute config object
104
104
  :param tag_config: the tag config object
105
- :param is_big_endian: if the file is big endian.
105
+ :param endian: if the file is big endian.
106
106
  :param encoding: the file encoding.
107
107
  :param version: the file version.
108
108
  :param section_count: the number of sections.
@@ -113,7 +113,8 @@ class MSBT:
113
113
  See https://github.com/AbdyyEee/PylibMS/wiki/MSBT#creating-a-msbt
114
114
  """
115
115
  return MSBT(
116
- LMS_FileInfo(is_big_endian, encoding, version, section_count),
116
+ LMS_FileInfo(endian, encoding, version, section_count),
117
+ section_list=["LBL1" if not uses_nli1 else "NLI1", "TXT2"],
117
118
  uses_nli1=uses_nli1,
118
119
  attribute_config=attribute_config,
119
120
  tag_config=tag_config,
@@ -129,6 +130,11 @@ class MSBT:
129
130
  """Tuple of all the MSBT entries."""
130
131
  return tuple(self._entries)
131
132
 
133
+ @property
134
+ def label_map(self) -> Mapping[str, MSBTEntry]:
135
+ """Mapping of all labels to MSBT entries."""
136
+ return MappingProxyType(self._label_map)
137
+
132
138
  @property
133
139
  def section_list(self) -> tuple[str, ...]:
134
140
  """The list of sections with order preserved."""
@@ -160,10 +166,14 @@ class MSBT:
160
166
  :param label: the label to check."""
161
167
  return label in self._label_map
162
168
 
169
+ # Backwards compatibility just in case
170
+ # Recommended to use accessors MSBT[index] or MSBT[label]
163
171
  def get_entry_by_index(self, index: int) -> MSBTEntry:
164
172
  """
165
173
  Retrieves an entry given its index. Supports negative indexing.
166
174
 
175
+ Recommended to use ``MSBT[index]`` over this function.
176
+
167
177
  :param index: the index of the entry.
168
178
  """
169
179
  if not (-len(self._entries) <= index < len(self._entries)):
@@ -175,6 +185,8 @@ class MSBT:
175
185
  """
176
186
  Retrieves an entry given its name.
177
187
 
188
+ Recommended to use ``MSBT[label]`` over this function.
189
+
178
190
  :param label: the label name for the entry.
179
191
  """
180
192
  if label not in self._label_map:
@@ -203,6 +215,9 @@ class MSBT:
203
215
  f"Entry '{entry.name}' can't be added with no attributes when attributes already exist!"
204
216
  )
205
217
  elif entry.attribute is not None:
218
+ if self._entries:
219
+ raise ValueError("All entries must have attributes tied to them!")
220
+
206
221
  self._section_list.insert(self.ATR1_INDEX, "ATR1")
207
222
  self._info.section_count += 1
208
223
 
@@ -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
 
@@ -37,7 +37,7 @@ class MSBTEntry:
37
37
  self.style_index = style_index
38
38
 
39
39
  @property
40
- def message(self) -> LMS_MessageText | None:
40
+ def message(self) -> LMS_MessageText:
41
41
  """The message object for the instance."""
42
42
  return self._message
43
43
 
@@ -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.
@@ -2,7 +2,8 @@
2
2
  IO interface for ``MSBT`` files.
3
3
  """
4
4
  import os
5
- from typing import BinaryIO
5
+ from types import MappingProxyType
6
+ from typing import BinaryIO, Mapping
6
7
 
7
8
  from lms.common import lms_exceptions
8
9
  from lms.common.stream.fileinfo import read_file_info, write_file_info, write_file_size
@@ -23,6 +24,8 @@ from lms.titleconfig.config import AttributeConfig, TagConfig
23
24
 
24
25
  __all__ = ("read_msbt_path", "read_msbt_directory", "read_msbt", "write_msbt", "write_msbt_path")
25
26
 
27
+ MSBT_EXTENSION = ".msbt"
28
+
26
29
 
27
30
  def read_msbt_path(
28
31
  file_path: str | os.PathLike[str],
@@ -60,11 +63,12 @@ def read_msbt_directory(directory: str | os.PathLike[str], *,
60
63
  attribute_config: AttributeConfig | None = None,
61
64
  tag_config: TagConfig | None = None,
62
65
  suppress_tag_errors: bool = False,
63
- ignore_extensions: bool = False) -> tuple[MSBT, ...]:
66
+ ignore_extensions: bool = False) -> Mapping[str, MSBT]:
64
67
  """
65
- Reads and retrieves MSBT all MSBT files from a given directory.
68
+ Reads and retrieves all MSBT files from a given directory.
66
69
 
67
- :param directory: directory of MSBT files. All files must have the `.msbt` extension.
70
+ :param directory: directory of MSBT files. All files must have the `.msbt` extension (unless ``ignore_extensions``
71
+ is True)
68
72
  :param attribute_config: the attribute config to use for decoding attributes.
69
73
  :param tag_config: the tag config to use for decoding tags.
70
74
  :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
@@ -75,19 +79,21 @@ def read_msbt_directory(directory: str | os.PathLike[str], *,
75
79
  =====
76
80
  >>> msbt = read_msbt_directory("path/to/directory")
77
81
  """
78
- files: list[MSBT] = []
82
+ files: dict[str, MSBT] = {}
79
83
 
80
84
  for file in os.listdir(directory):
81
- if not file.endswith(".msbt") and not ignore_extensions:
82
- raise ValueError("This directory contains a non-MSBT file. Only directories with MSBT files can be used!")
83
-
84
- files.append(msbt := read_msbt_path(file_path := os.path.join(directory, file),
85
- attribute_config=attribute_config,
86
- tag_config=tag_config, suppress_tag_errors=suppress_tag_errors))
87
-
88
- msbt.filename = os.path.basename(file_path)
89
-
90
- return tuple(files)
85
+ if not file.endswith(MSBT_EXTENSION) and not ignore_extensions:
86
+ raise ValueError("This directory contains a non-MSBT file. "
87
+ "Only directories with MSBT files can be used! Set ignore_extensions to True to ignore"
88
+ "this error.")
89
+
90
+ msbt = read_msbt_path(os.path.join(directory, file),
91
+ attribute_config=attribute_config,
92
+ tag_config=tag_config, suppress_tag_errors=suppress_tag_errors)
93
+ msbt.filename = file
94
+ files[file] = msbt
95
+
96
+ return MappingProxyType(files)
91
97
 
92
98
 
93
99
  def read_msbt(
@@ -6,15 +6,15 @@ 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
 
13
13
  messages = []
14
14
  message_count = reader.read_uint32()
15
15
 
16
- encoding_format = encoding.to_string_format(reader.is_big_endian)
17
- tag_start, tag_close = get_tag_indicator(encoding, reader.is_big_endian)
16
+ encoding_format = encoding.to_string_format(reader.endian)
17
+ tag_start, tag_close = get_tag_indicator(encoding, reader.endian)
18
18
 
19
19
  for offset in reader.read_offset_array(message_count):
20
20
  reader.seek(offset)
@@ -1,4 +1,5 @@
1
1
  from lms.fileio.encoding import FileEncoding
2
+ from lms.fileio.endian import FileEndian
2
3
  from lms.fileio.io import FileReader, FileWriter
3
4
  from lms.message.tag.io.param_io import (
4
5
  read_decoded_parameters,
@@ -13,12 +14,12 @@ from lms.titleconfig.definitions.tags import TagConfig, TagDefinition
13
14
  TAG_PADDING_BYTE = b"\xcd"
14
15
 
15
16
 
16
- def get_tag_indicator(encoding: FileEncoding, is_big_endian: bool):
17
+ def get_tag_indicator(encoding: FileEncoding, endian: FileEndian):
17
18
  start_indicator = int.to_bytes(
18
- 0x0E, encoding.width, "little" if not is_big_endian else "big"
19
+ 0x0E, encoding.width, endian.string_name
19
20
  )
20
21
  closing_indicator = int.to_bytes(
21
- 0x0F, encoding.width, "little" if not is_big_endian else "big"
22
+ 0x0F, encoding.width, endian.string_name
22
23
  )
23
24
  return start_indicator, closing_indicator
24
25
 
@@ -95,7 +96,7 @@ def _read_decoded_tag(
95
96
 
96
97
  def write_tag(writer: FileWriter, tag: LMS_ControlTag) -> None:
97
98
  start_indicator, close_indicator = get_tag_indicator(
98
- writer.encoding, writer.is_big_endian
99
+ writer.encoding, writer.endian
99
100
  )
100
101
  writer.write_bytes(start_indicator if not tag.is_closing else close_indicator)
101
102
 
@@ -0,0 +1,91 @@
1
+ from types import MappingProxyType
2
+ from typing import overload, Mapping, Sequence, Iterator
3
+
4
+ from lms.message.msbt import MSBT
5
+
6
+
7
+ class UMSBT:
8
+ """Class that represents a UMSBT file, an archive for MSBT files."""
9
+
10
+ def __init__(self, files: Sequence[MSBT] | None = None) -> None:
11
+ self._files = list(files) if files is not None else []
12
+
13
+ for file in self._files:
14
+ if not isinstance(file, MSBT):
15
+ raise TypeError("Only MSBTs are allowed for UMSBT archives!")
16
+
17
+ self._realign_filenames()
18
+
19
+ @overload
20
+ def __getitem__(self, index: int) -> MSBT:
21
+ ...
22
+
23
+ @overload
24
+ def __getitem__(self, label: str) -> MSBT:
25
+ ...
26
+
27
+ def __getitem__(self, value: int | str) -> MSBT:
28
+ if isinstance(value, str):
29
+ return self.file_map[value]
30
+
31
+ return self._files[value]
32
+
33
+ def __len__(self) -> int:
34
+ return len(self._files)
35
+
36
+ def __iter__(self) -> Iterator[MSBT]:
37
+ return iter(self._files)
38
+
39
+ @property
40
+ def files(self) -> tuple[MSBT, ...]:
41
+ """Maps of files tied to this instance."""
42
+ return tuple(self._files)
43
+
44
+ @property
45
+ def file_map(self) -> Mapping[str, MSBT]:
46
+ """Maps of files tied to this instance."""
47
+ return MappingProxyType({msbt.filename: msbt for msbt in self._files})
48
+
49
+ def add_msbt(self, msbt: MSBT) -> None:
50
+ """
51
+ Adds a MSBT to the UMSBT archive.
52
+
53
+ :param msbt: the msbt to add.
54
+ """
55
+ self._files.append(msbt)
56
+ self._realign_filenames()
57
+
58
+ def insert_msbt(self, position: int, msbt: MSBT) -> None:
59
+ """
60
+ Inserts a MSBT in the archive at a specific index
61
+
62
+ :param position: position of the MSBT to insert.
63
+ :param msbt: the msbt to insert.
64
+ """
65
+ self._files.insert(position, msbt)
66
+ self._realign_filenames()
67
+
68
+ @overload
69
+ def remove_msbt(self, msbt: MSBT) -> None:
70
+ ...
71
+
72
+ @overload
73
+ def remove_msbt(self, msbt: int) -> None:
74
+ ...
75
+
76
+ def remove_msbt(self, msbt: MSBT | int) -> None:
77
+ """
78
+ Deletes a MSBT from the UMSBT archive.
79
+
80
+ :param msbt: the msbt object or index to remove.
81
+ """
82
+ if isinstance(msbt, MSBT):
83
+ self._files.remove(msbt)
84
+ else:
85
+ del self._files[msbt]
86
+
87
+ self._realign_filenames()
88
+
89
+ def _realign_filenames(self) -> None:
90
+ for i, msbt in enumerate(self._files):
91
+ msbt.filename = f"{i}.msbt"
@@ -0,0 +1,183 @@
1
+ """
2
+ IO interface for ``UMSBT`` files.
3
+ """
4
+ import os
5
+ from typing import Mapping, BinaryIO
6
+
7
+ from lms.common import lms_exceptions
8
+ from lms.fileio.io import FileReader, FileWriter
9
+ from lms.message.msbt import MSBT
10
+ from lms.message.msbtio import read_msbt, write_msbt
11
+ from lms.message.umsbt import UMSBT
12
+ from lms.titleconfig.definitions.attribute import AttributeConfig
13
+ from lms.titleconfig.definitions.tags import TagConfig
14
+
15
+ __all__ = ("read_umsbt_path", "read_umsbt_directory", "read_umsbt", "write_umsbt_path", "write_umsbt")
16
+
17
+ UMSBT_EXTENSION = ".umsbt"
18
+ UMSBT_ALIGNMENT = 16
19
+ SIZE_PER_METADATA = 8
20
+
21
+
22
+ def read_umsbt_path(
23
+ file_path: str | os.PathLike[str], *,
24
+ attribute_config: AttributeConfig | None = None,
25
+ tag_config: TagConfig | None = None,
26
+ suppress_tag_errors: bool = False,
27
+ ) -> UMSBT:
28
+ """
29
+ Reads and retrieves a UMSBT file from a given path.
30
+
31
+ :param file_path: the path to the UMSBT file.
32
+ :param attribute_config: the attribute config to use for decoding attributes.
33
+ :param tag_config: the tag config to use for decoding tags.
34
+ :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
35
+
36
+ =====
37
+ Usage
38
+ =====
39
+ >>> umsbt = read_umsbt_path("path/to/file.umsbt")
40
+ """
41
+ with open(file_path, "rb") as stream:
42
+ umsbt = read_umsbt(stream,
43
+ attribute_config=attribute_config,
44
+ tag_config=tag_config,
45
+ suppress_tag_errors=suppress_tag_errors)
46
+
47
+ return umsbt
48
+
49
+
50
+ def read_umsbt_directory(directory: str | os.PathLike[str], *,
51
+ attribute_config: AttributeConfig | None = None,
52
+ tag_config: TagConfig | None = None,
53
+ suppress_tag_errors: bool = False,
54
+ ignore_extensions: bool = False) -> Mapping[str, tuple[MSBT, ...]]:
55
+ """
56
+ Reads and retrieves all UMSBT files from a given directory.
57
+
58
+ :param directory: directory of UMSBT files. All files must have the `.umsbt` extension (unless ``ignore_extensions``
59
+ is True)
60
+ :param attribute_config: the attribute config to use for decoding attributes.
61
+ :param tag_config: the tag config to use for decoding tags.
62
+ :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
63
+ :param ignore_extensions: ignore the extensions in the directory.
64
+
65
+ =====
66
+ Usage
67
+ =====
68
+ >>> umsbts = read_msbt_directory("path/to/directory")
69
+ """
70
+ files: dict[str, tuple[MSBT, ...]] = {}
71
+
72
+ for file in os.listdir(directory):
73
+ if not file.endswith(UMSBT_EXTENSION) and not ignore_extensions:
74
+ raise ValueError("This directory contains a non-UMSBT file. "
75
+ "Only directories with UMSBT files can be used! Set ignore_extensions to True to ignore"
76
+ "this error.")
77
+
78
+ umsbt = read_umsbt_path(os.path.join(directory, file),
79
+ attribute_config=attribute_config,
80
+ tag_config=tag_config, suppress_tag_errors=suppress_tag_errors)
81
+ files[os.path.basename(file)] = umsbt
82
+
83
+ return files
84
+
85
+
86
+ def read_umsbt(
87
+ stream: BinaryIO | bytes, *,
88
+ attribute_config: AttributeConfig | None = None,
89
+ tag_config: TagConfig | None = None,
90
+ suppress_tag_errors: bool = False,
91
+ ) -> UMSBT:
92
+ """
93
+ Reads and retrieves a UMSBT file from a specified stream.
94
+
95
+ :param stream: an ``IOBase``, ``BytesIO``, ``memoryview``, or ``bytes`` object.
96
+ :param attribute_config: the attribute config to use for decoding attributes.
97
+ :param tag_config: the tag config to use for decoding tags.
98
+ :param suppress_tag_errors: when a tag config is used, suppress any errors while reading decoded tags.
99
+
100
+ =====
101
+ Usage
102
+ =====
103
+ >>> umsbt = read_umsbt(stream)
104
+ """
105
+ reader = FileReader(stream)
106
+ data_start = reader.read_uint32()
107
+
108
+ offsets, sizes = [], []
109
+
110
+ reader.seek(0)
111
+ while reader.tell() < data_start:
112
+ offset, size = reader.read_uint32(), reader.read_uint32()
113
+
114
+ # Reached padding section
115
+ if offset == 0 and size == 0:
116
+ break
117
+
118
+ offsets.append(offset)
119
+ sizes.append(size)
120
+
121
+ files = []
122
+ for i, (offset, size) in enumerate(zip(offsets, sizes)):
123
+ reader.seek(offset)
124
+ files.append(read_msbt(reader.read_bytes(size),
125
+ attribute_config=attribute_config,
126
+ tag_config=tag_config,
127
+ suppress_tag_errors=suppress_tag_errors))
128
+
129
+ return UMSBT(files)
130
+
131
+
132
+ def write_umsbt_path(file_path: str | os.PathLike[str], file: UMSBT) -> None:
133
+ """
134
+ Writes a UMSBT file to a given file path. If the target path does not exist, it will be created.
135
+
136
+ :param file_path: the path to write the file to.
137
+ :param file: the UMSBT file object.
138
+
139
+ =====
140
+ Usage
141
+ =====
142
+ >>> write_umsbt_path("path/to/file.umsbt", umsbt)
143
+ """
144
+ with open(file_path, "wb") as stream:
145
+ data = write_umsbt(file)
146
+ stream.write(data)
147
+
148
+
149
+ def write_umsbt(file: UMSBT) -> bytes:
150
+ """
151
+ Writes a UMSBT file and returns the data.
152
+
153
+ :param file: a UMSBT object.
154
+
155
+ =====
156
+ Usage
157
+ =====
158
+ >>> data = write_umsbt(umsbt)
159
+ """
160
+ if not isinstance(file, UMSBT):
161
+ raise lms_exceptions.LMS_Error(
162
+ f"File provided is not valid. Expected UMSBT got {type(file)}."
163
+ )
164
+
165
+ writer = FileWriter()
166
+ msbt_count = len(file)
167
+ writer.write_bytes(b"\x00" * (msbt_count * SIZE_PER_METADATA))
168
+
169
+ writer.write_alignment(b"\x00", UMSBT_ALIGNMENT)
170
+ writer.write_bytes(b"\x00" * UMSBT_ALIGNMENT)
171
+
172
+ for i, msbt in enumerate(file):
173
+ offset = writer.tell()
174
+ data = write_msbt(msbt)
175
+ writer.write_bytes(data)
176
+ last_position = writer.tell()
177
+
178
+ writer.seek(i * SIZE_PER_METADATA)
179
+ writer.write_uint32(offset)
180
+ writer.write_uint32(len(data))
181
+ writer.seek(last_position)
182
+
183
+ return writer.get_data()
@@ -50,6 +50,7 @@ def read_msbp(stream: BinaryIO | bytes) -> MSBP:
50
50
  >>> msbp = read_msbp(stream)
51
51
  """
52
52
  reader = FileReader(stream)
53
+
53
54
  file_info = read_file_info(reader, MSBP.MAGIC)
54
55
 
55
56
  colors = ()
@@ -7,7 +7,7 @@ include = ["lms*"]
7
7
 
8
8
  [project]
9
9
  name = "PyLibMS"
10
- version = "5.0.4"
10
+ version = "5.0.5"
11
11
  requires-python = ">=3.11"
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