koro 2.0.0rc3__tar.gz → 2.0.2__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,24 +1,24 @@
1
- This is free and unencumbered software released into the public domain.
2
-
3
- Anyone is free to copy, modify, publish, use, compile, sell, or
4
- distribute this software, either in source code form or as a compiled
5
- binary, for any purpose, commercial or non-commercial, and by any
6
- means.
7
-
8
- In jurisdictions that recognize copyright laws, the author or authors
9
- of this software dedicate any and all copyright interest in the
10
- software to the public domain. We make this dedication for the benefit
11
- of the public at large and to the detriment of our heirs and
12
- successors. We intend this dedication to be an overt act of
13
- relinquishment in perpetuity of all present and future rights to this
14
- software under copyright law.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
- IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- OTHER DEALINGS IN THE SOFTWARE.
23
-
24
- For more information, please refer to <https://unlicense.org>
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
koro-2.0.2/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.1
2
+ Name: koro
3
+ Version: 2.0.2
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
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+
24
+ koro (stylized in all lowercase) is a Python package that can read and modify stages from *Marble Saga: Kororinpa*.
25
+
26
+ # *Marble Saga: Kororinpa*
27
+
28
+ *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&hyphen;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.
29
+
30
+ # Problems
31
+
32
+ *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.
33
+
34
+ # This package
35
+
36
+ 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&hyphen;engineered replicas of the game's compression format used internally, allowing for stage substitution in mods.
37
+
38
+ # Usage
39
+
40
+ To install this package, simply run
41
+ ```
42
+ pip install koro
43
+ ```
44
+ in a command prompt. For detailed documentation of the contents of the package, please view the wiki. For basic users, simple command&hyphen;line tools are available in the `scripts` folder of this repository. **Use of these tools requires installing the package from PyPI.**
45
+
46
+ ## Playing downloaded stages
47
+
48
+ `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&hyphen;click the game and select **Open Wii Save Folder**.
49
+
50
+ ## Uploading your stages
51
+
52
+ `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&hyphen;game. To share single levels, extract files from the resulting archive.
koro-2.0.2/README.md ADDED
@@ -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&hyphen;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&hyphen;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&hyphen;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&hyphen;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&hyphen;game. To share single levels, extract files from the resulting archive.
@@ -1,3 +1,3 @@
1
- [build-system]
2
- requires = ["setuptools>=42"]
1
+ [build-system]
2
+ requires = ["setuptools>=42"]
3
3
  build-backend = "setuptools.build_meta"
