koro 1.1.6__py3-none-any.whl → 2.0.0rc1__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 +0 -8
- koro/slot/__init__.py +21 -0
- koro/{file → slot}/bin.py +12 -36
- koro/slot/file.py +65 -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.0rc1.dist-info}/METADATA +7 -2
- koro-2.0.0rc1.dist-info/RECORD +14 -0
- {koro-1.1.6.dist-info → koro-2.0.0rc1.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.0rc1.dist-info}/LICENSE +0 -0
- {koro-1.1.6.dist-info → koro-2.0.0rc1.dist-info}/top_level.txt +0 -0
koro/__init__.py
CHANGED
koro/slot/__init__.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from ..stage import Stage
|
4
|
+
|
5
|
+
__all__ = ["Slot"]
|
6
|
+
|
7
|
+
|
8
|
+
class Slot(ABC):
|
9
|
+
__slots__ = ()
|
10
|
+
|
11
|
+
def __bool__(self) -> bool:
|
12
|
+
"""Return whether this slot is filled."""
|
13
|
+
return self.load() is not None
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def load(self) -> Stage | None:
|
17
|
+
pass
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
def save(self, data: Stage | None, /) -> None:
|
21
|
+
pass
|
koro/{file → slot}/bin.py
RENAMED
@@ -1,22 +1,16 @@
|
|
1
1
|
from itertools import chain
|
2
2
|
from typing import Final
|
3
3
|
|
4
|
-
from ..
|
5
|
-
from . import
|
4
|
+
from ..stage import Stage
|
5
|
+
from .file import FileSlot
|
6
|
+
from .xml import XmlSlot
|
6
7
|
|
7
|
-
__all__ = ["BinLevel", "BinLevelNotFoundError"]
|
8
8
|
|
9
|
-
|
10
|
-
class BinLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
|
11
|
-
pass
|
12
|
-
|
13
|
-
|
14
|
-
class BinLevel(Location, Level):
|
9
|
+
class BinSlot(FileSlot):
|
15
10
|
__slots__ = ()
|
16
11
|
|
17
12
|
@staticmethod
|
18
13
|
def compress(data: bytes, /) -> bytes:
|
19
|
-
"""Compress the given level data into the game's format."""
|
20
14
|
buffer: bytearray = bytearray(1024)
|
21
15
|
buffer_index: int = 958
|
22
16
|
chunk: bytearray
|
@@ -117,11 +111,10 @@ class BinLevel(Location, Level):
|
|
117
111
|
)
|
118
112
|
data_index += test_length
|
119
113
|
output.extend(chunk)
|
120
|
-
return bytes(output
|
114
|
+
return bytes(output)
|
121
115
|
|
122
116
|
@staticmethod
|
123
117
|
def decompress(data: bytes, /) -> bytes:
|
124
|
-
"""Decompress the given data into raw level data."""
|
125
118
|
buffer: Final[bytearray] = bytearray(1024)
|
126
119
|
buffer_index: int = 958
|
127
120
|
handle: int | bytearray
|
@@ -151,29 +144,12 @@ class BinLevel(Location, Level):
|
|
151
144
|
buffer_index = buffer_index + 1 & 1023
|
152
145
|
result.extend(handle)
|
153
146
|
flags >>= 1
|
154
|
-
return bytes(result
|
155
|
-
|
156
|
-
def delete(self) -> None:
|
157
|
-
try:
|
158
|
-
return super().delete()
|
159
|
-
except FileNotFoundError as e:
|
160
|
-
raise BinLevelNotFoundError(*e.args)
|
147
|
+
return bytes(result)
|
161
148
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
f.seek(8)
|
166
|
-
return int.from_bytes(f.read(4), byteorder="big")
|
167
|
-
except FileNotFoundError as e:
|
168
|
-
BinLevelNotFoundError(*e.args)
|
169
|
-
|
170
|
-
def read(self) -> bytes:
|
171
|
-
try:
|
172
|
-
with open(self.path, "rb") as f:
|
173
|
-
return self.decompress(f.read())
|
174
|
-
except FileNotFoundError as e:
|
175
|
-
raise BinLevelNotFoundError(*e.args)
|
149
|
+
@staticmethod
|
150
|
+
def deserialize(data: bytes, /) -> Stage:
|
151
|
+
return XmlSlot.deserialize(BinSlot.decompress(data))
|
176
152
|
|
177
|
-
|
178
|
-
|
179
|
-
|
153
|
+
@staticmethod
|
154
|
+
def serialize(level: Stage, /) -> bytes:
|
155
|
+
return BinSlot.compress(XmlSlot.serialize(level))
|
koro/slot/file.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from os import remove
|
3
|
+
from os.path import isfile
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
from ..stage import Stage
|
7
|
+
from . import Slot
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from _typeshed import StrOrBytesPath
|
11
|
+
|
12
|
+
|
13
|
+
class FileSlot(Slot, ABC):
|
14
|
+
__match_args__ = ("path",)
|
15
|
+
__slots__ = ("_path",)
|
16
|
+
|
17
|
+
_path: StrOrBytesPath
|
18
|
+
|
19
|
+
def __init__(self, path: StrOrBytesPath, /) -> None:
|
20
|
+
self._path = path
|
21
|
+
|
22
|
+
def __bool__(self) -> bool:
|
23
|
+
return isfile(self.path)
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
@abstractmethod
|
27
|
+
def deserialize(data: bytes, /) -> Stage:
|
28
|
+
pass
|
29
|
+
|
30
|
+
def __eq__(self, other: object, /) -> bool:
|
31
|
+
if isinstance(other, FileSlot) and (
|
32
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
33
|
+
):
|
34
|
+
return self.path == other.path
|
35
|
+
else:
|
36
|
+
return NotImplemented
|
37
|
+
|
38
|
+
def __hash__(self) -> int:
|
39
|
+
return hash(self.path)
|
40
|
+
|
41
|
+
def load(self) -> Stage | None:
|
42
|
+
try:
|
43
|
+
with open(self.path, "rb") as f:
|
44
|
+
return self.deserialize(f.read())
|
45
|
+
except FileNotFoundError:
|
46
|
+
return None
|
47
|
+
|
48
|
+
@property
|
49
|
+
def path(self) -> StrOrBytesPath:
|
50
|
+
return self._path
|
51
|
+
|
52
|
+
def __repr__(self) -> str:
|
53
|
+
return f"{type(self).__name__}({self.path!r})"
|
54
|
+
|
55
|
+
def save(self, data: Stage | None) -> None:
|
56
|
+
if data is None:
|
57
|
+
remove(self.path)
|
58
|
+
else:
|
59
|
+
with open(self.path, "wb") as f:
|
60
|
+
f.write(self.serialize(data))
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
@abstractmethod
|
64
|
+
def serialize(stage: Stage, /) -> bytes:
|
65
|
+
pass
|
koro/slot/save.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
from enum import Enum, unique
|
2
|
+
from io import BytesIO
|
3
|
+
from operator import index as ix
|
4
|
+
from os.path import basename, dirname, join
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, SupportsIndex
|
6
|
+
from collections.abc import Mapping, Sequence
|
7
|
+
|
8
|
+
from ..stage import Stage
|
9
|
+
|
10
|
+
from . import Slot
|
11
|
+
from .xml import XmlSlot
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from _typeshed import StrOrBytesPath
|
15
|
+
|
16
|
+
|
17
|
+
__all__ = ["EditorPage", "get_slots", "SaveSlot"]
|
18
|
+
|
19
|
+
|
20
|
+
@unique
|
21
|
+
class EditorPage(Enum):
|
22
|
+
ORIGINAL = 0
|
23
|
+
FRIEND = 1
|
24
|
+
HUDSON = 2
|
25
|
+
|
26
|
+
|
27
|
+
class SaveSlot(Slot):
|
28
|
+
__match_args__ = ("path", "page", "index")
|
29
|
+
__slots__ = ("_offset", "_path")
|
30
|
+
|
31
|
+
_offset: Literal[8, 156392, 312776, 469160]
|
32
|
+
_path: str | bytes
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
path: StrOrBytesPath,
|
37
|
+
page: EditorPage,
|
38
|
+
index: Annotated[
|
39
|
+
SupportsIndex,
|
40
|
+
Literal[
|
41
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
|
42
|
+
],
|
43
|
+
],
|
44
|
+
) -> None:
|
45
|
+
index = ix(index) - 1
|
46
|
+
if index in range(0, 20):
|
47
|
+
self._offset = 8 + 156864 * (index & 3) # type: ignore[assignment]
|
48
|
+
self._path = join(path, f"ed{(index >> 2) + 5 * page.value:02}.dat") # type: ignore[arg-type]
|
49
|
+
else:
|
50
|
+
raise ValueError("index must be between 1 and 20")
|
51
|
+
|
52
|
+
def __bool__(self) -> bool:
|
53
|
+
try:
|
54
|
+
with open(self._path, "rb") as f:
|
55
|
+
f.seek(self._offset)
|
56
|
+
return f.read(1) != b"\x00"
|
57
|
+
except FileNotFoundError:
|
58
|
+
return False
|
59
|
+
|
60
|
+
def __eq__(self, other: Any, /) -> bool:
|
61
|
+
if isinstance(other, SaveSlot):
|
62
|
+
return self._path == other._path and self._offset == other._offset
|
63
|
+
else:
|
64
|
+
return NotImplemented
|
65
|
+
|
66
|
+
def __hash__(self) -> int:
|
67
|
+
return hash((self._offset, self._path))
|
68
|
+
|
69
|
+
@property
|
70
|
+
def index(
|
71
|
+
self,
|
72
|
+
) -> Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]:
|
73
|
+
return (int(basename(self._path)[2:4]) % 5 >> 2 | self._offset // 156864) + 1 # type: ignore[return-value]
|
74
|
+
|
75
|
+
def load(self) -> Stage | None:
|
76
|
+
try:
|
77
|
+
with open(self._path, "rb") as f:
|
78
|
+
f.seek(self._offset)
|
79
|
+
with BytesIO() as b:
|
80
|
+
block: bytearray = bytearray()
|
81
|
+
while True:
|
82
|
+
block.clear()
|
83
|
+
f.readinto1(block)
|
84
|
+
if len(b.getbuffer()) + len(block) > 156864:
|
85
|
+
del block[156864 - len(b.getbuffer()) :]
|
86
|
+
if block[-1]:
|
87
|
+
b.write(block)
|
88
|
+
else:
|
89
|
+
while block:
|
90
|
+
if block[len(block) >> 1]:
|
91
|
+
b.write(block[: (len(block) >> 1) + 1])
|
92
|
+
del block[: (len(block) >> 1) + 1]
|
93
|
+
else:
|
94
|
+
del block[len(block) >> 1 :]
|
95
|
+
data: bytes = b.getvalue()
|
96
|
+
if data:
|
97
|
+
return XmlSlot.deserialize(data)
|
98
|
+
else:
|
99
|
+
return None
|
100
|
+
except FileNotFoundError:
|
101
|
+
return None
|
102
|
+
|
103
|
+
@property
|
104
|
+
def page(self) -> EditorPage:
|
105
|
+
return EditorPage(int(basename(self._path)[2:4]) // 5)
|
106
|
+
|
107
|
+
@property
|
108
|
+
def path(self) -> StrOrBytesPath:
|
109
|
+
return dirname(self._path)
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
return f"{type(self).__name__}({self.path!r}, {self.page!r}, {self.index!r})"
|
113
|
+
|
114
|
+
def save(self, data: Stage | None) -> None:
|
115
|
+
binary: bytes = b"" if data is None else XmlSlot.serialize(data)
|
116
|
+
if len(binary) > 156864:
|
117
|
+
raise ValueError("serialized level data is too large to save")
|
118
|
+
try:
|
119
|
+
with open(self._path, "xb") as f:
|
120
|
+
f.write(bytes(638976))
|
121
|
+
if data is None:
|
122
|
+
return
|
123
|
+
except FileExistsError:
|
124
|
+
pass
|
125
|
+
with open(self._path, "r+b") as f:
|
126
|
+
f.seek(self._offset)
|
127
|
+
f.write(binary)
|
128
|
+
f.write(bytes(156864 - len(binary)))
|
129
|
+
|
130
|
+
|
131
|
+
def get_slots(save: StrOrBytesPath, /) -> Mapping[EditorPage, Sequence[SaveSlot]]:
|
132
|
+
return {
|
133
|
+
page: tuple(SaveSlot(save, page, i) for i in range(1, 21))
|
134
|
+
for page in EditorPage
|
135
|
+
}
|