legacy-puyo-tools 0.0.1__py3-none-any.whl → 0.2.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.
- legacy_puyo_tools/__init__.py +2 -2
- legacy_puyo_tools/__main__.py +10 -0
- legacy_puyo_tools/cli.py +121 -147
- legacy_puyo_tools/exceptions.py +5 -0
- legacy_puyo_tools/fpd.py +61 -55
- legacy_puyo_tools/mtx.py +50 -33
- legacy_puyo_tools-0.2.0.dist-info/METADATA +128 -0
- legacy_puyo_tools-0.2.0.dist-info/RECORD +12 -0
- legacy_puyo_tools-0.2.0.dist-info/entry_points.txt +2 -0
- {legacy_puyo_tools-0.0.1.dist-info → legacy_puyo_tools-0.2.0.dist-info}/licenses/LICENSE +2 -1
- legacy_puyo_tools-0.0.1.dist-info/METADATA +0 -34
- legacy_puyo_tools-0.0.1.dist-info/RECORD +0 -11
- legacy_puyo_tools-0.0.1.dist-info/entry_points.txt +0 -2
- {legacy_puyo_tools-0.0.1.dist-info → legacy_puyo_tools-0.2.0.dist-info}/WHEEL +0 -0
legacy_puyo_tools/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""A conversion tool for files used by the older Puyo games.
|
2
2
|
|
3
|
-
Supports Puyo Puyo 7 and possibly Puyo Puyo! 15th Anniversary `mtx` and
|
4
|
-
|
3
|
+
Supports Puyo Puyo 7 and possibly Puyo Puyo! 15th Anniversary `mtx` and `fpd` files.
|
4
|
+
Puyo Puyo!! 20th Anniversary is still not supported yet.
|
5
5
|
|
6
6
|
SPDX-FileCopyrightText: 2025 Samuel Wu
|
7
7
|
SPDX-License-Identifier: MIT
|
legacy_puyo_tools/cli.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""A commandline
|
1
|
+
"""A commandline interface for the conversion tools.
|
2
2
|
|
3
3
|
SPDX-FileCopyrightText: 2025 Samuel Wu
|
4
4
|
SPDX-License-Identifier: MIT
|
@@ -6,172 +6,146 @@ SPDX-License-Identifier: MIT
|
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
-
import argparse
|
10
|
-
import sys
|
11
9
|
from codecs import BOM_UTF16_LE
|
12
|
-
from collections.abc import Callable
|
13
|
-
from importlib import metadata
|
14
10
|
from pathlib import Path
|
15
11
|
from typing import BinaryIO
|
16
12
|
|
17
|
-
|
13
|
+
import cloup
|
14
|
+
from cloup import option, option_group
|
15
|
+
from cloup.constraints import require_one
|
18
16
|
|
19
|
-
from legacy_puyo_tools.exceptions import FileFormatError
|
17
|
+
from legacy_puyo_tools.exceptions import ArgumentError, FileFormatError
|
20
18
|
from legacy_puyo_tools.fpd import Fpd
|
21
19
|
from legacy_puyo_tools.mtx import Mtx
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
21
|
+
output_option = option(
|
22
|
+
"--output",
|
23
|
+
"-o",
|
24
|
+
"output_file",
|
25
|
+
help="Output file. Defaults to an appropriate filename and extension.",
|
26
|
+
type=cloup.File("wb"),
|
27
|
+
)
|
28
|
+
|
29
|
+
mtx_options = option_group(
|
30
|
+
"Character table options",
|
31
|
+
option(
|
32
|
+
"--fpd",
|
33
|
+
help="Use a fpd file as the character table.",
|
34
|
+
type=cloup.Path(exists=True, dir_okay=False, path_type=Path),
|
35
|
+
),
|
36
|
+
option(
|
37
|
+
"--unicode",
|
38
|
+
help="Use a unicode text file as the character table.",
|
39
|
+
type=cloup.Path(exists=True, dir_okay=False, path_type=Path),
|
40
|
+
),
|
41
|
+
constraint=require_one.rephrased(
|
42
|
+
"exactly 1 character table required for mtx files",
|
43
|
+
"exactly 1 character table must be specified",
|
44
|
+
),
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
@cloup.group()
|
49
|
+
@cloup.version_option()
|
50
|
+
def app() -> None:
|
51
|
+
"""A conversion tool for files used by older Puyo games."""
|
52
|
+
|
53
|
+
|
54
|
+
@app.group()
|
55
|
+
def create() -> None:
|
56
|
+
"""Create files to used by older Puyo games."""
|
57
|
+
|
58
|
+
|
59
|
+
@create.command(name="fpd")
|
60
|
+
@cloup.argument(
|
61
|
+
"input_file",
|
62
|
+
help="Unicode text file encoded in UTF-16 little-endian.",
|
63
|
+
type=cloup.File("rb"),
|
64
|
+
)
|
65
|
+
@output_option
|
66
|
+
def create_fpd(input_file: BinaryIO, output_file: BinaryIO | None) -> None:
|
67
|
+
"""Create a fpd file from a unicode text file.""" # noqa: DOC501
|
68
|
+
if input_file.read(2) != BOM_UTF16_LE:
|
36
69
|
raise FileFormatError(
|
37
|
-
f"{
|
38
|
-
"file."
|
70
|
+
f"{input_file.name} is not a UTF-16 little-endian encoded text file."
|
39
71
|
)
|
40
72
|
|
41
|
-
if
|
42
|
-
Fpd.read_unicode(
|
73
|
+
if output_file:
|
74
|
+
Fpd.read_unicode(input_file).write_fpd(output_file)
|
43
75
|
return
|
44
76
|
|
45
|
-
path = Path(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
77
|
+
path = Path(input_file.name).with_suffix(".fpd")
|
78
|
+
|
79
|
+
Fpd.read_unicode(input_file).write_fpd_to_path(path)
|
80
|
+
|
81
|
+
|
82
|
+
@create.command(name="mtx", show_constraints=True)
|
83
|
+
@cloup.argument(
|
84
|
+
"input_file",
|
85
|
+
help="XML file that contains markup text or dialog.",
|
86
|
+
type=cloup.File("rb"),
|
87
|
+
)
|
88
|
+
@output_option
|
89
|
+
@mtx_options
|
90
|
+
def create_mtx(
|
91
|
+
input_file: BinaryIO, output_file: BinaryIO, fpd: Path | None, unicode: Path | None
|
92
|
+
) -> None:
|
93
|
+
"""Create a mtx file from a XML file."""
|
94
|
+
raise NotImplementedError("Creating MTX files is currently not implemented yet.")
|
95
|
+
|
96
|
+
|
97
|
+
@app.group()
|
98
|
+
def convert() -> None:
|
99
|
+
"""Convert files used by older Puyo games to an editable format."""
|
100
|
+
|
101
|
+
|
102
|
+
@convert.command(name="fpd")
|
103
|
+
@cloup.argument(
|
104
|
+
"input_file", help="Fpd file containing character data.", type=cloup.File("rb")
|
105
|
+
)
|
106
|
+
@output_option
|
107
|
+
def convert_fpd(input_file: BinaryIO, output_file: BinaryIO | None) -> None:
|
108
|
+
"""Convert a fpd file to a UTF-16 little-endian unicode text file."""
|
109
|
+
if output_file:
|
110
|
+
output_file.write(BOM_UTF16_LE)
|
111
|
+
Fpd.read_fpd(input_file).write_unicode(output_file)
|
61
112
|
return
|
62
113
|
|
63
|
-
path = Path(
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
114
|
+
path = Path(input_file.name).with_suffix(".txt")
|
115
|
+
|
116
|
+
Fpd.read_fpd(input_file).write_unicode_to_path(path)
|
117
|
+
|
118
|
+
|
119
|
+
@convert.command(name="mtx", show_constraints=True)
|
120
|
+
@cloup.argument(
|
121
|
+
"input_file", help="Mtx file containing Manzai text.", type=cloup.File("rb")
|
122
|
+
)
|
123
|
+
@output_option
|
124
|
+
@mtx_options
|
125
|
+
def convert_mtx(
|
126
|
+
input_file: BinaryIO,
|
127
|
+
output_file: BinaryIO | None,
|
128
|
+
fpd: Path | None,
|
129
|
+
unicode: Path | None,
|
130
|
+
) -> None:
|
131
|
+
"""Convert a mtx file to a XML file.""" # noqa: DOC501
|
132
|
+
if fpd:
|
133
|
+
fpd_data = Fpd.read_fpd_from_path(fpd)
|
134
|
+
elif unicode:
|
135
|
+
fpd_data = Fpd.read_unicode_from_path(unicode)
|
74
136
|
else:
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
"file."
|
79
|
-
)
|
80
|
-
|
81
|
-
fpd_data = Fpd.read_unicode(args.unicode)
|
137
|
+
raise ArgumentError(
|
138
|
+
"You must specify a character table using --fpd or --unicode."
|
139
|
+
)
|
82
140
|
|
83
|
-
if
|
84
|
-
Mtx.read_mtx(
|
141
|
+
if output_file:
|
142
|
+
Mtx.read_mtx(input_file).write_xml(output_file, fpd_data)
|
85
143
|
return
|
86
144
|
|
87
|
-
path = Path(
|
88
|
-
|
89
|
-
if path.suffix != ".xml":
|
90
|
-
path = path.with_suffix(".xml")
|
91
|
-
|
92
|
-
Mtx.read_mtx(args.input).write_xml_to_file(path, fpd_data)
|
93
|
-
|
94
|
-
|
95
|
-
def _create_parsers(main_parser: argparse.ArgumentParser) -> None:
|
96
|
-
shared_options = argparse.ArgumentParser(add_help=False)
|
97
|
-
shared_options.add_argument(
|
98
|
-
"input", type=argparse.FileType("rb"), help="input file"
|
99
|
-
)
|
100
|
-
shared_options.add_argument(
|
101
|
-
"-o", "--output", type=argparse.FileType("wb"), help="output file"
|
102
|
-
)
|
103
|
-
|
104
|
-
mtx_options = argparse.ArgumentParser(
|
105
|
-
add_help=False, parents=[shared_options]
|
106
|
-
)
|
107
|
-
mtx_options_group = mtx_options.add_mutually_exclusive_group(required=True)
|
108
|
-
mtx_options_group.add_argument(
|
109
|
-
"--fpd", type=argparse.FileType("rb"), help="fpd file"
|
110
|
-
)
|
111
|
-
mtx_options_group.add_argument(
|
112
|
-
"--unicode", type=argparse.FileType("rb"), help="unicode text file"
|
113
|
-
)
|
114
|
-
|
115
|
-
sub_parser = main_parser.add_subparsers()
|
116
|
-
|
117
|
-
create_parser = sub_parser.add_parser("create")
|
118
|
-
create_sub_parser = create_parser.add_subparsers(required=True)
|
119
|
-
|
120
|
-
create_fpd_parser = create_sub_parser.add_parser(
|
121
|
-
"fpd",
|
122
|
-
help="create a fpd file from a unicode text file",
|
123
|
-
parents=[shared_options],
|
124
|
-
)
|
125
|
-
create_fpd_parser.set_defaults(func=_create_fpd)
|
126
|
-
|
127
|
-
create_mtx_parser = create_sub_parser.add_parser(
|
128
|
-
"mtx", help="create a mtx file from a XML file", parents=[mtx_options]
|
129
|
-
)
|
130
|
-
create_mtx_parser.set_defaults(func=_create_mtx)
|
131
|
-
|
132
|
-
convert_parser = sub_parser.add_parser("convert")
|
133
|
-
convert_sub_parser = convert_parser.add_subparsers(required=True)
|
134
|
-
|
135
|
-
convert_fpd_parser = convert_sub_parser.add_parser(
|
136
|
-
"fpd",
|
137
|
-
help="convert a fpd file to a unicode file",
|
138
|
-
parents=[shared_options],
|
139
|
-
)
|
140
|
-
convert_fpd_parser.set_defaults(func=_convert_fpd)
|
141
|
-
|
142
|
-
convert_mtx_parser = convert_sub_parser.add_parser(
|
143
|
-
"mtx", help="convert a mtx file to XML file", parents=[mtx_options]
|
144
|
-
)
|
145
|
-
convert_mtx_parser.set_defaults(func=_convert_mtx)
|
146
|
-
|
147
|
-
|
148
|
-
def main() -> None:
|
149
|
-
"""Entry point for the commandline application."""
|
150
|
-
main_parser = argparse.ArgumentParser(
|
151
|
-
description="A conversion tool for files used by older Puyo games."
|
152
|
-
)
|
153
|
-
main_parser.add_argument(
|
154
|
-
"-v", "--version", help="show version", action="store_true"
|
155
|
-
)
|
156
|
-
|
157
|
-
_create_parsers(main_parser)
|
158
|
-
|
159
|
-
args = main_parser.parse_args(namespace=_CliNamespace)
|
160
|
-
|
161
|
-
if args.version is True:
|
162
|
-
package_name = "legacy-puyo-tools"
|
163
|
-
version = metadata.version(package_name)
|
164
|
-
|
165
|
-
print(f"{package_name} {version}")
|
166
|
-
|
167
|
-
sys.exit(0)
|
168
|
-
|
169
|
-
if not hasattr(args, "func"):
|
170
|
-
main_parser.print_help(sys.stderr)
|
171
|
-
sys.exit(1)
|
145
|
+
path = Path(input_file.name).with_suffix(".xml")
|
172
146
|
|
173
|
-
|
147
|
+
Mtx.read_mtx(input_file).write_xml_to_file(path, fpd_data)
|
174
148
|
|
175
149
|
|
176
150
|
if __name__ == "__main__":
|
177
|
-
|
151
|
+
app()
|
legacy_puyo_tools/exceptions.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Exceptions that might be thrown when parsing files.
|
2
2
|
|
3
|
+
SPDX-FileCopyrightText: 2021 Nick Woronekin
|
3
4
|
SPDX-FileCopyrightText: 2025 Samuel Wu
|
4
5
|
SPDX-License-Identifier: MIT
|
5
6
|
"""
|
@@ -11,3 +12,7 @@ class FormatError(Exception):
|
|
11
12
|
|
12
13
|
class FileFormatError(FormatError):
|
13
14
|
"""The file does not conform to a file format or is malformed."""
|
15
|
+
|
16
|
+
|
17
|
+
class ArgumentError(Exception):
|
18
|
+
"""One of the argument is invalid or missing."""
|
legacy_puyo_tools/fpd.py
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
-
"""
|
1
|
+
"""Fpd conversion tool for older Puyo games.
|
2
2
|
|
3
|
-
This module supports the encoding and decoding of the fpd format used by
|
4
|
-
|
3
|
+
This module supports the encoding and decoding of the fpd format used by Puyo Puyo! 15th
|
4
|
+
Anniversary and Puyo Puyo 7.
|
5
5
|
|
6
6
|
SPDX-FileCopyrightText: 2025 Samuel Wu
|
7
7
|
SPDX-License-Identifier: MIT
|
8
8
|
"""
|
9
9
|
|
10
|
+
from __future__ import annotations
|
11
|
+
|
10
12
|
from codecs import BOM_UTF16_LE
|
11
|
-
from io import BytesIO
|
13
|
+
from io import BytesIO, StringIO
|
14
|
+
from os import PathLike
|
12
15
|
from pathlib import Path
|
13
|
-
from typing import BinaryIO
|
16
|
+
from typing import BinaryIO
|
14
17
|
|
15
|
-
|
18
|
+
import attrs
|
16
19
|
|
17
20
|
from legacy_puyo_tools.exceptions import FileFormatError, FormatError
|
18
21
|
|
@@ -22,20 +25,20 @@ UTF16_LENGTH = 2
|
|
22
25
|
WIDTH_ENTRY_OFFSET = 2
|
23
26
|
|
24
27
|
|
25
|
-
@define
|
28
|
+
@attrs.define
|
26
29
|
class FpdCharacter:
|
27
30
|
"""A fpd character entry.
|
28
31
|
|
29
|
-
A fpd character is a binary entry that is 3 bytes long and formatted
|
30
|
-
|
31
|
-
|
32
|
+
A fpd character is a binary entry that is 3 bytes long and formatted as follows:
|
33
|
+
`XX XX YY`. Where `XX XX` is the character encoded in UTF-16 little-endian and `YY`
|
34
|
+
is the width of the character.
|
32
35
|
|
33
36
|
Attributes:
|
34
37
|
code_point:
|
35
38
|
A string that stores a single character.
|
36
39
|
width:
|
37
|
-
How wide should the character be, only used in the Nintendo
|
38
|
-
|
40
|
+
How wide should the character be, only used in the Nintendo DS versions of
|
41
|
+
the games.
|
39
42
|
"""
|
40
43
|
|
41
44
|
code_point: str
|
@@ -53,16 +56,21 @@ class FpdCharacter:
|
|
53
56
|
self.code_point = code_point.decode(ENCODING)
|
54
57
|
self.width = width
|
55
58
|
|
59
|
+
def __str__(self) -> str:
|
60
|
+
"""Returns the character as a single character string."""
|
61
|
+
return self.code_point
|
62
|
+
|
56
63
|
def encode(self) -> bytes:
|
57
64
|
"""Encodes the character back to a fpd character entry.
|
58
65
|
|
59
66
|
Returns:
|
60
67
|
The character in UTF-16 LE format and its width.
|
61
68
|
"""
|
62
|
-
|
69
|
+
# TODO: When updating Python to 3.11, remove arguments from width.to_bytes
|
70
|
+
return self.code_point.encode(ENCODING) + self.width.to_bytes(1, "little")
|
63
71
|
|
64
72
|
@classmethod
|
65
|
-
def decode(cls, fpd_entry: bytes) ->
|
73
|
+
def decode(cls, fpd_entry: bytes) -> FpdCharacter:
|
66
74
|
"""Decodes a fpd character into its code point and width.
|
67
75
|
|
68
76
|
Args:
|
@@ -71,28 +79,25 @@ class FpdCharacter:
|
|
71
79
|
|
72
80
|
Raises:
|
73
81
|
FormatError:
|
74
|
-
The entry given does not conform to the fpd character
|
75
|
-
format.
|
82
|
+
The entry given does not conform to the fpd character format.
|
76
83
|
|
77
84
|
Returns:
|
78
85
|
A fpd character entry containing its code point and width.
|
79
86
|
"""
|
80
87
|
if len(fpd_entry) != FPD_ENTRY_LENGTH:
|
81
|
-
raise FormatError(
|
82
|
-
f"{fpd_entry} does not matches size {FPD_ENTRY_LENGTH}"
|
83
|
-
)
|
88
|
+
raise FormatError(f"{fpd_entry} does not matches size {FPD_ENTRY_LENGTH}")
|
84
89
|
|
85
90
|
return cls(fpd_entry[:UTF16_LENGTH], fpd_entry[WIDTH_ENTRY_OFFSET])
|
86
91
|
|
87
92
|
|
88
|
-
@define
|
93
|
+
@attrs.define
|
89
94
|
class Fpd:
|
90
95
|
"""A fpd character table.
|
91
96
|
|
92
|
-
The fpd stores character table in which each entry is placed right
|
93
|
-
|
94
|
-
|
95
|
-
|
97
|
+
The fpd stores character table in which each entry is placed right next to each
|
98
|
+
other and the indices is offset by multiples of `0x03`. I.e. The 1st character is at
|
99
|
+
index `0x00`, the 2nd character is at index `0x03`, the 3rd character is at index
|
100
|
+
`0x06`, etc.
|
96
101
|
|
97
102
|
Attributes:
|
98
103
|
entries:
|
@@ -113,19 +118,26 @@ class Fpd:
|
|
113
118
|
"""
|
114
119
|
return self.entries[index].code_point
|
115
120
|
|
121
|
+
def __str__(self) -> str:
|
122
|
+
"""Returns a string representation of the fpd character table."""
|
123
|
+
with StringIO() as string_buffer:
|
124
|
+
for character in self.entries:
|
125
|
+
string_buffer.write(str(character))
|
126
|
+
|
127
|
+
return string_buffer.getvalue()
|
128
|
+
|
116
129
|
@classmethod
|
117
|
-
def read_fpd_from_path(cls, path:
|
130
|
+
def read_fpd_from_path(cls, path: str | PathLike[str]) -> Fpd:
|
118
131
|
"""Reads and extract characters from a fpd file.
|
119
132
|
|
120
133
|
Args:
|
121
134
|
path:
|
122
|
-
A path to a fpd encoded file that contains a fpd
|
123
|
-
character table.
|
135
|
+
A path to a fpd encoded file that contains a fpd character table.
|
124
136
|
|
125
137
|
Raises:
|
126
138
|
FileFormatError:
|
127
|
-
The fpd file contain a entry that does not conform
|
128
|
-
|
139
|
+
The fpd file contain a entry that does not conform to the fpd character
|
140
|
+
format.
|
129
141
|
|
130
142
|
Returns:
|
131
143
|
A fpd character table.
|
@@ -137,7 +149,7 @@ class Fpd:
|
|
137
149
|
raise FileFormatError(f"{path} is not a valid fpd file") from e
|
138
150
|
|
139
151
|
@classmethod
|
140
|
-
def read_fpd(cls, fp: BinaryIO) ->
|
152
|
+
def read_fpd(cls, fp: BinaryIO) -> Fpd:
|
141
153
|
"""Reads and extract characters from a file object.
|
142
154
|
|
143
155
|
Args:
|
@@ -150,13 +162,12 @@ class Fpd:
|
|
150
162
|
return cls.decode_fpd(fp.read())
|
151
163
|
|
152
164
|
@classmethod
|
153
|
-
def decode_fpd(cls, data: bytes) ->
|
165
|
+
def decode_fpd(cls, data: bytes) -> Fpd:
|
154
166
|
"""Extracts the fpd character table from a fpd encoded stream.
|
155
167
|
|
156
168
|
Args:
|
157
169
|
data:
|
158
|
-
A fpd encoded stream that contains a fpd character
|
159
|
-
table.
|
170
|
+
A fpd encoded stream that contains a fpd character table.
|
160
171
|
|
161
172
|
Returns:
|
162
173
|
A fpd character table.
|
@@ -166,7 +177,7 @@ class Fpd:
|
|
166
177
|
for i in range(0, len(data), FPD_ENTRY_LENGTH)
|
167
178
|
])
|
168
179
|
|
169
|
-
def write_fpd_to_path(self, path:
|
180
|
+
def write_fpd_to_path(self, path: str | PathLike[str]) -> None:
|
170
181
|
"""Writes the fpd character table to a fpd encoded file.
|
171
182
|
|
172
183
|
Args:
|
@@ -198,31 +209,32 @@ class Fpd:
|
|
198
209
|
return bytes_buffer.getvalue()
|
199
210
|
|
200
211
|
@classmethod
|
201
|
-
def read_unicode_from_path(cls, path:
|
202
|
-
"""Reads and convert characters from a UTF-16
|
212
|
+
def read_unicode_from_path(cls, path: str | PathLike[str]) -> Fpd:
|
213
|
+
"""Reads and convert characters from a UTF-16 little-endian text file.
|
203
214
|
|
204
215
|
Arguments:
|
205
|
-
path:
|
206
|
-
A path to a UTF-16 LE text file.
|
216
|
+
path: A path to a UTF-16 LE text file.
|
207
217
|
|
208
218
|
Raises:
|
209
219
|
FileFormatError:
|
210
|
-
The file is not a UTF-16
|
211
|
-
|
220
|
+
The file is not a UTF-16 little-endian encoded text file or is missing
|
221
|
+
the Byte Order Mark for UTF-16 little-endian.
|
212
222
|
|
213
223
|
Returns:
|
214
224
|
A fpd character table.
|
215
225
|
"""
|
216
226
|
with Path(path).open("rb") as fp:
|
217
|
-
# Check the Byte Order Mark (BOM) to see if it is really a
|
218
|
-
#
|
227
|
+
# Check the Byte Order Mark (BOM) to see if it is really a UTF-16 LE text
|
228
|
+
# file
|
219
229
|
if fp.read(2) != BOM_UTF16_LE:
|
220
|
-
raise FileFormatError(
|
230
|
+
raise FileFormatError(
|
231
|
+
f"{path} is not a UTF-16 little-endian text file."
|
232
|
+
)
|
221
233
|
|
222
234
|
return cls.read_unicode(fp)
|
223
235
|
|
224
236
|
@classmethod
|
225
|
-
def read_unicode(cls, fp: BinaryIO) ->
|
237
|
+
def read_unicode(cls, fp: BinaryIO) -> Fpd:
|
226
238
|
"""Reads and convert UTF-16 LE characters from a file object.
|
227
239
|
|
228
240
|
Args:
|
@@ -234,10 +246,9 @@ class Fpd:
|
|
234
246
|
"""
|
235
247
|
return cls.decode_unicode(fp.read())
|
236
248
|
|
237
|
-
# TODO: Somehow allow people to specify the width of the character
|
238
|
-
# during decoding
|
249
|
+
# TODO: Somehow allow people to specify the width of the character during decoding
|
239
250
|
@classmethod
|
240
|
-
def decode_unicode(cls, unicode: bytes) ->
|
251
|
+
def decode_unicode(cls, unicode: bytes) -> Fpd:
|
241
252
|
"""Converts a UTF-16 LE stream into a fpd character table.
|
242
253
|
|
243
254
|
Args:
|
@@ -252,7 +263,7 @@ class Fpd:
|
|
252
263
|
for i in range(0, len(unicode), UTF16_LENGTH)
|
253
264
|
])
|
254
265
|
|
255
|
-
def write_unicode_to_path(self, path:
|
266
|
+
def write_unicode_to_path(self, path: str | PathLike[str]) -> None:
|
256
267
|
"""Writes the fpd character table to a UTF-16 LE text file.
|
257
268
|
|
258
269
|
Args:
|
@@ -278,11 +289,6 @@ class Fpd:
|
|
278
289
|
"""Encodes the fpd character table into a UTF-16 LE text stream.
|
279
290
|
|
280
291
|
Returns:
|
281
|
-
A UTF-16 LE encoded text stream with characters from the
|
282
|
-
fpd.
|
292
|
+
A UTF-16 LE encoded text stream with characters from the fpd.
|
283
293
|
"""
|
284
|
-
|
285
|
-
for character in self.entries:
|
286
|
-
bytes_buffer.write(character.code_point.encode(ENCODING))
|
287
|
-
|
288
|
-
return bytes_buffer.getvalue()
|
294
|
+
return str(self).encode(ENCODING)
|
legacy_puyo_tools/mtx.py
CHANGED
@@ -1,24 +1,46 @@
|
|
1
1
|
"""Manzai text conversion tool for older Puyo Puyo games.
|
2
2
|
|
3
|
-
This module converts
|
4
|
-
|
5
|
-
Anniversary.
|
3
|
+
This module converts mtx files to and from XML for modding Puyo games. Currently
|
4
|
+
supports Puyo Puyo 7 and might support Puyo Puyo! 15th Anniversary.
|
6
5
|
|
6
|
+
SPDX-FileCopyrightText: 2021 Nick Woronekin
|
7
7
|
SPDX-FileCopyrightText: 2025 Samuel Wu
|
8
8
|
SPDX-License-Identifier: MIT
|
9
9
|
"""
|
10
10
|
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
import sys
|
11
14
|
from collections.abc import Callable
|
12
|
-
from
|
15
|
+
from os import PathLike
|
13
16
|
from pathlib import Path
|
14
|
-
from typing import BinaryIO, Literal
|
17
|
+
from typing import BinaryIO, Literal
|
15
18
|
|
16
|
-
|
19
|
+
import attrs
|
17
20
|
from lxml import etree
|
18
21
|
|
19
22
|
from legacy_puyo_tools.exceptions import FileFormatError, FormatError
|
20
23
|
from legacy_puyo_tools.fpd import Fpd
|
21
24
|
|
25
|
+
# TODO: When updating to Python 3.10, remove the stub implementation of pairwise
|
26
|
+
# from the python documentation:
|
27
|
+
# https://docs.python.org/3/library/itertools.html#itertools.pairwise
|
28
|
+
if sys.version_info >= (3, 10):
|
29
|
+
from itertools import pairwise
|
30
|
+
else:
|
31
|
+
from collections.abc import Iterable
|
32
|
+
from itertools import tee
|
33
|
+
from typing import TypeVar
|
34
|
+
|
35
|
+
T = TypeVar("T")
|
36
|
+
|
37
|
+
def pairwise(iterable: Iterable[T]) -> Iterable[tuple[T, T]]:
|
38
|
+
"""Simple pairwise implementation for Python < 3.10.""" # noqa: DOC201
|
39
|
+
a, b = tee(iterable)
|
40
|
+
next(b, None)
|
41
|
+
return zip(a, b)
|
42
|
+
|
43
|
+
|
22
44
|
CHARACTER_WIDTH = 2
|
23
45
|
ENDIAN = "little"
|
24
46
|
|
@@ -49,29 +71,28 @@ def _identify_mtx(data: bytes) -> tuple[Literal[8, 16], Literal[4, 8]]:
|
|
49
71
|
raise FormatError("The given data is not in a valid `mtx` format")
|
50
72
|
|
51
73
|
|
52
|
-
type
|
74
|
+
# TODO: When upgrading to Python 3.12, add type to the beginning of the alias
|
75
|
+
MtxString = list[int]
|
53
76
|
|
54
77
|
|
55
|
-
@define
|
78
|
+
@attrs.define
|
56
79
|
class Mtx:
|
57
80
|
strings: list[MtxString]
|
58
81
|
|
59
82
|
@classmethod
|
60
|
-
def read_mtx_from_file(cls, path:
|
83
|
+
def read_mtx_from_file(cls, path: str | PathLike[str]) -> Mtx:
|
61
84
|
with Path(path).open("rb") as fp:
|
62
85
|
try:
|
63
86
|
return cls.read_mtx(fp)
|
64
87
|
except FormatError as e:
|
65
|
-
raise FileFormatError(
|
66
|
-
f"{path} is not a valid `mtx` file"
|
67
|
-
) from e
|
88
|
+
raise FileFormatError(f"{path} is not a valid `mtx` file") from e
|
68
89
|
|
69
90
|
@classmethod
|
70
|
-
def read_mtx(cls, fp: BinaryIO) ->
|
91
|
+
def read_mtx(cls, fp: BinaryIO) -> Mtx:
|
71
92
|
return cls.decode_mtx(fp.read())
|
72
93
|
|
73
94
|
@classmethod
|
74
|
-
def decode_mtx(cls, data: bytes) ->
|
95
|
+
def decode_mtx(cls, data: bytes) -> Mtx:
|
75
96
|
length = int.from_bytes(data[:4], ENDIAN)
|
76
97
|
|
77
98
|
if length != len(data):
|
@@ -85,9 +106,7 @@ class Mtx:
|
|
85
106
|
|
86
107
|
sections = [
|
87
108
|
read_offset(data, section_table_offset + (i * int_width))
|
88
|
-
for i in range(
|
89
|
-
(string_table_offset - section_table_offset) // int_width
|
90
|
-
)
|
109
|
+
for i in range((string_table_offset - section_table_offset) // int_width)
|
91
110
|
]
|
92
111
|
|
93
112
|
# Add the length to the sections so we can read to end of stream
|
@@ -97,15 +116,13 @@ class Mtx:
|
|
97
116
|
|
98
117
|
for current_string_offset, next_string_offset in pairwise(sections):
|
99
118
|
strings.append([
|
100
|
-
_read_character(
|
101
|
-
data, current_string_offset + (i * CHARACTER_WIDTH)
|
102
|
-
)
|
119
|
+
_read_character(data, current_string_offset + (i * CHARACTER_WIDTH))
|
103
120
|
for i in range(next_string_offset - current_string_offset)
|
104
121
|
])
|
105
122
|
|
106
123
|
return cls(strings)
|
107
124
|
|
108
|
-
def write_xml_to_file(self, path:
|
125
|
+
def write_xml_to_file(self, path: str | PathLike[str], fpd: Fpd) -> None:
|
109
126
|
with Path(path).open("wb") as fp:
|
110
127
|
self.write_xml(fp, fpd)
|
111
128
|
|
@@ -121,18 +138,18 @@ class Mtx:
|
|
121
138
|
dialog.text = "\n"
|
122
139
|
|
123
140
|
for character in string:
|
124
|
-
match
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
141
|
+
# TODO: When upgrading to Python 3.10, use `match` statements
|
142
|
+
if character == 0xF813:
|
143
|
+
dialog.append(etree.Element("arrow"))
|
144
|
+
# TODO: Figure out what this control character does
|
145
|
+
elif character == 0xF883:
|
146
|
+
dialog.text += "0xF883"
|
147
|
+
elif character == 0xFFFD:
|
148
|
+
dialog.text += "\n"
|
149
|
+
elif character == 0xFFFF:
|
150
|
+
break
|
151
|
+
else:
|
152
|
+
dialog.text += fpd[character]
|
136
153
|
|
137
154
|
etree.indent(root)
|
138
155
|
|
@@ -0,0 +1,128 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: legacy-puyo-tools
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: A tool to edit text for older Puyo Puyo games.
|
5
|
+
Project-URL: changelog, https://github.com/wushenrong/legacy-puyo-tools/blob/main/CHANGELOG.md
|
6
|
+
Project-URL: homepage, https://github.com/wushenrong/legacy-puyo-tools
|
7
|
+
Project-URL: issues, https://github.com/wushenrong/legacy-puyo-tools/issues
|
8
|
+
Project-URL: source, https://github.com/wushenrong/legacy-puyo-tools.git
|
9
|
+
Author-email: Samuel Wu <twopizza9621536@gmail.com>
|
10
|
+
License-Expression: MIT
|
11
|
+
License-File: LICENSE
|
12
|
+
Keywords: puyopuyo
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
14
|
+
Classifier: Environment :: Console
|
15
|
+
Classifier: Intended Audience :: Other Audience
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
17
|
+
Classifier: Operating System :: OS Independent
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
24
|
+
Classifier: Topic :: File Formats
|
25
|
+
Classifier: Topic :: Games/Entertainment :: Puzzle Games
|
26
|
+
Classifier: Topic :: Utilities
|
27
|
+
Requires-Python: >=3.9
|
28
|
+
Requires-Dist: attrs>=25.3.0
|
29
|
+
Requires-Dist: click>=8.1.8
|
30
|
+
Requires-Dist: cloup>=3.0.7
|
31
|
+
Requires-Dist: lxml>=6.0.0
|
32
|
+
Description-Content-Type: text/markdown
|
33
|
+
|
34
|
+
# Legacy Puyo Tools
|
35
|
+
|
36
|
+
A command line tool for modding older Puyo Puyo games (Yes, the name is using a
|
37
|
+
[reversed naming scheme](https://github.com/microsoft/WSL)).
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Install [Python](https://www.python.org/) 3.9 or later, preferably the latest
|
42
|
+
version.
|
43
|
+
|
44
|
+
`legacy-python-tools` is published to
|
45
|
+
[PyPI](https://pypi.org/project/legacy-puyo-tools/). It is recommended to
|
46
|
+
install tools from PyPI into an isolated Python environment.
|
47
|
+
|
48
|
+
You can use [`pipx`](https://pipx.pypa.io):
|
49
|
+
|
50
|
+
```bash
|
51
|
+
pipx install legacy-python-tools
|
52
|
+
# Or to run the cli without installing legacy-python-tools
|
53
|
+
pipx run legacy-python-tools
|
54
|
+
```
|
55
|
+
|
56
|
+
Or [`uv`](https://docs.astral.sh/uv):
|
57
|
+
|
58
|
+
```bash
|
59
|
+
uv tool install legacy-python-tools
|
60
|
+
# Or to run the cli without installing legacy-python-tools
|
61
|
+
uv tool run legacy-python-tools
|
62
|
+
# Or the shorter uvx
|
63
|
+
uvx legacy-python-tools
|
64
|
+
```
|
65
|
+
|
66
|
+
And of course, you can use good old pip in a virtual Python environment using
|
67
|
+
[`virualenv`](https://virtualenv.pypa.io) or the built-in `venv` library:
|
68
|
+
|
69
|
+
```bash
|
70
|
+
# Create a virtual python environment
|
71
|
+
python -m venv .venv
|
72
|
+
# Activate the virtual environment
|
73
|
+
./.venv/Scripts/activate
|
74
|
+
# Install legacy-python-tools
|
75
|
+
python -m pip install legacy-python-tools
|
76
|
+
```
|
77
|
+
|
78
|
+
## Usage
|
79
|
+
|
80
|
+
Create a `fpd` file from a UTF-16 little-endian encoded text file.
|
81
|
+
|
82
|
+
```bash
|
83
|
+
legacy-puyo-tools create fpd puyo14.txt
|
84
|
+
```
|
85
|
+
|
86
|
+
Or convert a `mtx` file to an editable XML file using a `fpd` file.
|
87
|
+
|
88
|
+
```bash
|
89
|
+
legacy-puyo-tools convert mtx --output custom_als.mtx --fpd puyo14.fpd als.xml
|
90
|
+
```
|
91
|
+
|
92
|
+
You can use the `--help` flag to see what sub-commands and options are
|
93
|
+
available.
|
94
|
+
|
95
|
+
## Supported Games
|
96
|
+
|
97
|
+
This tool will try to support formats from the following Puyo games:
|
98
|
+
|
99
|
+
- Puyo Puyo! 15th Annversivery
|
100
|
+
- Puyo Puyo 7
|
101
|
+
- Puyo Puyo!! 20th Annversivery (If there is demand)
|
102
|
+
|
103
|
+
See
|
104
|
+
[Formats](https://github.com/wushenrong/legacy-puyo-tools/blob/main/formats.md)
|
105
|
+
for detailed information about these formats, and the current progress on
|
106
|
+
creating and converting them.
|
107
|
+
|
108
|
+
## Why
|
109
|
+
|
110
|
+
The [Puyo Text Editor][puyo-text-editor] can already do what `legacy-puyo-tools`
|
111
|
+
does and is the inspiration of this tool, but there are advantages to rewrite it
|
112
|
+
in Python:
|
113
|
+
|
114
|
+
[puyo-text-editor]: https://github.com/nickworonekin/puyo-text-editor
|
115
|
+
|
116
|
+
- Better cross compatibility with Linux.
|
117
|
+
- Easier migration when upgrade away from end of life language versions.
|
118
|
+
- Avoids the rigidness of using a pure object-oriented design.
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
If you want to contribute to the project check out
|
123
|
+
[Contributing](https://github.com/wushenrong/legacy-puyo-tools/blob/main/CONTRIBUTING.md).
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
Under the MIT License. Based on [Puyo Text Editor][puyo-text-editor] which is
|
128
|
+
also under the MIT License.
|
@@ -0,0 +1,12 @@
|
|
1
|
+
legacy_puyo_tools/__init__.py,sha256=LjS0jFv5pdhJceptQG9WtToZKBMzwCCXRiCa9YofnO0,277
|
2
|
+
legacy_puyo_tools/__main__.py,sha256=LCp7zRagZzAyqpsJSzgMlRF36sWvMMNC7Ufq52cUdVQ,232
|
3
|
+
legacy_puyo_tools/cli.py,sha256=ql8nEAiojQTpl1ug6VMYIaWEXHko2_JJHk8_w7ZhCac,4128
|
4
|
+
legacy_puyo_tools/exceptions.py,sha256=0xT7q1t1r_YSZAv7OUPUgiui1QN3AwBw4cNyxOt_pkA,472
|
5
|
+
legacy_puyo_tools/fpd.py,sha256=2_FBRSZjmioYYbZ7govMqtEkXdh1Exl3NmfswvZQ1qY,8763
|
6
|
+
legacy_puyo_tools/mtx.py,sha256=3DukplefdG7jnsLaDXoQ68QaUto9qpsmYgMPmF0Pksk,4972
|
7
|
+
legacy_puyo_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
legacy_puyo_tools-0.2.0.dist-info/METADATA,sha256=PqX1AU9Jr4lkZ6ynmTWFLlt1qVP0b1wCJEn-oohLMXY,4042
|
9
|
+
legacy_puyo_tools-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
legacy_puyo_tools-0.2.0.dist-info/entry_points.txt,sha256=U1UoBDFd576Xi_8PuLVUXZcGV6Idk1Bc-UlF9UNYYh0,64
|
11
|
+
legacy_puyo_tools-0.2.0.dist-info/licenses/LICENSE,sha256=4G2c45JIaRJFR-0AbNA1uV354akZGEcbnBgpdNokvGE,1117
|
12
|
+
legacy_puyo_tools-0.2.0.dist-info/RECORD,,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
MIT License
|
2
2
|
|
3
|
-
Copyright (c)
|
3
|
+
Copyright (c) 2021 Nick Woronekin
|
4
|
+
Copyright (c) 2025 Samuel Wu and contributors
|
4
5
|
|
5
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
7
|
this software and associated documentation files (the "Software"), to deal in
|
@@ -1,34 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: legacy-puyo-tools
|
3
|
-
Version: 0.0.1
|
4
|
-
Summary: A tool to edit text for older Puyo Puyo games.
|
5
|
-
Author-email: Samuel Wu <twopizza9621536@gmail.com>
|
6
|
-
License-Expression: MIT
|
7
|
-
License-File: LICENSE
|
8
|
-
Requires-Python: >=3.13
|
9
|
-
Requires-Dist: attrs>=25.3.0
|
10
|
-
Requires-Dist: lxml>=6.0.0
|
11
|
-
Description-Content-Type: text/markdown
|
12
|
-
|
13
|
-
# Legacy Puyo Tools
|
14
|
-
|
15
|
-
Supports Puyo Puyo 7 and possibly Puyo Puyo! 15th Anniversary `mtx` and `fpd`
|
16
|
-
files. Puyo Puyo!! 20th Anniversary is still not supported yet. (Create an issue
|
17
|
-
or pull request to support `fnt` and additional `mtx` controls). Also, legacy as
|
18
|
-
in older Puyo Puyo games, not that this tool is decapitated.
|
19
|
-
|
20
|
-
## Why
|
21
|
-
|
22
|
-
The [Puyo Text Editor][1] can already do what Legacy Puyo Tools does and is the
|
23
|
-
inspiration of this tool, but there are advantages to rewrite it in Python:
|
24
|
-
|
25
|
-
- Better cross compatibility with Linux.
|
26
|
-
- Don't have to update the language version every time it becomes End of Life.
|
27
|
-
- Avoids the rigidness of using a pure object-oriented design.
|
28
|
-
|
29
|
-
## License
|
30
|
-
|
31
|
-
Under the MIT License. Based on [Puyo Text Editor][1] which is also under the
|
32
|
-
MIT License.
|
33
|
-
|
34
|
-
[1]: https://github.com/nickworonekin/puyo-text-editor
|
@@ -1,11 +0,0 @@
|
|
1
|
-
legacy_puyo_tools/__init__.py,sha256=tTzyNju0CFsXWki9ttjO6qnNx4Dlmur-1zgJ-n9toB0,277
|
2
|
-
legacy_puyo_tools/cli.py,sha256=gqwjiuSqAzuJRc_Tei1WRxRWGMC0AycWhf940oY-UrY,4934
|
3
|
-
legacy_puyo_tools/exceptions.py,sha256=vqoMG7VjUddzWGMIvzWtE18U6fnmMW7VcpwdOIoOiBc,341
|
4
|
-
legacy_puyo_tools/fpd.py,sha256=XXVCMlYD_eJAhmIQzDlRFvqm-FG9imm68bLooM2-mi4,8306
|
5
|
-
legacy_puyo_tools/mtx.py,sha256=8dGHE719LuOZ9fi8uEgAKh5YIdCuvo9bXAEbcwxaC8Q,4253
|
6
|
-
legacy_puyo_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
legacy_puyo_tools-0.0.1.dist-info/METADATA,sha256=PCxhnsrQHopcml8PQbgEuBGBP4z_4rPbZWHMkFftFxc,1167
|
8
|
-
legacy_puyo_tools-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
-
legacy_puyo_tools-0.0.1.dist-info/entry_points.txt,sha256=SWkgMVVrmWMKaFJXbb0vx-qAjqMWnxm3_0AVwYCdZLs,65
|
10
|
-
legacy_puyo_tools-0.0.1.dist-info/licenses/LICENSE,sha256=hu6L3ztT5uuc-EMaEgsjrhAHSTh56efyD-tVaEpc7Rw,1066
|
11
|
-
legacy_puyo_tools-0.0.1.dist-info/RECORD,,
|
File without changes
|