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,16 @@
|
|
|
1
|
+
class LMS_Error(Exception):
|
|
2
|
+
"""Exception for non-specifc errors"""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LMS_UnexpectedMagicError(Exception):
|
|
8
|
+
"""Exception for wrong magic of a LMS file"""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LMS_MisalignedSizeError(Exception):
|
|
14
|
+
"""Exception for when size is not aligned."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
@@ -0,0 +1,64 @@
|
|
|
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.Stream import FileReader, FileWriter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_file_info(reader: FileReader, expected_magic: str) -> LMS_FileInfo:
|
|
8
|
+
magic = reader.read_string_len(8)
|
|
9
|
+
|
|
10
|
+
if magic != expected_magic:
|
|
11
|
+
raise LMS_Exceptions.LMS_UnexpectedMagicError(
|
|
12
|
+
f"Invalid magic!' Expected {expected_magic}', got '{magic}'."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
big_endian = reader.read_bytes(2) == b"\xfe\xff"
|
|
16
|
+
reader.big_endian = big_endian
|
|
17
|
+
|
|
18
|
+
reader.skip(2)
|
|
19
|
+
|
|
20
|
+
encoding = FileEncoding(reader.read_uint8())
|
|
21
|
+
reader.encoding = encoding
|
|
22
|
+
|
|
23
|
+
version = reader.read_uint8()
|
|
24
|
+
section_count = reader.read_uint16()
|
|
25
|
+
|
|
26
|
+
reader.skip(2)
|
|
27
|
+
file_size = reader.read_uint32()
|
|
28
|
+
|
|
29
|
+
reader.seek(0, 2)
|
|
30
|
+
if file_size != reader.tell():
|
|
31
|
+
raise LMS_Exceptions.LMS_MisalignedSizeError(f"Filesize is misaligned!")
|
|
32
|
+
|
|
33
|
+
# Seek to the start of data
|
|
34
|
+
reader.seek(0x20)
|
|
35
|
+
|
|
36
|
+
return LMS_FileInfo(
|
|
37
|
+
big_endian,
|
|
38
|
+
encoding,
|
|
39
|
+
version,
|
|
40
|
+
section_count,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_file_info(writer: FileWriter, magic: str, file_info: LMS_FileInfo) -> None:
|
|
45
|
+
"""Writes the file info to a stream.
|
|
46
|
+
|
|
47
|
+
:param writer: a Writer object.
|
|
48
|
+
:param file_info: the file_info object."""
|
|
49
|
+
writer.big_endian = file_info.big_endian
|
|
50
|
+
writer.encoding = file_info.encoding
|
|
51
|
+
|
|
52
|
+
writer.write_string(magic)
|
|
53
|
+
writer.write_bytes(b"\xff\xfe" if not file_info.big_endian else b"\xfe\xff")
|
|
54
|
+
writer.write_bytes(b"\x00\x00")
|
|
55
|
+
|
|
56
|
+
writer.write_uint8(file_info.encoding.value)
|
|
57
|
+
writer.write_uint8(file_info.version)
|
|
58
|
+
writer.write_uint16(file_info.section_count)
|
|
59
|
+
|
|
60
|
+
# Padding
|
|
61
|
+
writer.write_bytes(b"\x00\x00")
|
|
62
|
+
writer.write_bytes(b"\x00" * 4)
|
|
63
|
+
writer.write_bytes(b"\x00" * 10)
|
|
64
|
+
writer.seek(0x20)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_labels(reader: FileReader) -> tuple[dict[int, str], int]:
|
|
5
|
+
labels = {}
|
|
6
|
+
|
|
7
|
+
data_start = reader.tell()
|
|
8
|
+
slot_count = reader.read_uint32()
|
|
9
|
+
for _ in range(slot_count):
|
|
10
|
+
# Read initial label data
|
|
11
|
+
label_count = reader.read_uint32()
|
|
12
|
+
offset = reader.read_uint32()
|
|
13
|
+
next_offset = reader.tell()
|
|
14
|
+
|
|
15
|
+
# Read the actual label data
|
|
16
|
+
reader.seek(data_start + offset)
|
|
17
|
+
for _ in range(label_count):
|
|
18
|
+
length = reader.read_uint8()
|
|
19
|
+
label = reader.read_string_len(length)
|
|
20
|
+
item_index = reader.read_uint32()
|
|
21
|
+
labels[item_index] = label
|
|
22
|
+
|
|
23
|
+
reader.seek(next_offset)
|
|
24
|
+
|
|
25
|
+
sorted_labels = {i: labels[i] for i in sorted(labels)}
|
|
26
|
+
|
|
27
|
+
# While the slot count is consistent for most files, some vary them.
|
|
28
|
+
# Return the slot count to ensure that this change can be recorded.
|
|
29
|
+
return (sorted_labels, slot_count)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
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
|
+
writer.write_uint32(slot_count)
|
|
40
|
+
|
|
41
|
+
hash_slots = {}
|
|
42
|
+
index_map = {}
|
|
43
|
+
# Add each label to each hash slot
|
|
44
|
+
for i, label in enumerate(labels):
|
|
45
|
+
hash = _calculate_hash(label, slot_count)
|
|
46
|
+
|
|
47
|
+
# Add to the list if hash exists, and create a new list for each new hash
|
|
48
|
+
if hash not in hash_slots:
|
|
49
|
+
hash_slots[hash] = [label]
|
|
50
|
+
else:
|
|
51
|
+
hash_slots[hash].append(label)
|
|
52
|
+
|
|
53
|
+
index_map[label] = i
|
|
54
|
+
label_offsets = slot_count * 8 + 4
|
|
55
|
+
|
|
56
|
+
# Sort by the hash slots
|
|
57
|
+
hash_slots = dict(sorted(hash_slots.items(), key=lambda x: x[0]))
|
|
58
|
+
|
|
59
|
+
# Write the slots
|
|
60
|
+
for i in range(slot_count):
|
|
61
|
+
if i in hash_slots:
|
|
62
|
+
writer.write_uint32(len(hash_slots[i]))
|
|
63
|
+
writer.write_uint32(label_offsets)
|
|
64
|
+
for label in hash_slots[i]:
|
|
65
|
+
label_offsets += len(label) + 5
|
|
66
|
+
else:
|
|
67
|
+
writer.write_uint32(0)
|
|
68
|
+
writer.write_uint32(label_offsets)
|
|
69
|
+
|
|
70
|
+
# Write the labels
|
|
71
|
+
for key in hash_slots:
|
|
72
|
+
stored_labels = hash_slots[key]
|
|
73
|
+
for label in stored_labels:
|
|
74
|
+
writer.write_uint8(len(label))
|
|
75
|
+
writer.write_string(label)
|
|
76
|
+
writer.write_uint32(index_map[label])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
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
|
+
hash = 0
|
|
85
|
+
for character in label:
|
|
86
|
+
hash = hash * 0x492 + ord(character)
|
|
87
|
+
return (hash & 0xFFFFFFFF) % slot_count
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Any, Callable, Generator
|
|
2
|
+
|
|
3
|
+
from LMS.FileIO.Stream import FileReader, FileWriter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_section_data(
|
|
7
|
+
reader: FileReader, section_count: int
|
|
8
|
+
) -> Generator[tuple[str, int], Any, None]:
|
|
9
|
+
reader.seek(0x20)
|
|
10
|
+
for _ in range(section_count):
|
|
11
|
+
magic = reader.read_string_len(4)
|
|
12
|
+
size = reader.read_uint32()
|
|
13
|
+
|
|
14
|
+
# Skip to start of data
|
|
15
|
+
reader.skip(8)
|
|
16
|
+
end = reader.tell() + size
|
|
17
|
+
|
|
18
|
+
yield (magic, size)
|
|
19
|
+
|
|
20
|
+
# Seek past the AB padding on next iteration
|
|
21
|
+
reader.seek(end)
|
|
22
|
+
reader.align(16)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_section(
|
|
26
|
+
writer: FileWriter,
|
|
27
|
+
magic: str,
|
|
28
|
+
section_call: Callable,
|
|
29
|
+
data: list[Any],
|
|
30
|
+
*write_parameters,
|
|
31
|
+
) -> None:
|
|
32
|
+
writer.write_string(magic)
|
|
33
|
+
size_offset = writer.tell()
|
|
34
|
+
|
|
35
|
+
writer.write_uint32(0)
|
|
36
|
+
writer.write_bytes(b"\x00" * 8)
|
|
37
|
+
data_start = writer.tell()
|
|
38
|
+
|
|
39
|
+
section_call(writer, data, *write_parameters)
|
|
40
|
+
|
|
41
|
+
_write_end_data(writer, data_start, size_offset)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_unsupported_section(writer: FileWriter, magic: str, data: bytes) -> None:
|
|
45
|
+
writer.write_string(magic)
|
|
46
|
+
size_offset = writer.tell()
|
|
47
|
+
writer.write_uint32(0)
|
|
48
|
+
writer.write_bytes(b"\x00" * 8)
|
|
49
|
+
data_start = writer.tell()
|
|
50
|
+
writer.write_bytes(data)
|
|
51
|
+
_write_end_data(writer, data_start, size_offset)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write_end_data(writer: FileWriter, data_start: int, size_offset: int) -> None:
|
|
55
|
+
end = writer.tell()
|
|
56
|
+
size = end - data_start
|
|
57
|
+
writer.seek(size_offset)
|
|
58
|
+
writer.write_uint32(size)
|
|
59
|
+
writer.seek(end)
|
|
60
|
+
writer.write_alignment(b"\xAB", 16)
|
LMS/Common/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from LMS.Config.Definitions.Value import ValueDefinition
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class AttributeConfig:
|
|
8
|
+
"""Class that represents an attribute structure definition.
|
|
9
|
+
|
|
10
|
+
Configs may have multiple, linked to the if a game has multiple MSBP files."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
description: str = field(repr=False)
|
|
14
|
+
definitions: list[ValueDefinition]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from LMS.Config.Definitions.Value import ValueDefinition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class TagDefinition:
|
|
10
|
+
"""Class that represents a signle tag definition in the structure."""
|
|
11
|
+
|
|
12
|
+
group_name: str
|
|
13
|
+
group_index: int
|
|
14
|
+
tag_name: str
|
|
15
|
+
tag_index: int
|
|
16
|
+
description: str
|
|
17
|
+
parameters: list[ValueDefinition] | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class TagConfig:
|
|
22
|
+
"""Class that represents an tag structure definition."""
|
|
23
|
+
|
|
24
|
+
group_map: dict[int, str]
|
|
25
|
+
definitions: list[TagDefinition]
|
|
26
|
+
|
|
27
|
+
def get_definition_by_names(self, group: str, tag: str) -> TagDefinition:
|
|
28
|
+
group_index = None
|
|
29
|
+
for i, name in self.group_map.items():
|
|
30
|
+
if name == group:
|
|
31
|
+
group_index = i
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
if group_index is None:
|
|
35
|
+
raise KeyError(f"Group name '{group}' was not found! Is the group defined?")
|
|
36
|
+
|
|
37
|
+
for tag_def in self.definitions:
|
|
38
|
+
if tag_def.group_index == group_index and tag_def.tag_name == tag:
|
|
39
|
+
return tag_def
|
|
40
|
+
|
|
41
|
+
raise KeyError(
|
|
42
|
+
f"Tag name '{tag}' not found in group '{group}'. Os the tag defined?"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_definition_by_indexes(self, group: int, tag: int) -> TagDefinition:
|
|
46
|
+
for tag_def in self.definitions:
|
|
47
|
+
if tag_def.group_index == group and tag_def.tag_index == tag:
|
|
48
|
+
return tag_def
|
|
49
|
+
|
|
50
|
+
raise KeyError(
|
|
51
|
+
f"Tag index of {tag} was not found in group index {group}. Is the tag defined?"
|
|
52
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import importlib.resources as pkg_resources
|
|
2
|
+
import os
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from LMS.Config.Definitions.Attributes import AttributeConfig
|
|
8
|
+
from LMS.Config.Definitions.Tags import TagConfig, TagDefinition
|
|
9
|
+
from LMS.Config.Definitions.Value import ValueDefinition
|
|
10
|
+
from LMS.Field.LMS_DataType import LMS_DataType
|
|
11
|
+
from LMS.Project.MSBP import MSBP
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TitleConfig:
|
|
15
|
+
"""Represents a configuration for a specific game/title."""
|
|
16
|
+
|
|
17
|
+
# Populate the preset list from the directory
|
|
18
|
+
PRESET_LIST = [
|
|
19
|
+
name.removesuffix(".yaml") for name in pkg_resources.contents("LMS.Config.Presets") if name.endswith(".yaml")
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __init__(self, attribute_configs: dict[str, AttributeConfig], tag_config: TagConfig):
|
|
23
|
+
self._attribute_configs = attribute_configs
|
|
24
|
+
self._tag_config = tag_config
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def attribute_configs(self) -> dict[str, AttributeConfig]:
|
|
28
|
+
"""The map of attribute config instances."""
|
|
29
|
+
return self._attribute_configs
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def tag_config(self) -> TagConfig:
|
|
33
|
+
"""The loaded tag config instance."""
|
|
34
|
+
return self._tag_config
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load_preset(cls, game: str) -> Self:
|
|
38
|
+
"""Loads an existing preset from the package."""
|
|
39
|
+
if game not in cls.PRESET_LIST:
|
|
40
|
+
raise FileNotFoundError(f"Preset '{game}' not found.")
|
|
41
|
+
|
|
42
|
+
with pkg_resources.open_text("LMS.Config.Presets", f"{game}.yaml") as f:
|
|
43
|
+
return cls.load_config(f.read())
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def load_file(self, file_path: str) -> Self:
|
|
47
|
+
"""Loads a config from a file.
|
|
48
|
+
|
|
49
|
+
:param file_path: the path to the config."""
|
|
50
|
+
with open(file_path, "r") as f:
|
|
51
|
+
return TitleConfig.load_config(f.read())
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def load_config(cls, content: str | dict) -> Self:
|
|
55
|
+
"""Loads the config of a specified game.
|
|
56
|
+
|
|
57
|
+
:param content: the config content, as a string or loaded as a dictionary."""
|
|
58
|
+
if isinstance(content, str):
|
|
59
|
+
parsed_content = yaml.safe_load(content)
|
|
60
|
+
else:
|
|
61
|
+
parsed_content = content
|
|
62
|
+
|
|
63
|
+
group_map = {0: "System"}
|
|
64
|
+
tag_config = []
|
|
65
|
+
|
|
66
|
+
# Add the System tag definitions to the config
|
|
67
|
+
with open(r"LMS\Message\Tag\System.yaml", "r") as f:
|
|
68
|
+
system_tags = yaml.safe_load(f)
|
|
69
|
+
tag_config = system_tags
|
|
70
|
+
|
|
71
|
+
# Combine with the rest of the cofnig
|
|
72
|
+
group_map |= parsed_content["tag_definitions"]["groups"]
|
|
73
|
+
for tag_def in parsed_content["tag_definitions"]["tags"]:
|
|
74
|
+
tag_config.append(tag_def)
|
|
75
|
+
|
|
76
|
+
# Load tag definitions
|
|
77
|
+
tag_definitions = []
|
|
78
|
+
for tag_def in tag_config:
|
|
79
|
+
tag_name = tag_def["name"]
|
|
80
|
+
group_index, tag_index = tag_def["group_index"], tag_def["tag_index"]
|
|
81
|
+
group_name = group_map[group_index]
|
|
82
|
+
tag_description = tag_def["description"]
|
|
83
|
+
|
|
84
|
+
group_name = group_map[group_index]
|
|
85
|
+
|
|
86
|
+
parameters = None
|
|
87
|
+
if "parameters" in tag_def:
|
|
88
|
+
parameters = []
|
|
89
|
+
for param_def in tag_def["parameters"]:
|
|
90
|
+
param_name = param_def["name"]
|
|
91
|
+
param_description = param_def["description"]
|
|
92
|
+
datatype = LMS_DataType.from_string(param_def["datatype"])
|
|
93
|
+
list_items = param_def.get("list_items")
|
|
94
|
+
parameters.append(ValueDefinition(param_name, param_description, datatype, list_items))
|
|
95
|
+
|
|
96
|
+
tag_definitions.append(
|
|
97
|
+
TagDefinition(
|
|
98
|
+
group_name,
|
|
99
|
+
group_index,
|
|
100
|
+
tag_name,
|
|
101
|
+
tag_index,
|
|
102
|
+
tag_description,
|
|
103
|
+
parameters,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
tag_config = TagConfig(group_map, tag_definitions)
|
|
108
|
+
|
|
109
|
+
# Load the attribute definitions
|
|
110
|
+
attribute_config = {}
|
|
111
|
+
for structure in parsed_content["attributes"]:
|
|
112
|
+
structure_name = structure["name"]
|
|
113
|
+
|
|
114
|
+
definitions = []
|
|
115
|
+
for info in structure["definitions"]:
|
|
116
|
+
name, description = info["name"], info["description"]
|
|
117
|
+
datatype = LMS_DataType.from_string(info["datatype"])
|
|
118
|
+
list_items = info.get("list_items")
|
|
119
|
+
|
|
120
|
+
definition = ValueDefinition(name, description, datatype, list_items)
|
|
121
|
+
definitions.append(definition)
|
|
122
|
+
|
|
123
|
+
structure = AttributeConfig(structure_name, structure["description"], definitions)
|
|
124
|
+
attribute_config[structure_name] = structure
|
|
125
|
+
|
|
126
|
+
return cls(attribute_config, tag_config)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def generate_file(file_path: str, project: MSBP) -> None:
|
|
130
|
+
with open(file_path, "w+") as f:
|
|
131
|
+
yaml.safe_dump(
|
|
132
|
+
TitleConfig.generate_profile(project),
|
|
133
|
+
f,
|
|
134
|
+
default_flow_style=False,
|
|
135
|
+
sort_keys=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def generate_config(project: MSBP) -> dict | None:
|
|
140
|
+
"""Creates a message config file for the game.
|
|
141
|
+
|
|
142
|
+
:param project: a MSBP object."""
|
|
143
|
+
# TODO: Add custom node definitions
|
|
144
|
+
|
|
145
|
+
tag_key = "tag_definitions"
|
|
146
|
+
attr_key = "attributes"
|
|
147
|
+
|
|
148
|
+
config = {}
|
|
149
|
+
# Source files may be a path to a non-existent directory.
|
|
150
|
+
# These files were generated from the source machine from the actual libMS tool
|
|
151
|
+
# Shorten the filename with basname and replace the extension with .msbt for lookup later when reading a MSBT
|
|
152
|
+
config[tag_key] = {
|
|
153
|
+
"groups": {i + 1: group.name for i, group in enumerate(project.tag_groups[1:])},
|
|
154
|
+
"tags": [],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Slice to exclude System group
|
|
158
|
+
for group_i, group in enumerate(project.tag_groups[1:], start=1):
|
|
159
|
+
for tag_i, info in enumerate(group.tag_definitions):
|
|
160
|
+
|
|
161
|
+
definition = {
|
|
162
|
+
"name": info.name,
|
|
163
|
+
"group_index": group_i,
|
|
164
|
+
"tag_index": tag_i,
|
|
165
|
+
"description": "",
|
|
166
|
+
"parameters": [],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for param_info in info.param_info:
|
|
170
|
+
param_definition = {
|
|
171
|
+
"name": param_info.name,
|
|
172
|
+
"description": "",
|
|
173
|
+
"datatype": param_info.datatype.to_string(),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if param_info.datatype is LMS_DataType.LIST:
|
|
177
|
+
param_definition["list_items"] = param_info.list_items
|
|
178
|
+
|
|
179
|
+
definition["parameters"].append(param_definition)
|
|
180
|
+
|
|
181
|
+
config[tag_key]["tags"].append(definition)
|
|
182
|
+
|
|
183
|
+
# Set main attribute entries as the definitions from the MSBP
|
|
184
|
+
# Since most games use one MSBP these act the primary definitions
|
|
185
|
+
config[attr_key] = []
|
|
186
|
+
|
|
187
|
+
attr_definitions = []
|
|
188
|
+
for attr_info in project.attribute_info:
|
|
189
|
+
definition = {
|
|
190
|
+
"name": attr_info.name,
|
|
191
|
+
"description": "",
|
|
192
|
+
"datatype": attr_info.datatype.to_string(),
|
|
193
|
+
}
|
|
194
|
+
if attr_info.datatype is LMS_DataType.LIST:
|
|
195
|
+
definition["list_items"] = attr_info.list_items
|
|
196
|
+
|
|
197
|
+
attr_definitions.append(definition)
|
|
198
|
+
|
|
199
|
+
# Main attribute entries
|
|
200
|
+
config[attr_key].append(
|
|
201
|
+
{
|
|
202
|
+
"name": project.name,
|
|
203
|
+
"description": "",
|
|
204
|
+
"definitions": attr_definitions,
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return config
|
LMS/Config/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Literal, Type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LMS_DataType(Enum):
|
|
6
|
+
|
|
7
|
+
UINT8 = 0
|
|
8
|
+
UINT16 = 1
|
|
9
|
+
UINT32 = 2
|
|
10
|
+
|
|
11
|
+
INT8 = 3
|
|
12
|
+
INT16 = 4
|
|
13
|
+
INT32 = 5
|
|
14
|
+
|
|
15
|
+
FLOAT32 = 6
|
|
16
|
+
|
|
17
|
+
# Unknown 16 bit type (value of 6) has yet to be documented
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
STRING = 8
|
|
21
|
+
LIST = 9
|
|
22
|
+
|
|
23
|
+
# Interface types
|
|
24
|
+
BOOL = "bool"
|
|
25
|
+
BYTE = "byte"
|
|
26
|
+
|
|
27
|
+
def to_string(self) -> str:
|
|
28
|
+
return self._name_.lower()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def signed(self) -> bool:
|
|
32
|
+
"""Property for if the type is signed or not."""
|
|
33
|
+
if self not in [
|
|
34
|
+
LMS_DataType.STRING,
|
|
35
|
+
LMS_DataType.LIST,
|
|
36
|
+
LMS_DataType.BOOL,
|
|
37
|
+
LMS_DataType.BYTE,
|
|
38
|
+
]:
|
|
39
|
+
return self in [LMS_DataType.INT8, LMS_DataType.INT16, LMS_DataType.INT32]
|
|
40
|
+
|
|
41
|
+
raise TypeError(f"Signed is not a valid property for '{self.to_string()}'!")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def builtin_type(self) -> Type[int | str | float | bool | bytes]:
|
|
45
|
+
"""The enum as the builtin python type."""
|
|
46
|
+
return {
|
|
47
|
+
LMS_DataType.UINT8: int,
|
|
48
|
+
LMS_DataType.UINT16: int,
|
|
49
|
+
LMS_DataType.UINT32: int,
|
|
50
|
+
LMS_DataType.INT8: int,
|
|
51
|
+
LMS_DataType.INT16: int,
|
|
52
|
+
LMS_DataType.INT32: int,
|
|
53
|
+
LMS_DataType.FLOAT32: float,
|
|
54
|
+
LMS_DataType.STRING: str,
|
|
55
|
+
LMS_DataType.LIST: str,
|
|
56
|
+
LMS_DataType.BOOL: bool,
|
|
57
|
+
LMS_DataType.BYTE: bytes,
|
|
58
|
+
}[self]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def stream_size(self) -> Literal[1, 2, 4]:
|
|
62
|
+
"""The size the datatype takes up in a strema."""
|
|
63
|
+
return {
|
|
64
|
+
LMS_DataType.UINT8: 1,
|
|
65
|
+
LMS_DataType.UINT16: 2,
|
|
66
|
+
LMS_DataType.UINT32: 4,
|
|
67
|
+
LMS_DataType.INT8: 1,
|
|
68
|
+
LMS_DataType.INT16: 2,
|
|
69
|
+
LMS_DataType.INT32: 4,
|
|
70
|
+
LMS_DataType.FLOAT32: 4,
|
|
71
|
+
LMS_DataType.LIST: 1,
|
|
72
|
+
LMS_DataType.BOOL: 1,
|
|
73
|
+
LMS_DataType.BYTE: 1,
|
|
74
|
+
}[self]
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_string(cls, string: str):
|
|
78
|
+
"""Creates a LMS_Datatype enum value from it's string representation"""
|
|
79
|
+
string = string.upper()
|
|
80
|
+
if string in cls.__members__:
|
|
81
|
+
return cls[string]
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(f"Unknown value of '{string}' was provided!")
|