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.
@@ -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
- `fpd` files. Puyo Puyo!! 20th Anniversary is still not supported yet.
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
@@ -0,0 +1,10 @@
1
+ """Allow the cli to be run from runpy or using `python -m legacy_puyo_tools`.
2
+
3
+ SPDX-FileCopyrightText: 2025 Samuel Wu
4
+ SPDX-License-Identifier: MIT
5
+ """
6
+
7
+ if __name__ == "__main__":
8
+ from legacy_puyo_tools.cli import app
9
+
10
+ app()
legacy_puyo_tools/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- """A commandline application that interfaces with conversion tools.
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
- from attrs import define
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
- @define
25
- class _CliNamespace(argparse.Namespace):
26
- func: Callable[[type[_CliNamespace]], None]
27
- input: BinaryIO
28
- output: BinaryIO
29
- unicode: BinaryIO
30
- fpd: BinaryIO
31
- version: bool
32
-
33
-
34
- def _create_fpd(args: _CliNamespace) -> None:
35
- if args.input.read(2) != BOM_UTF16_LE:
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"{args.input.name} is not a UTF-16 little-endian encoded text"
38
- "file."
70
+ f"{input_file.name} is not a UTF-16 little-endian encoded text file."
39
71
  )
40
72
 
41
- if args.output:
42
- Fpd.read_unicode(args.input).write_fpd(args.output)
73
+ if output_file:
74
+ Fpd.read_unicode(input_file).write_fpd(output_file)
43
75
  return
44
76
 
45
- path = Path(args.input.name).with_suffix("")
46
-
47
- if path.suffix != ".fpd":
48
- path = path.with_suffix(".fpd")
49
-
50
- Fpd.read_unicode(args.input).write_fpd_to_path(path)
51
-
52
-
53
- def _create_mtx(args: _CliNamespace) -> None:
54
- raise NotImplementedError()
55
-
56
-
57
- def _convert_fpd(args: _CliNamespace) -> None:
58
- if args.output:
59
- args.output.write(BOM_UTF16_LE)
60
- Fpd.read_fpd(args.input).write_fpd(args.output)
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(args.input.name).with_suffix("")
64
-
65
- if path.suffix != ".fpd":
66
- path = path.with_suffix(".fpd")
67
-
68
- Fpd.read_fpd(args.input).write_unicode_to_path(path)
69
-
70
-
71
- def _convert_mtx(args: _CliNamespace) -> None:
72
- if args.fpd:
73
- fpd_data = Fpd.read_fpd(args.fpd)
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
- if args.unicode.read(2) != BOM_UTF16_LE:
76
- raise FileFormatError(
77
- f"{args.input.name} is not a UTF-16 little-endian encoded text"
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 args.output:
84
- Mtx.read_mtx(args.input).write_xml(args.output, fpd_data)
141
+ if output_file:
142
+ Mtx.read_mtx(input_file).write_xml(output_file, fpd_data)
85
143
  return
86
144
 
87
- path = Path(args.input.name).with_suffix("")
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
- args.func(args)
147
+ Mtx.read_mtx(input_file).write_xml_to_file(path, fpd_data)
174
148
 
175
149
 
176
150
  if __name__ == "__main__":
177
- main()
151
+ app()
@@ -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
- """fpd conversion tool for older puyo games.
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
- Puyo Puyo! 15th Anniversary and Puyo Puyo 7.
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, Self
16
+ from typing import BinaryIO
14
17
 
15
- from attrs import define
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
- as follows: `XX XX YY`. Where `XX XX` is the character encoded in
31
- UTF-16 LE and `YY` is the width of the character.
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
- DS versions of the games.
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
- return self.code_point.encode(ENCODING) + self.width.to_bytes()
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) -> Self:
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
- next to each other and the indices is offset by multiples of `0x03`.
94
- I.e. The 1st character is at index `0x00`, the 2nd character is at
95
- index `0x03`, the 3rd character is at index `0x06`, etc.
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: Path) -> Self:
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
- to the fpd character format.
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) -> Self:
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) -> Self:
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: Path) -> None:
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: Path) -> Self:
202
- """Reads and convert characters from a UTF-16 LE text file.
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 LE encoded text file or is
211
- missing the Byte Order Mark for UTF-16 LE.
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
- # UTF-16 LE text file
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(f"{path} is not a UTF-16 LE text file.")
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) -> Self:
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) -> Self:
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: Path) -> None:
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
- with BytesIO() as bytes_buffer:
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 `mtx` files to and from XML for modding Puyo games.
4
- Currently supports Puyo Puyo 7 and might support Puyo Puyo! 15th
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 itertools import pairwise
15
+ from os import PathLike
13
16
  from pathlib import Path
14
- from typing import BinaryIO, Literal, Self
17
+ from typing import BinaryIO, Literal
15
18
 
16
- from attrs import define
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 MtxString = list[int]
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: Path) -> Self:
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) -> Self:
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) -> Self:
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: Path, fpd: Fpd) -> None:
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 character:
125
- case 0xF813:
126
- dialog.append(etree.Element("arrow"))
127
- # TODO: Figure out what this control character does
128
- case 0xF883:
129
- dialog.text += "0xF883"
130
- case 0xFFFD:
131
- dialog.text += "\n"
132
- case 0xFFFF:
133
- break
134
- case _:
135
- dialog.text += fpd[character]
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,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ legacy-puyo-tools = legacy_puyo_tools.cli:app
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Samuel Wu
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,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- legacy-puyo-tools = legacy_puyo_tools.cli:main