koro 1.1.6__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- koro/__init__.py +8 -8
- koro/slot/__init__.py +21 -0
- koro/{file → slot}/bin.py +12 -36
- koro/slot/file.py +67 -0
- koro/slot/save.py +135 -0
- koro/slot/xml.py +783 -0
- koro/stage/__init__.py +98 -0
- koro/stage/model.py +284 -0
- koro/stage/part.py +1704 -0
- {koro-1.1.6.dist-info → koro-2.0.0rc2.dist-info}/METADATA +7 -2
- koro-2.0.0rc2.dist-info/RECORD +14 -0
- {koro-1.1.6.dist-info → koro-2.0.0rc2.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.6.dist-info/RECORD +0 -15
- {koro-1.1.6.dist-info → koro-2.0.0rc2.dist-info}/LICENSE +0 -0
- {koro-1.1.6.dist-info → koro-2.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: koro
|
3
|
-
Version:
|
3
|
+
Version: 2.0.0rc2
|
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
|
@@ -10,7 +10,12 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
11
11
|
Classifier: Operating System :: OS Independent
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Classifier: Topic :: File Formats
|
13
17
|
Classifier: Topic :: Games/Entertainment
|
14
|
-
|
18
|
+
Classifier: Typing :: Typed
|
19
|
+
Requires-Python: >=3.11
|
15
20
|
License-File: LICENSE
|
16
21
|
|
@@ -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=D_v8RWoSz4OQ2KezQnVCGX1st6oCcC10vowHD_QjlBg,6659
|
4
|
+
koro/slot/file.py,sha256=GqixeShwrZ6N4YRmlX46fF5OV7zV_fkg9S_r1zewd6U,1698
|
5
|
+
koro/slot/save.py,sha256=G8BN2YwWtwJMG8b_7HCtmRYexS8lswV1SEsHMNpQtzU,4549
|
6
|
+
koro/slot/xml.py,sha256=XmKPlyc-vXRyJY2Tn3E60i2D5OHPYn0YlIBJtLtOWwg,40840
|
7
|
+
koro/stage/__init__.py,sha256=D3OnCSnS5MTTEfYwUOKeUUTwP1R2Q2yqzAUrE6wyC0k,2510
|
8
|
+
koro/stage/model.py,sha256=927aOmUKdQVCJfxs2l1U5upfh37yY3hSQug-cKnCcVo,8561
|
9
|
+
koro/stage/part.py,sha256=yaVSkjA9Snkv-aEANsPyZZVCbbIQakIrjv8nK4rFSEQ,47349
|
10
|
+
koro-2.0.0rc2.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
|
11
|
+
koro-2.0.0rc2.dist-info/METADATA,sha256=kIntIatispnk2v6n_FxzNNOBhb_KoadK7soK4jdaZjQ,857
|
12
|
+
koro-2.0.0rc2.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
13
|
+
koro-2.0.0rc2.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
|
14
|
+
koro-2.0.0rc2.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)
|