PyLibMS 2.1.4__tar.gz → 3.0.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-2.1.4 → pylibms-3.0.2}/PKG-INFO +5 -7
- {pylibms-2.1.4 → pylibms-3.0.2}/PyLibMS.egg-info/PKG-INFO +5 -7
- pylibms-3.0.2/PyLibMS.egg-info/SOURCES.txt +58 -0
- pylibms-3.0.2/PyLibMS.egg-info/top_level.txt +1 -0
- {pylibms-2.1.4 → pylibms-3.0.2}/README.md +4 -6
- pylibms-2.1.4/LMS/Common/LMS_DataType.py → pylibms-3.0.2/lms/common/lms_datatype.py +7 -6
- pylibms-3.0.2/lms/common/lms_exceptions.py +7 -0
- pylibms-2.1.4/LMS/Common/LMS_FileInfo.py → pylibms-3.0.2/lms/common/lms_fileinfo.py +2 -2
- pylibms-2.1.4/LMS/Common/Stream/FileInfo.py → pylibms-3.0.2/lms/common/stream/fileinfo.py +11 -12
- pylibms-2.1.4/LMS/Common/Stream/Hashtable.py → pylibms-3.0.2/lms/common/stream/hashtable.py +5 -17
- pylibms-2.1.4/LMS/Common/Stream/Section.py → pylibms-3.0.2/lms/common/stream/section.py +4 -5
- pylibms-2.1.4/LMS/FileIO/Encoding.py → pylibms-3.0.2/lms/fileio/encoding.py +3 -6
- pylibms-2.1.4/LMS/FileIO/Stream.py → pylibms-3.0.2/lms/fileio/io.py +45 -43
- pylibms-2.1.4/LMS/Message/Definitions/Field/Stream.py → pylibms-3.0.2/lms/message/definitions/field/io.py +18 -15
- pylibms-2.1.4/LMS/Message/Definitions/Field/LMS_Field.py → pylibms-3.0.2/lms/message/definitions/field/lms_field.py +41 -23
- pylibms-3.0.2/lms/message/definitions/lms_messagetext.py +115 -0
- pylibms-2.1.4/LMS/Message/MSBT.py → pylibms-3.0.2/lms/message/msbt.py +18 -17
- pylibms-3.0.2/lms/message/msbtentry.py +86 -0
- pylibms-2.1.4/LMS/Message/MSBTStream.py → pylibms-3.0.2/lms/message/msbtio.py +60 -18
- pylibms-2.1.4/LMS/Message/Section/ATR1.py → pylibms-3.0.2/lms/message/section/atr1.py +18 -20
- pylibms-2.1.4/LMS/Message/Section/TSY1.py → pylibms-3.0.2/lms/message/section/tsy1.py +1 -1
- pylibms-3.0.2/lms/message/section/txt2.py +66 -0
- pylibms-3.0.2/lms/message/tag/io/param_io.py +90 -0
- pylibms-3.0.2/lms/message/tag/io/tag_io.py +107 -0
- pylibms-3.0.2/lms/message/tag/lms_tag.py +204 -0
- pylibms-2.1.4/LMS/Project/Definitions/Attribute.py → pylibms-3.0.2/lms/project/definitions/attribute.py +4 -7
- pylibms-3.0.2/lms/project/definitions/color.py +10 -0
- pylibms-3.0.2/lms/project/definitions/style.py +10 -0
- pylibms-3.0.2/lms/project/definitions/tag.py +95 -0
- pylibms-2.1.4/LMS/Project/MSBP.py → pylibms-3.0.2/lms/project/msbp.py +16 -16
- pylibms-3.0.2/lms/project/msbpread.py +109 -0
- pylibms-2.1.4/LMS/Project/Section/ALI2.py → pylibms-3.0.2/lms/project/section/ali2.py +1 -1
- pylibms-3.0.2/lms/project/section/ati2.py +18 -0
- pylibms-2.1.4/LMS/Project/Section/CLR1.py → pylibms-3.0.2/lms/project/section/clr1.py +2 -2
- pylibms-3.0.2/lms/project/section/string.py +17 -0
- pylibms-2.1.4/LMS/Project/Section/SYL3.py → pylibms-3.0.2/lms/project/section/syl3.py +2 -2
- pylibms-3.0.2/lms/project/section/tag2.py +18 -0
- pylibms-3.0.2/lms/project/section/tgg2.py +22 -0
- pylibms-3.0.2/lms/project/section/tgp2.py +28 -0
- pylibms-2.1.4/LMS/TitleConfig/Config.py → pylibms-3.0.2/lms/titleconfig/config.py +52 -38
- pylibms-2.1.4/LMS/TitleConfig/Definitions/Attributes.py → pylibms-3.0.2/lms/titleconfig/definitions/attribute.py +2 -2
- pylibms-2.1.4/LMS/TitleConfig/Definitions/Tags.py → pylibms-3.0.2/lms/titleconfig/definitions/tags.py +52 -42
- pylibms-2.1.4/LMS/TitleConfig/Definitions/Value.py → pylibms-3.0.2/lms/titleconfig/definitions/value.py +3 -3
- {pylibms-2.1.4 → pylibms-3.0.2}/pyproject.toml +1 -1
- pylibms-2.1.4/LMS/Common/LMS_Exceptions.py +0 -16
- pylibms-2.1.4/LMS/Message/Definitions/Field/LMS_FieldMap.py +0 -5
- pylibms-2.1.4/LMS/Message/Definitions/LMS_MessageText.py +0 -131
- pylibms-2.1.4/LMS/Message/MSBTEntry.py +0 -97
- pylibms-2.1.4/LMS/Message/Section/TXT2.py +0 -79
- pylibms-2.1.4/LMS/Message/Tag/LMS_Tag.py +0 -170
- pylibms-2.1.4/LMS/Message/Tag/Stream.py +0 -157
- pylibms-2.1.4/LMS/Message/Tag/System_Definitions.py +0 -96
- pylibms-2.1.4/LMS/Message/Tag/Tag_Formats.py +0 -10
- pylibms-2.1.4/LMS/Project/Definitions/Color.py +0 -10
- pylibms-2.1.4/LMS/Project/Definitions/Style.py +0 -14
- pylibms-2.1.4/LMS/Project/Definitions/Tag.py +0 -58
- pylibms-2.1.4/LMS/Project/MSBPRead.py +0 -102
- pylibms-2.1.4/LMS/Project/Section/ATI2.py +0 -17
- pylibms-2.1.4/LMS/Project/Section/String.py +0 -18
- pylibms-2.1.4/LMS/Project/Section/TAG2.py +0 -19
- pylibms-2.1.4/LMS/Project/Section/TGG2.py +0 -20
- pylibms-2.1.4/LMS/Project/Section/TGP2.py +0 -26
- pylibms-2.1.4/LMS/TitleConfig/Presets/Badge Arcade.yaml +0 -382
- pylibms-2.1.4/LMS/TitleConfig/Presets/Brain Age Concentration Training.yaml +0 -84
- pylibms-2.1.4/LMS/TitleConfig/Presets/Kirby Planet Robobot.yaml +0 -162
- pylibms-2.1.4/LMS/TitleConfig/Presets/Super Mario 3D Land.yaml +0 -320
- pylibms-2.1.4/LMS/TitleConfig/Presets/The Legend of Zelda a Link Between Worlds.yaml +0 -335
- pylibms-2.1.4/LMS/TitleConfig/Presets/Tomodachi Life.yaml +0 -7363
- pylibms-2.1.4/MANIFEST.in +0 -2
- pylibms-2.1.4/PyLibMS.egg-info/SOURCES.txt +0 -67
- pylibms-2.1.4/PyLibMS.egg-info/top_level.txt +0 -1
- {pylibms-2.1.4 → pylibms-3.0.2}/LICENSE +0 -0
- {pylibms-2.1.4 → pylibms-3.0.2}/PyLibMS.egg-info/dependency_links.txt +0 -0
- {pylibms-2.1.4 → pylibms-3.0.2}/PyLibMS.egg-info/requires.txt +0 -0
- {pylibms-2.1.4/LMS/Common → pylibms-3.0.2/lms}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Message/Definitions/Field → pylibms-3.0.2/lms/common}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Message/Definitions → pylibms-3.0.2/lms/message}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Message/Section → pylibms-3.0.2/lms/message/definitions}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Message/Tag → pylibms-3.0.2/lms/message/definitions/field}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Message → pylibms-3.0.2/lms/message/section}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/Project/Definitions → pylibms-3.0.2/lms/message/tag}/__init__.py +0 -0
- /pylibms-2.1.4/LMS/Message/Tag/LMS_TagExceptions.py → /pylibms-3.0.2/lms/message/tag/lms_tagexceptions.py +0 -0
- {pylibms-2.1.4/LMS/Project → pylibms-3.0.2/lms/project}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/TitleConfig/Definitions → pylibms-3.0.2/lms/project/definitions}/__init__.py +0 -0
- {pylibms-2.1.4/LMS/TitleConfig → pylibms-3.0.2/lms/titleconfig}/__init__.py +0 -0
- {pylibms-2.1.4/LMS → pylibms-3.0.2/lms/titleconfig/definitions}/__init__.py +0 -0
- {pylibms-2.1.4 → pylibms-3.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyLibMS
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.2
|
|
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
|
|
@@ -39,17 +39,15 @@ Simple preview of the library is below. See [the wiki](https://github.com/AbdyyE
|
|
|
39
39
|
## Reading
|
|
40
40
|
MSBT
|
|
41
41
|
```py
|
|
42
|
-
from
|
|
42
|
+
from lms.message.msbtio import read_msbt_path
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
msbt = read_msbt(f)
|
|
44
|
+
msbt = read_msbt_path("Game.msbt")
|
|
46
45
|
```
|
|
47
46
|
## Writing
|
|
48
47
|
```py
|
|
49
|
-
from
|
|
48
|
+
from lms.message.msbtio import write_msbt_path
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
write_msbt(f, msbt)
|
|
50
|
+
write_msbt_path("Out_Game.msbt")
|
|
53
51
|
```
|
|
54
52
|
# Adding/Editing Presets
|
|
55
53
|
To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyLibMS
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.2
|
|
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
|
|
@@ -39,17 +39,15 @@ Simple preview of the library is below. See [the wiki](https://github.com/AbdyyE
|
|
|
39
39
|
## Reading
|
|
40
40
|
MSBT
|
|
41
41
|
```py
|
|
42
|
-
from
|
|
42
|
+
from lms.message.msbtio import read_msbt_path
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
msbt = read_msbt(f)
|
|
44
|
+
msbt = read_msbt_path("Game.msbt")
|
|
46
45
|
```
|
|
47
46
|
## Writing
|
|
48
47
|
```py
|
|
49
|
-
from
|
|
48
|
+
from lms.message.msbtio import write_msbt_path
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
write_msbt(f, msbt)
|
|
50
|
+
write_msbt_path("Out_Game.msbt")
|
|
53
51
|
```
|
|
54
52
|
# Adding/Editing Presets
|
|
55
53
|
To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
PyLibMS.egg-info/PKG-INFO
|
|
5
|
+
PyLibMS.egg-info/SOURCES.txt
|
|
6
|
+
PyLibMS.egg-info/dependency_links.txt
|
|
7
|
+
PyLibMS.egg-info/requires.txt
|
|
8
|
+
PyLibMS.egg-info/top_level.txt
|
|
9
|
+
lms/__init__.py
|
|
10
|
+
lms/common/__init__.py
|
|
11
|
+
lms/common/lms_datatype.py
|
|
12
|
+
lms/common/lms_exceptions.py
|
|
13
|
+
lms/common/lms_fileinfo.py
|
|
14
|
+
lms/common/stream/fileinfo.py
|
|
15
|
+
lms/common/stream/hashtable.py
|
|
16
|
+
lms/common/stream/section.py
|
|
17
|
+
lms/fileio/encoding.py
|
|
18
|
+
lms/fileio/io.py
|
|
19
|
+
lms/message/__init__.py
|
|
20
|
+
lms/message/msbt.py
|
|
21
|
+
lms/message/msbtentry.py
|
|
22
|
+
lms/message/msbtio.py
|
|
23
|
+
lms/message/definitions/__init__.py
|
|
24
|
+
lms/message/definitions/lms_messagetext.py
|
|
25
|
+
lms/message/definitions/field/__init__.py
|
|
26
|
+
lms/message/definitions/field/io.py
|
|
27
|
+
lms/message/definitions/field/lms_field.py
|
|
28
|
+
lms/message/section/__init__.py
|
|
29
|
+
lms/message/section/atr1.py
|
|
30
|
+
lms/message/section/tsy1.py
|
|
31
|
+
lms/message/section/txt2.py
|
|
32
|
+
lms/message/tag/__init__.py
|
|
33
|
+
lms/message/tag/lms_tag.py
|
|
34
|
+
lms/message/tag/lms_tagexceptions.py
|
|
35
|
+
lms/message/tag/io/param_io.py
|
|
36
|
+
lms/message/tag/io/tag_io.py
|
|
37
|
+
lms/project/__init__.py
|
|
38
|
+
lms/project/msbp.py
|
|
39
|
+
lms/project/msbpread.py
|
|
40
|
+
lms/project/definitions/__init__.py
|
|
41
|
+
lms/project/definitions/attribute.py
|
|
42
|
+
lms/project/definitions/color.py
|
|
43
|
+
lms/project/definitions/style.py
|
|
44
|
+
lms/project/definitions/tag.py
|
|
45
|
+
lms/project/section/ali2.py
|
|
46
|
+
lms/project/section/ati2.py
|
|
47
|
+
lms/project/section/clr1.py
|
|
48
|
+
lms/project/section/string.py
|
|
49
|
+
lms/project/section/syl3.py
|
|
50
|
+
lms/project/section/tag2.py
|
|
51
|
+
lms/project/section/tgg2.py
|
|
52
|
+
lms/project/section/tgp2.py
|
|
53
|
+
lms/titleconfig/__init__.py
|
|
54
|
+
lms/titleconfig/config.py
|
|
55
|
+
lms/titleconfig/definitions/__init__.py
|
|
56
|
+
lms/titleconfig/definitions/attribute.py
|
|
57
|
+
lms/titleconfig/definitions/tags.py
|
|
58
|
+
lms/titleconfig/definitions/value.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lms
|
|
@@ -18,17 +18,15 @@ Simple preview of the library is below. See [the wiki](https://github.com/AbdyyE
|
|
|
18
18
|
## Reading
|
|
19
19
|
MSBT
|
|
20
20
|
```py
|
|
21
|
-
from
|
|
21
|
+
from lms.message.msbtio import read_msbt_path
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
msbt = read_msbt(f)
|
|
23
|
+
msbt = read_msbt_path("Game.msbt")
|
|
25
24
|
```
|
|
26
25
|
## Writing
|
|
27
26
|
```py
|
|
28
|
-
from
|
|
27
|
+
from lms.message.msbtio import write_msbt_path
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
write_msbt(f, msbt)
|
|
29
|
+
write_msbt_path("Out_Game.msbt")
|
|
32
30
|
```
|
|
33
31
|
# Adding/Editing Presets
|
|
34
32
|
To add or edit Preset, you may create an issue with the relevant yaml file and the game it is for.
|
|
@@ -4,6 +4,7 @@ from typing import Literal, Type
|
|
|
4
4
|
|
|
5
5
|
class LMS_DataType(Enum):
|
|
6
6
|
"""Enum that represents a datatype for a value entry in a MSBT/MSBP file."""
|
|
7
|
+
|
|
7
8
|
UINT8 = 0
|
|
8
9
|
UINT16 = 1
|
|
9
10
|
UINT32 = 2
|
|
@@ -22,12 +23,12 @@ class LMS_DataType(Enum):
|
|
|
22
23
|
|
|
23
24
|
STRING = 8
|
|
24
25
|
LIST = 9
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
# These types are not offical, but allow for abstraction from the value of the actual type
|
|
27
28
|
# As an example, BOOL can be utilized for UInt8 values that act like a bool
|
|
28
29
|
# Byte types can also be used for when the type/value is unknown or if there is extra data in the tag.
|
|
29
30
|
BOOL = "bool"
|
|
30
|
-
|
|
31
|
+
BYTES = "byte"
|
|
31
32
|
|
|
32
33
|
def to_string(self) -> str:
|
|
33
34
|
return self._name_.lower()
|
|
@@ -39,7 +40,7 @@ class LMS_DataType(Enum):
|
|
|
39
40
|
LMS_DataType.STRING,
|
|
40
41
|
LMS_DataType.LIST,
|
|
41
42
|
LMS_DataType.BOOL,
|
|
42
|
-
LMS_DataType.
|
|
43
|
+
LMS_DataType.BYTES,
|
|
43
44
|
]:
|
|
44
45
|
return self in [LMS_DataType.INT8, LMS_DataType.INT16, LMS_DataType.INT32]
|
|
45
46
|
|
|
@@ -59,11 +60,11 @@ class LMS_DataType(Enum):
|
|
|
59
60
|
LMS_DataType.STRING: str,
|
|
60
61
|
LMS_DataType.LIST: str,
|
|
61
62
|
LMS_DataType.BOOL: bool,
|
|
62
|
-
LMS_DataType.
|
|
63
|
+
LMS_DataType.BYTES: bytes,
|
|
63
64
|
}[self]
|
|
64
65
|
|
|
65
66
|
@property
|
|
66
|
-
def stream_size(self)
|
|
67
|
+
def stream_size(self):
|
|
67
68
|
"""The size the datatype takes up in a stream."""
|
|
68
69
|
return {
|
|
69
70
|
LMS_DataType.UINT8: 1,
|
|
@@ -75,7 +76,7 @@ class LMS_DataType(Enum):
|
|
|
75
76
|
LMS_DataType.FLOAT32: 4,
|
|
76
77
|
LMS_DataType.LIST: 1,
|
|
77
78
|
LMS_DataType.BOOL: 1,
|
|
78
|
-
LMS_DataType.
|
|
79
|
+
LMS_DataType.BYTES: 1,
|
|
79
80
|
}[self]
|
|
80
81
|
|
|
81
82
|
@classmethod
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from lms.fileio.encoding import FileEncoding
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
@dataclass(frozen=True)
|
|
7
7
|
class LMS_FileInfo:
|
|
8
|
-
|
|
8
|
+
is_big_endian: bool
|
|
9
9
|
encoding: FileEncoding
|
|
10
10
|
version: int
|
|
11
11
|
section_count: int
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from
|
|
1
|
+
from lms.common import lms_exceptions
|
|
2
|
+
from lms.common.lms_fileinfo import LMS_FileInfo
|
|
3
|
+
from lms.fileio.encoding import FileEncoding
|
|
4
|
+
from lms.fileio.io import FileReader, FileWriter
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
|
|
8
8
|
magic = reader.read_string_len(8)
|
|
9
9
|
|
|
10
10
|
if magic != expected_magic:
|
|
11
|
-
raise
|
|
11
|
+
raise lms_exceptions.LMS_UnexpectedMagicError(
|
|
12
12
|
f"Invalid magic!' Expected {expected_magic}', got '{magic}'."
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
reader.
|
|
15
|
+
is_big_endian = reader.read_bytes(2) == b"\xfe\xff"
|
|
16
|
+
reader.is_big_endian = is_big_endian
|
|
17
17
|
|
|
18
18
|
reader.skip(2)
|
|
19
19
|
|
|
@@ -28,13 +28,13 @@ def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
|
|
|
28
28
|
|
|
29
29
|
reader.seek(0, 2)
|
|
30
30
|
if file_size != reader.tell():
|
|
31
|
-
raise
|
|
31
|
+
raise lms_exceptions.LMS_MisalignedSizeError(f"Filesize is misaligned!")
|
|
32
32
|
|
|
33
33
|
# Seek to the start of data
|
|
34
34
|
reader.seek(0x20)
|
|
35
35
|
|
|
36
36
|
return LMS_FileInfo(
|
|
37
|
-
|
|
37
|
+
is_big_endian,
|
|
38
38
|
encoding,
|
|
39
39
|
version,
|
|
40
40
|
section_count,
|
|
@@ -46,18 +46,17 @@ def write_file_info(writer: FileWriter, magic: str, file_info: LMS_FileInfo) ->
|
|
|
46
46
|
|
|
47
47
|
:param writer: a Writer object.
|
|
48
48
|
:param file_info: the file_info object."""
|
|
49
|
-
writer.
|
|
49
|
+
writer.is_big_endian = file_info.is_big_endian
|
|
50
50
|
writer.encoding = file_info.encoding
|
|
51
51
|
|
|
52
52
|
writer.write_string(magic)
|
|
53
|
-
writer.write_bytes(b"\xff\xfe" if not file_info.
|
|
53
|
+
writer.write_bytes(b"\xff\xfe" if not file_info.is_big_endian else b"\xfe\xff")
|
|
54
54
|
writer.write_bytes(b"\x00\x00")
|
|
55
55
|
|
|
56
56
|
writer.write_uint8(file_info.encoding.value)
|
|
57
57
|
writer.write_uint8(file_info.version)
|
|
58
58
|
writer.write_uint16(file_info.section_count)
|
|
59
59
|
|
|
60
|
-
# Padding
|
|
61
60
|
writer.write_bytes(b"\x00\x00")
|
|
62
61
|
writer.write_bytes(b"\x00" * 4)
|
|
63
62
|
writer.write_bytes(b"\x00" * 10)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from lms.fileio.io import FileReader, FileWriter
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def read_labels(reader: FileReader) -> tuple[dict[int, str], int]:
|
|
@@ -7,12 +7,10 @@ def read_labels(reader: FileReader) -> tuple[dict[int, str], int]:
|
|
|
7
7
|
data_start = reader.tell()
|
|
8
8
|
slot_count = reader.read_uint32()
|
|
9
9
|
for _ in range(slot_count):
|
|
10
|
-
# Read initial label data
|
|
11
10
|
label_count = reader.read_uint32()
|
|
12
11
|
offset = reader.read_uint32()
|
|
13
12
|
next_offset = reader.tell()
|
|
14
13
|
|
|
15
|
-
# Read the actual label data
|
|
16
14
|
reader.seek(data_start + offset)
|
|
17
15
|
for _ in range(label_count):
|
|
18
16
|
length = reader.read_uint8()
|
|
@@ -26,16 +24,10 @@ def read_labels(reader: FileReader) -> tuple[dict[int, str], int]:
|
|
|
26
24
|
|
|
27
25
|
# While the slot count is consistent for most files, some vary them.
|
|
28
26
|
# Return the slot count to ensure that this change can be recorded.
|
|
29
|
-
return
|
|
27
|
+
return sorted_labels, slot_count
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
def write_labels(writer: FileWriter, labels: list[str], slot_count: int) -> None:
|
|
33
|
-
"""Writes the hashtable block to a stream.
|
|
34
|
-
|
|
35
|
-
:param writer: a Writer object.
|
|
36
|
-
:param labels: the dictionary of labels.
|
|
37
|
-
:param slot_count: the amount of hash slots.
|
|
38
|
-
"""
|
|
39
31
|
writer.write_uint32(slot_count)
|
|
40
32
|
|
|
41
33
|
hash_slots = {}
|
|
@@ -44,19 +36,18 @@ def write_labels(writer: FileWriter, labels: list[str], slot_count: int) -> None
|
|
|
44
36
|
for i, label in enumerate(labels):
|
|
45
37
|
hash = _calculate_hash(label, slot_count)
|
|
46
38
|
|
|
47
|
-
# Add to the list if hash exists, and create a new list for each new hash
|
|
48
39
|
if hash not in hash_slots:
|
|
49
40
|
hash_slots[hash] = [label]
|
|
50
41
|
else:
|
|
51
42
|
hash_slots[hash].append(label)
|
|
52
43
|
|
|
53
44
|
index_map[label] = i
|
|
45
|
+
|
|
54
46
|
label_offsets = slot_count * 8 + 4
|
|
55
47
|
|
|
56
|
-
# Sort by the hash slots
|
|
57
48
|
hash_slots = dict(sorted(hash_slots.items(), key=lambda x: x[0]))
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
|
|
60
51
|
for i in range(slot_count):
|
|
61
52
|
if i in hash_slots:
|
|
62
53
|
writer.write_uint32(len(hash_slots[i]))
|
|
@@ -76,11 +67,8 @@ def write_labels(writer: FileWriter, labels: list[str], slot_count: int) -> None
|
|
|
76
67
|
writer.write_uint32(index_map[label])
|
|
77
68
|
|
|
78
69
|
|
|
70
|
+
# See https://nintendo-formats.com/libs/lms/overview.html#hash-tables
|
|
79
71
|
def _calculate_hash(label: str, slot_count: int) -> int:
|
|
80
|
-
"""Calculates the hash of a label.
|
|
81
|
-
|
|
82
|
-
See https://nintendo-formats.com/libs/lms/overview.html#hash-tables
|
|
83
|
-
"""
|
|
84
72
|
hash = 0
|
|
85
73
|
for character in label:
|
|
86
74
|
hash = hash * 0x492 + ord(character)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any, Callable, Generator
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from lms.fileio.io import FileReader, FileWriter
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def read_section_data(
|
|
@@ -11,7 +11,6 @@ def read_section_data(
|
|
|
11
11
|
magic = reader.read_string_len(4)
|
|
12
12
|
size = reader.read_uint32()
|
|
13
13
|
|
|
14
|
-
# Skip to start of data
|
|
15
14
|
reader.skip(8)
|
|
16
15
|
end = reader.tell() + size
|
|
17
16
|
|
|
@@ -27,7 +26,7 @@ def write_section(
|
|
|
27
26
|
magic: str,
|
|
28
27
|
section_call: Callable,
|
|
29
28
|
data: list[Any],
|
|
30
|
-
*
|
|
29
|
+
*write_arguments,
|
|
31
30
|
) -> None:
|
|
32
31
|
writer.write_string(magic)
|
|
33
32
|
size_offset = writer.tell()
|
|
@@ -36,7 +35,7 @@ def write_section(
|
|
|
36
35
|
writer.write_bytes(b"\x00" * 8)
|
|
37
36
|
data_start = writer.tell()
|
|
38
37
|
|
|
39
|
-
section_call(writer, data, *
|
|
38
|
+
section_call(writer, data, *write_arguments)
|
|
40
39
|
|
|
41
40
|
_write_end_data(writer, data_start, size_offset)
|
|
42
41
|
|
|
@@ -57,4 +56,4 @@ def _write_end_data(writer: FileWriter, data_start: int, size_offset: int) -> No
|
|
|
57
56
|
writer.seek(size_offset)
|
|
58
57
|
writer.write_uint32(size)
|
|
59
58
|
writer.seek(end)
|
|
60
|
-
writer.write_alignment(b"\
|
|
59
|
+
writer.write_alignment(b"\xab", 16)
|
|
@@ -11,8 +11,7 @@ class FileEncoding(Enum):
|
|
|
11
11
|
|
|
12
12
|
def to_string_format(
|
|
13
13
|
self, big_endian: bool = False
|
|
14
|
-
)
|
|
15
|
-
"""Returns the encoding in the string format"""
|
|
14
|
+
):
|
|
16
15
|
match self:
|
|
17
16
|
case FileEncoding.UTF8:
|
|
18
17
|
return "UTF-8"
|
|
@@ -26,8 +25,7 @@ class FileEncoding(Enum):
|
|
|
26
25
|
return "UTF-32-LE"
|
|
27
26
|
|
|
28
27
|
@property
|
|
29
|
-
def width(self)
|
|
30
|
-
"""The width of a character in a stream."""
|
|
28
|
+
def width(self):
|
|
31
29
|
match self:
|
|
32
30
|
case FileEncoding.UTF8:
|
|
33
31
|
return 1
|
|
@@ -37,6 +35,5 @@ class FileEncoding(Enum):
|
|
|
37
35
|
return 4
|
|
38
36
|
|
|
39
37
|
@property
|
|
40
|
-
def terminator(self)
|
|
41
|
-
"""The terminator of the string in a stream."""
|
|
38
|
+
def terminator(self):
|
|
42
39
|
return b"\x00" * self.width
|
|
@@ -4,7 +4,7 @@ import struct
|
|
|
4
4
|
from io import BytesIO
|
|
5
5
|
from typing import BinaryIO, Generator
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from lms.fileio.encoding import FileEncoding
|
|
8
8
|
|
|
9
9
|
STRUCT_TYPES = {
|
|
10
10
|
"little": {
|
|
@@ -28,34 +28,34 @@ STRUCT_TYPES = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class FileReader:
|
|
32
|
-
def __init__(self,
|
|
33
|
-
if isinstance(
|
|
34
|
-
self.
|
|
31
|
+
class FileReader:
|
|
32
|
+
def __init__(self, data: BinaryIO | bytes, big_endian: bool = False):
|
|
33
|
+
if isinstance(data, BinaryIO):
|
|
34
|
+
self._stream = BytesIO(data.read())
|
|
35
35
|
else:
|
|
36
|
-
self.
|
|
36
|
+
self._stream = BytesIO(data)
|
|
37
37
|
|
|
38
|
-
self.encoding
|
|
39
|
-
self.
|
|
38
|
+
self.encoding = FileEncoding.UTF8
|
|
39
|
+
self.is_big_endian = big_endian
|
|
40
40
|
|
|
41
41
|
def tell(self) -> int:
|
|
42
|
-
return self.
|
|
42
|
+
return self._stream.tell()
|
|
43
43
|
|
|
44
44
|
def skip(self, length: int) -> None:
|
|
45
|
-
self.
|
|
45
|
+
self._stream.read(length)
|
|
46
46
|
|
|
47
|
-
def align(self, alignment: int) ->
|
|
47
|
+
def align(self, alignment: int) -> None:
|
|
48
48
|
alignment = (-self.tell() % alignment + alignment) % alignment
|
|
49
49
|
self.skip(alignment)
|
|
50
50
|
|
|
51
51
|
def read_bytes(self, length: int) -> bytes:
|
|
52
|
-
return self.
|
|
52
|
+
return self._stream.read(length)
|
|
53
53
|
|
|
54
54
|
def seek(self, offset: int, whence: int = 0) -> None:
|
|
55
55
|
if offset < 0:
|
|
56
|
-
self.
|
|
56
|
+
self._stream.seek(self.tell() + offset)
|
|
57
57
|
else:
|
|
58
|
-
self.
|
|
58
|
+
self._stream.seek(offset, whence)
|
|
59
59
|
|
|
60
60
|
def read_offset_array(self, count: int) -> Generator[int, None, None]:
|
|
61
61
|
start = self.tell() - 4
|
|
@@ -68,28 +68,28 @@ class FileReader:
|
|
|
68
68
|
return [self.read_uint16() for _ in range(count)]
|
|
69
69
|
|
|
70
70
|
def read_int8(self) -> int:
|
|
71
|
-
return struct.unpack(self._get_datatype("int8"), self.
|
|
71
|
+
return struct.unpack(self._get_datatype("int8"), self._stream.read(1))[0]
|
|
72
72
|
|
|
73
73
|
def read_int16(self) -> int:
|
|
74
|
-
return struct.unpack(self._get_datatype("int16"), self.
|
|
74
|
+
return struct.unpack(self._get_datatype("int16"), self._stream.read(2))[0]
|
|
75
75
|
|
|
76
76
|
def read_int32(self) -> int:
|
|
77
|
-
return struct.unpack(self._get_datatype("int32"), self.
|
|
77
|
+
return struct.unpack(self._get_datatype("int32"), self._stream.read(4))[0]
|
|
78
78
|
|
|
79
79
|
def read_uint8(self) -> int:
|
|
80
|
-
return struct.unpack(self._get_datatype("uint8"), self.
|
|
80
|
+
return struct.unpack(self._get_datatype("uint8"), self._stream.read(1))[0]
|
|
81
81
|
|
|
82
82
|
def read_uint16(self) -> int:
|
|
83
|
-
return struct.unpack(self._get_datatype("uint16"), self.
|
|
83
|
+
return struct.unpack(self._get_datatype("uint16"), self._stream.read(2))[0]
|
|
84
84
|
|
|
85
85
|
def read_uint32(self) -> int:
|
|
86
|
-
return struct.unpack(self._get_datatype("uint32"), self.
|
|
86
|
+
return struct.unpack(self._get_datatype("uint32"), self._stream.read(4))[0]
|
|
87
87
|
|
|
88
88
|
def read_float32(self) -> float:
|
|
89
|
-
return struct.unpack(self._get_datatype("float"), self.
|
|
89
|
+
return struct.unpack(self._get_datatype("float"), self._stream.read(4))[0]
|
|
90
90
|
|
|
91
91
|
def read_string_len(self, length: int) -> str:
|
|
92
|
-
return self.
|
|
92
|
+
return self._stream.read(length).decode("UTF-8")
|
|
93
93
|
|
|
94
94
|
def read_str_variable_encoding(self):
|
|
95
95
|
message = b""
|
|
@@ -97,24 +97,26 @@ class FileReader:
|
|
|
97
97
|
raw_char := self.read_bytes(self.encoding.width)
|
|
98
98
|
) != self.encoding.terminator:
|
|
99
99
|
message += raw_char
|
|
100
|
-
return message.decode(self.encoding.to_string_format(self.
|
|
100
|
+
return message.decode(self.encoding.to_string_format(self.is_big_endian))
|
|
101
101
|
|
|
102
102
|
def read_len_string_variable_encoding(self):
|
|
103
|
-
self.align(
|
|
103
|
+
self.align(
|
|
104
|
+
len("\x00".encode(self.encoding.to_string_format(self.is_big_endian)))
|
|
105
|
+
)
|
|
104
106
|
length = self.read_uint16()
|
|
105
107
|
return self.read_bytes(length).decode(
|
|
106
|
-
self.encoding.to_string_format(self.
|
|
108
|
+
self.encoding.to_string_format(self.is_big_endian)
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
def _get_datatype(self, name: str) -> str:
|
|
110
|
-
return STRUCT_TYPES["little" if not self.
|
|
112
|
+
return STRUCT_TYPES["little" if not self.is_big_endian else "big"][name]
|
|
111
113
|
|
|
112
114
|
|
|
113
115
|
class FileWriter:
|
|
114
116
|
def __init__(self, encoding: FileEncoding):
|
|
115
117
|
self.data = BytesIO(b"")
|
|
116
118
|
self.encoding = encoding
|
|
117
|
-
self.
|
|
119
|
+
self.is_big_endian = False
|
|
118
120
|
|
|
119
121
|
def skip(self, length: int) -> None:
|
|
120
122
|
self.data.seek(length, 1)
|
|
@@ -126,8 +128,8 @@ class FileWriter:
|
|
|
126
128
|
self.seek(last_position)
|
|
127
129
|
return size
|
|
128
130
|
|
|
129
|
-
def write_bytes(self, data: bytes) ->
|
|
130
|
-
|
|
131
|
+
def write_bytes(self, data: bytes) -> None:
|
|
132
|
+
self.data.write(data)
|
|
131
133
|
|
|
132
134
|
def seek(self, offset: int, whence: int = 0) -> None:
|
|
133
135
|
self.data.seek(offset, whence)
|
|
@@ -142,38 +144,38 @@ class FileWriter:
|
|
|
142
144
|
for number in array:
|
|
143
145
|
self.write_uint16(number)
|
|
144
146
|
|
|
145
|
-
def write_int8(self, value: int) ->
|
|
146
|
-
|
|
147
|
+
def write_int8(self, value: int) -> None:
|
|
148
|
+
self.data.write(struct.pack(self._get_datatype("int8"), value))
|
|
147
149
|
|
|
148
|
-
def write_int16(self, value: int) ->
|
|
149
|
-
|
|
150
|
+
def write_int16(self, value: int) -> None:
|
|
151
|
+
self.data.write(struct.pack(self._get_datatype("int16"), value))
|
|
150
152
|
|
|
151
153
|
def write_int32(self, value: int) -> None:
|
|
152
|
-
|
|
154
|
+
self.data.write(struct.pack(self._get_datatype("int32"), value))
|
|
153
155
|
|
|
154
156
|
def write_uint8(self, value: int) -> None:
|
|
155
|
-
|
|
157
|
+
self.data.write(struct.pack(self._get_datatype("uint8"), value))
|
|
156
158
|
|
|
157
159
|
def write_uint16(self, value: int) -> None:
|
|
158
|
-
|
|
160
|
+
self.data.write(struct.pack(self._get_datatype("uint16"), value))
|
|
159
161
|
|
|
160
162
|
def write_uint32(self, value: int) -> None:
|
|
161
|
-
|
|
163
|
+
self.data.write(struct.pack(self._get_datatype("uint32"), value))
|
|
162
164
|
|
|
163
165
|
def write_float32(self, value: float) -> None:
|
|
164
|
-
|
|
166
|
+
self.data.write(struct.pack(self._get_datatype("float"), value))
|
|
165
167
|
|
|
166
168
|
def write_string(self, string: str):
|
|
167
169
|
self.write_bytes(string.encode("UTF-8"))
|
|
168
170
|
|
|
169
171
|
def write_len_variable_encoding_string(self, string: str) -> None:
|
|
170
172
|
self.write_uint16(len(string) * self.encoding.width)
|
|
171
|
-
self.write_variable_encoding_string
|
|
172
|
-
string, False
|
|
173
|
-
)
|
|
173
|
+
self.write_variable_encoding_string(string, False)
|
|
174
174
|
|
|
175
175
|
def write_variable_encoding_string(self, string: str, terminate: bool = True):
|
|
176
|
-
self.write_bytes(
|
|
176
|
+
self.write_bytes(
|
|
177
|
+
string.encode(self.encoding.to_string_format(self.is_big_endian))
|
|
178
|
+
)
|
|
177
179
|
if terminate:
|
|
178
180
|
self.write_bytes(b"\x00" * self.encoding.width)
|
|
179
181
|
|
|
@@ -181,7 +183,7 @@ class FileWriter:
|
|
|
181
183
|
return (-number % alignment + alignment) % alignment
|
|
182
184
|
|
|
183
185
|
def _get_datatype(self, name: str) -> str:
|
|
184
|
-
return STRUCT_TYPES["little" if not self.
|
|
186
|
+
return STRUCT_TYPES["little" if not self.is_big_endian else "big"][name]
|
|
185
187
|
|
|
186
188
|
def get_data(self) -> bytes:
|
|
187
189
|
return self.data.getvalue()
|