PyLibMS 3.3__tar.gz → 3.3.2__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.3 → pylibms-3.3.2}/PKG-INFO +3 -3
- {pylibms-3.3 → pylibms-3.3.2}/PyLibMS.egg-info/PKG-INFO +3 -3
- {pylibms-3.3 → pylibms-3.3.2}/README.md +2 -2
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/lms_datatype.py +15 -16
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/stream/fileinfo.py +1 -1
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/definitions/field/lms_field.py +9 -6
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/definitions/lms_messagetext.py +16 -16
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/msbt.py +27 -12
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/msbtio.py +4 -1
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/section/atr1.py +1 -1
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/section/txt2.py +6 -7
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/msbp.py +11 -4
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/msbpread.py +1 -1
- {pylibms-3.3 → pylibms-3.3.2}/pyproject.toml +1 -1
- {pylibms-3.3 → pylibms-3.3.2}/LICENSE +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/MANIFEST.in +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/PyLibMS.egg-info/SOURCES.txt +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/PyLibMS.egg-info/dependency_links.txt +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/PyLibMS.egg-info/requires.txt +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/PyLibMS.egg-info/top_level.txt +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/lms_exceptions.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/lms_fileinfo.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/stream/hashtable.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/common/stream/section.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/fileio/encoding.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/fileio/io.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/definitions/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/definitions/field/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/definitions/field/io.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/msbtentry.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/section/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/section/nli1.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/section/tsy1.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/tag/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/tag/io/param_io.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/tag/io/tag_io.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/tag/lms_tag.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/message/tag/lms_tagexceptions.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/definitions/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/definitions/attribute.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/definitions/color.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/definitions/style.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/definitions/tag.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/ali2.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/ati2.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/clr1.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/string.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/syl3.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/tag2.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/tgg2.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/project/section/tgp2.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/config.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/definitions/__init__.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/definitions/attribute.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/definitions/tags.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/definitions/value.py +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Badge Arcade.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Brain Age Concentration Training.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Kirby Planet Robobot.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Super Mario 3D Land.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Super Mario Odyssey.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Tomodachi Life.yaml +0 -0
- {pylibms-3.3 → pylibms-3.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyLibMS
|
|
3
|
-
Version: 3.3
|
|
3
|
+
Version: 3.3.2
|
|
4
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
|
|
@@ -20,7 +20,7 @@ Requires-Dist: PyYAML>=6.0.1
|
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
# PylibMS
|
|
23
|
-
PylibMS is a library built in Python 3.
|
|
23
|
+
PylibMS is a library built in Python 3.12+ for the libMessageStudio (LMS) proprietary file formats (`msbt`, `msbp`, `msbf`) from Nintendo. It supports the following:
|
|
24
24
|
|
|
25
25
|
* Full reading and writing of MSBT files.
|
|
26
26
|
* Full reading of MSBP files.
|
|
@@ -35,7 +35,7 @@ This library is designed to support LMS revision 3.0 and above, with the associa
|
|
|
35
35
|
* Mobile (Specific titles only)
|
|
36
36
|
* Nintendo Switch
|
|
37
37
|
|
|
38
|
-
MSBF is
|
|
38
|
+
MSBF is currently in the works.
|
|
39
39
|
|
|
40
40
|
# Features and Usage
|
|
41
41
|
Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyLibMS
|
|
3
|
-
Version: 3.3
|
|
3
|
+
Version: 3.3.2
|
|
4
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
|
|
@@ -20,7 +20,7 @@ Requires-Dist: PyYAML>=6.0.1
|
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
# PylibMS
|
|
23
|
-
PylibMS is a library built in Python 3.
|
|
23
|
+
PylibMS is a library built in Python 3.12+ for the libMessageStudio (LMS) proprietary file formats (`msbt`, `msbp`, `msbf`) from Nintendo. It supports the following:
|
|
24
24
|
|
|
25
25
|
* Full reading and writing of MSBT files.
|
|
26
26
|
* Full reading of MSBP files.
|
|
@@ -35,7 +35,7 @@ This library is designed to support LMS revision 3.0 and above, with the associa
|
|
|
35
35
|
* Mobile (Specific titles only)
|
|
36
36
|
* Nintendo Switch
|
|
37
37
|
|
|
38
|
-
MSBF is
|
|
38
|
+
MSBF is currently in the works.
|
|
39
39
|
|
|
40
40
|
# Features and Usage
|
|
41
41
|
Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# PylibMS
|
|
2
|
-
PylibMS is a library built in Python 3.
|
|
2
|
+
PylibMS is a library built in Python 3.12+ for the libMessageStudio (LMS) proprietary file formats (`msbt`, `msbp`, `msbf`) from Nintendo. It supports the following:
|
|
3
3
|
|
|
4
4
|
* Full reading and writing of MSBT files.
|
|
5
5
|
* Full reading of MSBP files.
|
|
@@ -14,7 +14,7 @@ This library is designed to support LMS revision 3.0 and above, with the associa
|
|
|
14
14
|
* Mobile (Specific titles only)
|
|
15
15
|
* Nintendo Switch
|
|
16
16
|
|
|
17
|
-
MSBF is
|
|
17
|
+
MSBF is currently in the works.
|
|
18
18
|
|
|
19
19
|
# Features and Usage
|
|
20
20
|
Simple preview of the library is below. See [the wiki](https://github.com/AbdyyEee/PylibMS/wiki) for more explanations and examples.
|
|
@@ -3,6 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Type, TypeGuard
|
|
5
5
|
|
|
6
|
+
ALIAS_MAP = {
|
|
7
|
+
"u8": "UINT8",
|
|
8
|
+
"u16": "UINT16",
|
|
9
|
+
"u32": "UINT32",
|
|
10
|
+
"i8": "INT8",
|
|
11
|
+
"i16": "INT16",
|
|
12
|
+
"i32": "INT32",
|
|
13
|
+
"f32": "FLOAT32",
|
|
14
|
+
"str": "STRING",
|
|
15
|
+
"bool": "BOOL",
|
|
16
|
+
"byte": "BYTES",
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
def is_number_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[int | float]:
|
|
8
20
|
return datatype in (
|
|
@@ -13,7 +25,7 @@ def is_number_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[int |
|
|
|
13
25
|
LMS_DataType.INT16,
|
|
14
26
|
LMS_DataType.INT32,
|
|
15
27
|
LMS_DataType.FLOAT32,
|
|
16
|
-
) and isinstance(value,
|
|
28
|
+
) and isinstance(value, int)
|
|
17
29
|
|
|
18
30
|
|
|
19
31
|
def is_list_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[str]:
|
|
@@ -77,7 +89,7 @@ class LMS_DataType(Enum):
|
|
|
77
89
|
raise TypeError(f"Signed is not a valid property for '{self.to_string()}'!")
|
|
78
90
|
|
|
79
91
|
@property
|
|
80
|
-
def builtin_type(self) -> Type[int |
|
|
92
|
+
def builtin_type(self) -> Type[int | float | str | bool | bytes]:
|
|
81
93
|
"""The enum as the builtin python type."""
|
|
82
94
|
return {
|
|
83
95
|
LMS_DataType.UINT8: int,
|
|
@@ -116,20 +128,7 @@ class LMS_DataType(Enum):
|
|
|
116
128
|
if member in cls.__members__:
|
|
117
129
|
return cls[member]
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
"u8": "UINT8",
|
|
121
|
-
"u16": "UINT16",
|
|
122
|
-
"u32": "UINT32",
|
|
123
|
-
"i8": "INT8",
|
|
124
|
-
"i16": "INT16",
|
|
125
|
-
"i32": "INT32",
|
|
126
|
-
"f32": "FLOAT32",
|
|
127
|
-
"str": "STRING",
|
|
128
|
-
"bool": "BOOL",
|
|
129
|
-
"byte": "BYTES",
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
alias_member = alias_map.get(string.lower())
|
|
131
|
+
alias_member = ALIAS_MAP.get(string.lower())
|
|
133
132
|
if alias_member is not None:
|
|
134
133
|
return cls[alias_member]
|
|
135
134
|
else:
|
|
@@ -33,7 +33,7 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
|
|
|
33
33
|
|
|
34
34
|
reader.seek(0, 2)
|
|
35
35
|
if file_size != reader.tell():
|
|
36
|
-
raise lms_exceptions.LMS_MisalignedSizeError(f"
|
|
36
|
+
raise lms_exceptions.LMS_MisalignedSizeError(f"File size is misaligned!")
|
|
37
37
|
|
|
38
38
|
reader.seek(DATA_START)
|
|
39
39
|
|
|
@@ -15,8 +15,7 @@ type FieldValue = int | str | float | bool | bytes
|
|
|
15
15
|
@dataclass(frozen=True)
|
|
16
16
|
class LMS_FieldMap:
|
|
17
17
|
"""
|
|
18
|
-
A wrapper for a
|
|
19
|
-
and abstraction from the dict object.
|
|
18
|
+
A wrapper for a basic ``dict[str, LMS_Field]``. Difference is validation upon modification of a field.
|
|
20
19
|
"""
|
|
21
20
|
|
|
22
21
|
fields: dict[str, LMS_Field]
|
|
@@ -52,8 +51,10 @@ class LMS_FieldMap:
|
|
|
52
51
|
@classmethod
|
|
53
52
|
def from_string_dict(cls, data: dict[str, str], definitions: list[ValueDefinition]):
|
|
54
53
|
fields = {}
|
|
54
|
+
casted_value = None
|
|
55
55
|
|
|
56
56
|
for definition in definitions:
|
|
57
|
+
# These datatypes do not require any casting
|
|
57
58
|
if definition.datatype in (LMS_DataType.STRING, LMS_DataType.LIST):
|
|
58
59
|
fields[definition.name] = LMS_Field(data[definition.name], definition)
|
|
59
60
|
continue
|
|
@@ -64,7 +65,7 @@ class LMS_FieldMap:
|
|
|
64
65
|
casted_value = bytes.fromhex(value)
|
|
65
66
|
case LMS_DataType.BOOL:
|
|
66
67
|
if value not in ("false", "true"):
|
|
67
|
-
raise ValueError("
|
|
68
|
+
raise ValueError("Invalid boolean value!")
|
|
68
69
|
casted_value = value.strip().lower() == "true"
|
|
69
70
|
case LMS_DataType.FLOAT32:
|
|
70
71
|
casted_value = float(value)
|
|
@@ -115,7 +116,7 @@ class LMS_Field:
|
|
|
115
116
|
|
|
116
117
|
@property
|
|
117
118
|
def list_items(self) -> list[str]:
|
|
118
|
-
"""The list items bound to the field instance. Only is valid for
|
|
119
|
+
"""The list items bound to the field instance. Only is valid for ``LMS_Datatype.LIST`` values."""
|
|
119
120
|
return self._definition.list_items
|
|
120
121
|
|
|
121
122
|
@value.setter
|
|
@@ -141,7 +142,8 @@ def _verify_value(
|
|
|
141
142
|
case LMS_DataType.LIST if isinstance(value, str):
|
|
142
143
|
if value not in definition.list_items:
|
|
143
144
|
raise ValueError(
|
|
144
|
-
f"The value of '{value}' provided for field '{definition.name}' is not
|
|
145
|
+
f"""The value of '{value}' provided for field '{definition.name}' is not a
|
|
146
|
+
valid item in the list {definition.list_items}."""
|
|
145
147
|
)
|
|
146
148
|
else:
|
|
147
149
|
return
|
|
@@ -172,5 +174,6 @@ def _verify_number_is_in_range(
|
|
|
172
174
|
):
|
|
173
175
|
if not min_value <= value <= max_value:
|
|
174
176
|
raise ValueError(
|
|
175
|
-
f"The value '{value}' of type '{definition.datatype}' provided for field '{definition.name}'
|
|
177
|
+
f"""The value '{value}' of type '{definition.datatype}' provided for field '{definition.name}'
|
|
178
|
+
is out of range of ({min_value}, {max_value})"""
|
|
176
179
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
-
from lms.message.definitions.field.lms_field import LMS_FieldMap
|
|
3
|
+
from lms.message.definitions.field.lms_field import LMS_FieldMap, FieldValue
|
|
4
4
|
from lms.message.tag.lms_tag import (LMS_ControlTag, LMS_DecodedTag,
|
|
5
5
|
LMS_EncodedTag, is_tag)
|
|
6
6
|
from lms.message.tag.lms_tagexceptions import LMS_TagForbiddenParametersError
|
|
@@ -20,18 +20,18 @@ class LMS_MessageText:
|
|
|
20
20
|
self._tag_config = tag_config
|
|
21
21
|
|
|
22
22
|
if isinstance(message, str):
|
|
23
|
-
self.
|
|
23
|
+
self._set_segments(message)
|
|
24
24
|
else:
|
|
25
|
-
self.
|
|
25
|
+
self._segments = message
|
|
26
26
|
|
|
27
27
|
def __iter__(self):
|
|
28
|
-
return iter(self.
|
|
28
|
+
return iter(self._segments)
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
31
|
def text(self) -> str:
|
|
32
32
|
"""The raw text of the message."""
|
|
33
33
|
result = []
|
|
34
|
-
for part in self.
|
|
34
|
+
for part in self._segments:
|
|
35
35
|
if is_tag(part):
|
|
36
36
|
result.append(part.to_text())
|
|
37
37
|
else:
|
|
@@ -40,19 +40,19 @@ class LMS_MessageText:
|
|
|
40
40
|
|
|
41
41
|
@text.setter
|
|
42
42
|
def text(self, string: str) -> None:
|
|
43
|
-
self.
|
|
43
|
+
self._set_segments(string)
|
|
44
44
|
|
|
45
45
|
@property
|
|
46
46
|
def tags(self) -> list[LMS_ControlTag]:
|
|
47
47
|
"""The list of control tags in the message."""
|
|
48
|
-
return [part for part in self.
|
|
48
|
+
return [part for part in self._segments if is_tag(part)]
|
|
49
49
|
|
|
50
50
|
@property
|
|
51
51
|
def tag_positions(self) -> dict[LMS_ControlTag, tuple[int, int]]:
|
|
52
52
|
"""Dict of tag objects to their start and end positions in text."""
|
|
53
53
|
positions = {}
|
|
54
54
|
pos = 0
|
|
55
|
-
for part in self.
|
|
55
|
+
for part in self._segments:
|
|
56
56
|
text_len = len(part)
|
|
57
57
|
if is_tag(part):
|
|
58
58
|
positions[part] = (pos, pos + text_len)
|
|
@@ -86,7 +86,7 @@ class LMS_MessageText:
|
|
|
86
86
|
group_id, tag_index, None if not parameters else list(parameters)
|
|
87
87
|
)
|
|
88
88
|
|
|
89
|
-
self.
|
|
89
|
+
self._segments.append(tag)
|
|
90
90
|
return tag
|
|
91
91
|
|
|
92
92
|
def append_decoded_tag(
|
|
@@ -94,7 +94,7 @@ class LMS_MessageText:
|
|
|
94
94
|
group_name: str,
|
|
95
95
|
tag_name: str,
|
|
96
96
|
is_closing: bool = False,
|
|
97
|
-
**parameters:
|
|
97
|
+
**parameters: FieldValue
|
|
98
98
|
) -> LMS_DecodedTag:
|
|
99
99
|
"""
|
|
100
100
|
Appends a decoded tag to the current message and returns that tag.
|
|
@@ -120,7 +120,7 @@ class LMS_MessageText:
|
|
|
120
120
|
raise LMS_TagForbiddenParametersError("There may not be parameters for closing tags!")
|
|
121
121
|
|
|
122
122
|
tag = LMS_DecodedTag(definition, is_closing=True)
|
|
123
|
-
self.
|
|
123
|
+
self._segments.append(tag)
|
|
124
124
|
return tag
|
|
125
125
|
|
|
126
126
|
if parameters:
|
|
@@ -129,7 +129,7 @@ class LMS_MessageText:
|
|
|
129
129
|
else:
|
|
130
130
|
tag = LMS_DecodedTag(definition)
|
|
131
131
|
|
|
132
|
-
self.
|
|
132
|
+
self._segments.append(tag)
|
|
133
133
|
return tag
|
|
134
134
|
|
|
135
135
|
def append_tag_string(self, tag: str) -> LMS_ControlTag:
|
|
@@ -158,13 +158,13 @@ class LMS_MessageText:
|
|
|
158
158
|
else:
|
|
159
159
|
raise ValueError(f"Invalid format in tag '{tag}'.")
|
|
160
160
|
|
|
161
|
-
self.
|
|
161
|
+
self._segments.append(tag_obj)
|
|
162
162
|
return tag_obj
|
|
163
163
|
|
|
164
|
-
def
|
|
165
|
-
self.
|
|
164
|
+
def _set_segments(self, text: str) -> None:
|
|
165
|
+
self._segments = []
|
|
166
166
|
for part in self.TAG_FORMAT.split(text):
|
|
167
167
|
if bool(re.match(self.TAG_FORMAT, part)):
|
|
168
168
|
self.append_tag_string(part)
|
|
169
169
|
else:
|
|
170
|
-
self.
|
|
170
|
+
self._segments.append(part)
|
|
@@ -9,20 +9,28 @@ class MSBT:
|
|
|
9
9
|
"""
|
|
10
10
|
A class that represents a MSBT file.
|
|
11
11
|
|
|
12
|
+
======
|
|
13
|
+
Usages
|
|
14
|
+
======
|
|
15
|
+
https://github.com/AbdyyEee/PylibMS/wiki/MSBT
|
|
16
|
+
|
|
17
|
+
=========
|
|
18
|
+
File Info
|
|
19
|
+
=========
|
|
12
20
|
https://nintendo-formats.com/libs/lms/msbt.html
|
|
13
21
|
"""
|
|
14
|
-
|
|
15
22
|
MAGIC = "MsgStdBn"
|
|
16
23
|
|
|
17
24
|
DEFAULT_SLOT_COUNT = 101
|
|
18
25
|
|
|
19
26
|
ATR1_INDEX = 1
|
|
20
27
|
TXT2_INDEX = 2
|
|
21
|
-
|
|
28
|
+
TSY1_INDEX = 3
|
|
22
29
|
|
|
23
30
|
def __init__(
|
|
24
31
|
self,
|
|
25
32
|
info: LMS_FileInfo | None = None,
|
|
33
|
+
uses_nli1: bool = False,
|
|
26
34
|
section_list: list[str] | None = None,
|
|
27
35
|
unsupported_section_map: dict[str, bytes] | None = None,
|
|
28
36
|
attribute_config: AttributeConfig | None = None,
|
|
@@ -43,13 +51,14 @@ class MSBT:
|
|
|
43
51
|
self._unsupported_section_map = unsupported_section_map or {}
|
|
44
52
|
|
|
45
53
|
# Store the section list so that the order of any and all sections is preserved when writing
|
|
46
|
-
self._section_list: list[str] = section_list or ["LBL1"]
|
|
54
|
+
self._section_list: list[str] = section_list or ["LBL1" if not uses_nli1 else "NLI1"]
|
|
47
55
|
|
|
48
56
|
self._attribute_config = attribute_config
|
|
49
57
|
self._tag_config = tag_config
|
|
50
58
|
|
|
51
59
|
@classmethod
|
|
52
60
|
def new(cls,
|
|
61
|
+
uses_nli1: bool = False,
|
|
53
62
|
attribute_config: AttributeConfig | None = None,
|
|
54
63
|
tag_config: TagConfig | None = None,
|
|
55
64
|
is_big_endian: bool = False,
|
|
@@ -58,6 +67,7 @@ class MSBT:
|
|
|
58
67
|
section_count: int = 2):
|
|
59
68
|
"""Create a new MSBT instance.
|
|
60
69
|
|
|
70
|
+
:param uses_nli1: flag to determine if to use nli1 section for labels.
|
|
61
71
|
:param attribute_config: the attribute config object
|
|
62
72
|
:param tag_config: the tag config object
|
|
63
73
|
:param is_big_endian: if the file is big endian.
|
|
@@ -65,13 +75,13 @@ class MSBT:
|
|
|
65
75
|
:param version: the file version.
|
|
66
76
|
:param section_count: the number of sections.
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
======
|
|
79
|
+
Usages
|
|
80
|
+
======
|
|
71
81
|
See https://github.com/AbdyyEee/PylibMS/wiki/MSBT#creating-a-msbt
|
|
72
82
|
"""
|
|
73
|
-
return MSBT(
|
|
74
|
-
attribute_config=attribute_config, tag_config=tag_config)
|
|
83
|
+
return MSBT(LMS_FileInfo(is_big_endian, encoding, version, section_count),
|
|
84
|
+
uses_nli1=uses_nli1, attribute_config=attribute_config, tag_config=tag_config)
|
|
75
85
|
|
|
76
86
|
def __len__(self) -> int:
|
|
77
87
|
return len(self._entries)
|
|
@@ -100,12 +110,17 @@ class MSBT:
|
|
|
100
110
|
return tuple(self._unsupported_section_map.keys())
|
|
101
111
|
|
|
102
112
|
@property
|
|
103
|
-
def
|
|
113
|
+
def uses_nli1(self) -> bool:
|
|
114
|
+
"""If the MSBT contains the NLI1 section."""
|
|
115
|
+
return self.section_exists("NLI1")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def contains_attributes(self) -> bool:
|
|
104
119
|
"""If the MSBT contains attributes."""
|
|
105
120
|
return self.section_exists("ATR1")
|
|
106
121
|
|
|
107
122
|
@property
|
|
108
|
-
def
|
|
123
|
+
def contains_styles(self) -> bool:
|
|
109
124
|
"""If the MSBT contains style indexes."""
|
|
110
125
|
return self.section_exists("TSY1")
|
|
111
126
|
|
|
@@ -141,7 +156,7 @@ class MSBT:
|
|
|
141
156
|
raise KeyError(f"The label '{entry.name}' already exists!")
|
|
142
157
|
|
|
143
158
|
# 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.
|
|
159
|
+
# In most MSBT files, it goes as LBL1/NLI1 -> TXT2 -> ATR1 -> TSY1.
|
|
145
160
|
# If add_entry is utilized on a new MSBT instance, then these indexes ensure the order is maintained.
|
|
146
161
|
# While technically, undocumented sections (i.e. ATO1) can prefix TXT2 and other sections,
|
|
147
162
|
# we do not need to account for that scenario as they aren't supported by the library
|
|
@@ -164,7 +179,7 @@ class MSBT:
|
|
|
164
179
|
f"Entry '{entry.name}' can't be added with no style index when styles already exist!"
|
|
165
180
|
)
|
|
166
181
|
elif entry.style_index is not None:
|
|
167
|
-
self._section_list.insert(self.
|
|
182
|
+
self._section_list.insert(self.TSY1_INDEX, "TSY1")
|
|
168
183
|
self._info.section_count += 1
|
|
169
184
|
|
|
170
185
|
self._entries.append(entry)
|
|
@@ -79,12 +79,14 @@ def read_msbt(
|
|
|
79
79
|
messages = atr1_data = style_indexes = None
|
|
80
80
|
|
|
81
81
|
labels: dict[int, str] = {}
|
|
82
|
+
uses_nli1 = False
|
|
82
83
|
for magic, size in read_section_data(reader, file_info.section_count):
|
|
83
84
|
match magic:
|
|
84
85
|
case "LBL1":
|
|
85
86
|
labels, slot_count = read_labels(reader)
|
|
86
87
|
case "NLI1":
|
|
87
88
|
labels = read_nli1(reader)
|
|
89
|
+
uses_nli1 = True
|
|
88
90
|
case "ATR1":
|
|
89
91
|
atr1_data = read_atr1(reader, attribute_config, size)
|
|
90
92
|
case "TXT2":
|
|
@@ -98,7 +100,8 @@ def read_msbt(
|
|
|
98
100
|
section_list.append(magic)
|
|
99
101
|
|
|
100
102
|
file = MSBT(
|
|
101
|
-
file_info,
|
|
103
|
+
file_info, uses_nli1, section_list,
|
|
104
|
+
unsupported_sections, attribute_config, tag_config
|
|
102
105
|
)
|
|
103
106
|
file.slot_count = slot_count
|
|
104
107
|
|
|
@@ -39,7 +39,7 @@ def read_encoded_atr1(reader: FileReader, section_size: int) -> ATR1Data:
|
|
|
39
39
|
|
|
40
40
|
def read_decoded_atr1(reader: FileReader, config: AttributeConfig) -> ATR1Data:
|
|
41
41
|
# String table as a list is not necessary for decoded attributes
|
|
42
|
-
# It is easier to save all changes from the user
|
|
42
|
+
# It is easier to save all changes from the user when writing
|
|
43
43
|
# to recreate the table from all the string parameters in every attribute
|
|
44
44
|
string_table = None
|
|
45
45
|
|
|
@@ -19,24 +19,23 @@ def read_txt2(
|
|
|
19
19
|
for offset in reader.read_offset_array(message_count):
|
|
20
20
|
reader.seek(offset)
|
|
21
21
|
|
|
22
|
-
text,
|
|
22
|
+
text, text_segments = b"", []
|
|
23
23
|
while (data := reader.read_bytes(encoding.width)) != encoding.terminator:
|
|
24
|
-
is_opening_tag = data == tag_start
|
|
25
24
|
is_closing_tag = data == tag_close
|
|
26
25
|
|
|
27
|
-
if
|
|
28
|
-
|
|
26
|
+
if data == tag_start or is_closing_tag:
|
|
27
|
+
text_segments.append(text.decode(encoding_format))
|
|
29
28
|
tag = read_tag(reader, config, is_closing_tag, suppress_tag_errors)
|
|
30
|
-
|
|
29
|
+
text_segments.append(tag)
|
|
31
30
|
text = b""
|
|
32
31
|
else:
|
|
33
32
|
text += data
|
|
34
33
|
|
|
35
34
|
# Add the remaining text in case there were no control tags
|
|
36
35
|
if text:
|
|
37
|
-
|
|
36
|
+
text_segments.append(text.decode(encoding_format))
|
|
38
37
|
|
|
39
|
-
message = LMS_MessageText(
|
|
38
|
+
message = LMS_MessageText(text_segments, config)
|
|
40
39
|
messages.append(message)
|
|
41
40
|
|
|
42
41
|
return messages
|
|
@@ -9,7 +9,15 @@ class MSBP:
|
|
|
9
9
|
"""
|
|
10
10
|
A class that represents a MSBP file.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
======
|
|
13
|
+
Usages
|
|
14
|
+
======
|
|
15
|
+
https://github.com/AbdyyEee/PylibMS/wiki/MSBP
|
|
16
|
+
|
|
17
|
+
=========
|
|
18
|
+
File Info
|
|
19
|
+
=========
|
|
20
|
+
https://nintendo-formats.com/libs/lms/msbp.html
|
|
13
21
|
"""
|
|
14
22
|
|
|
15
23
|
MAGIC = "MsgPrjBn"
|
|
@@ -23,10 +31,9 @@ class MSBP:
|
|
|
23
31
|
styles: list[LMS_Style] | None,
|
|
24
32
|
source_files: list[str] | None,
|
|
25
33
|
):
|
|
26
|
-
self.name
|
|
34
|
+
self.name = ""
|
|
27
35
|
|
|
28
36
|
self._info = info
|
|
29
|
-
|
|
30
37
|
self._colors = colors
|
|
31
38
|
self._attribute_definitions = config
|
|
32
39
|
self._tag_groups = tag_groups
|
|
@@ -35,7 +42,7 @@ class MSBP:
|
|
|
35
42
|
|
|
36
43
|
@property
|
|
37
44
|
def info(self) -> LMS_FileInfo:
|
|
38
|
-
"""The
|
|
45
|
+
"""The file info for the MSBP instance."""
|
|
39
46
|
return self._info
|
|
40
47
|
|
|
41
48
|
@property
|
|
@@ -62,7 +62,7 @@ def read_msbp(stream: BinaryIO | bytes) -> MSBP:
|
|
|
62
62
|
for magic, _ in read_section_data(reader, file_info.section_count):
|
|
63
63
|
match magic:
|
|
64
64
|
case "CLB1" | "ALB1" | "SLB1":
|
|
65
|
-
# Set the name
|
|
65
|
+
# Set the name attribute of last read item
|
|
66
66
|
labels, _ = read_labels(reader)
|
|
67
67
|
for i in labels:
|
|
68
68
|
items[i].name = labels[i]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "PyLibMS"
|
|
7
|
-
version = "3.3"
|
|
7
|
+
version = "3.3.2"
|
|
8
8
|
requires-python = ">=3.12"
|
|
9
9
|
description = "Python library built for the libMessageStudio (LMS) proprietary file formats from Nintendo. Supports MSBT, MSBP, and soon to be MSBF."
|
|
10
10
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/Super Mario 3D World + Bowsers Fury.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/The Legend of Zelda Echos of Wisdom.yaml
RENAMED
|
File without changes
|
{pylibms-3.3 → pylibms-3.3.2}/lms/titleconfig/presets/The Legend of Zelda a Link Between Worlds.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|