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.
- {pylibms-3.0.8 → pylibms-3.1.1}/PKG-INFO +10 -10
- {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/PKG-INFO +10 -10
- {pylibms-3.0.8 → pylibms-3.1.1}/README.md +9 -9
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_datatype.py +1 -1
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/hashtable.py +1 -1
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/section.py +1 -2
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/fileio/encoding.py +15 -10
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/fileio/io.py +6 -7
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/io.py +9 -7
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/lms_field.py +63 -18
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/lms_messagetext.py +54 -33
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbt.py +38 -21
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbtentry.py +36 -24
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/msbtio.py +47 -43
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/atr1.py +32 -24
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/txt2.py +4 -2
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/io/param_io.py +18 -19
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/io/tag_io.py +14 -13
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/lms_tag.py +18 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/tag.py +5 -5
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/msbpread.py +15 -9
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/config.py +51 -25
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/attribute.py +2 -2
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/tags.py +16 -17
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Badge Arcade.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario 3D Land.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +1 -1
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Super Mario Odyssey.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +1 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +1 -3
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/presets/Tomodachi Life.yaml +30 -29
- {pylibms-3.0.8 → pylibms-3.1.1}/pyproject.toml +1 -1
- {pylibms-3.0.8 → pylibms-3.1.1}/LICENSE +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/MANIFEST.in +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/SOURCES.txt +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/dependency_links.txt +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/requires.txt +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/PyLibMS.egg-info/top_level.txt +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_exceptions.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/lms_fileinfo.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/common/stream/fileinfo.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/definitions/field/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/section/tsy1.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/message/tag/lms_tagexceptions.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/attribute.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/color.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/definitions/style.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/msbp.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/ali2.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/ati2.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/clr1.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/string.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/syl3.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tag2.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tgg2.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/project/section/tgp2.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/__init__.py +0 -0
- {pylibms-3.0.8 → pylibms-3.1.1}/lms/titleconfig/definitions/value.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
*
|
|
32
|
-
* Nintendo
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
*
|
|
32
|
-
* Nintendo
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
*
|
|
11
|
-
* Nintendo
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
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
|
```
|
|
@@ -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
|
|
1
|
+
from enum import IntEnum
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class FileEncoding(
|
|
5
|
+
class FileEncoding(IntEnum):
|
|
6
6
|
"""An enum that represents a file encoding."""
|
|
7
7
|
|
|
8
|
-
UTF8 =
|
|
9
|
-
UTF16 =
|
|
10
|
-
UTF32 =
|
|
8
|
+
UTF8 = 0x00
|
|
9
|
+
UTF16 = 0x01
|
|
10
|
+
UTF32 = 0x02
|
|
11
11
|
|
|
12
|
-
def to_string_format(
|
|
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
|
|
20
|
+
if is_big_endian:
|
|
18
21
|
return "UTF-16-BE"
|
|
19
22
|
return "UTF-16-LE"
|
|
20
23
|
case FileEncoding.UTF32:
|
|
21
|
-
if
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
43
|
+
writer.write_uint8(value)
|
|
42
44
|
case LMS_DataType.INT8:
|
|
43
|
-
writer.write_int8(
|
|
45
|
+
writer.write_int8(value)
|
|
44
46
|
case LMS_DataType.UINT16:
|
|
45
|
-
writer.write_uint16(
|
|
47
|
+
writer.write_uint16(value)
|
|
46
48
|
case LMS_DataType.INT16:
|
|
47
|
-
writer.write_int16(
|
|
49
|
+
writer.write_int16(value)
|
|
48
50
|
case LMS_DataType.UINT32:
|
|
49
|
-
writer.write_uint32(
|
|
51
|
+
writer.write_uint32(value)
|
|
50
52
|
case LMS_DataType.INT32:
|
|
51
|
-
writer.write_int32(
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
32
|
+
result = []
|
|
31
33
|
for part in self._parts:
|
|
32
|
-
|
|
33
|
-
part.to_text()
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
) ->
|
|
72
|
-
"""
|
|
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.
|
|
93
|
+
if self._tag_config is None:
|
|
84
94
|
raise ValueError("A TitleConfig is required to append decoded tags.")
|
|
85
95
|
|
|
86
|
-
definition = self.
|
|
96
|
+
definition = self._tag_config.get_definition_by_names(group_name, tag_name)
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
33
|
+
self.uses_encoded_attributes = True
|
|
33
34
|
|
|
34
|
-
|
|
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
|
|
54
|
-
"""
|
|
55
|
-
return self.
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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:
|