koro 1.1.6__tar.gz → 2.0.0rc2__tar.gz

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,6 +10,11 @@ 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
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = koro
3
- version = 1.1.6
3
+ version = 2.0.0rc2
4
4
  author = DigitalDetective47
5
5
  author_email = ninji2701@gmail.com
6
6
  description = Tools for manipulating levels made in Marble Saga: Kororinpa
@@ -12,13 +12,18 @@ classifiers =
12
12
  License :: OSI Approved :: The Unlicense (Unlicense)
13
13
  Operating System :: OS Independent
14
14
  Programming Language :: Python :: 3
15
+ Programming Language :: Python :: 3 :: Only
16
+ Programming Language :: Python :: 3.11
17
+ Programming Language :: Python :: 3.12
18
+ Topic :: File Formats
15
19
  Topic :: Games/Entertainment
20
+ Typing :: Typed
16
21
 
17
22
  [options]
18
23
  package_dir =
19
24
  = src
20
25
  packages = find:
21
- python_requires = >=3.9
26
+ python_requires = >=3.11
22
27
 
23
28
  [options.packages.find]
24
29
  where = src
@@ -0,0 +1,8 @@
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 *
@@ -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))
@@ -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
@@ -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
+ }