legacy-puyo-tools 0.0.1__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.
@@ -0,0 +1,8 @@
1
+ """A conversion tool for files used by the older Puyo games.
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.
5
+
6
+ SPDX-FileCopyrightText: 2025 Samuel Wu
7
+ SPDX-License-Identifier: MIT
8
+ """
@@ -0,0 +1,177 @@
1
+ """A commandline application that interfaces with conversion tools.
2
+
3
+ SPDX-FileCopyrightText: 2025 Samuel Wu
4
+ SPDX-License-Identifier: MIT
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from codecs import BOM_UTF16_LE
12
+ from collections.abc import Callable
13
+ from importlib import metadata
14
+ from pathlib import Path
15
+ from typing import BinaryIO
16
+
17
+ from attrs import define
18
+
19
+ from legacy_puyo_tools.exceptions import FileFormatError
20
+ from legacy_puyo_tools.fpd import Fpd
21
+ from legacy_puyo_tools.mtx import Mtx
22
+
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:
36
+ raise FileFormatError(
37
+ f"{args.input.name} is not a UTF-16 little-endian encoded text"
38
+ "file."
39
+ )
40
+
41
+ if args.output:
42
+ Fpd.read_unicode(args.input).write_fpd(args.output)
43
+ return
44
+
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)
61
+ return
62
+
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)
74
+ 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)
82
+
83
+ if args.output:
84
+ Mtx.read_mtx(args.input).write_xml(args.output, fpd_data)
85
+ return
86
+
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)
172
+
173
+ args.func(args)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
@@ -0,0 +1,13 @@
1
+ """Exceptions that might be thrown when parsing files.
2
+
3
+ SPDX-FileCopyrightText: 2025 Samuel Wu
4
+ SPDX-License-Identifier: MIT
5
+ """
6
+
7
+
8
+ class FormatError(Exception):
9
+ """The current input data does not conform to an expected format."""
10
+
11
+
12
+ class FileFormatError(FormatError):
13
+ """The file does not conform to a file format or is malformed."""
@@ -0,0 +1,288 @@
1
+ """fpd conversion tool for older puyo games.
2
+
3
+ This module supports the encoding and decoding of the fpd format used by
4
+ Puyo Puyo! 15th Anniversary and Puyo Puyo 7.
5
+
6
+ SPDX-FileCopyrightText: 2025 Samuel Wu
7
+ SPDX-License-Identifier: MIT
8
+ """
9
+
10
+ from codecs import BOM_UTF16_LE
11
+ from io import BytesIO
12
+ from pathlib import Path
13
+ from typing import BinaryIO, Self
14
+
15
+ from attrs import define
16
+
17
+ from legacy_puyo_tools.exceptions import FileFormatError, FormatError
18
+
19
+ ENCODING = "utf-16-le"
20
+ FPD_ENTRY_LENGTH = 3
21
+ UTF16_LENGTH = 2
22
+ WIDTH_ENTRY_OFFSET = 2
23
+
24
+
25
+ @define
26
+ class FpdCharacter:
27
+ """A fpd character entry.
28
+
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
+
33
+ Attributes:
34
+ code_point:
35
+ A string that stores a single character.
36
+ width:
37
+ How wide should the character be, only used in the Nintendo
38
+ DS versions of the games.
39
+ """
40
+
41
+ code_point: str
42
+ width: int
43
+
44
+ def __init__(self, code_point: bytes, width: int = 0x00) -> None:
45
+ """Initializes a fpd character.
46
+
47
+ Args:
48
+ code_point:
49
+ A Unicode character in the UTF-16 LE format.
50
+ width:
51
+ The width of the character. Defaults to 0x00.
52
+ """
53
+ self.code_point = code_point.decode(ENCODING)
54
+ self.width = width
55
+
56
+ def encode(self) -> bytes:
57
+ """Encodes the character back to a fpd character entry.
58
+
59
+ Returns:
60
+ The character in UTF-16 LE format and its width.
61
+ """
62
+ return self.code_point.encode(ENCODING) + self.width.to_bytes()
63
+
64
+ @classmethod
65
+ def decode(cls, fpd_entry: bytes) -> Self:
66
+ """Decodes a fpd character into its code point and width.
67
+
68
+ Args:
69
+ fpd_entry:
70
+ A fpd character entry that is 3 bytes long.
71
+
72
+ Raises:
73
+ FormatError:
74
+ The entry given does not conform to the fpd character
75
+ format.
76
+
77
+ Returns:
78
+ A fpd character entry containing its code point and width.
79
+ """
80
+ if len(fpd_entry) != FPD_ENTRY_LENGTH:
81
+ raise FormatError(
82
+ f"{fpd_entry} does not matches size {FPD_ENTRY_LENGTH}"
83
+ )
84
+
85
+ return cls(fpd_entry[:UTF16_LENGTH], fpd_entry[WIDTH_ENTRY_OFFSET])
86
+
87
+
88
+ @define
89
+ class Fpd:
90
+ """A fpd character table.
91
+
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.
96
+
97
+ Attributes:
98
+ entries:
99
+ A list of fpd character entries.
100
+ """
101
+
102
+ entries: list[FpdCharacter]
103
+
104
+ def __getitem__(self, index: int) -> str:
105
+ """Retrieves a character from the fpd character table.
106
+
107
+ Args:
108
+ index:
109
+ The index of the character to retrieve.
110
+
111
+ Returns:
112
+ A string that contains the requested character.
113
+ """
114
+ return self.entries[index].code_point
115
+
116
+ @classmethod
117
+ def read_fpd_from_path(cls, path: Path) -> Self:
118
+ """Reads and extract characters from a fpd file.
119
+
120
+ Args:
121
+ path:
122
+ A path to a fpd encoded file that contains a fpd
123
+ character table.
124
+
125
+ Raises:
126
+ FileFormatError:
127
+ The fpd file contain a entry that does not conform
128
+ to the fpd character format.
129
+
130
+ Returns:
131
+ A fpd character table.
132
+ """
133
+ with Path(path).open("rb") as fp:
134
+ try:
135
+ return cls.read_fpd(fp)
136
+ except FormatError as e:
137
+ raise FileFormatError(f"{path} is not a valid fpd file") from e
138
+
139
+ @classmethod
140
+ def read_fpd(cls, fp: BinaryIO) -> Self:
141
+ """Reads and extract characters from a file object.
142
+
143
+ Args:
144
+ fp:
145
+ A file object that points to a fpd encoded stream.
146
+
147
+ Returns:
148
+ A fpd character table.
149
+ """
150
+ return cls.decode_fpd(fp.read())
151
+
152
+ @classmethod
153
+ def decode_fpd(cls, data: bytes) -> Self:
154
+ """Extracts the fpd character table from a fpd encoded stream.
155
+
156
+ Args:
157
+ data:
158
+ A fpd encoded stream that contains a fpd character
159
+ table.
160
+
161
+ Returns:
162
+ A fpd character table.
163
+ """
164
+ return cls([
165
+ FpdCharacter.decode(data[i : i + FPD_ENTRY_LENGTH])
166
+ for i in range(0, len(data), FPD_ENTRY_LENGTH)
167
+ ])
168
+
169
+ def write_fpd_to_path(self, path: Path) -> None:
170
+ """Writes the fpd character table to a fpd encoded file.
171
+
172
+ Args:
173
+ path:
174
+ A path to write the fpd encoded stream.
175
+ """
176
+ with Path(path).open("wb") as fp:
177
+ self.write_fpd(fp)
178
+
179
+ def write_fpd(self, fp: BinaryIO) -> None:
180
+ """Writes the fpd character table to a file object.
181
+
182
+ Args:
183
+ fp:
184
+ A binary file object.
185
+ """
186
+ fp.write(self.encode_fpd())
187
+
188
+ def encode_fpd(self) -> bytes:
189
+ """Encodes the fpd character table into a fpd encoded stream.
190
+
191
+ Returns:
192
+ A fpd encoded stream that contains the fpd character table.
193
+ """
194
+ with BytesIO() as bytes_buffer:
195
+ for character in self.entries:
196
+ bytes_buffer.write(character.encode())
197
+
198
+ return bytes_buffer.getvalue()
199
+
200
+ @classmethod
201
+ def read_unicode_from_path(cls, path: Path) -> Self:
202
+ """Reads and convert characters from a UTF-16 LE text file.
203
+
204
+ Arguments:
205
+ path:
206
+ A path to a UTF-16 LE text file.
207
+
208
+ Raises:
209
+ 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.
212
+
213
+ Returns:
214
+ A fpd character table.
215
+ """
216
+ 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
219
+ if fp.read(2) != BOM_UTF16_LE:
220
+ raise FileFormatError(f"{path} is not a UTF-16 LE text file.")
221
+
222
+ return cls.read_unicode(fp)
223
+
224
+ @classmethod
225
+ def read_unicode(cls, fp: BinaryIO) -> Self:
226
+ """Reads and convert UTF-16 LE characters from a file object.
227
+
228
+ Args:
229
+ fp:
230
+ A binary file object.
231
+
232
+ Returns:
233
+ A fpd character table.
234
+ """
235
+ return cls.decode_unicode(fp.read())
236
+
237
+ # TODO: Somehow allow people to specify the width of the character
238
+ # during decoding
239
+ @classmethod
240
+ def decode_unicode(cls, unicode: bytes) -> Self:
241
+ """Converts a UTF-16 LE stream into a fpd character table.
242
+
243
+ Args:
244
+ unicode:
245
+ A UTF-16 LE encoded character stream.
246
+
247
+ Returns:
248
+ A fpd character table.
249
+ """
250
+ return cls([
251
+ FpdCharacter(unicode[i : i + UTF16_LENGTH])
252
+ for i in range(0, len(unicode), UTF16_LENGTH)
253
+ ])
254
+
255
+ def write_unicode_to_path(self, path: Path) -> None:
256
+ """Writes the fpd character table to a UTF-16 LE text file.
257
+
258
+ Args:
259
+ path:
260
+ A path to store the converted UTF-16 LE text file.
261
+ """
262
+ with Path(path).open("wb") as fp:
263
+ # Write the Byte Order Mark (BOM) for plain text editors
264
+ fp.write(BOM_UTF16_LE)
265
+
266
+ self.write_unicode(fp)
267
+
268
+ def write_unicode(self, fp: BinaryIO) -> None:
269
+ """Writes the fpd character table to a file object.
270
+
271
+ Args:
272
+ fp:
273
+ A binary file object.
274
+ """
275
+ fp.write(self.encode_unicode())
276
+
277
+ def encode_unicode(self) -> bytes:
278
+ """Encodes the fpd character table into a UTF-16 LE text stream.
279
+
280
+ Returns:
281
+ A UTF-16 LE encoded text stream with characters from the
282
+ fpd.
283
+ """
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()
@@ -0,0 +1,141 @@
1
+ """Manzai text conversion tool for older Puyo Puyo games.
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.
6
+
7
+ SPDX-FileCopyrightText: 2025 Samuel Wu
8
+ SPDX-License-Identifier: MIT
9
+ """
10
+
11
+ from collections.abc import Callable
12
+ from itertools import pairwise
13
+ from pathlib import Path
14
+ from typing import BinaryIO, Literal, Self
15
+
16
+ from attrs import define
17
+ from lxml import etree
18
+
19
+ from legacy_puyo_tools.exceptions import FileFormatError, FormatError
20
+ from legacy_puyo_tools.fpd import Fpd
21
+
22
+ CHARACTER_WIDTH = 2
23
+ ENDIAN = "little"
24
+
25
+ INT32_OFFSET = 8
26
+ INT32_SIZE = 4
27
+ INT64_OFFSET = 16
28
+ INT64_SIZE = 8
29
+
30
+
31
+ def _read_character(data: bytes, i: int) -> int:
32
+ return int.from_bytes(data[i : i + CHARACTER_WIDTH], ENDIAN)
33
+
34
+
35
+ def _create_offset_reader(width: int) -> Callable[[bytes, int], int]:
36
+ def offset_reader(data: bytes, i: int) -> int:
37
+ return int.from_bytes(data[i : i + width], ENDIAN)
38
+
39
+ return offset_reader
40
+
41
+
42
+ def _identify_mtx(data: bytes) -> tuple[Literal[8, 16], Literal[4, 8]]:
43
+ if int.from_bytes(data[:4], ENDIAN) == INT32_OFFSET:
44
+ return (INT32_OFFSET, INT32_SIZE)
45
+
46
+ if int.from_bytes(data[8:16], ENDIAN) == INT64_OFFSET:
47
+ return (INT64_OFFSET, INT64_SIZE)
48
+
49
+ raise FormatError("The given data is not in a valid `mtx` format")
50
+
51
+
52
+ type MtxString = list[int]
53
+
54
+
55
+ @define
56
+ class Mtx:
57
+ strings: list[MtxString]
58
+
59
+ @classmethod
60
+ def read_mtx_from_file(cls, path: Path) -> Self:
61
+ with Path(path).open("rb") as fp:
62
+ try:
63
+ return cls.read_mtx(fp)
64
+ except FormatError as e:
65
+ raise FileFormatError(
66
+ f"{path} is not a valid `mtx` file"
67
+ ) from e
68
+
69
+ @classmethod
70
+ def read_mtx(cls, fp: BinaryIO) -> Self:
71
+ return cls.decode_mtx(fp.read())
72
+
73
+ @classmethod
74
+ def decode_mtx(cls, data: bytes) -> Self:
75
+ length = int.from_bytes(data[:4], ENDIAN)
76
+
77
+ if length != len(data):
78
+ raise FormatError("The size of the given data does not match")
79
+
80
+ section_table_index_offset, int_width = _identify_mtx(data[4:16])
81
+ read_offset = _create_offset_reader(int_width)
82
+
83
+ section_table_offset = read_offset(data, section_table_index_offset)
84
+ string_table_offset = read_offset(data, section_table_offset)
85
+
86
+ sections = [
87
+ read_offset(data, section_table_offset + (i * int_width))
88
+ for i in range(
89
+ (string_table_offset - section_table_offset) // int_width
90
+ )
91
+ ]
92
+
93
+ # Add the length to the sections so we can read to end of stream
94
+ sections.append(length)
95
+
96
+ strings: list[MtxString] = []
97
+
98
+ for current_string_offset, next_string_offset in pairwise(sections):
99
+ strings.append([
100
+ _read_character(
101
+ data, current_string_offset + (i * CHARACTER_WIDTH)
102
+ )
103
+ for i in range(next_string_offset - current_string_offset)
104
+ ])
105
+
106
+ return cls(strings)
107
+
108
+ def write_xml_to_file(self, path: Path, fpd: Fpd) -> None:
109
+ with Path(path).open("wb") as fp:
110
+ self.write_xml(fp, fpd)
111
+
112
+ def write_xml(self, fp: BinaryIO, fpd: Fpd) -> None:
113
+ fp.write(self.encode_xml(fpd))
114
+
115
+ def encode_xml(self, fpd: Fpd) -> bytes:
116
+ root = etree.Element("mtx")
117
+ sheet = etree.SubElement(root, "sheet")
118
+
119
+ for string in self.strings:
120
+ dialog = etree.SubElement(sheet, "text")
121
+ dialog.text = "\n"
122
+
123
+ 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]
136
+
137
+ etree.indent(root)
138
+
139
+ return etree.tostring(
140
+ root, encoding="utf-8", xml_declaration=True, pretty_print=True
141
+ )
File without changes
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,11 @@
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ legacy-puyo-tools = legacy_puyo_tools.cli:main
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Samuel Wu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.