koro 1.1.6__tar.gz → 2.0.0.post1__tar.gz
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-2.0.0.post1/PKG-INFO +51 -0
- koro-2.0.0.post1/README.md +29 -0
- {koro-1.1.6 → koro-2.0.0.post1}/setup.cfg +9 -2
- koro-2.0.0.post1/src/koro/__init__.py +8 -0
- koro-2.0.0.post1/src/koro/slot/__init__.py +21 -0
- {koro-1.1.6/src/koro/file → koro-2.0.0.post1/src/koro/slot}/bin.py +13 -34
- koro-2.0.0.post1/src/koro/slot/file.py +70 -0
- koro-2.0.0.post1/src/koro/slot/save.py +137 -0
- koro-2.0.0.post1/src/koro/slot/xml.py +845 -0
- koro-2.0.0.post1/src/koro/stage/__init__.py +99 -0
- koro-2.0.0.post1/src/koro/stage/model.py +288 -0
- koro-2.0.0.post1/src/koro/stage/part.py +1754 -0
- koro-2.0.0.post1/src/koro.egg-info/PKG-INFO +51 -0
- koro-2.0.0.post1/src/koro.egg-info/SOURCES.txt +17 -0
- koro-1.1.6/PKG-INFO +0 -15
- koro-1.1.6/src/koro/__init__.py +0 -8
- koro-1.1.6/src/koro/file/__init__.py +0 -40
- koro-1.1.6/src/koro/file/dir.py +0 -212
- koro-1.1.6/src/koro/file/lvl.py +0 -40
- koro-1.1.6/src/koro/file/zip.py +0 -223
- koro-1.1.6/src/koro/item/__init__.py +0 -0
- koro-1.1.6/src/koro/item/group.py +0 -48
- koro-1.1.6/src/koro/item/level.py +0 -108
- koro-1.1.6/src/koro/item/save.py +0 -29
- koro-1.1.6/src/koro.egg-info/PKG-INFO +0 -15
- koro-1.1.6/src/koro.egg-info/SOURCES.txt +0 -17
- {koro-1.1.6 → koro-2.0.0.post1}/LICENSE +0 -0
- {koro-1.1.6 → koro-2.0.0.post1}/pyproject.toml +0 -0
- {koro-1.1.6 → koro-2.0.0.post1}/src/koro.egg-info/dependency_links.txt +0 -0
- {koro-1.1.6 → koro-2.0.0.post1}/src/koro.egg-info/top_level.txt +0 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: koro
|
3
|
+
Version: 2.0.0.post1
|
4
|
+
Summary: Tools for manipulating levels made in Marble Saga: Kororinpa
|
5
|
+
Home-page: https://github.com/DigitalDetective47/koro
|
6
|
+
Author: DigitalDetective47
|
7
|
+
Author-email: ninji2701@gmail.com
|
8
|
+
Project-URL: Bug Tracker, https://github.com/DigitalDetective47/koro/issues
|
9
|
+
Project-URL: Documentation, https://github.com/DigitalDetective47/koro/wiki
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
11
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Topic :: File Formats
|
18
|
+
Classifier: Topic :: Games/Entertainment
|
19
|
+
Classifier: Typing :: Typed
|
20
|
+
Requires-Python: >=3.11
|
21
|
+
License-File: LICENSE
|
22
|
+
|
23
|
+
koro (stylized in all lowercase) is a Python package that can read and modify stages from *Marble Saga: Kororinpa*.
|
24
|
+
|
25
|
+
# *Marble Saga: Kororinpa*
|
26
|
+
|
27
|
+
*Marble Saga: Kororinpa* is a video game released for the Nintendo Wii in March of 2009 in North America; the game was released in PAL regions under the title *Marbles! Balance Challenge* in May of the same year. Like its predecessor *Kororinpa*/*Kororinpa: Marble Mania*, it is a ball‐rolling game which is very similar to the *Super Monkey Ball* series in which the player character is controlled by tilting the game world. This game makes use of the Wii Remote's motion control capabilities by using the orientation of the controller to manipulate the world.
|
28
|
+
|
29
|
+
# Problems
|
30
|
+
|
31
|
+
*Marble Saga: Kororinpa* included a stage editor in which parts could be created by combining junk parts collected within the main game. The game provides the player with 20 slots in which to save stages that they have created. During the time period following the game's release, players could share their created stages using the WiiConnect24 service. After WiiConnect24 shut down on the 28<sup>th</sup> of June 2013, sharing stages with other players became impossible through official means. Sharing save files is not possible through the Wii system menu as the game had online leaderboards for ten stages specifically designed for online competition. As a result, saves of this game are marked as protected and cannot be copied from the save manager present in the Wii system menu.
|
32
|
+
|
33
|
+
# This package
|
34
|
+
|
35
|
+
This package allows you to extract the saved stages from your save file and store them in their own files, and to import stages downloaded online into your existing save file. (This package does not provide tools to get saves to or from the Wii console, there is plenty of homebrew software already in existence for this purpose.) This package also contains reverse‐engineered replicas of the game's compression format used internally, allowing for stage substitution in mods.
|
36
|
+
|
37
|
+
# Usage
|
38
|
+
|
39
|
+
To install this package, simply run
|
40
|
+
```
|
41
|
+
pip install koro
|
42
|
+
```
|
43
|
+
in a command prompt. For detailed documentation of the contents of the package, please view the wiki. For basic users, simple command‐line tools are available in the `scripts` folder of this repository. **Use of these tools requires installing the package from PyPI.**
|
44
|
+
|
45
|
+
## Playing downloaded stages
|
46
|
+
|
47
|
+
`unpacker.py` is a script designed to inject stages downloaded online into your save file. Simply run the script with the stages, the data directory of your save file, and if injecting a single stage, the slot to inject it into. The stages should then appear in the **Friend** tab. To find the location of your save in Dophin, right‐click the game and select **Open Wii Save Folder**.
|
48
|
+
|
49
|
+
## Uploading your stages
|
50
|
+
|
51
|
+
`packer.py` is a script that allows you to easily extract and upload stages that you've created. Run the script with your save directory, destination (ZIP archive), and optionally which stages to export. This script only exports stages stored in the **Original** tab of the editor. To specify which stages to export, simply enter the stage numbers in the order that they should appear when downloaded. If a custom ordering is not specified, the default is to extract all 20 stages in the order that they appear in‐game. To share single levels, extract files from the resulting archive.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
koro (stylized in all lowercase) is a Python package that can read and modify stages from *Marble Saga: Kororinpa*.
|
2
|
+
|
3
|
+
# *Marble Saga: Kororinpa*
|
4
|
+
|
5
|
+
*Marble Saga: Kororinpa* is a video game released for the Nintendo Wii in March of 2009 in North America; the game was released in PAL regions under the title *Marbles! Balance Challenge* in May of the same year. Like its predecessor *Kororinpa*/*Kororinpa: Marble Mania*, it is a ball‐rolling game which is very similar to the *Super Monkey Ball* series in which the player character is controlled by tilting the game world. This game makes use of the Wii Remote's motion control capabilities by using the orientation of the controller to manipulate the world.
|
6
|
+
|
7
|
+
# Problems
|
8
|
+
|
9
|
+
*Marble Saga: Kororinpa* included a stage editor in which parts could be created by combining junk parts collected within the main game. The game provides the player with 20 slots in which to save stages that they have created. During the time period following the game's release, players could share their created stages using the WiiConnect24 service. After WiiConnect24 shut down on the 28<sup>th</sup> of June 2013, sharing stages with other players became impossible through official means. Sharing save files is not possible through the Wii system menu as the game had online leaderboards for ten stages specifically designed for online competition. As a result, saves of this game are marked as protected and cannot be copied from the save manager present in the Wii system menu.
|
10
|
+
|
11
|
+
# This package
|
12
|
+
|
13
|
+
This package allows you to extract the saved stages from your save file and store them in their own files, and to import stages downloaded online into your existing save file. (This package does not provide tools to get saves to or from the Wii console, there is plenty of homebrew software already in existence for this purpose.) This package also contains reverse‐engineered replicas of the game's compression format used internally, allowing for stage substitution in mods.
|
14
|
+
|
15
|
+
# Usage
|
16
|
+
|
17
|
+
To install this package, simply run
|
18
|
+
```
|
19
|
+
pip install koro
|
20
|
+
```
|
21
|
+
in a command prompt. For detailed documentation of the contents of the package, please view the wiki. For basic users, simple command‐line tools are available in the `scripts` folder of this repository. **Use of these tools requires installing the package from PyPI.**
|
22
|
+
|
23
|
+
## Playing downloaded stages
|
24
|
+
|
25
|
+
`unpacker.py` is a script designed to inject stages downloaded online into your save file. Simply run the script with the stages, the data directory of your save file, and if injecting a single stage, the slot to inject it into. The stages should then appear in the **Friend** tab. To find the location of your save in Dophin, right‐click the game and select **Open Wii Save Folder**.
|
26
|
+
|
27
|
+
## Uploading your stages
|
28
|
+
|
29
|
+
`packer.py` is a script that allows you to easily extract and upload stages that you've created. Run the script with your save directory, destination (ZIP archive), and optionally which stages to export. This script only exports stages stored in the **Original** tab of the editor. To specify which stages to export, simply enter the stage numbers in the order that they should appear when downloaded. If a custom ordering is not specified, the default is to extract all 20 stages in the order that they appear in‐game. To share single levels, extract files from the resulting archive.
|
@@ -1,24 +1,31 @@
|
|
1
1
|
[metadata]
|
2
2
|
name = koro
|
3
|
-
version =
|
3
|
+
version = 2.0.0.post1
|
4
4
|
author = DigitalDetective47
|
5
5
|
author_email = ninji2701@gmail.com
|
6
6
|
description = Tools for manipulating levels made in Marble Saga: Kororinpa
|
7
|
+
long_description = file: README.md
|
7
8
|
url = https://github.com/DigitalDetective47/koro
|
8
9
|
project_urls =
|
9
10
|
Bug Tracker = https://github.com/DigitalDetective47/koro/issues
|
11
|
+
Documentation = https://github.com/DigitalDetective47/koro/wiki
|
10
12
|
classifiers =
|
11
13
|
Development Status :: 5 - Production/Stable
|
12
14
|
License :: OSI Approved :: The Unlicense (Unlicense)
|
13
15
|
Operating System :: OS Independent
|
14
16
|
Programming Language :: Python :: 3
|
17
|
+
Programming Language :: Python :: 3 :: Only
|
18
|
+
Programming Language :: Python :: 3.11
|
19
|
+
Programming Language :: Python :: 3.12
|
20
|
+
Topic :: File Formats
|
15
21
|
Topic :: Games/Entertainment
|
22
|
+
Typing :: Typed
|
16
23
|
|
17
24
|
[options]
|
18
25
|
package_dir =
|
19
26
|
= src
|
20
27
|
packages = find:
|
21
|
-
python_requires = >=3.
|
28
|
+
python_requires = >=3.11
|
22
29
|
|
23
30
|
[options.packages.find]
|
24
31
|
where = src
|
@@ -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,19 @@
|
|
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
|
+
__all__ = ["BinSlot"]
|
9
10
|
|
10
|
-
class BinLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
|
11
|
-
pass
|
12
11
|
|
13
|
-
|
14
|
-
class BinLevel(Location, Level):
|
12
|
+
class BinSlot(FileSlot):
|
15
13
|
__slots__ = ()
|
16
14
|
|
17
15
|
@staticmethod
|
18
16
|
def compress(data: bytes, /) -> bytes:
|
19
|
-
"""Compress the given level data into the game's format."""
|
20
17
|
buffer: bytearray = bytearray(1024)
|
21
18
|
buffer_index: int = 958
|
22
19
|
chunk: bytearray
|
@@ -117,11 +114,10 @@ class BinLevel(Location, Level):
|
|
117
114
|
)
|
118
115
|
data_index += test_length
|
119
116
|
output.extend(chunk)
|
120
|
-
return bytes(output
|
117
|
+
return bytes(output)
|
121
118
|
|
122
119
|
@staticmethod
|
123
120
|
def decompress(data: bytes, /) -> bytes:
|
124
|
-
"""Decompress the given data into raw level data."""
|
125
121
|
buffer: Final[bytearray] = bytearray(1024)
|
126
122
|
buffer_index: int = 958
|
127
123
|
handle: int | bytearray
|
@@ -151,29 +147,12 @@ class BinLevel(Location, Level):
|
|
151
147
|
buffer_index = buffer_index + 1 & 1023
|
152
148
|
result.extend(handle)
|
153
149
|
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)
|
161
|
-
|
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)
|
150
|
+
return bytes(result)
|
169
151
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
return self.decompress(f.read())
|
174
|
-
except FileNotFoundError as e:
|
175
|
-
raise BinLevelNotFoundError(*e.args)
|
152
|
+
@staticmethod
|
153
|
+
def deserialize(data: bytes, /) -> Stage:
|
154
|
+
return XmlSlot.deserialize(BinSlot.decompress(data))
|
176
155
|
|
177
|
-
|
178
|
-
|
179
|
-
|
156
|
+
@staticmethod
|
157
|
+
def serialize(stage: Stage, /) -> bytes:
|
158
|
+
return BinSlot.compress(XmlSlot.serialize(stage))
|
@@ -0,0 +1,70 @@
|
|
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
|
+
__all__ = ["FileSlot"]
|
16
|
+
|
17
|
+
|
18
|
+
class FileSlot(Slot, ABC):
|
19
|
+
__match_args__ = ("path",)
|
20
|
+
__slots__ = ("_path",)
|
21
|
+
|
22
|
+
_path: StrOrBytesPath
|
23
|
+
|
24
|
+
def __init__(self, path: StrOrBytesPath, /) -> None:
|
25
|
+
self._path = path
|
26
|
+
|
27
|
+
def __bool__(self) -> bool:
|
28
|
+
return isfile(self.path)
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
@abstractmethod
|
32
|
+
def deserialize(data: bytes, /) -> Stage:
|
33
|
+
pass
|
34
|
+
|
35
|
+
def __eq__(self, other: object, /) -> bool:
|
36
|
+
if isinstance(other, FileSlot) and (
|
37
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
38
|
+
):
|
39
|
+
return self.path == other.path
|
40
|
+
else:
|
41
|
+
return NotImplemented
|
42
|
+
|
43
|
+
def __hash__(self) -> int:
|
44
|
+
return hash(self.path)
|
45
|
+
|
46
|
+
def load(self) -> Stage | None:
|
47
|
+
try:
|
48
|
+
with open(self.path, "rb") as f:
|
49
|
+
return self.deserialize(f.read())
|
50
|
+
except FileNotFoundError:
|
51
|
+
return None
|
52
|
+
|
53
|
+
@property
|
54
|
+
def path(self) -> StrOrBytesPath:
|
55
|
+
return self._path
|
56
|
+
|
57
|
+
def __repr__(self) -> str:
|
58
|
+
return f"{type(self).__name__}({self.path!r})"
|
59
|
+
|
60
|
+
def save(self, data: Stage | None) -> None:
|
61
|
+
if data is None:
|
62
|
+
remove(self.path)
|
63
|
+
else:
|
64
|
+
with open(self.path, "wb") as f:
|
65
|
+
f.write(self.serialize(data))
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
@abstractmethod
|
69
|
+
def serialize(stage: Stage, /) -> bytes:
|
70
|
+
pass
|
@@ -0,0 +1,137 @@
|
|
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
|
+
else:
|
16
|
+
StrOrBytesPath = Any
|
17
|
+
|
18
|
+
|
19
|
+
__all__ = ["EditorPage", "get_slots", "SaveSlot"]
|
20
|
+
|
21
|
+
|
22
|
+
@unique
|
23
|
+
class EditorPage(Enum):
|
24
|
+
ORIGINAL = 0
|
25
|
+
FRIEND = 1
|
26
|
+
HUDSON = 2
|
27
|
+
|
28
|
+
|
29
|
+
class SaveSlot(Slot):
|
30
|
+
__match_args__ = ("path", "page", "index")
|
31
|
+
__slots__ = ("_offset", "_path")
|
32
|
+
|
33
|
+
_offset: Literal[8, 156392, 312776, 469160]
|
34
|
+
_path: str | bytes
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
path: StrOrBytesPath,
|
39
|
+
page: EditorPage,
|
40
|
+
index: Annotated[
|
41
|
+
SupportsIndex,
|
42
|
+
Literal[
|
43
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
|
44
|
+
],
|
45
|
+
],
|
46
|
+
) -> None:
|
47
|
+
index = ix(index) - 1
|
48
|
+
if index in range(0, 20):
|
49
|
+
self._offset = 8 + 156864 * (index & 3) # type: ignore[assignment]
|
50
|
+
self._path = join(path, f"ed{(index >> 2) + 5 * page.value:02}.dat") # type: ignore[arg-type]
|
51
|
+
else:
|
52
|
+
raise ValueError("index must be between 1 and 20")
|
53
|
+
|
54
|
+
def __bool__(self) -> bool:
|
55
|
+
try:
|
56
|
+
with open(self._path, "rb") as f:
|
57
|
+
f.seek(self._offset)
|
58
|
+
return f.read(1) != b"\x00"
|
59
|
+
except FileNotFoundError:
|
60
|
+
return False
|
61
|
+
|
62
|
+
def __eq__(self, other: Any, /) -> bool:
|
63
|
+
if isinstance(other, SaveSlot):
|
64
|
+
return self._path == other._path and self._offset == other._offset
|
65
|
+
else:
|
66
|
+
return NotImplemented
|
67
|
+
|
68
|
+
def __hash__(self) -> int:
|
69
|
+
return hash((self._offset, self._path))
|
70
|
+
|
71
|
+
@property
|
72
|
+
def index(
|
73
|
+
self,
|
74
|
+
) -> Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]:
|
75
|
+
return (int(basename(self._path)[2:4]) % 5 >> 2 | self._offset // 156864) + 1 # type: ignore[return-value]
|
76
|
+
|
77
|
+
def load(self) -> Stage | None:
|
78
|
+
try:
|
79
|
+
with open(self._path, "rb") as f:
|
80
|
+
f.seek(self._offset)
|
81
|
+
with BytesIO() as b:
|
82
|
+
block: bytearray = bytearray()
|
83
|
+
while True:
|
84
|
+
block.clear()
|
85
|
+
f.readinto1(block)
|
86
|
+
if len(b.getbuffer()) + len(block) > 156864:
|
87
|
+
del block[156864 - len(b.getbuffer()) :]
|
88
|
+
if block[-1]:
|
89
|
+
b.write(block)
|
90
|
+
else:
|
91
|
+
while block:
|
92
|
+
if block[len(block) >> 1]:
|
93
|
+
b.write(block[: (len(block) >> 1) + 1])
|
94
|
+
del block[: (len(block) >> 1) + 1]
|
95
|
+
else:
|
96
|
+
del block[len(block) >> 1 :]
|
97
|
+
data: bytes = b.getvalue()
|
98
|
+
if data:
|
99
|
+
return XmlSlot.deserialize(data)
|
100
|
+
else:
|
101
|
+
return None
|
102
|
+
except FileNotFoundError:
|
103
|
+
return None
|
104
|
+
|
105
|
+
@property
|
106
|
+
def page(self) -> EditorPage:
|
107
|
+
return EditorPage(int(basename(self._path)[2:4]) // 5)
|
108
|
+
|
109
|
+
@property
|
110
|
+
def path(self) -> StrOrBytesPath:
|
111
|
+
return dirname(self._path)
|
112
|
+
|
113
|
+
def __repr__(self) -> str:
|
114
|
+
return f"{type(self).__name__}({self.path!r}, {self.page!r}, {self.index!r})"
|
115
|
+
|
116
|
+
def save(self, data: Stage | None) -> None:
|
117
|
+
binary: bytes = b"" if data is None else XmlSlot.serialize(data)
|
118
|
+
if len(binary) > 156864:
|
119
|
+
raise ValueError("serialized stage data is too large to save")
|
120
|
+
try:
|
121
|
+
with open(self._path, "xb") as f:
|
122
|
+
f.write(bytes(638976))
|
123
|
+
if data is None:
|
124
|
+
return
|
125
|
+
except FileExistsError:
|
126
|
+
pass
|
127
|
+
with open(self._path, "r+b") as f:
|
128
|
+
f.seek(self._offset)
|
129
|
+
f.write(binary)
|
130
|
+
f.write(bytes(156864 - len(binary)))
|
131
|
+
|
132
|
+
|
133
|
+
def get_slots(save: StrOrBytesPath, /) -> Mapping[EditorPage, Sequence[SaveSlot]]:
|
134
|
+
return {
|
135
|
+
page: tuple(SaveSlot(save, page, i) for i in range(1, 21))
|
136
|
+
for page in EditorPage
|
137
|
+
}
|