koro 1.1.5__py3-none-any.whl → 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.
@@ -1,16 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: koro
3
- Version: 1.1.5
3
+ Version: 2.0.0
4
4
  Summary: Tools for manipulating levels made in Marble Saga: Kororinpa
5
5
  Home-page: https://github.com/DigitalDetective47/koro
6
6
  Author: DigitalDetective47
7
7
  Author-email: ninji2701@gmail.com
8
8
  Project-URL: Bug Tracker, https://github.com/DigitalDetective47/koro/issues
9
+ Project-URL: Documentation, https://github.com/DigitalDetective47/koro/wiki
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
11
12
  Classifier: Operating System :: OS Independent
12
13
  Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: File Formats
13
18
  Classifier: Topic :: Games/Entertainment
14
- Requires-Python: >=3.9
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
15
21
  License-File: LICENSE
16
22
 
@@ -0,0 +1,14 @@
1
+ koro/__init__.py,sha256=xS3s862eN55uy83K2Dwchurhh9Euh1te2ZPia2lRVLI,200
2
+ koro/slot/__init__.py,sha256=ldVeVLwXIgPZJsCp4-ix78ewRgsAKVwa8BnpcdnpfIc,419
3
+ koro/slot/bin.py,sha256=XuvEc1rn85j6EYirjvDZhQ0y81lO8dRJPk4JVnyzln4,6686
4
+ koro/slot/file.py,sha256=g5nlOddY2MiGdDVnxY7lpbxE74ZHWhG5VPJJddSY7PY,1726
5
+ koro/slot/save.py,sha256=vZLftvjDyiHgZrMgthE8H-EbWIxtdT_MN3Jb-WIc4Eo,4582
6
+ koro/slot/xml.py,sha256=C2g1OlGDwhlVocEeUg7tYxQf1mfRVNopH0yH1UG1Cvw,42992
7
+ koro/stage/__init__.py,sha256=kQs0k1NwhrRBhNvZOj8edYpKH_v8AkrAkuNm1ioQfI8,2547
8
+ koro/stage/model.py,sha256=h2d9w68iXjQHcpVpEK61QjbYq9p9diRv-aCTiUgpW9c,8717
9
+ koro/stage/part.py,sha256=o7ox1Y2RsfXyWG0Z4MQ49f7rXKP7y_Xl4qYqBBOjtZM,48640
10
+ koro-2.0.0.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
11
+ koro-2.0.0.dist-info/METADATA,sha256=U-1AZdiGe4PjdSmNyJ3B3XNE67p4ipcFxsgADxSKRW8,931
12
+ koro-2.0.0.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
13
+ koro-2.0.0.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
14
+ koro-2.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.1)
2
+ Generator: setuptools (70.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
koro/file/__init__.py DELETED
@@ -1,40 +0,0 @@
1
- from os import remove
2
- from os.path import abspath, exists
3
- from shutil import rmtree
4
-
5
- __all__ = ["Location"]
6
-
7
-
8
- class Location:
9
- __match_args__ = ("path",)
10
- __slots__ = ("_path",)
11
-
12
- _path: str
13
-
14
- def __init__(self, path: str, /) -> None:
15
- self._path = abspath(path)
16
-
17
- def __bool__(self) -> bool:
18
- """Whether this file location is accessible and exists."""
19
- return exists(self.path)
20
-
21
- def delete(self) -> None:
22
- try:
23
- remove(self.path)
24
- except IsADirectoryError:
25
- rmtree(self.path)
26
-
27
- def __eq__(self, other: object, /) -> bool:
28
- return (
29
- self.path == other.path if isinstance(other, Location) else NotImplemented
30
- )
31
-
32
- def __hash__(self) -> int:
33
- return hash(self.path)
34
-
35
- @property
36
- def path(self) -> str:
37
- return self._path
38
-
39
- def __repr__(self) -> str:
40
- return f"{type(self).__name__}({self.path!r})"
koro/file/dir.py DELETED
@@ -1,212 +0,0 @@
1
- from collections.abc import Sequence
2
- from operator import index
3
- from os import SEEK_CUR
4
- from os.path import abspath, dirname, isfile, join
5
- from typing import Final, Optional, SupportsIndex, TypeGuard, overload
6
-
7
- from ..item.group import Group
8
- from ..item.level import Level, LevelNotFoundError
9
- from ..item.save import Page, Save
10
- from . import Location
11
-
12
- __all__ = ["DirGroup", "DirLevel", "DirLevelNotFoundError", "DirSave"]
13
-
14
- _BLOCK_SIZE: Final[int] = 2048
15
- _EMPTY_BLOCK: Final[bytes] = b"\x00" * _BLOCK_SIZE
16
- _LEVEL_ALLOCATION_SIZE: Final[int] = 156864
17
-
18
-
19
- class DirLevelNotFoundError(LevelNotFoundError):
20
- pass
21
-
22
-
23
- class DirLevel(Level):
24
- __match_args__ = ("path", "page", "id")
25
- __slots__ = ("_offset", "_path")
26
-
27
- _offset: int
28
- _path: str
29
-
30
- def __init__(self, path: str, /, page: Page, id: SupportsIndex) -> None:
31
- i = index(id)
32
- if 0 <= i < 20:
33
- self._offset = 8 + _LEVEL_ALLOCATION_SIZE * (i & 3)
34
- self._path = join(abspath(path), f"ed0{(id >> 2) + 5 * page.value}.dat")
35
- else:
36
- raise ValueError(id)
37
-
38
- def __bool__(self) -> bool:
39
- with open(self._path, "rb") as f:
40
- f.seek(self._offset)
41
- return f.read(1) != b"\x00"
42
-
43
- def delete(self) -> None:
44
- with open(self._path, "r+b") as f:
45
- f.seek(self._offset)
46
- more: bool = f.read(1) != b"\x00"
47
- if more:
48
- while more:
49
- f.seek(-1, SEEK_CUR)
50
- f.write(_EMPTY_BLOCK)
51
- more = f.read(1) != b"\x00"
52
- else:
53
- raise DirLevelNotFoundError
54
-
55
- def __eq__(self, other: object, /) -> bool:
56
- return (
57
- self._offset == other._offset and self._path == other._path
58
- if isinstance(other, DirLevel)
59
- else NotImplemented
60
- )
61
-
62
- def __hash__(self) -> int:
63
- return hash((self._offset, self._path))
64
-
65
- @property
66
- def id(self) -> int:
67
- return int(self._path[-5]) % 5 << 2 | self._offset // _LEVEL_ALLOCATION_SIZE
68
-
69
- def __len__(self) -> int:
70
- if self:
71
- with open(self._path, "rb") as f:
72
- f.seek(self._offset)
73
- block: Final[bytearray] = bytearray(f.read(_BLOCK_SIZE))
74
- block_offset: int = 0
75
- while block[-1]:
76
- f.readinto(block)
77
- block_offset += _BLOCK_SIZE
78
- hi: int = _BLOCK_SIZE - 1
79
- lo: int = 0
80
- test: int
81
- while hi != lo:
82
- test = (hi - lo >> 1) + lo
83
- if block[test]:
84
- lo = test + 1
85
- else:
86
- hi = test
87
- return block_offset + hi
88
- else:
89
- raise DirLevelNotFoundError
90
-
91
- @property
92
- def page(self) -> Page:
93
- return Page(ord(self._path[-5]) > 52)
94
-
95
- @property
96
- def path(self) -> str:
97
- return dirname(self._path)
98
-
99
- def read(self) -> bytes:
100
- with open(self._path, "rb") as f:
101
- f.seek(self._offset)
102
- block: Final[bytearray] = bytearray(f.read(_BLOCK_SIZE))
103
- if block[0]:
104
- result: Final[bytearray] = bytearray()
105
- while block[-1]:
106
- result.extend(block)
107
- f.readinto(block)
108
- hi: int = _BLOCK_SIZE - 1
109
- lo: int = 0
110
- test: int
111
- while hi != lo:
112
- test = (hi - lo >> 1) + lo
113
- if block[test]:
114
- lo = test + 1
115
- else:
116
- hi = test
117
- return bytes(result + block[:hi])
118
- else:
119
- raise DirLevelNotFoundError
120
-
121
- def __repr__(self) -> str:
122
- return f"{type(self).__name__}({self.path!r}, {self.page!r}, {self.id!r})"
123
-
124
- def write(self, new_content: bytes, /) -> None:
125
- with open(self._path, "r+b") as f:
126
- f.seek(self._offset)
127
- f.write(new_content)
128
- while f.read(1) != b"\x00":
129
- f.seek(-1, SEEK_CUR)
130
- f.write(_EMPTY_BLOCK)
131
-
132
-
133
- class DirGroup(Group[DirLevel]):
134
- __match_args__ = ("path", "page")
135
- __slots__ = ("_page", "_path")
136
-
137
- _page: Page
138
- _path: str
139
-
140
- def __init__(self, path: str, /, page: Page) -> None:
141
- self._page = page
142
- self._path = abspath(path)
143
-
144
- def __contains__(self, value: object, /) -> TypeGuard[DirLevel]:
145
- return (
146
- isinstance(value, DirLevel)
147
- and value.path == self.path
148
- and value.page is self.page
149
- )
150
-
151
- def count(self, value: object) -> int:
152
- return int(value in self)
153
-
154
- def __eq__(self, other: object, /) -> bool:
155
- return (
156
- self.page == other.page and self.path == other.path
157
- if isinstance(other, DirGroup)
158
- else NotImplemented
159
- )
160
-
161
- @overload
162
- def __getitem__(self, index: SupportsIndex, /) -> DirLevel:
163
- pass
164
-
165
- @overload
166
- def __getitem__(self, indices: slice, /) -> Sequence[DirLevel]:
167
- pass
168
-
169
- def __getitem__(
170
- self, key: SupportsIndex | slice, /
171
- ) -> DirLevel | Sequence[DirLevel]:
172
- if isinstance(key, slice):
173
- return tuple((self[i] for i in range(*key.indices(20))))
174
- else:
175
- i: int = index(key)
176
- if -20 <= i < 20:
177
- return DirLevel(self.path, self.page, i % 20)
178
- else:
179
- raise IndexError(key)
180
-
181
- def __hash__(self) -> int:
182
- return hash((self.path, self.page))
183
-
184
- def index(self, value: object, start: int = 0, stop: Optional[int] = None) -> int:
185
- if value in self and value.id in range(*slice(start, stop).indices(20)):
186
- return value.id
187
- else:
188
- raise ValueError
189
-
190
- @property
191
- def page(self) -> Page:
192
- return self._page
193
-
194
- @property
195
- def path(self) -> str:
196
- return self._path
197
-
198
- def __repr__(self) -> str:
199
- return f"{type(self).__name__}({self.path!r}, {self.page!r})"
200
-
201
-
202
- class DirSave(Location, Save[DirGroup]):
203
- __slots__ = ()
204
-
205
- def __bool__(self) -> bool:
206
- return all((isfile(join(self.path, f"ed0{i}.dat")) for i in range(10)))
207
-
208
- def __getitem__(self, key: Page, /) -> DirGroup:
209
- if isinstance(key, Page):
210
- return DirGroup(self.path, key)
211
- else:
212
- raise KeyError(key)
koro/file/lvl.py DELETED
@@ -1,40 +0,0 @@
1
- from os.path import getsize
2
- from warnings import warn
3
-
4
- from ..item.level import Level, LevelNotFoundError
5
- from . import Location
6
-
7
- __all__ = ["LvlLevel", "LvlLevelNotFoundError"]
8
-
9
-
10
- class LvlLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
11
- pass
12
-
13
-
14
- class LvlLevel(Location, Level):
15
- __slots__ = ()
16
-
17
- def __init__(self, path: str, /) -> None:
18
- super().__init__(path)
19
- warn(
20
- FutureWarning(
21
- "The LVL format for storing level data is deprecated. Level data should be converted to the BIN format using BinLevel for better compression and compatibility."
22
- )
23
- )
24
-
25
- def delete(self) -> None:
26
- try:
27
- return super().delete()
28
- except FileNotFoundError as e:
29
- raise LvlLevelNotFoundError(*e.args)
30
-
31
- def __len__(self) -> int:
32
- return getsize(self.path)
33
-
34
- def read(self) -> bytes:
35
- with open(self.path, "rb") as f:
36
- return f.read()
37
-
38
- def write(self, new_content: bytes, /) -> None:
39
- with open(self.path, "wb") as f:
40
- f.write(new_content)
koro/file/zip.py DELETED
@@ -1,223 +0,0 @@
1
- from collections.abc import Iterable, Sequence
2
- from itertools import filterfalse
3
- from operator import index
4
- from os.path import abspath
5
- from re import fullmatch
6
- from typing import Final, Optional, SupportsIndex, TypeGuard, overload
7
- from warnings import warn
8
- from zipfile import ZipFile, ZipInfo
9
-
10
- from ..item.group import Group
11
- from ..item.level import Level, LevelNotFoundError
12
- from . import Location
13
- from .bin import BinLevel
14
-
15
- __all__ = ["ZipGroup", "ZipLevel", "ZipLevelNotFoundError"]
16
-
17
-
18
- def _id_to_fn(id: SupportsIndex, /) -> str:
19
- return f"{str(index(id) + 1).zfill(2)}.bin"
20
-
21
-
22
- def _id_to_old_fn(id: SupportsIndex, /) -> str:
23
- return f"{str(index(id) + 1).zfill(2)}.lvl"
24
-
25
-
26
- class ZipLevelNotFoundError(KeyError, LevelNotFoundError):
27
- pass
28
-
29
-
30
- class ZipLevel(Level):
31
- __match_args__ = ("path", "id")
32
- __slots__ = ("_id", "_path")
33
-
34
- _id: int
35
- _path: str
36
-
37
- def __init__(self, path: str, /, id: SupportsIndex) -> None:
38
- i: int = index(id)
39
- if 0 <= i < 20:
40
- self._id = i
41
- self._path = abspath(path)
42
- else:
43
- raise ValueError(id)
44
-
45
- def __bool__(self) -> bool:
46
- with ZipFile(self.path) as a:
47
- return self.fn in a.namelist()
48
-
49
- def delete(self) -> None:
50
- with ZipFile(self.path) as a:
51
- if self.fn not in a.namelist() and self.old_fn not in a.namelist():
52
- raise ZipLevelNotFoundError
53
- contents: Final[dict[ZipInfo, bytes]] = {}
54
- for info in filterfalse(
55
- lambda info: info.filename == self.fn or info.filename == self.old_fn,
56
- a.infolist(),
57
- ):
58
- contents[info] = a.read(info)
59
- with ZipFile(self.path, "w") as a:
60
- for x in contents.items():
61
- a.writestr(*x)
62
-
63
- def __eq__(self, other: object, /) -> bool:
64
- return (
65
- self.path == other.path and self.id == other.id
66
- if isinstance(other, ZipLevel)
67
- else NotImplemented
68
- )
69
-
70
- def __hash__(self) -> int:
71
- return hash((self.path, self.id))
72
-
73
- @property
74
- def fn(self) -> str:
75
- return _id_to_fn(self.id)
76
-
77
- @property
78
- def id(self) -> int:
79
- return self._id
80
-
81
- def __len__(self) -> int:
82
- with ZipFile(self.path) as a:
83
- try:
84
- a.getinfo(self.fn)
85
- except KeyError:
86
- try:
87
- return a.getinfo(self.old_fn).file_size
88
- except KeyError:
89
- raise ZipLevelNotFoundError
90
- else:
91
- return int.from_bytes(a.read(self.fn)[8:12], byteorder="big")
92
-
93
- @property
94
- def old_fn(self) -> str:
95
- return _id_to_old_fn(self.id)
96
-
97
- @property
98
- def path(self) -> str:
99
- return self._path
100
-
101
- def read(self) -> bytes:
102
- with ZipFile(self.path) as a:
103
- try:
104
- a.getinfo(self.fn)
105
- except KeyError:
106
- try:
107
- a.getinfo(self.old_fn)
108
- except KeyError:
109
- raise ZipLevelNotFoundError
110
- else:
111
- return a.read(self.old_fn)
112
- else:
113
- return BinLevel.decompress(a.read(self.fn))
114
-
115
- def __repr__(self) -> str:
116
- return f"{type(self).__name__}({self.path!r}, {self.id!r})"
117
-
118
- def write(self, new_content: bytes, /) -> None:
119
- contents: Final[dict[ZipInfo, bytes]] = {}
120
- with ZipFile(self.path, "a") as a:
121
- for info in filterfalse(
122
- lambda info: info.filename == self.fn or info.filename == self.old_fn,
123
- a.infolist(),
124
- ):
125
- contents[info] = a.read(info)
126
- with ZipFile(self.path, "w") as a:
127
- for x in contents.items():
128
- a.writestr(*x)
129
- a.writestr(self.fn, BinLevel.compress(new_content))
130
-
131
-
132
- class ZipGroup(Location, Group[ZipLevel]):
133
- __slots__ = ()
134
-
135
- def __init__(self, path: str) -> None:
136
- super().__init__(path)
137
- try:
138
- with ZipFile(self.path) as a:
139
- for id in range(20):
140
- if _id_to_old_fn(id) in a.namelist():
141
- warn(
142
- FutureWarning(
143
- "This ZipGroup contains levels using the deprecated LVL format. Update this file by writing to it or by using the update function."
144
- )
145
- )
146
- break
147
- except FileNotFoundError:
148
- pass
149
-
150
- def __contains__(self, value: object, /) -> TypeGuard[ZipLevel]:
151
- return isinstance(value, ZipLevel) and value.path == self.path
152
-
153
- def count(self, value: object) -> int:
154
- return int(value in self)
155
-
156
- @overload
157
- def __getitem__(self, index: SupportsIndex, /) -> ZipLevel:
158
- pass
159
-
160
- @overload
161
- def __getitem__(self, indices: slice, /) -> Sequence[ZipLevel]:
162
- pass
163
-
164
- def __getitem__(
165
- self, key: SupportsIndex | slice, /
166
- ) -> ZipLevel | Sequence[ZipLevel]:
167
- if isinstance(key, slice):
168
- return tuple((self[i] for i in range(*key.indices(20))))
169
- elif -20 <= index(key) < 20:
170
- return ZipLevel(self.path, index(key) % 20)
171
- else:
172
- raise IndexError(key)
173
-
174
- def index(self, value: object, start: int = 0, stop: Optional[int] = None) -> int:
175
- if value in self and value.id in range(*slice(start, stop).indices(20)):
176
- return value.id
177
- else:
178
- raise ValueError
179
-
180
- def fill_mask(self) -> Sequence[bool]:
181
- with ZipFile(self.path) as a:
182
- return [
183
- _id_to_fn(i) in a.namelist() or _id_to_old_fn in a.namelist()
184
- for i in range(20)
185
- ]
186
-
187
- def read(self) -> Iterable[Optional[bytes]]:
188
- with ZipFile(self.path) as a:
189
- return [
190
- BinLevel.decompress(a.read(_id_to_fn(id)))
191
- if _id_to_fn(id) in a.namelist()
192
- else a.read(_id_to_old_fn(id))
193
- if _id_to_old_fn(id) in a.namelist()
194
- else None
195
- for id in range(20)
196
- ]
197
-
198
- def update(self) -> None:
199
- self.write(self.read())
200
- warn(
201
- FutureWarning(
202
- "This function will be removed alongside support for the LVL format, and exists soley as a convienient way of converting existing ZIP files to the new BIN format."
203
- )
204
- )
205
-
206
- def write(self, new_content: Iterable[Optional[bytes]], /) -> None:
207
- contents: Final[dict[ZipInfo, bytes]] = {}
208
- if self:
209
- with ZipFile(self.path) as a:
210
- for info in filterfalse(
211
- lambda info: fullmatch(
212
- r"(0[1-9]|1\d|20)\.(bin|lvl)", info.filename
213
- ),
214
- a.infolist(),
215
- ):
216
- contents[info] = a.read(info)
217
- with ZipFile(self.path, "w") as a:
218
- for x in contents.items():
219
- a.writestr(*x)
220
- for id, content in filterfalse(
221
- lambda x: x[1] is None, enumerate(new_content)
222
- ):
223
- a.writestr(_id_to_fn(id), BinLevel.compress(content))
koro/item/__init__.py DELETED
File without changes
koro/item/group.py DELETED
@@ -1,48 +0,0 @@
1
- from abc import ABC
2
- from collections.abc import Iterable, Sequence
3
- from itertools import chain, repeat
4
- from typing import Final, Generic, Optional, TypeVar
5
-
6
- from .level import Level, LevelNotFoundError
7
-
8
- __all__ = ["Group"]
9
-
10
- _L = TypeVar("_L", bound=Level)
11
-
12
-
13
- class Group(ABC, Generic[_L], Sequence[_L]):
14
- """A group of 20 levels.
15
- Note that levels are 0-indexed within this interface, but 1-indexed in-game.
16
- """
17
-
18
- __slots__ = ()
19
-
20
- def fill_mask(self) -> Sequence[bool]:
21
- """Return which level IDs within this Group exist."""
22
- return [bool(l) for l in self]
23
-
24
- def __len__(self) -> int:
25
- return 20
26
-
27
- def read(self) -> Iterable[Optional[bytes]]:
28
- """Read all of the levels within this Group. Empty slots yield None."""
29
- content: Optional[bytes]
30
- result: Final[list[Optional[bytes]]] = []
31
- for level in self:
32
- try:
33
- content = level.read()
34
- except LevelNotFoundError:
35
- content = None
36
- result.append(content)
37
- return result
38
-
39
- def write(self, new_content: Iterable[Optional[bytes]], /) -> None:
40
- """Replace the contents of this Group with the specified new content. None values will empty the slot they correspond to."""
41
- for src, dest in zip(chain(new_content, repeat(None)), self):
42
- if src is None:
43
- try:
44
- dest.delete()
45
- except LevelNotFoundError:
46
- pass
47
- else:
48
- dest.write(src)