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
koro/__init__.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
from .
|
2
|
-
from .
|
3
|
-
from .file
|
4
|
-
from .
|
5
|
-
from .
|
6
|
-
from .
|
7
|
-
from .
|
8
|
-
from .
|
1
|
+
from .slot import *
|
2
|
+
from .slot.bin import *
|
3
|
+
from .slot.file import *
|
4
|
+
from .slot.save import *
|
5
|
+
from .slot.xml import *
|
6
|
+
from .stage import *
|
7
|
+
from .stage.model import *
|
8
|
+
from .stage.part import *
|
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,67 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from os import remove
|
3
|
+
from os.path import isfile
|
4
|
+
from typing import TYPE_CHECKING, Any
|
5
|
+
|
6
|
+
from ..stage import Stage
|
7
|
+
from . import Slot
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from _typeshed import StrOrBytesPath
|
11
|
+
else:
|
12
|
+
StrOrBytesPath = Any
|
13
|
+
|
14
|
+
|
15
|
+
class FileSlot(Slot, ABC):
|
16
|
+
__match_args__ = ("path",)
|
17
|
+
__slots__ = ("_path",)
|
18
|
+
|
19
|
+
_path: StrOrBytesPath
|
20
|
+
|
21
|
+
def __init__(self, path: StrOrBytesPath, /) -> None:
|
22
|
+
self._path = path
|
23
|
+
|
24
|
+
def __bool__(self) -> bool:
|
25
|
+
return isfile(self.path)
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
@abstractmethod
|
29
|
+
def deserialize(data: bytes, /) -> Stage:
|
30
|
+
pass
|
31
|
+
|
32
|
+
def __eq__(self, other: object, /) -> bool:
|
33
|
+
if isinstance(other, FileSlot) and (
|
34
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
35
|
+
):
|
36
|
+
return self.path == other.path
|
37
|
+
else:
|
38
|
+
return NotImplemented
|
39
|
+
|
40
|
+
def __hash__(self) -> int:
|
41
|
+
return hash(self.path)
|
42
|
+
|
43
|
+
def load(self) -> Stage | None:
|
44
|
+
try:
|
45
|
+
with open(self.path, "rb") as f:
|
46
|
+
return self.deserialize(f.read())
|
47
|
+
except FileNotFoundError:
|
48
|
+
return None
|
49
|
+
|
50
|
+
@property
|
51
|
+
def path(self) -> StrOrBytesPath:
|
52
|
+
return self._path
|
53
|
+
|
54
|
+
def __repr__(self) -> str:
|
55
|
+
return f"{type(self).__name__}({self.path!r})"
|
56
|
+
|
57
|
+
def save(self, data: Stage | None) -> None:
|
58
|
+
if data is None:
|
59
|
+
remove(self.path)
|
60
|
+
else:
|
61
|
+
with open(self.path, "wb") as f:
|
62
|
+
f.write(self.serialize(data))
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
@abstractmethod
|
66
|
+
def serialize(stage: Stage, /) -> bytes:
|
67
|
+
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
|
+
}
|