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