PyLibMS 2.0.0__py3-none-any.whl
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.
- LMS/Common/LMS_Exceptions.py +16 -0
- LMS/Common/LMS_FileInfo.py +11 -0
- LMS/Common/Stream/FileInfo.py +64 -0
- LMS/Common/Stream/Hashtable.py +87 -0
- LMS/Common/Stream/Section.py +60 -0
- LMS/Common/__init__.py +0 -0
- LMS/Config/Definitions/Attributes.py +14 -0
- LMS/Config/Definitions/Tags.py +52 -0
- LMS/Config/Definitions/Value.py +11 -0
- LMS/Config/Definitions/__init__.py +0 -0
- LMS/Config/TitleConfig.py +208 -0
- LMS/Config/__init__.py +0 -0
- LMS/Field/LMS_DataType.py +83 -0
- LMS/Field/LMS_Field.py +123 -0
- LMS/Field/Stream.py +56 -0
- LMS/Field/__init__.py +0 -0
- LMS/FileIO/Encoding.py +42 -0
- LMS/FileIO/Stream.py +181 -0
- LMS/Message/Definitions/LMS_FieldMap.py +5 -0
- LMS/Message/Definitions/LMS_MessageText.py +124 -0
- LMS/Message/Definitions/__init__.py +0 -0
- LMS/Message/MSBT.py +63 -0
- LMS/Message/MSBTEntry.py +33 -0
- LMS/Message/MSBTStream.py +138 -0
- LMS/Message/Section/ATR1.py +101 -0
- LMS/Message/Section/TSY1.py +13 -0
- LMS/Message/Section/TXT2.py +79 -0
- LMS/Message/Section/__init__.py +0 -0
- LMS/Message/Tag/LMS_Tag.py +169 -0
- LMS/Message/Tag/LMS_TagExceptions.py +7 -0
- LMS/Message/Tag/Stream.py +149 -0
- LMS/Message/Tag/System.yaml +47 -0
- LMS/Message/Tag/Tag_Formats.py +10 -0
- LMS/Message/Tag/__init__.py +0 -0
- LMS/Message/__init__.py +0 -0
- LMS/Project/Definitions/Attribute.py +27 -0
- LMS/Project/Definitions/Color.py +10 -0
- LMS/Project/Definitions/Style.py +14 -0
- LMS/Project/Definitions/Tag.py +58 -0
- LMS/Project/Definitions/__init__.py +0 -0
- LMS/Project/MSBP.py +64 -0
- LMS/Project/MSBPRead.py +101 -0
- LMS/Project/Section/ALI2.py +19 -0
- LMS/Project/Section/ATI2.py +17 -0
- LMS/Project/Section/CLR1.py +15 -0
- LMS/Project/Section/SYL3.py +17 -0
- LMS/Project/Section/String.py +18 -0
- LMS/Project/Section/TAG2.py +19 -0
- LMS/Project/Section/TGG2.py +20 -0
- LMS/Project/Section/TGP2.py +26 -0
- LMS/Project/__init__.py +0 -0
- LMS/__init__.py +0 -0
- pylibms-2.0.0.dist-info/METADATA +40 -0
- pylibms-2.0.0.dist-info/RECORD +57 -0
- pylibms-2.0.0.dist-info/WHEEL +5 -0
- pylibms-2.0.0.dist-info/licenses/LICENSE +7 -0
- pylibms-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import BinaryIO
|
|
2
|
+
|
|
3
|
+
from LMS.Common import LMS_Exceptions
|
|
4
|
+
from LMS.Common.Stream.FileInfo import read_file_info, write_file_info
|
|
5
|
+
from LMS.Common.Stream.Hashtable import read_labels, write_labels
|
|
6
|
+
from LMS.Common.Stream.Section import (read_section_data, write_section,
|
|
7
|
+
write_unsupported_section)
|
|
8
|
+
from LMS.Config.TitleConfig import AttributeConfig, TagConfig
|
|
9
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
10
|
+
from LMS.Message.MSBT import MSBT
|
|
11
|
+
from LMS.Message.MSBTEntry import MSBTEntry
|
|
12
|
+
from LMS.Message.Section.ATR1 import (read_decoded_atr1, read_encoded_atr1,
|
|
13
|
+
write_decoded_atr1, write_encoded_atr1)
|
|
14
|
+
from LMS.Message.Section.TSY1 import read_tsy1, write_tsy1
|
|
15
|
+
from LMS.Message.Section.TXT2 import read_txt2, write_txt2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_msbt(
|
|
19
|
+
stream: BinaryIO,
|
|
20
|
+
attribute_config: AttributeConfig = None,
|
|
21
|
+
tag_config: TagConfig = None,
|
|
22
|
+
) -> MSBT:
|
|
23
|
+
"""Reads a MSBT file from a stream.
|
|
24
|
+
|
|
25
|
+
:param stream: a stream object.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
```
|
|
29
|
+
with open(file_path, "rb") as file:
|
|
30
|
+
msbt = read_msbt(file, "Game.yaml")
|
|
31
|
+
...
|
|
32
|
+
```"""
|
|
33
|
+
if stream is None:
|
|
34
|
+
raise ValueError("Stream must be valid!")
|
|
35
|
+
|
|
36
|
+
reader = FileReader(stream)
|
|
37
|
+
file_info = read_file_info(reader, "MsgStdBn")
|
|
38
|
+
|
|
39
|
+
file = MSBT(file_info)
|
|
40
|
+
|
|
41
|
+
if attribute_config is not None:
|
|
42
|
+
file.encoded_attributes = False
|
|
43
|
+
|
|
44
|
+
messages = attributes = style_indexes = None
|
|
45
|
+
for magic, size in read_section_data(reader, file_info.section_count):
|
|
46
|
+
match magic:
|
|
47
|
+
case "LBL1":
|
|
48
|
+
labels, slot_count = read_labels(reader)
|
|
49
|
+
file.slot_count = slot_count
|
|
50
|
+
case "ATR1":
|
|
51
|
+
# Map to shared variable to make assignment cleaner
|
|
52
|
+
if attribute_config is None:
|
|
53
|
+
data = read_encoded_atr1(reader, size)
|
|
54
|
+
else:
|
|
55
|
+
file.encoded_attributes = False
|
|
56
|
+
data = read_decoded_atr1(reader, attribute_config)
|
|
57
|
+
|
|
58
|
+
attributes, size_per_attribute, string_table = data
|
|
59
|
+
file.size_per_attribute = size_per_attribute
|
|
60
|
+
file.attr_string_table = string_table
|
|
61
|
+
case "TXT2":
|
|
62
|
+
messages = read_txt2(reader, file_info.encoding, tag_config)
|
|
63
|
+
case "TSY1":
|
|
64
|
+
style_indexes = read_tsy1(reader, len(labels))
|
|
65
|
+
case _:
|
|
66
|
+
file.unsupported_sections[magic] = reader.read_bytes(size)
|
|
67
|
+
|
|
68
|
+
file.section_list.append(magic)
|
|
69
|
+
|
|
70
|
+
for i, label in labels.items():
|
|
71
|
+
text = None if messages is None else messages[i]
|
|
72
|
+
attribute = None if attributes is None else attributes[i]
|
|
73
|
+
style_index = None if style_indexes is None else style_indexes[i]
|
|
74
|
+
file.entries.append(MSBTEntry(label, text, attribute, style_index))
|
|
75
|
+
|
|
76
|
+
return file
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def write_msbt(stream: BinaryIO, file: MSBT) -> None:
|
|
80
|
+
"""Writes a MSBT file to a stream.
|
|
81
|
+
|
|
82
|
+
:param stream: the filestream.
|
|
83
|
+
:param file: the MSBT file object.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
```
|
|
87
|
+
with open(file_path, "wb") as file:
|
|
88
|
+
write_msbt(file, msbt)
|
|
89
|
+
...
|
|
90
|
+
```
|
|
91
|
+
"""
|
|
92
|
+
if stream is None:
|
|
93
|
+
raise LMS_Exceptions.LMS_Error("Stream is not valid!")
|
|
94
|
+
|
|
95
|
+
if not isinstance(file, MSBT):
|
|
96
|
+
raise LMS_Exceptions.LMS_Error("File provided is not a MSBT.")
|
|
97
|
+
|
|
98
|
+
writer = FileWriter(file.info.encoding)
|
|
99
|
+
write_file_info(writer, "MsgStdBn", file.info)
|
|
100
|
+
|
|
101
|
+
for section in file.section_list:
|
|
102
|
+
match section:
|
|
103
|
+
case "LBL1":
|
|
104
|
+
labels = [entry.name for entry in file.entries]
|
|
105
|
+
write_section(writer, "LBL1", write_labels, labels, file.slot_count)
|
|
106
|
+
case "ATR1":
|
|
107
|
+
attributes = [entry.attribute for entry in file.entries]
|
|
108
|
+
if file.encoded_attributes:
|
|
109
|
+
write_section(
|
|
110
|
+
writer,
|
|
111
|
+
"ATR1",
|
|
112
|
+
write_encoded_atr1,
|
|
113
|
+
attributes,
|
|
114
|
+
file.size_per_attribute,
|
|
115
|
+
file.attr_string_table,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
write_section(
|
|
119
|
+
writer,
|
|
120
|
+
"ATR1",
|
|
121
|
+
write_decoded_atr1,
|
|
122
|
+
attributes,
|
|
123
|
+
file.size_per_attribute,
|
|
124
|
+
)
|
|
125
|
+
case "TXT2":
|
|
126
|
+
messages = [entry.message for entry in file.entries]
|
|
127
|
+
write_section(writer, "TXT2", write_txt2, messages)
|
|
128
|
+
case "TSY1":
|
|
129
|
+
style_indexes = [entry.style_index for entry in file.entries]
|
|
130
|
+
write_section(writer, "TSY1", write_tsy1, style_indexes)
|
|
131
|
+
case _:
|
|
132
|
+
write_unsupported_section(
|
|
133
|
+
writer, section, file.unsupported_sections[section]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
writer.seek(0x12)
|
|
137
|
+
writer.write_uint32(writer.get_stream_size())
|
|
138
|
+
stream.write(writer.get_data())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from LMS.Config.Definitions.Attributes import AttributeConfig
|
|
2
|
+
from LMS.Field.LMS_DataType import LMS_DataType
|
|
3
|
+
from LMS.Field.LMS_Field import LMS_DataType, LMS_Field
|
|
4
|
+
from LMS.Field.Stream import read_field, write_field
|
|
5
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
6
|
+
from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_encoded_atr1(
|
|
10
|
+
reader: FileReader, section_size: int
|
|
11
|
+
) -> tuple[list[bytes], int, list[str]]:
|
|
12
|
+
absolute_size = section_size + reader.tell()
|
|
13
|
+
|
|
14
|
+
attribute_count = reader.read_uint32()
|
|
15
|
+
size_per_attribute = reader.read_uint32()
|
|
16
|
+
|
|
17
|
+
attributes = [reader.read_bytes(size_per_attribute) for _ in range(attribute_count)]
|
|
18
|
+
string_table = None
|
|
19
|
+
|
|
20
|
+
if section_size > 8 + size_per_attribute * attribute_count:
|
|
21
|
+
string_table = reader.read_bytes(absolute_size - reader.tell())
|
|
22
|
+
|
|
23
|
+
return (attributes, size_per_attribute, string_table)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_encoded_atr1(
|
|
27
|
+
writer: FileWriter,
|
|
28
|
+
attributes: list[bytes],
|
|
29
|
+
size_per_attribute: int,
|
|
30
|
+
string_table: bytes | None,
|
|
31
|
+
):
|
|
32
|
+
writer.write_uint32(len(attributes))
|
|
33
|
+
|
|
34
|
+
if attributes:
|
|
35
|
+
writer.write_uint32(size_per_attribute)
|
|
36
|
+
else:
|
|
37
|
+
writer.write_uint32(0)
|
|
38
|
+
|
|
39
|
+
for attr in attributes:
|
|
40
|
+
writer.write_bytes(attr)
|
|
41
|
+
|
|
42
|
+
if string_table is not None:
|
|
43
|
+
writer.write_bytes(string_table)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_decoded_atr1(
|
|
47
|
+
reader: FileReader, structure: AttributeConfig
|
|
48
|
+
) -> tuple[list[LMS_FieldMap], int, None]:
|
|
49
|
+
section_start = reader.tell()
|
|
50
|
+
|
|
51
|
+
attr_count = reader.read_uint32()
|
|
52
|
+
size_per_attr = reader.read_uint32()
|
|
53
|
+
|
|
54
|
+
attributes = []
|
|
55
|
+
attr_start = reader.tell()
|
|
56
|
+
|
|
57
|
+
for _ in range(attr_count):
|
|
58
|
+
reader.seek(attr_start)
|
|
59
|
+
|
|
60
|
+
attribute = {}
|
|
61
|
+
for definition in structure.definitions:
|
|
62
|
+
if definition.datatype is LMS_DataType.STRING:
|
|
63
|
+
last = reader.tell() + 4
|
|
64
|
+
reader.seek(section_start + reader.read_uint32())
|
|
65
|
+
value = LMS_Field(reader.read_str_variable_encoding(), definition)
|
|
66
|
+
|
|
67
|
+
reader.seek(last)
|
|
68
|
+
else:
|
|
69
|
+
value = read_field(reader, definition)
|
|
70
|
+
|
|
71
|
+
attribute[definition.name] = value
|
|
72
|
+
|
|
73
|
+
attributes.append(attribute)
|
|
74
|
+
|
|
75
|
+
# Move to the start of the next attribute
|
|
76
|
+
attr_start += size_per_attr
|
|
77
|
+
|
|
78
|
+
return attributes, size_per_attr, None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_decoded_atr1(
|
|
82
|
+
writer: FileWriter, attributes: list[LMS_FieldMap], size_per_attribute: int
|
|
83
|
+
) -> None:
|
|
84
|
+
writer.write_uint32(len(attributes))
|
|
85
|
+
writer.write_uint32(size_per_attribute)
|
|
86
|
+
|
|
87
|
+
string_table = []
|
|
88
|
+
string_offset = 8 + size_per_attribute * len(attributes)
|
|
89
|
+
for attr in attributes:
|
|
90
|
+
for field in attr.values():
|
|
91
|
+
if field.datatype is not LMS_DataType.STRING:
|
|
92
|
+
write_field(writer, field)
|
|
93
|
+
else:
|
|
94
|
+
writer.write_uint32(string_offset)
|
|
95
|
+
string_offset += (
|
|
96
|
+
len(field.value) * writer.encoding.width
|
|
97
|
+
) + writer.encoding.width
|
|
98
|
+
string_table.append(field.value)
|
|
99
|
+
|
|
100
|
+
for string in string_table:
|
|
101
|
+
writer.write_variable_encoding_string(string)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_tsy1(reader: FileReader, message_count: int) -> list[int]:
|
|
5
|
+
style_indexes = []
|
|
6
|
+
for _ in range(message_count):
|
|
7
|
+
style_indexes.append(reader.read_uint32())
|
|
8
|
+
return style_indexes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def write_tsy1(writer: FileWriter, style_indexes: list[int]) -> None:
|
|
12
|
+
for i in style_indexes:
|
|
13
|
+
writer.write_uint32(i)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from LMS.Config.Definitions.Tags import TagConfig
|
|
2
|
+
from LMS.FileIO.Encoding import FileEncoding
|
|
3
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
4
|
+
from LMS.Message.Definitions.LMS_MessageText import LMS_MessageText
|
|
5
|
+
from LMS.Message.Tag.LMS_Tag import LMS_TagBase
|
|
6
|
+
from LMS.Message.Tag.Stream import read_tag, write_tag
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_txt2(
|
|
10
|
+
reader: FileReader, encoding: FileEncoding, config: TagConfig
|
|
11
|
+
) -> list[LMS_MessageText]:
|
|
12
|
+
text_list = []
|
|
13
|
+
count = reader.read_uint32()
|
|
14
|
+
|
|
15
|
+
encoding_format = encoding.to_string_format(reader.big_endian)
|
|
16
|
+
tag_indicator = b"\x0e" + (b"\x00" * (reader.encoding.width - 1))
|
|
17
|
+
|
|
18
|
+
if reader.big_endian:
|
|
19
|
+
tag_indicator = tag_indicator[::-1]
|
|
20
|
+
|
|
21
|
+
for offset in reader.read_offset_array(count):
|
|
22
|
+
reader.seek(offset)
|
|
23
|
+
|
|
24
|
+
text, parts = b"", []
|
|
25
|
+
while (data := reader.read_bytes(encoding.width)) != encoding.terminator:
|
|
26
|
+
if data == tag_indicator:
|
|
27
|
+
# Add all the text before the control tag
|
|
28
|
+
parts.append(text.decode(encoding_format))
|
|
29
|
+
|
|
30
|
+
# Read the tag data
|
|
31
|
+
group_index = reader.read_uint16()
|
|
32
|
+
tag_index = reader.read_uint16()
|
|
33
|
+
param_size = reader.read_uint16()
|
|
34
|
+
|
|
35
|
+
tag = read_tag(reader, param_size, group_index, tag_index, config)
|
|
36
|
+
|
|
37
|
+
parts.append(tag)
|
|
38
|
+
text = b""
|
|
39
|
+
# TODO: Add support for closing tags
|
|
40
|
+
elif data == b"\x0f":
|
|
41
|
+
raise NotImplementedError(
|
|
42
|
+
"MSBT files with closing tags are unsupported at this time!"
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
text += data
|
|
46
|
+
|
|
47
|
+
# Add the remaining text in case there were no control tags
|
|
48
|
+
if text:
|
|
49
|
+
parts.append(text.decode(encoding_format))
|
|
50
|
+
|
|
51
|
+
message = LMS_MessageText(parts, config)
|
|
52
|
+
text_list.append(message)
|
|
53
|
+
|
|
54
|
+
return text_list
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_txt2(writer: FileWriter, messages: list[LMS_MessageText]) -> None:
|
|
58
|
+
start = writer.tell()
|
|
59
|
+
writer.write_uint32(len(messages))
|
|
60
|
+
|
|
61
|
+
offset = 4 + 4 * len(messages)
|
|
62
|
+
for message in messages:
|
|
63
|
+
writer.write_uint32(offset)
|
|
64
|
+
next = writer.tell()
|
|
65
|
+
|
|
66
|
+
writer.seek(start + offset)
|
|
67
|
+
text_start = writer.tell()
|
|
68
|
+
for part in message:
|
|
69
|
+
if isinstance(part, LMS_TagBase):
|
|
70
|
+
write_tag(writer, part)
|
|
71
|
+
else:
|
|
72
|
+
writer.write_variable_encoding_string(part, False)
|
|
73
|
+
|
|
74
|
+
writer.write_bytes(writer.encoding.terminator)
|
|
75
|
+
offset += writer.tell() - text_start
|
|
76
|
+
writer.seek(next)
|
|
77
|
+
|
|
78
|
+
# Seek to the end of the text to ensure end data can be written properly
|
|
79
|
+
writer.seek(offset + start)
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from LMS.Config.Definitions.Tags import TagConfig
|
|
5
|
+
from LMS.Field.LMS_Field import LMS_Field, cast_value
|
|
6
|
+
from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
|
|
7
|
+
from LMS.Message.Tag.LMS_TagExceptions import LMS_InvalidTagFormatError
|
|
8
|
+
from LMS.Message.Tag.Tag_Formats import (DECODED_FORMAT, ENCODED_FORMAT,
|
|
9
|
+
PARAMETER_FORMAT)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LMS_TagBase(ABC):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
group_index: int,
|
|
16
|
+
tag_index: int,
|
|
17
|
+
parameters: LMS_FieldMap | list[str] | None = None,
|
|
18
|
+
group_name: str = None,
|
|
19
|
+
tag_name: str = None,
|
|
20
|
+
):
|
|
21
|
+
self._group_index = group_index
|
|
22
|
+
self._tag_index = tag_index
|
|
23
|
+
self._parameters = parameters or {}
|
|
24
|
+
self._group_name, self._tag_name = group_name, tag_name
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def group_index(self) -> int:
|
|
28
|
+
"""The index of the group."""
|
|
29
|
+
return self._group_index
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def tag_index(self) -> int:
|
|
33
|
+
"""The index of the tag in it's group."""
|
|
34
|
+
return self._tag_index
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def group_name(self) -> str:
|
|
38
|
+
"""The name of the tag group."""
|
|
39
|
+
return self._group_name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tag_name(self) -> str:
|
|
43
|
+
"""The name of the tag in the tag group."""
|
|
44
|
+
return self._tag_name
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def parameters(self) -> list[str] | LMS_FieldMap:
|
|
48
|
+
"""The parameters of the tag."""
|
|
49
|
+
return self._parameters
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def to_text(self) -> str:
|
|
53
|
+
"""Converts the tag to its string representation."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def from_string(self, string: str, config: TagConfig | None = None) -> str:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LMS_EncodedTag(LMS_TagBase):
|
|
63
|
+
"""A class that represents an encoded tag.
|
|
64
|
+
|
|
65
|
+
Example encoded tags:
|
|
66
|
+
- `[0:3 00-00-00-FF]`
|
|
67
|
+
- `[0:4]`
|
|
68
|
+
- `[1:0 01-00-00-CD]`"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
group_index: int,
|
|
73
|
+
tag_index: int,
|
|
74
|
+
parameters: list[str] = None,
|
|
75
|
+
group_name: str = None,
|
|
76
|
+
tag_name: str = None,
|
|
77
|
+
):
|
|
78
|
+
super().__init__(group_index, tag_index, parameters)
|
|
79
|
+
self._group_name = group_name
|
|
80
|
+
self._tag_name = tag_name
|
|
81
|
+
|
|
82
|
+
def to_text(self) -> str:
|
|
83
|
+
if self._group_name is not None and self._tag_name is not None:
|
|
84
|
+
# Determine what to display based on if the names are provided
|
|
85
|
+
group = self._group_name or self._group_index
|
|
86
|
+
tag = self._tag_name or self._tag_index
|
|
87
|
+
else:
|
|
88
|
+
group, tag = self._group_index, self._tag_index
|
|
89
|
+
|
|
90
|
+
if not self._parameters:
|
|
91
|
+
return f"[{group}:{tag}]"
|
|
92
|
+
|
|
93
|
+
parameters = "-".join(self.parameters)
|
|
94
|
+
return f"[{group}:{tag} {parameters}]"
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_string(cls, string: str, config: TagConfig | None = None):
|
|
98
|
+
if match := re.match(ENCODED_FORMAT, string):
|
|
99
|
+
group, tag = match.group(1), match.group(2)
|
|
100
|
+
parameters = match.group(3).split("-")
|
|
101
|
+
|
|
102
|
+
if group.isdigit() and tag.isdigit():
|
|
103
|
+
return cls(int(group), int(tag), parameters)
|
|
104
|
+
|
|
105
|
+
tag_definition = config.get_definition_by_names(group, tag)
|
|
106
|
+
return cls(
|
|
107
|
+
group,
|
|
108
|
+
tag,
|
|
109
|
+
parameters,
|
|
110
|
+
tag_definition.group_name,
|
|
111
|
+
tag_definition.tag_name,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
raise LMS_InvalidTagFormatError(
|
|
115
|
+
f"Invalid encoded tag format detected for tag'{string}'"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LMS_DecodedTag(LMS_TagBase):
|
|
120
|
+
"""A class that represents a decoded tag.
|
|
121
|
+
|
|
122
|
+
Example encoded tags:
|
|
123
|
+
- `[System:Color r="0" g="255" b="255" a="255"]`
|
|
124
|
+
- `[System:Pagebreak]`
|
|
125
|
+
- `[Mii:Nickname buffer="1" type="Text" conversion="None"]`"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
group_index: int,
|
|
130
|
+
tag_index: int,
|
|
131
|
+
group_name: str,
|
|
132
|
+
tag_name: str,
|
|
133
|
+
parameters: LMS_FieldMap = None,
|
|
134
|
+
):
|
|
135
|
+
super().__init__(group_index, tag_index, parameters, group_name, tag_name)
|
|
136
|
+
|
|
137
|
+
def to_text(self) -> str:
|
|
138
|
+
if not self._parameters:
|
|
139
|
+
return f"[{self.group_name}:{self.tag_name}]"
|
|
140
|
+
|
|
141
|
+
parameters = " ".join(
|
|
142
|
+
f'{key}="{param._value}"' for key, param in self._parameters.items()
|
|
143
|
+
)
|
|
144
|
+
return f"[{self.group_name}:{self.tag_name} {parameters}]"
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_string(cls, tag: str, config: TagConfig):
|
|
148
|
+
if match := re.match(DECODED_FORMAT, tag):
|
|
149
|
+
group_name, tag_name = match.group(1), match.group(2)
|
|
150
|
+
parameters = dict(re.findall(PARAMETER_FORMAT, tag))
|
|
151
|
+
|
|
152
|
+
tag_definition = config.get_definition_by_names(group_name, tag_name)
|
|
153
|
+
for definition in tag_definition.parameters:
|
|
154
|
+
casted_value = cast_value(
|
|
155
|
+
parameters[definition.name], definition.datatype
|
|
156
|
+
)
|
|
157
|
+
parameters[definition.name] = LMS_Field(casted_value, definition)
|
|
158
|
+
|
|
159
|
+
return cls(
|
|
160
|
+
tag_definition.group_index,
|
|
161
|
+
tag_definition.tag_index,
|
|
162
|
+
group_name,
|
|
163
|
+
tag_name,
|
|
164
|
+
parameters,
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
raise LMS_InvalidTagFormatError(
|
|
168
|
+
f"Invalid decoded tag format detectefd for tag '{tag}'"
|
|
169
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from LMS.Config.Definitions.Tags import TagConfig, TagDefinition
|
|
2
|
+
from LMS.Field.LMS_DataType import LMS_DataType
|
|
3
|
+
from LMS.Field.LMS_Field import LMS_Field
|
|
4
|
+
from LMS.Field.Stream import read_field, write_field
|
|
5
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
6
|
+
from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
|
|
7
|
+
from LMS.Message.Tag.LMS_Tag import LMS_DecodedTag, LMS_EncodedTag, LMS_TagBase
|
|
8
|
+
from LMS.Message.Tag.LMS_TagExceptions import (LMS_TagReadingError,
|
|
9
|
+
LMS_TagWritingException)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_tag(
|
|
13
|
+
reader: FileReader,
|
|
14
|
+
param_size: int,
|
|
15
|
+
group_index: int,
|
|
16
|
+
tag_index: int,
|
|
17
|
+
config: TagConfig | None,
|
|
18
|
+
) -> LMS_EncodedTag | LMS_DecodedTag:
|
|
19
|
+
end = reader.tell() + param_size
|
|
20
|
+
|
|
21
|
+
if config is None:
|
|
22
|
+
parameters = _read_encoded_parameters(reader, param_size)
|
|
23
|
+
|
|
24
|
+
if parameters is None:
|
|
25
|
+
return LMS_EncodedTag(group_index, tag_index)
|
|
26
|
+
|
|
27
|
+
return LMS_EncodedTag(group_index, tag_index, parameters)
|
|
28
|
+
|
|
29
|
+
definition = config.get_definition_by_indexes(group_index, tag_index)
|
|
30
|
+
|
|
31
|
+
# If the parameters were omitted from the definition but the tag still has defined parameters, read them.
|
|
32
|
+
# This is to account for encoded tags that group have tag names attatched.
|
|
33
|
+
if definition.parameters is None and param_size:
|
|
34
|
+
parameters = _read_encoded_parameters(reader, param_size)
|
|
35
|
+
return LMS_EncodedTag(
|
|
36
|
+
group_index,
|
|
37
|
+
tag_index,
|
|
38
|
+
parameters,
|
|
39
|
+
definition.group_name,
|
|
40
|
+
definition.tag_name,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
parameters = _read_decoded_parameters(reader, definition)
|
|
44
|
+
|
|
45
|
+
reader.seek(end)
|
|
46
|
+
return LMS_DecodedTag(
|
|
47
|
+
group_index, tag_index, definition.group_name, definition.tag_name, parameters
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write_tag(writer: FileWriter, tag: LMS_TagBase) -> None:
|
|
52
|
+
tag_indicator = b"\x0e" + (b"\x00" * (writer.encoding.width - 1))
|
|
53
|
+
if writer.big_endian:
|
|
54
|
+
tag_indicator = tag_indicator[::-1]
|
|
55
|
+
|
|
56
|
+
writer.write_bytes(tag_indicator)
|
|
57
|
+
writer.write_uint16(tag.group_index)
|
|
58
|
+
writer.write_uint16(tag.tag_index)
|
|
59
|
+
|
|
60
|
+
if tag.parameters is None:
|
|
61
|
+
writer.write_uint16(0)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
if isinstance(tag, LMS_EncodedTag):
|
|
65
|
+
_write_encoded_parameters(writer, tag.parameters)
|
|
66
|
+
else:
|
|
67
|
+
_write_decoded_parameters(writer, tag)
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --
|
|
73
|
+
def _read_encoded_parameters(reader: FileReader, param_size: int) -> list[str] | None:
|
|
74
|
+
if param_size == 0:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
hex_parameters = reader.read_bytes(param_size).hex().upper()
|
|
78
|
+
encoded_parameters = [
|
|
79
|
+
hex_parameters[i : i + 2] for i in range(0, len(hex_parameters), 2)
|
|
80
|
+
]
|
|
81
|
+
return encoded_parameters
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_encoded_parameters(writer: FileWriter, parameters: list[str] | None) -> None:
|
|
85
|
+
writer.write_uint16(len(parameters))
|
|
86
|
+
for param in parameters:
|
|
87
|
+
writer.write_bytes(bytes.fromhex(param))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_decoded_parameters(
|
|
91
|
+
reader: FileReader, definition: TagDefinition
|
|
92
|
+
) -> LMS_FieldMap:
|
|
93
|
+
parameters = {}
|
|
94
|
+
for param in definition.parameters:
|
|
95
|
+
param_offset = reader.tell()
|
|
96
|
+
try:
|
|
97
|
+
if param.datatype is LMS_DataType.STRING:
|
|
98
|
+
value = LMS_Field(reader.read_len_string_variable_encoding(), param)
|
|
99
|
+
else:
|
|
100
|
+
value = read_field(reader, param)
|
|
101
|
+
# There could be multiple errors related to reading, share the extra info but display the original exception
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise LMS_TagReadingError(
|
|
104
|
+
f"An error occured reading tag '[{definition.group_name}:{definition.tag_name}]', parameter '{param.name}' at offset {param_offset}"
|
|
105
|
+
) from e
|
|
106
|
+
|
|
107
|
+
parameters[param.name] = value
|
|
108
|
+
return parameters
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_decoded_parameters(writer: FileWriter, tag: LMS_DecodedTag) -> None:
|
|
112
|
+
param_size = 0
|
|
113
|
+
|
|
114
|
+
# Tags are padded by 0xCD if the size is not aligned to the encoding
|
|
115
|
+
# This can occur before a string parameter, or at the end of the tag.
|
|
116
|
+
# Set a flag in order to be able to pad the first string correctly
|
|
117
|
+
needs_padding = False
|
|
118
|
+
first_string = True
|
|
119
|
+
|
|
120
|
+
for field in tag.parameters.values():
|
|
121
|
+
if field.datatype is LMS_DataType.STRING:
|
|
122
|
+
param_size += 2 + len(field.value) * writer.encoding.width
|
|
123
|
+
else:
|
|
124
|
+
param_size += field.datatype.stream_size
|
|
125
|
+
|
|
126
|
+
if param_size % 2 == 1:
|
|
127
|
+
needs_padding = True
|
|
128
|
+
param_size += 1
|
|
129
|
+
|
|
130
|
+
writer.write_uint16(param_size)
|
|
131
|
+
for field in tag.parameters.values():
|
|
132
|
+
try:
|
|
133
|
+
if field.datatype is LMS_DataType.STRING:
|
|
134
|
+
if first_string and needs_padding:
|
|
135
|
+
writer.write_bytes(b"\xcd")
|
|
136
|
+
first_string = False
|
|
137
|
+
needs_padding = False
|
|
138
|
+
|
|
139
|
+
writer.write_uint16(len(field.value) * writer.encoding.width)
|
|
140
|
+
writer.write_variable_encoding_string(field.value, False)
|
|
141
|
+
else:
|
|
142
|
+
write_field(writer, field)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise LMS_TagWritingException(
|
|
145
|
+
f"An error occured writing tag '{tag}', parameter '{field.name}' at offset {writer.tell()}!"
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
if needs_padding:
|
|
149
|
+
writer.write_bytes(b"\xcd")
|