@@ -1,34 +1,37 @@
1
- [metadata]
2
- name = koro
3
- version = 2.0.0rc3
4
- author = DigitalDetective47
5
- author_email = ninji2701@gmail.com
6
- description = Tools for manipulating levels made in Marble Saga: Kororinpa
7
- url = https://github.com/DigitalDetective47/koro
8
- project_urls =
9
- Bug Tracker = https://github.com/DigitalDetective47/koro/issues
10
- classifiers =
11
- Development Status :: 5 - Production/Stable
12
- License :: OSI Approved :: The Unlicense (Unlicense)
13
- Operating System :: OS Independent
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
19
- Topic :: Games/Entertainment
20
- Typing :: Typed
21
-
22
- [options]
23
- package_dir =
24
- = src
25
- packages = find:
26
- python_requires = >=3.11
27
-
28
- [options.packages.find]
29
- where = src
30
-
31
- [egg_info]
32
- tag_build =
33
- tag_date = 0
34
-
1
+ [metadata]
2
+ name = koro
3
+ version = 2.0.2
4
+ author = DigitalDetective47
5
+ author_email = ninji2701@gmail.com
6
+ description = Tools for manipulating levels made in Marble Saga: Kororinpa
7
+ long_description = file: README.md
8
+ long_description_content_type = text/markdown
9
+ url = https://github.com/DigitalDetective47/koro
10
+ project_urls =
11
+ Bug Tracker = https://github.com/DigitalDetective47/koro/issues
12
+ Documentation = https://github.com/DigitalDetective47/koro/wiki
13
+ classifiers =
14
+ Development Status :: 5 - Production/Stable
15
+ License :: OSI Approved :: The Unlicense (Unlicense)
16
+ Operating System :: OS Independent
17
+ Programming Language :: Python :: 3
18
+ Programming Language :: Python :: 3 :: Only
19
+ Programming Language :: Python :: 3.11
20
+ Programming Language :: Python :: 3.12
21
+ Topic :: File Formats
22
+ Topic :: Games/Entertainment
23
+ Typing :: Typed
24
+
25
+ [options]
26
+ package_dir =
27
+ = src
28
+ packages = find:
29
+ python_requires = >=3.11
30
+
31
+ [options.packages.find]
32
+ where = src
33
+
34
+ [egg_info]
35
+ tag_build =
36
+ tag_date = 0
37
+
@@ -1,8 +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 *
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 *
@@ -1,21 +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
+ 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,155 +1,158 @@
1
- from itertools import chain
2
- from typing import Final
3
-
4
- from ..stage import Stage
5
- from .file import FileSlot
6
- from .xml import XmlSlot
7
-
8
-
9
- class BinSlot(FileSlot):
10
- __slots__ = ()
11
-
12
- @staticmethod
13
- def compress(data: bytes, /) -> bytes:
14
- buffer: bytearray = bytearray(1024)
15
- buffer_index: int = 958
16
- chunk: bytearray
17
- data_index: int = 0
18
- output: Final[bytearray] = bytearray(
19
- b"\x00\x00\x00\x01\x00\x00\x00\x08"
20
- + len(data).to_bytes(4, byteorder="big")
21
- + b"\x00\x00\x00\x01"
22
- )
23
- reference_indices: list[int]
24
- test_buffer: bytearray
25
- test_length: int
26
- test_reference_indicies: list[int]
27
- while data_index < len(data):
28
- chunk = bytearray(b"\x00")
29
- for bit in range(8):
30
- if data_index >= len(data):
31
- chunk[0] >>= 8 - bit
32
- output.extend(chunk)
33
- return output + b"\x00" * (len(output) & 1)
34
- if len(data) - data_index <= 2:
35
- buffer[buffer_index] = data[data_index]
36
- buffer_index = buffer_index + 1 & 1023
37
- chunk[0] = chunk[0] >> 1 | 128
38
- chunk.append(data[data_index])
39
- data_index += 1
40
- continue
41
- reference_indices = []
42
- for i in chain(range(buffer_index, 1024), range(buffer_index)):
43
- if data[data_index] == buffer[i]:
44
- reference_indices.append(i)
45
- if not reference_indices:
46
- buffer[buffer_index] = data[data_index]
47
- buffer_index = buffer_index + 1 & 1023
48
- chunk[0] = chunk[0] >> 1 | 128
49
- chunk.append(data[data_index])
50
- data_index += 1
51
- continue
52
- test_buffer = buffer.copy()
53
- test_buffer[buffer_index] = data[data_index]
54
- for i in reference_indices.copy():
55
- if data[data_index + 1] != test_buffer[i - 1023]:
56
- reference_indices.remove(i)
57
- if not reference_indices:
58
- buffer[buffer_index] = data[data_index]
59
- buffer_index = buffer_index + 1 & 1023
60
- chunk[0] = chunk[0] >> 1 | 128
61
- chunk.append(data[data_index])
62
- data_index += 1
63
- continue
64
- test_buffer[buffer_index - 1023] = data[data_index + 1]
65
- for i in reference_indices.copy():
66
- if data[data_index + 2] != test_buffer[i - 1022]:
67
- reference_indices.remove(i)
68
- if not reference_indices:
69
- buffer[buffer_index] = data[data_index]
70
- buffer_index = buffer_index + 1 & 1023
71
- chunk[0] = chunk[0] >> 1 | 128
72
- chunk.append(data[data_index])
73
- data_index += 1
74
- continue
75
- test_length = 4
76
- test_reference_indicies = reference_indices.copy()
77
- while test_length <= min(66, len(data) - data_index):
78
- test_buffer[buffer_index + test_length - 1026] = data[
79
- data_index + test_length - 2
80
- ]
81
- for i in test_reference_indicies.copy():
82
- if (
83
- data[data_index + test_length - 1]
84
- != test_buffer[i + test_length - 1025]
85
- ):
86
- test_reference_indicies.remove(i)
87
- if test_reference_indicies:
88
- reference_indices = test_reference_indicies.copy()
89
- else:
90
- break
91
- test_length += 1
92
- chunk[0] >>= 1
93
- test_length -= 1
94
- if buffer_index + test_length >= 1024:
95
- buffer[buffer_index:] = data[
96
- data_index : data_index + 1024 - buffer_index
97
- ]
98
- buffer[: buffer_index + test_length - 1024] = data[
99
- data_index + 1024 - buffer_index : data_index + test_length
100
- ]
101
- else:
102
- buffer[buffer_index : buffer_index + test_length] = data[
103
- data_index : data_index + test_length
104
- ]
105
- buffer_index = buffer_index + test_length & 1023
106
- chunk.extend(
107
- (
108
- reference_indices[0] & 255,
109
- reference_indices[0] >> 2 & 192 | test_length - 3,
110
- )
111
- )
112
- data_index += test_length
113
- output.extend(chunk)
114
- return bytes(output)
115
-
116
- @staticmethod
117
- def decompress(data: bytes, /) -> bytes:
118
- buffer: Final[bytearray] = bytearray(1024)
119
- buffer_index: int = 958
120
- handle: int | bytearray
121
- flags: int
122
- offset: int
123
- raw: Final[bytearray] = bytearray(data[:15:-1])
124
- ref: bytes
125
- result: Final[bytearray] = bytearray()
126
- result_size: Final[int] = int.from_bytes(data[8:12], byteorder="big")
127
- while len(result) < result_size:
128
- flags = raw.pop()
129
- for _ in range(8):
130
- if flags & 1:
131
- handle = raw.pop()
132
- buffer[buffer_index] = handle
133
- buffer_index = buffer_index + 1 & 1023
134
- result.append(handle)
135
- else:
136
- if len(raw) < 2:
137
- return result
138
- ref = bytes((raw.pop() for _ in range(2)))
139
- offset = (ref[1] << 2 & 768) + ref[0]
140
- handle = bytearray()
141
- for i in range((ref[1] & 63) + 3):
142
- handle.append(buffer[offset + i - 1024])
143
- buffer[buffer_index] = handle[-1]
144
- buffer_index = buffer_index + 1 & 1023
145
- result.extend(handle)
146
- flags >>= 1
147
- return bytes(result)
148
-
149
- @staticmethod
150
- def deserialize(data: bytes, /) -> Stage:
151
- return XmlSlot.deserialize(BinSlot.decompress(data))
152
-
153
- @staticmethod
154
- def serialize(level: Stage, /) -> bytes:
155
- return BinSlot.compress(XmlSlot.serialize(level))
1
+ from itertools import chain
2
+ from typing import Final
3
+
4
+ from ..stage import Stage
5
+ from .file import FileSlot
6
+ from .xml import XmlSlot
7
+
8
+
9
+ __all__ = ["BinSlot"]
10
+
11
+
12
+ class BinSlot(FileSlot):
13
+ __slots__ = ()
14
+
15
+ @staticmethod
16
+ def compress(data: bytes, /) -> bytes:
17
+ buffer: bytearray = bytearray(1024)
18
+ buffer_index: int = 958
19
+ chunk: bytearray
20
+ data_index: int = 0
21
+ output: Final[bytearray] = bytearray(
22
+ b"\x00\x00\x00\x01\x00\x00\x00\x08"
23
+ + len(data).to_bytes(4, byteorder="big")
24
+ + b"\x00\x00\x00\x01"
25
+ )
26
+ reference_indices: list[int]
27
+ test_buffer: bytearray
28
+ test_length: int
29
+ test_reference_indicies: list[int]
30
+ while data_index < len(data):
31
+ chunk = bytearray(b"\x00")
32
+ for bit in range(8):
33
+ if data_index >= len(data):
34
+ chunk[0] >>= 8 - bit
35
+ output.extend(chunk)
36
+ return output + b"\x00" * (len(output) & 1)
37
+ if len(data) - data_index <= 2:
38
+ buffer[buffer_index] = data[data_index]
39
+ buffer_index = buffer_index + 1 & 1023
40
+ chunk[0] = chunk[0] >> 1 | 128
41
+ chunk.append(data[data_index])
42
+ data_index += 1
43
+ continue
44
+ reference_indices = []
45
+ for i in chain(range(buffer_index, 1024), range(buffer_index)):
46
+ if data[data_index] == buffer[i]:
47
+ reference_indices.append(i)
48
+ if not reference_indices:
49
+ buffer[buffer_index] = data[data_index]
50
+ buffer_index = buffer_index + 1 & 1023
51
+ chunk[0] = chunk[0] >> 1 | 128
52
+ chunk.append(data[data_index])
53
+ data_index += 1
54
+ continue
55
+ test_buffer = buffer.copy()
56
+ test_buffer[buffer_index] = data[data_index]
57
+ for i in reference_indices.copy():
58
+ if data[data_index + 1] != test_buffer[i - 1023]:
59
+ reference_indices.remove(i)
60
+ if not reference_indices:
61
+ buffer[buffer_index] = data[data_index]
62
+ buffer_index = buffer_index + 1 & 1023
63
+ chunk[0] = chunk[0] >> 1 | 128
64
+ chunk.append(data[data_index])
65
+ data_index += 1
66
+ continue
67
+ test_buffer[buffer_index - 1023] = data[data_index + 1]
68
+ for i in reference_indices.copy():
69
+ if data[data_index + 2] != test_buffer[i - 1022]:
70
+ reference_indices.remove(i)
71
+ if not reference_indices:
72
+ buffer[buffer_index] = data[data_index]
73
+ buffer_index = buffer_index + 1 & 1023
74
+ chunk[0] = chunk[0] >> 1 | 128
75
+ chunk.append(data[data_index])
76
+ data_index += 1
77
+ continue
78
+ test_length = 4
79
+ test_reference_indicies = reference_indices.copy()
80
+ while test_length <= min(66, len(data) - data_index):
81
+ test_buffer[buffer_index + test_length - 1026] = data[
82
+ data_index + test_length - 2
83
+ ]
84
+ for i in test_reference_indicies.copy():
85
+ if (
86
+ data[data_index + test_length - 1]
87
+ != test_buffer[i + test_length - 1025]
88
+ ):
89
+ test_reference_indicies.remove(i)
90
+ if test_reference_indicies:
91
+ reference_indices = test_reference_indicies.copy()
92
+ else:
93
+ break
94
+ test_length += 1
95
+ chunk[0] >>= 1
96
+ test_length -= 1
97
+ if buffer_index + test_length >= 1024:
98
+ buffer[buffer_index:] = data[
99
+ data_index : data_index + 1024 - buffer_index
100
+ ]
101
+ buffer[: buffer_index + test_length - 1024] = data[
102
+ data_index + 1024 - buffer_index : data_index + test_length
103
+ ]
104
+ else:
105
+ buffer[buffer_index : buffer_index + test_length] = data[
106
+ data_index : data_index + test_length
107
+ ]
108
+ buffer_index = buffer_index + test_length & 1023
109
+ chunk.extend(
110
+ (
111
+ reference_indices[0] & 255,
112
+ reference_indices[0] >> 2 & 192 | test_length - 3,
113
+ )
114
+ )
115
+ data_index += test_length
116
+ output.extend(chunk)
117
+ return bytes(output)
118
+
119
+ @staticmethod
120
+ def decompress(data: bytes, /) -> bytes:
121
+ buffer: Final[bytearray] = bytearray(1024)
122
+ buffer_index: int = 958
123
+ handle: int | bytearray
124
+ flags: int
125
+ offset: int
126
+ raw: Final[bytearray] = bytearray(data[:15:-1])
127
+ ref: bytes
128
+ result: Final[bytearray] = bytearray()
129
+ result_size: Final[int] = int.from_bytes(data[8:12], byteorder="big")
130
+ while len(result) < result_size:
131
+ flags = raw.pop()
132
+ for _ in range(8):
133
+ if flags & 1:
134
+ handle = raw.pop()
135
+ buffer[buffer_index] = handle
136
+ buffer_index = buffer_index + 1 & 1023
137
+ result.append(handle)
138
+ else:
139
+ if len(raw) < 2:
140
+ return result
141
+ ref = bytes((raw.pop() for _ in range(2)))
142
+ offset = (ref[1] << 2 & 768) + ref[0]
143
+ handle = bytearray()
144
+ for i in range((ref[1] & 63) + 3):
145
+ handle.append(buffer[offset + i - 1024])
146
+ buffer[buffer_index] = handle[-1]
147
+ buffer_index = buffer_index + 1 & 1023
148
+ result.extend(handle)
149
+ flags >>= 1
150
+ return bytes(result)
151
+
152
+ @staticmethod
153
+ def deserialize(data: bytes, /) -> Stage:
154
+ return XmlSlot.deserialize(BinSlot.decompress(data))
155
+
156
+ @staticmethod
157
+ def serialize(stage: Stage, /) -> bytes:
158
+ return BinSlot.compress(XmlSlot.serialize(stage))