PyLibMS 3.0.8__tar.gz → 3.1.1__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 (70) hide show
  1. {pylibms-3.0.8 → pylibms-3.1.1}/PKG-INFO +10 -10
  2. {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/PKG-INFO +10 -10
  3. {pylibms-3.0.8 → pylibms-3.1.1}/README.md +9 -9
  4. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_datatype.py +1 -1
  5. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/hashtable.py +1 -1
  6. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/section.py +1 -2
  7. {pylibms-3.0.8 → pylibms-3.1.1}/lms/fileio/encoding.py +15 -10
  8. {pylibms-3.0.8 → pylibms-3.1.1}/lms/fileio/io.py +6 -7
  9. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/io.py +9 -7
  10. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/lms_field.py +63 -18
  11. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/lms_messagetext.py +54 -33
  12. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbt.py +38 -21
  13. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbtentry.py +36 -24
  14. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbtio.py +47 -43
  15. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/atr1.py +32 -24
  16. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/txt2.py +4 -2
  17. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/io/param_io.py +18 -19
  18. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/io/tag_io.py +14 -13
  19. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/lms_tag.py +18 -0
  20. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/tag.py +5 -5
  21. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/msbpread.py +15 -9
  22. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/config.py +51 -25
  23. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/attribute.py +2 -2
  24. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/tags.py +16 -17
  25. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Badge Arcade.yaml +1 -0
  26. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +1 -0
  27. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +1 -0
  28. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario 3D Land.yaml +1 -0
  29. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +1 -1
  30. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario Odyssey.yaml +1 -0
  31. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +1 -0
  32. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +1 -3
  33. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Tomodachi Life.yaml +30 -29
  34. {pylibms-3.0.8 → pylibms-3.1.1}/pyproject.toml +1 -1
  35. {pylibms-3.0.8 → pylibms-3.1.1}/LICENSE +0 -0
  36. {pylibms-3.0.8 → pylibms-3.1.1}/MANIFEST.in +0 -0
  37. {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/SOURCES.txt +0 -0
  38. {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/dependency_links.txt +0 -0
  39. {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/requires.txt +0 -0
  40. {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/top_level.txt +0 -0
  41. {pylibms-3.0.8 → pylibms-3.1.1}/lms/__init__.py +0 -0
  42. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/__init__.py +0 -0
  43. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_exceptions.py +0 -0
  44. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_fileinfo.py +0 -0
  45. {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/fileinfo.py +0 -0
  46. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/__init__.py +0 -0
  47. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/__init__.py +0 -0
  48. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/__init__.py +0 -0
  49. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/__init__.py +0 -0
  50. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/tsy1.py +0 -0
  51. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/__init__.py +0 -0
  52. {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/lms_tagexceptions.py +0 -0
  53. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/__init__.py +0 -0
  54. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/__init__.py +0 -0
  55. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/attribute.py +0 -0
  56. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/color.py +0 -0
  57. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/style.py +0 -0
  58. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/msbp.py +0 -0
  59. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/ali2.py +0 -0
  60. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/ati2.py +0 -0
  61. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/clr1.py +0 -0
  62. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/string.py +0 -0
  63. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/syl3.py +0 -0
  64. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tag2.py +0 -0
  65. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tgg2.py +0 -0
  66. {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tgp2.py +0 -0
  67. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/__init__.py +0 -0
  68. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/__init__.py +0 -0
  69. {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/value.py +0 -0
  70. {pylibms-3.0.8 → pylibms-3.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.0.8
3
+ Version: 3.1.1
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
@@ -20,20 +20,20 @@ Requires-Dist: PyYAML==6.0.1
20
20
  Dynamic: license-file
21
21
 
22
22
  # PylibMS
23
- Python library built in Python 3.10+ or the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports the following:
23
+ PylibMS is a library built in Python 3.10+ for the libMessageStudio (LMS) proprietary file formats from Nintendo. It supports the following:
24
24
 
25
25
  * Full reading and writing of MSBT files.
26
26
  * Full reading of MSBP files.
27
27
  * Supports encoded/decoded attributes and encoding/decoded tags.
28
28
  * Additonal tag manipulation.
29
29
 
30
- Games that work with the library, including but not limited to:
31
- * Tomodachi Life
32
- * Nintendo Badge Arcade
33
- * The Legend of Zelda: A Link Between Worlds
34
- * Animal Crossing: Amiibo Festival
35
- * Super Mario 3D World
36
- * Super Mario 3D Land.
30
+ This library is designed to support LMS revision 3.0 and above, with the associated file formats used across the following Nintendo platforms:
31
+ * Wii (Specific titles only)
32
+ * Nintendo 3DS
33
+ * Wii U
34
+ * Mobile (Specific titles only)
35
+ * Nintendo Switch
36
+
37
37
  # Features and Usage
38
38
  Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
39
39
  ## Reading
@@ -50,7 +50,7 @@ from lms.message.msbtio import write_msbt_path
50
50
  write_msbt_path("Out_Game.msbt")
51
51
  ```
52
52
  # Adding/Editing Presets
53
- To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
53
+ To add or edit Preset, you may create an issue with the relevant `yaml` file and the game it is for.
54
54
 
55
55
  # Installation
56
56
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLibMS
3
- Version: 3.0.8
3
+ Version: 3.1.1
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
@@ -20,20 +20,20 @@ Requires-Dist: PyYAML==6.0.1
20
20
  Dynamic: license-file
21
21
 
22
22
  # PylibMS
23
- Python library built in Python 3.10+ or the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports the following:
23
+ PylibMS is a library built in Python 3.10+ for the libMessageStudio (LMS) proprietary file formats from Nintendo. It supports the following:
24
24
 
25
25
  * Full reading and writing of MSBT files.
26
26
  * Full reading of MSBP files.
27
27
  * Supports encoded/decoded attributes and encoding/decoded tags.
28
28
  * Additonal tag manipulation.
29
29
 
30
- Games that work with the library, including but not limited to:
31
- * Tomodachi Life
32
- * Nintendo Badge Arcade
33
- * The Legend of Zelda: A Link Between Worlds
34
- * Animal Crossing: Amiibo Festival
35
- * Super Mario 3D World
36
- * Super Mario 3D Land.
30
+ This library is designed to support LMS revision 3.0 and above, with the associated file formats used across the following Nintendo platforms:
31
+ * Wii (Specific titles only)
32
+ * Nintendo 3DS
33
+ * Wii U
34
+ * Mobile (Specific titles only)
35
+ * Nintendo Switch
36
+
37
37
  # Features and Usage
38
38
  Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
39
39
  ## Reading
@@ -50,7 +50,7 @@ from lms.message.msbtio import write_msbt_path
50
50
  write_msbt_path("Out_Game.msbt")
51
51
  ```
52
52
  # Adding/Editing Presets
53
- To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
53
+ To add or edit Preset, you may create an issue with the relevant `yaml` file and the game it is for.
54
54
 
55
55
  # Installation
56
56
  ```
@@ -1,18 +1,18 @@
1
1
  # PylibMS
2
- Python library built in Python 3.10+ or the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports the following:
2
+ PylibMS is a library built in Python 3.10+ for the libMessageStudio (LMS) proprietary file formats from Nintendo. It supports the following:
3
3
 
4
4
  * Full reading and writing of MSBT files.
5
5
  * Full reading of MSBP files.
6
6
  * Supports encoded/decoded attributes and encoding/decoded tags.
7
7
  * Additonal tag manipulation.
8
8
 
9
- Games that work with the library, including but not limited to:
10
- * Tomodachi Life
11
- * Nintendo Badge Arcade
12
- * The Legend of Zelda: A Link Between Worlds
13
- * Animal Crossing: Amiibo Festival
14
- * Super Mario 3D World
15
- * Super Mario 3D Land.
9
+ This library is designed to support LMS revision 3.0 and above, with the associated file formats used across the following Nintendo platforms:
10
+ * Wii (Specific titles only)
11
+ * Nintendo 3DS
12
+ * Wii U
13
+ * Mobile (Specific titles only)
14
+ * Nintendo Switch
15
+
16
16
  # Features and Usage
17
17
  Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
18
18
  ## Reading
@@ -29,7 +29,7 @@ from lms.message.msbtio import write_msbt_path
29
29
  write_msbt_path("Out_Game.msbt")
30
30
  ```
31
31
  # Adding/Editing Presets
32
- To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
32
+ To add or edit Preset, you may create an issue with the relevant `yaml` file and the game it is for.
33
33
 
34
34
  # Installation
35
35
  ```
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import Literal, Type
2
+ from typing import Type
3
3
 
4
4
 
5
5
  class LMS_DataType(Enum):
@@ -1,7 +1,7 @@
1
1
  from lms.fileio.io import FileReader, FileWriter
2
2
 
3
3
 
4
- def read_labels(reader: FileReader) -> tuple[dict[int, str], int]:
4
+ def read_labels(reader: FileReader) -> tuple[dict, int]:
5
5
  labels = {}
6
6
 
7
7
  data_start = reader.tell()
@@ -16,7 +16,6 @@ def read_section_data(
16
16
 
17
17
  yield (magic, size)
18
18
 
19
- # Seek past the AB padding on next iteration
20
19
  reader.seek(end)
21
20
  reader.align(16)
22
21
 
@@ -26,7 +25,7 @@ def write_section(
26
25
  magic: str,
27
26
  section_call: Callable,
28
27
  data: list[Any],
29
- *write_arguments,
28
+ *write_arguments: Any,
30
29
  ) -> None:
31
30
  writer.write_string(magic)
32
31
  size_offset = writer.tell()
@@ -1,29 +1,33 @@
1
- from enum import Enum
1
+ from enum import IntEnum
2
2
  from typing import Literal
3
3
 
4
4
 
5
- class FileEncoding(Enum):
5
+ class FileEncoding(IntEnum):
6
6
  """An enum that represents a file encoding."""
7
7
 
8
- UTF8 = 0
9
- UTF16 = 1
10
- UTF32 = 2
8
+ UTF8 = 0x00
9
+ UTF16 = 0x01
10
+ UTF32 = 0x02
11
11
 
12
- def to_string_format(self, big_endian: bool = False):
12
+ def to_string_format(
13
+ self, is_big_endian: bool = False
14
+ ) -> Literal["UTF-8", "UTF-16-BE", "UTF-16-LE", "UTF-32-BE", "UTF-32-LE"]:
15
+ """Converts the FileEncoding to string format."""
13
16
  match self:
14
17
  case FileEncoding.UTF8:
15
18
  return "UTF-8"
16
19
  case FileEncoding.UTF16:
17
- if big_endian:
20
+ if is_big_endian:
18
21
  return "UTF-16-BE"
19
22
  return "UTF-16-LE"
20
23
  case FileEncoding.UTF32:
21
- if big_endian:
24
+ if is_big_endian:
22
25
  return "UTF-32-BE"
23
26
  return "UTF-32-LE"
24
27
 
25
28
  @property
26
- def width(self):
29
+ def width(self) -> Literal[1, 2, 4]:
30
+ """The width of the encoding in the stream."""
27
31
  match self:
28
32
  case FileEncoding.UTF8:
29
33
  return 1
@@ -33,5 +37,6 @@ class FileEncoding(Enum):
33
37
  return 4
34
38
 
35
39
  @property
36
- def terminator(self):
40
+ def terminator(self) -> bytes:
41
+ """The typical terminator for a string in the encoding."""
37
42
  return b"\x00" * self.width
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import struct
4
- from io import BytesIO
4
+ from io import BytesIO, IOBase
5
5
  from typing import BinaryIO, Generator, cast
6
6
 
7
7
  from lms.fileio.encoding import FileEncoding
@@ -30,11 +30,12 @@ STRUCT_TYPES = {
30
30
 
31
31
  class FileReader:
32
32
  def __init__(self, data: BinaryIO | bytes, big_endian: bool = False):
33
- if isinstance(data, bytes):
33
+ if isinstance(data, IOBase):
34
+ self._stream = data
35
+ elif isinstance(data, (bytes, bytearray, memoryview)):
34
36
  self._stream = BytesIO(data)
35
37
  else:
36
- data = cast(BinaryIO, data)
37
- self._stream = BytesIO(data.read())
38
+ raise TypeError("The stream provided is not valid!")
38
39
 
39
40
  self.encoding = FileEncoding.UTF8
40
41
  self.is_big_endian = big_endian
@@ -101,9 +102,7 @@ class FileReader:
101
102
  return message.decode(self.encoding.to_string_format(self.is_big_endian))
102
103
 
103
104
  def read_len_string_variable_encoding(self):
104
- self.align(
105
- len("\x00".encode(self.encoding.to_string_format(self.is_big_endian)))
106
- )
105
+ self.align(self.encoding.width)
107
106
  length = self.read_uint16()
108
107
  return self.read_bytes(length).decode(
109
108
  self.encoding.to_string_format(self.is_big_endian)
@@ -35,20 +35,22 @@ def read_field(reader: FileReader, definition: ValueDefinition) -> LMS_Field:
35
35
 
36
36
 
37
37
  def write_field(writer: FileWriter, field: LMS_Field) -> None:
38
- # All values are verifed before this point due to LMS_Field._verify_value
38
+ if isinstance(field.value, int):
39
+ value = cast(int, field.value)
40
+
39
41
  match field.datatype:
40
42
  case LMS_DataType.UINT8:
41
- writer.write_uint8(cast(int, field.value))
43
+ writer.write_uint8(value)
42
44
  case LMS_DataType.INT8:
43
- writer.write_int8(cast(int, field.value))
45
+ writer.write_int8(value)
44
46
  case LMS_DataType.UINT16:
45
- writer.write_uint16(cast(int, field.value))
47
+ writer.write_uint16(value)
46
48
  case LMS_DataType.INT16:
47
- writer.write_int16(cast(int, field.value))
49
+ writer.write_int16(value)
48
50
  case LMS_DataType.UINT32:
49
- writer.write_uint32(cast(int, field.value))
51
+ writer.write_uint32(value)
50
52
  case LMS_DataType.INT32:
51
- writer.write_int32(cast(int, field.value))
53
+ writer.write_int32(value)
52
54
  case LMS_DataType.LIST:
53
55
  writer.write_uint8(field.list_items.index(cast(str, field.value)))
54
56
  case LMS_DataType.BOOL:
@@ -1,24 +1,73 @@
1
- from typing import Iterable, cast
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass, field
5
+ from typing import cast
2
6
 
3
7
  from lms.common.lms_datatype import LMS_DataType
4
8
  from lms.titleconfig.definitions.value import ValueDefinition
5
9
 
6
- # Typehint that represents a map of strings to field instances.
7
- # Utilized as a means of storing attributes, or parameters in decoded tags mapped to their string names.
8
- type LMS_FieldMap = dict[str, LMS_Field]
10
+ FLOAT_MIN = 1.17549435e-38
11
+ FLOAT_MAX = 3.4028235e38
12
+
13
+
14
+ type FieldValue = int | str | float | bool | bytes
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class LMS_FieldMap:
19
+ """
20
+ A wrapper for a dictionary of LMS_Field objects for easier access and abstraction from the dictionary object.
21
+ """
22
+
23
+ _fields: dict[str, LMS_Field]
24
+
25
+ def __iter__(self) -> Iterator[LMS_Field]:
26
+ return iter(self._fields.values())
9
27
 
28
+ def get_field(self, name: str) -> LMS_Field:
29
+ """
30
+ Returns the field associated with the name.
10
31
 
11
- def dict_to_field_map(data: dict, definitions: Iterable[ValueDefinition]):
12
- return {
13
- definition.name: LMS_Field(data[definition.name], definition)
14
- for definition in definitions
15
- }
32
+ :param name: the name of the field.
33
+ """
34
+ if name not in self._fields:
35
+ raise KeyError(f"Field '{name}' does not exist")
36
+
37
+ return self._fields[name]
38
+
39
+ def set_value(self, name: str, value: FieldValue) -> None:
40
+ """
41
+ Sets the value of the specified field.
42
+
43
+ :param name: the name of the field.
44
+ :param value: the new value for the field.
45
+ """
46
+ if name not in self._fields:
47
+ raise KeyError(f"Field '{name}' does not exist")
48
+
49
+ self._fields[name].value = value
50
+
51
+ def to_dict(self) -> dict[str, FieldValue]:
52
+ """Converts the field map to a regular dictionary."""
53
+ return {field.name: field.value for field in self._fields.values()}
54
+
55
+ @classmethod
56
+ def from_dict(cls, data: dict[str, FieldValue], definitions: list[ValueDefinition]):
57
+ return cls(
58
+ {
59
+ definition.name: LMS_Field(data[definition.name], definition)
60
+ for definition in definitions
61
+ }
62
+ )
16
63
 
17
64
 
18
65
  class LMS_Field:
19
- """A class that represents a mapped field linked to a config definition.
66
+ """
67
+ A class that represents a mapped field linked to a config definition.
20
68
 
21
- Acts as values for Attributes and Tag Parameters."""
69
+ Acts as values for Attributes and Tag Parameters.
70
+ """
22
71
 
23
72
  def __init__(
24
73
  self, value: int | str | float | bytes | bool, definition: ValueDefinition
@@ -29,11 +78,7 @@ class LMS_Field:
29
78
 
30
79
  def __repr__(self):
31
80
  if self.datatype is LMS_DataType.LIST:
32
- if len(self.list_items) > 6:
33
- preview = self.list_items[:3] + ["..."]
34
- else:
35
- preview = self.list_items
36
- return f"LMS_Field(value={self._value}, options={preview})"
81
+ return f"LMS_Field(value={self._value}, list_items={self.list_items})"
37
82
  return f"LMS_Field(value={self._value}, type={self.datatype.name})"
38
83
 
39
84
  @property
@@ -58,7 +103,7 @@ class LMS_Field:
58
103
 
59
104
  @property
60
105
  def list_items(self) -> list[str]:
61
- """The list items bound to the field instance. Only is valid for LMS_Datatype.LIST values."""
106
+ """The list items bound to the field instance. Only is valid for `LMS_Datatype.LIST` values."""
62
107
  return self._definition.list_items
63
108
 
64
109
  @value.setter
@@ -86,7 +131,7 @@ class LMS_Field:
86
131
  else:
87
132
  return
88
133
  case LMS_DataType.FLOAT32 if isinstance(value, float):
89
- _verify_range(value, -3.4028235e38, 3.4028235e38, self._definition)
134
+ _verify_range(value, FLOAT_MIN, FLOAT_MAX, self._definition)
90
135
  return
91
136
  case _ if isinstance(value, int):
92
137
  bits = datatype.stream_size * 8
@@ -1,8 +1,8 @@
1
1
  import re
2
2
 
3
- from lms.message.definitions.field.lms_field import LMS_Field
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
- LMS_EncodedTag)
5
+ LMS_EncodedTag, is_tag)
6
6
  from lms.titleconfig.config import TagConfig
7
7
 
8
8
 
@@ -12,14 +12,16 @@ class LMS_MessageText:
12
12
  TAG_FORMAT = re.compile(r"(\[[^\]]+\])")
13
13
 
14
14
  def __init__(
15
- self, message: str | list[str | LMS_ControlTag], config: TagConfig | None = None
15
+ self,
16
+ message: str | list[str | LMS_ControlTag],
17
+ tag_config: TagConfig | None = None,
16
18
  ):
17
19
  if isinstance(message, str):
18
20
  self._set_parts(message)
19
21
  else:
20
22
  self._parts = message
21
23
 
22
- self._config = config
24
+ self._tag_config = tag_config
23
25
 
24
26
  def __iter__(self):
25
27
  return iter(self._parts)
@@ -27,14 +29,13 @@ class LMS_MessageText:
27
29
  @property
28
30
  def text(self) -> str:
29
31
  """The raw text of the message."""
30
- text_list = []
32
+ result = []
31
33
  for part in self._parts:
32
- text_list.append(
33
- part.to_text()
34
- if isinstance(part, (LMS_EncodedTag, LMS_DecodedTag))
35
- else part
36
- )
37
- return "".join(text_list)
34
+ if is_tag(part):
35
+ result.append(part.to_text())
36
+ else:
37
+ result.append(part)
38
+ return "".join(result)
38
39
 
39
40
  @text.setter
40
41
  def text(self, string: str):
@@ -43,14 +44,13 @@ class LMS_MessageText:
43
44
  @property
44
45
  def tags(self) -> list[LMS_ControlTag]:
45
46
  """The list of control tags in the message."""
46
- return [
47
- part
48
- for part in self._parts
49
- if isinstance(part, (LMS_EncodedTag, LMS_DecodedTag))
50
- ]
47
+ return [part for part in self._parts if is_tag(part)]
51
48
 
52
- def append_encoded_tag(self, group_id: int, tag_index: int, *parameters: str):
53
- """Appends an encoded tag to the current message.
49
+ def append_encoded_tag(
50
+ self, group_id: int, tag_index: int, is_closing: bool = False, *parameters: str
51
+ ) -> LMS_EncodedTag:
52
+ """
53
+ Appends an encoded tag to the current message and returns that tag.
54
54
 
55
55
  :param group: the group name or index.
56
56
  :param tag: the group tag or index:
@@ -61,15 +61,25 @@ class LMS_MessageText:
61
61
  message.append_encoded_tag(1, 2, "01", "00", "00", "CD")
62
62
  ```
63
63
  """
64
- self._parts.append(LMS_EncodedTag(group_id, tag_index, list(parameters)))
64
+ if is_closing:
65
+ tag = LMS_EncodedTag(group_id, tag_index, is_closing=True)
66
+ else:
67
+ tag = LMS_EncodedTag(
68
+ group_id, tag_index, None if not parameters else list(parameters)
69
+ )
70
+
71
+ self._parts.append(tag)
72
+ return tag
65
73
 
66
74
  def append_decoded_tag(
67
75
  self,
68
76
  group_name: str,
69
77
  tag_name: str,
78
+ is_closing: bool = False,
70
79
  **parameters: int | str | float | bool | bytes,
71
- ) -> None:
72
- """Appends an decoded tag to the current message.
80
+ ) -> LMS_DecodedTag:
81
+ """
82
+ Appends an decoded tag to the current message and returns that tag.
73
83
 
74
84
  :param group_name: the group name.
75
85
  :param tag_name: the tag name.:
@@ -80,27 +90,38 @@ class LMS_MessageText:
80
90
  message.append_decoded_tag("Mii", "Nickname", buffer=1, type="Voice", conversion="None")
81
91
  ```
82
92
  """
83
- if self._config is None:
93
+ if self._tag_config is None:
84
94
  raise ValueError("A TitleConfig is required to append decoded tags.")
85
95
 
86
- definition = self._config.get_definition_by_names(group_name, tag_name)
96
+ definition = self._tag_config.get_definition_by_names(group_name, tag_name)
87
97
 
88
- converted_params = {}
89
- for param_def in definition.parameters:
90
- converted_params[param_def.name] = LMS_Field(
91
- parameters[param_def.name], param_def
92
- )
98
+ param_map = None
99
+
100
+ if not parameters:
101
+ tag = LMS_DecodedTag(definition)
102
+ self._parts.append(tag)
103
+ return tag
93
104
 
94
- self._parts.append(LMS_DecodedTag(definition, converted_params))
105
+ param_map = LMS_FieldMap.from_dict(parameters, definition.parameters)
106
+
107
+ if is_closing:
108
+ tag = LMS_DecodedTag(definition, is_closing=True)
109
+ else:
110
+ tag = LMS_DecodedTag(definition, param_map)
111
+
112
+ self._parts.append(tag)
113
+ return tag
95
114
 
96
115
  def append_tag_string(self, tag: str) -> None:
97
- """Appends a tag to the current message given a string.
116
+ """
117
+ Appends a tag to the current message given a string.
98
118
 
99
- :param tag: the tag string."""
119
+ :param tag: the tag string.
120
+ """
100
121
  if re.match(LMS_DecodedTag.TAG_FORMAT, tag):
101
- if self._config is None:
122
+ if self._tag_config is None:
102
123
  raise ValueError("TagConfig is required to append decoded tags.")
103
- self._parts.append(LMS_DecodedTag.from_string(tag, self._config))
124
+ self._parts.append(LMS_DecodedTag.from_string(tag, self._tag_config))
104
125
  elif re.match(LMS_EncodedTag.TAG_FORMAT, tag):
105
126
  self._parts.append(LMS_EncodedTag.from_string(tag))
106
127
  else:
@@ -1,6 +1,7 @@
1
+ from typing import overload
2
+
1
3
  from lms.common.lms_fileinfo import LMS_FileInfo
2
- from lms.message.definitions.field.lms_field import (LMS_Field,
3
- dict_to_field_map)
4
+ from lms.message.definitions.field.lms_field import LMS_FieldMap
4
5
  from lms.message.definitions.lms_messagetext import LMS_MessageText
5
6
  from lms.message.msbtentry import MSBTEntry
6
7
  from lms.titleconfig.definitions.attribute import AttributeConfig
@@ -29,35 +30,44 @@ class MSBT:
29
30
  self.slot_count = 101
30
31
 
31
32
  self.attr_string_table: list[str] | bytes | None = None
32
- self.encoded_attributes = True
33
+ self.uses_encoded_attributes = True
33
34
 
34
- # List of section names to preserve order
35
+ self.unsupported_sections: dict[str, bytes] = {}
35
36
  self.section_list: list[str] = []
36
37
 
37
38
  # List of unsupported sections mapped to their raw data
38
- self.unsupported_sections: dict[str, bytes] = {}
39
-
40
- # Store configs so if new labels are added LMS_MessageText objects and Attributes can be made properly
41
39
  self._attribute_config = attribute_config
42
40
  self._tag_config = tag_config
43
41
 
44
42
  def __iter__(self):
45
43
  return iter(self._entries)
46
44
 
45
+ @property
46
+ def entries(self) -> list[MSBTEntry]:
47
+ """The list of entries for the MSBT instance."""
48
+ return self._entries
49
+
47
50
  @property
48
51
  def info(self) -> LMS_FileInfo:
49
52
  """The file info for the MSBT instance."""
50
53
  return self._info
51
54
 
52
55
  @property
53
- def entries(self) -> list[MSBTEntry]:
54
- """The list of entries for the MSBT instance."""
55
- return self._entries
56
+ def has_attributes(self) -> bool:
57
+ """If the msbt contains attributs."""
58
+ return self.section_exists("ATR1")
59
+
60
+ @property
61
+ def has_style_indexes(self) -> bool:
62
+ """If the msbt contains style indexes."""
63
+ return self.section_exists("TSY1")
56
64
 
57
65
  def section_exists(self, name: str) -> bool:
58
- """Determines if a section exists in the MSBT.
66
+ """
67
+ Determines if a section exists in the current MSBT.
59
68
 
60
- :param name: the name of the section."""
69
+ :param name: the name of the section.
70
+ """
61
71
  return name in self.section_list
62
72
 
63
73
  def add_entry(
@@ -67,9 +77,14 @@ class MSBT:
67
77
  attribute: dict | bytes | None = None,
68
78
  style_index: int | None = None,
69
79
  ) -> None:
70
- """Adds an entry to the MSBT instance.
71
-
72
- :param name: the name of the entry."""
80
+ """
81
+ Adds an entry to the MSBT instance.
82
+
83
+ :param name: the name of the entry.
84
+ :param text: the message text to add.
85
+ :param attribute: the attribute data to add. can be a dictionary of data or raw bytes.
86
+ :param style_index: the index of a style for the message.
87
+ """
73
88
  if name in [entry.name for entry in self.entries]:
74
89
  raise KeyError(f"The label '{name}' already exists!")
75
90
 
@@ -78,22 +93,24 @@ class MSBT:
78
93
  raise ValueError(
79
94
  "The attribute config must have been provided when reading to add decoded attributes!"
80
95
  )
81
- converted_attribute = dict_to_field_map(
96
+ converted_attribute = LMS_FieldMap.from_dict(
82
97
  attribute, self._attribute_config.definitions
83
98
  )
84
99
  else:
85
100
  converted_attribute = attribute
86
101
 
87
102
  if text is not None:
88
- if self._tag_config is not None:
89
- message_text = LMS_MessageText(text, self._tag_config)
90
- else:
91
- message_text = LMS_MessageText(text)
103
+ message_text = LMS_MessageText(text, self._tag_config)
92
104
  else:
93
105
  message_text = ""
94
106
 
95
107
  self._entries.append(
96
- MSBTEntry(name, message_text, converted_attribute, style_index)
108
+ MSBTEntry(
109
+ name,
110
+ message=message_text,
111
+ attribute=converted_attribute,
112
+ style_index=style_index,
113
+ )
97
114
  )
98
115
 
99
116
  def get_entry(self, name: str) -> MSBTEntry: