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