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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: koro
3
- Version: 1.1.6
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
- Requires-Python: >=3.9
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (70.2.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)