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.
- koro/__init__.py +8 -8
- koro/slot/__init__.py +21 -0
- koro/{file → slot}/bin.py +13 -34
- koro/slot/file.py +70 -0
- koro/slot/save.py +137 -0
- koro/slot/xml.py +845 -0
- koro/stage/__init__.py +99 -0
- koro/stage/model.py +288 -0
- koro/stage/part.py +1754 -0
- {koro-1.1.5.dist-info → koro-2.0.0.dist-info}/METADATA +8 -2
- koro-2.0.0.dist-info/RECORD +14 -0
- {koro-1.1.5.dist-info → koro-2.0.0.dist-info}/WHEEL +1 -1
- koro/file/__init__.py +0 -40
- koro/file/dir.py +0 -212
- koro/file/lvl.py +0 -40
- koro/file/zip.py +0 -223
- koro/item/__init__.py +0 -0
- koro/item/group.py +0 -48
- koro/item/level.py +0 -108
- koro/item/save.py +0 -29
- koro-1.1.5.dist-info/RECORD +0 -15
- {koro-1.1.5.dist-info → koro-2.0.0.dist-info}/LICENSE +0 -0
- {koro-1.1.5.dist-info → koro-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,22 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: koro
|
3
|
-
Version:
|
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
|
-
|
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,,
|
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)
|