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 CHANGED
@@ -1,8 +0,0 @@
1
- from .file import *
2
- from .file.bin import *
3
- from .file.dir import *
4
- from .file.lvl import *
5
- from .file.zip import *
6
- from .item.group import *
7
- from .item.level import *
8
- from .item.save 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
@@ -1,22 +1,16 @@
1
1
  from itertools import chain
2
2
  from typing import Final
3
3
 
4
- from ..item.level import Level, LevelNotFoundError
5
- from . import Location
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 + b"\x00" * (len(output) & 1))
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.replace(b"<EDITUSER> 3 </EDITUSER>", b"<EDITUSER> 2 </EDITUSER>"))
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
- def __len__(self) -> int:
163
- try:
164
- with open(self.path, "rb") as f:
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
- def write(self, new_content: bytes, /) -> None:
178
- with open(self.path, "wb") as f:
179
- f.write(self.compress(new_content))
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
+ }