koro 1.0.8__py3-none-any.whl → 1.1.0__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/file/__init__.py CHANGED
@@ -2,6 +2,10 @@ from os import remove
2
2
  from os.path import abspath, exists
3
3
  from shutil import rmtree
4
4
 
5
+ from . import bin, dir, lvl, zip
6
+
7
+ __all__ = ["bin", "dir", "Location", "lvl", "zip"]
8
+
5
9
 
6
10
  class Location:
7
11
  __match_args__ = ("path",)
koro/file/bin.py ADDED
@@ -0,0 +1,178 @@
1
+ from itertools import chain
2
+ from typing import Final
3
+
4
+ from ..item.level import Level, LevelNotFoundError
5
+ from . import Location
6
+
7
+ __all__ = ["BinLevel", "BinLevelNotFoundError"]
8
+
9
+
10
+ class BinLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
11
+ pass
12
+
13
+
14
+ class BinLevel(Location, Level):
15
+ __slots__ = ()
16
+
17
+ @staticmethod
18
+ def compress(data: bytes, /) -> bytes:
19
+ """Compress the given level data into the game's format."""
20
+ buffer: bytearray = bytearray(1024)
21
+ buffer_index: int = 958
22
+ chunk: bytearray
23
+ data_index: int = 0
24
+ output: Final[bytearray] = bytearray(
25
+ b"\x00\x00\x00\x01\x00\x00\x00\x08"
26
+ + len(data).to_bytes(4, byteorder="big")
27
+ + b"\x00\x00\x00\x01"
28
+ )
29
+ reference_indices: list[int]
30
+ test_buffer: bytearray
31
+ test_length: int
32
+ test_reference_indicies: list[int]
33
+ while data_index < len(data):
34
+ chunk = bytearray(b"\x00")
35
+ for _ in range(8):
36
+ if data_index >= len(data):
37
+ output.extend(chunk)
38
+ return output
39
+ if len(data) - data_index <= 2:
40
+ buffer[buffer_index] = data[data_index]
41
+ buffer_index = buffer_index + 1 & 1023
42
+ chunk[0] = chunk[0] >> 1 | 128
43
+ chunk.append(data[data_index])
44
+ data_index += 1
45
+ continue
46
+ reference_indices = []
47
+ for i in chain(range(buffer_index, 1024), range(buffer_index)):
48
+ if data[data_index] == buffer[i]:
49
+ reference_indices.append(i)
50
+ if not reference_indices:
51
+ buffer[buffer_index] = data[data_index]
52
+ buffer_index = buffer_index + 1 & 1023
53
+ chunk[0] = chunk[0] >> 1 | 128
54
+ chunk.append(data[data_index])
55
+ data_index += 1
56
+ continue
57
+ test_buffer = buffer.copy()
58
+ test_buffer[buffer_index] = data[data_index]
59
+ for i in reference_indices.copy():
60
+ if data[data_index + 1] != test_buffer[i - 1023]:
61
+ reference_indices.remove(i)
62
+ if not reference_indices:
63
+ buffer[buffer_index] = data[data_index]
64
+ buffer_index = buffer_index + 1 & 1023
65
+ chunk[0] = chunk[0] >> 1 | 128
66
+ chunk.append(data[data_index])
67
+ data_index += 1
68
+ continue
69
+ test_buffer[buffer_index - 1023] = data[data_index + 1]
70
+ for i in reference_indices.copy():
71
+ if data[data_index + 2] != test_buffer[i - 1022]:
72
+ reference_indices.remove(i)
73
+ if not reference_indices:
74
+ buffer[buffer_index] = data[data_index]
75
+ buffer_index = buffer_index + 1 & 1023
76
+ chunk[0] = chunk[0] >> 1 | 128
77
+ chunk.append(data[data_index])
78
+ data_index += 1
79
+ continue
80
+ test_length = 4
81
+ test_reference_indicies = reference_indices.copy()
82
+ while test_length <= min(66, len(data) - data_index):
83
+ test_buffer[buffer_index + test_length - 1025] = data[
84
+ data_index + test_length - 1
85
+ ]
86
+ for i in test_reference_indicies.copy():
87
+ if (
88
+ data[data_index + test_length - 1]
89
+ != test_buffer[i + test_length - 1025]
90
+ ):
91
+ test_reference_indicies.remove(i)
92
+ if test_reference_indicies:
93
+ reference_indices = test_reference_indicies.copy()
94
+ else:
95
+ break
96
+ test_length += 1
97
+ chunk[0] >>= 1
98
+ test_length -= 1
99
+ if buffer_index + test_length >= 1024:
100
+ buffer[buffer_index:] = data[
101
+ data_index : data_index + 1024 - buffer_index
102
+ ]
103
+ buffer[: buffer_index + test_length - 1024] = data[
104
+ data_index + 1024 - buffer_index : data_index + test_length
105
+ ]
106
+ else:
107
+ buffer[buffer_index : buffer_index + test_length] = data[
108
+ data_index : data_index + test_length
109
+ ]
110
+ buffer_index = buffer_index + test_length & 1023
111
+ chunk.extend(
112
+ (
113
+ reference_indices[0] & 255,
114
+ reference_indices[0] >> 2 & 192 | test_length - 3,
115
+ )
116
+ )
117
+ data_index += test_length
118
+ output.extend(chunk)
119
+ return output + b"\x00"
120
+
121
+ @staticmethod
122
+ def decompress(data: bytes, /) -> bytes:
123
+ """Decompress the given data into raw level data."""
124
+ buffer: Final[bytearray] = bytearray(1024)
125
+ buffer_index: int = 958
126
+ handle: int | bytearray
127
+ flags: int
128
+ offset: int
129
+ raw: Final[bytearray] = bytearray(data[:15:-1])
130
+ ref: bytes
131
+ result: Final[bytearray] = bytearray()
132
+ result_size: Final[int] = int.from_bytes(data[8:12], byteorder="big")
133
+ while len(result) < result_size:
134
+ flags = raw.pop()
135
+ for _ in range(8):
136
+ if flags & 1:
137
+ handle = raw.pop()
138
+ buffer[buffer_index] = handle
139
+ buffer_index = buffer_index + 1 & 1023
140
+ result.append(handle)
141
+ else:
142
+ if len(raw) < 2:
143
+ return result
144
+ ref = bytes((raw.pop() for _ in range(2)))
145
+ offset = (ref[1] << 2 & 768) + ref[0]
146
+ handle = bytearray()
147
+ for i in range((ref[1] & 63) + 3):
148
+ handle.append(buffer[offset + i - 1024])
149
+ buffer[buffer_index] = handle[-1]
150
+ buffer_index = buffer_index + 1 & 1023
151
+ result.extend(handle)
152
+ flags >>= 1
153
+ return result.replace(b"<EDITUSER> 3 </EDITUSER>", b"<EDITUSER> 2 </EDITUSER>")
154
+
155
+ def delete(self) -> None:
156
+ try:
157
+ return super().delete()
158
+ except FileNotFoundError as e:
159
+ raise BinLevelNotFoundError(*e.args)
160
+
161
+ def __len__(self) -> int:
162
+ try:
163
+ with open(self.path, "rb") as f:
164
+ f.seek(8)
165
+ return int.from_bytes(f.read(4), byteorder="big")
166
+ except FileNotFoundError as e:
167
+ BinLevelNotFoundError(*e.args)
168
+
169
+ def read(self) -> bytes:
170
+ try:
171
+ with open(self.path, "rb") as f:
172
+ return self.decompress(f.read())
173
+ except FileNotFoundError as e:
174
+ raise BinLevelNotFoundError(*e.args)
175
+
176
+ def write(self, new_content: bytes, /) -> None:
177
+ with open(self.path, "wb") as f:
178
+ f.write(self.compress(new_content))
koro/file/dir.py CHANGED
@@ -7,14 +7,19 @@ from typing import Final, Optional, SupportsIndex, TypeGuard, overload
7
7
  from ..item.group import Group
8
8
  from ..item.level import Level, LevelNotFoundError
9
9
  from ..item.save import Page, Save
10
-
11
10
  from . import Location
12
11
 
12
+ __all__ = ["DirGroup", "DirLevel", "DirLevelNotFoundError", "DirSave"]
13
+
13
14
  _BLOCK_SIZE: Final[int] = 2048
14
15
  _EMPTY_BLOCK: Final[bytes] = b"\x00" * _BLOCK_SIZE
15
16
  _LEVEL_ALLOCATION_SIZE: Final[int] = 156864
16
17
 
17
18
 
19
+ class DirLevelNotFoundError(LevelNotFoundError):
20
+ pass
21
+
22
+
18
23
  class DirLevel(Level):
19
24
  __match_args__ = ("path", "page", "id")
20
25
  __slots__ = ("_offset", "_path")
@@ -45,7 +50,7 @@ class DirLevel(Level):
45
50
  f.write(_EMPTY_BLOCK)
46
51
  more = f.read(1) != b"\x00"
47
52
  else:
48
- raise LevelNotFoundError
53
+ raise DirLevelNotFoundError
49
54
 
50
55
  def __eq__(self, other: object, /) -> bool:
51
56
  return (
@@ -62,23 +67,26 @@ class DirLevel(Level):
62
67
  return int(self._path[-5]) % 5 << 2 | self._offset // _LEVEL_ALLOCATION_SIZE
63
68
 
64
69
  def __len__(self) -> int:
65
- with open(self._path, "rb") as f:
66
- f.seek(self._offset)
67
- block: Final[bytearray] = bytearray(f.read(_BLOCK_SIZE))
68
- block_offset: int = 0
69
- while block[-1]:
70
- f.readinto(block)
71
- block_offset += _BLOCK_SIZE
72
- hi: int = _BLOCK_SIZE - 1
73
- lo: int = 0
74
- test: int
75
- while hi != lo:
76
- test = (hi - lo >> 1) + lo
77
- if block[test]:
78
- lo = test + 1
79
- else:
80
- hi = test
81
- return block_offset + hi
70
+ if self:
71
+ with open(self._path, "rb") as f:
72
+ f.seek(self._offset)
73
+ block: Final[bytearray] = bytearray(f.read(_BLOCK_SIZE))
74
+ block_offset: int = 0
75
+ while block[-1]:
76
+ f.readinto(block)
77
+ block_offset += _BLOCK_SIZE
78
+ hi: int = _BLOCK_SIZE - 1
79
+ lo: int = 0
80
+ test: int
81
+ while hi != lo:
82
+ test = (hi - lo >> 1) + lo
83
+ if block[test]:
84
+ lo = test + 1
85
+ else:
86
+ hi = test
87
+ return block_offset + hi
88
+ else:
89
+ raise DirLevelNotFoundError
82
90
 
83
91
  @property
84
92
  def page(self) -> Page:
@@ -108,7 +116,7 @@ class DirLevel(Level):
108
116
  hi = test
109
117
  return bytes(result + block[:hi])
110
118
  else:
111
- raise LevelNotFoundError
119
+ raise DirLevelNotFoundError
112
120
 
113
121
  def __repr__(self) -> str:
114
122
  return f"{type(self).__name__}({self.path!r}, {self.page!r}, {self.id!r})"
koro/file/lvl.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from os.path import getsize
2
+ from warnings import warn
2
3
 
3
4
  from ..item.level import Level, LevelNotFoundError
4
-
5
5
  from . import Location
6
6
 
7
- __all__ = ["LvlLevel"]
7
+ __all__ = ["LvlLevel", "LvlLevelNotFoundError"]
8
8
 
9
9
 
10
10
  class LvlLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
@@ -14,6 +14,14 @@ class LvlLevelNotFoundError(FileNotFoundError, LevelNotFoundError):
14
14
  class LvlLevel(Location, Level):
15
15
  __slots__ = ()
16
16
 
17
+ def __init__(self, path: str, /) -> None:
18
+ super().__init__(path)
19
+ warn(
20
+ FutureWarning(
21
+ "The LVL format for storing level data is deprecated. Level data should be converted to the BIN format using BinLevel for better compression and compatibility."
22
+ )
23
+ )
24
+
17
25
  def delete(self) -> None:
18
26
  try:
19
27
  return super().delete()
koro/file/zip.py CHANGED
@@ -4,17 +4,22 @@ from operator import index
4
4
  from os.path import abspath
5
5
  from re import fullmatch
6
6
  from typing import Final, Optional, SupportsIndex, TypeGuard, overload
7
+ from warnings import warn
7
8
  from zipfile import ZipFile, ZipInfo
8
9
 
9
10
  from ..item.group import Group
10
11
  from ..item.level import Level, LevelNotFoundError
11
-
12
12
  from . import Location
13
+ from .bin import BinLevel
13
14
 
14
- __all__ = ["ZipGroup", "ZipLevel"]
15
+ __all__ = ["ZipGroup", "ZipLevel", "ZipLevelNotFoundError"]
15
16
 
16
17
 
17
18
  def _id_to_fn(id: SupportsIndex, /) -> str:
19
+ return f"{str(index(id) + 1).zfill(2)}.bin"
20
+
21
+
22
+ def _id_to_old_fn(id: SupportsIndex, /) -> str:
18
23
  return f"{str(index(id) + 1).zfill(2)}.lvl"
19
24
 
20
25
 
@@ -43,11 +48,12 @@ class ZipLevel(Level):
43
48
 
44
49
  def delete(self) -> None:
45
50
  with ZipFile(self.path) as a:
46
- if self.fn not in a.namelist():
51
+ if self.fn not in a.namelist() and self.old_fn not in a.namelist():
47
52
  raise ZipLevelNotFoundError
48
53
  contents: Final[dict[ZipInfo, bytes]] = {}
49
54
  for info in filterfalse(
50
- lambda info: info.filename == self.fn, a.infolist()
55
+ lambda info: info.filename == self.fn or info.filename == self.old_fn,
56
+ a.infolist(),
51
57
  ):
52
58
  contents[info] = a.read(info)
53
59
  with ZipFile(self.path, "w") as a:
@@ -73,22 +79,38 @@ class ZipLevel(Level):
73
79
  return self._id
74
80
 
75
81
  def __len__(self) -> int:
76
- try:
77
- with ZipFile(self.path) as a:
78
- return a.getinfo(self.fn).file_size
79
- except KeyError as e:
80
- raise ZipLevelNotFoundError(*e.args)
82
+ with ZipFile(self.path) as a:
83
+ try:
84
+ a.getinfo(self.fn)
85
+ except KeyError:
86
+ try:
87
+ return a.getinfo(self.old_fn).file_size
88
+ except KeyError:
89
+ raise ZipLevelNotFoundError
90
+ else:
91
+ return int.from_bytes(a.read(self.fn)[8:12], byteorder="big")
92
+
93
+ @property
94
+ def old_fn(self) -> str:
95
+ return _id_to_old_fn(self.id)
81
96
 
82
97
  @property
83
98
  def path(self) -> str:
84
99
  return self._path
85
100
 
86
101
  def read(self) -> bytes:
87
- try:
88
- with ZipFile(self.path) as a:
89
- return a.read(self.fn)
90
- except KeyError as e:
91
- raise ZipLevelNotFoundError(*e.args)
102
+ with ZipFile(self.path) as a:
103
+ try:
104
+ a.getinfo(self.fn)
105
+ except KeyError:
106
+ try:
107
+ a.getinfo(self.old_fn)
108
+ except KeyError:
109
+ raise ZipLevelNotFoundError
110
+ else:
111
+ return a.read(self.old_fn)
112
+ else:
113
+ return BinLevel.decompress(a.read(self.fn))
92
114
 
93
115
  def __repr__(self) -> str:
94
116
  return f"{type(self).__name__}({self.path!r}, {self.id!r})"
@@ -97,18 +119,31 @@ class ZipLevel(Level):
97
119
  contents: Final[dict[ZipInfo, bytes]] = {}
98
120
  with ZipFile(self.path, "a") as a:
99
121
  for info in filterfalse(
100
- lambda info: info.filename == self.fn, a.infolist()
122
+ lambda info: info.filename == self.fn or info.filename == self.old_fn,
123
+ a.infolist(),
101
124
  ):
102
125
  contents[info] = a.read(info)
103
126
  with ZipFile(self.path, "w") as a:
104
127
  for x in contents.items():
105
128
  a.writestr(*x)
106
- a.writestr(self.fn, new_content)
129
+ a.writestr(self.fn, BinLevel.compress(new_content))
107
130
 
108
131
 
109
- class ZipGroup(Group[ZipLevel], Location):
132
+ class ZipGroup(Location, Group[ZipLevel]):
110
133
  __slots__ = ()
111
134
 
135
+ def __init__(self, path: str) -> None:
136
+ super().__init__(path)
137
+ with ZipFile(self.path) as a:
138
+ for id in range(20):
139
+ if _id_to_old_fn(id) in a.namelist():
140
+ warn(
141
+ FutureWarning(
142
+ "This ZipGroup contains levels using the deprecated LVL format. Update this file by writing to it or by using the update function."
143
+ )
144
+ )
145
+ break
146
+
112
147
  def __contains__(self, value: object, /) -> TypeGuard[ZipLevel]:
113
148
  return isinstance(value, ZipLevel) and value.path == self.path
114
149
 
@@ -139,28 +174,40 @@ class ZipGroup(Group[ZipLevel], Location):
139
174
  else:
140
175
  raise ValueError
141
176
 
142
- def init(self) -> None:
143
- with ZipFile(self.path, "x") as a:
144
- pass
145
-
146
177
  def fill_mask(self) -> Sequence[bool]:
147
178
  with ZipFile(self.path) as a:
148
- return [_id_to_fn(i) in a.namelist() for i in range(20)]
179
+ return [
180
+ _id_to_fn(i) in a.namelist() or _id_to_old_fn in a.namelist()
181
+ for i in range(20)
182
+ ]
149
183
 
150
184
  def read(self) -> Iterable[Optional[bytes]]:
151
185
  with ZipFile(self.path) as a:
152
- name_list: Final[list[str]] = a.namelist()
153
186
  return [
154
- a.read(fn) if fn in name_list else None
155
- for fn in map(_id_to_fn, range(20))
187
+ BinLevel.decompress(a.read(_id_to_fn(id)))
188
+ if _id_to_fn(id) in a.namelist()
189
+ else a.read(_id_to_old_fn(id))
190
+ if _id_to_old_fn(id) in a.namelist()
191
+ else None
192
+ for id in range(20)
156
193
  ]
157
194
 
195
+ def update(self) -> None:
196
+ self.write(self.read())
197
+ warn(
198
+ FutureWarning(
199
+ "This function will be removed alongside support for the LVL format, and exists soley as a convienient way of converting existing ZIP files to the new BIN format."
200
+ )
201
+ )
202
+
158
203
  def write(self, new_content: Iterable[Optional[bytes]], /) -> None:
159
204
  contents: Final[dict[ZipInfo, bytes]] = {}
160
205
  if self:
161
206
  with ZipFile(self.path) as a:
162
207
  for info in filterfalse(
163
- lambda info: fullmatch(r"(0[1-9]|1\d|20)\.lvl", info.filename),
208
+ lambda info: fullmatch(
209
+ r"(0[1-9]|1\d|20)\.(bin|lvl)", info.filename
210
+ ),
164
211
  a.infolist(),
165
212
  ):
166
213
  contents[info] = a.read(info)
@@ -170,4 +217,4 @@ class ZipGroup(Group[ZipLevel], Location):
170
217
  for id, content in filterfalse(
171
218
  lambda x: x[1] is None, enumerate(new_content)
172
219
  ):
173
- a.writestr(_id_to_fn(id), content)
220
+ a.writestr(_id_to_fn(id), BinLevel.compress(content))
koro/item/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ from .group import *
2
+ from .level import *
3
+ from .save import *
koro/item/group.py CHANGED
@@ -4,6 +4,8 @@ from typing import Final, Generic, Optional, TypeVar
4
4
 
5
5
  from .level import Level, LevelNotFoundError
6
6
 
7
+ __all__ = ["Group"]
8
+
7
9
  _L = TypeVar("_L", bound=Level)
8
10
 
9
11
 
koro/item/level.py CHANGED
@@ -3,6 +3,9 @@ from collections.abc import Sized
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum, unique
5
5
  from typing import Final
6
+ from warnings import warn
7
+
8
+ __all__ = ["Level", "LevelNotFoundError", "LevelStatistics", "Theme"]
6
9
 
7
10
 
8
11
  @unique
@@ -73,10 +76,15 @@ class Level(ABC, Sized):
73
76
 
74
77
  def encode(self) -> bytes:
75
78
  """Return a bytes object that when written to a file can overwrite an official level."""
79
+ warn(
80
+ FutureWarning(
81
+ "The use of this function is deprecated as the new BIN format is compatible with the official levels and can be substituted into the game directly."
82
+ )
83
+ )
76
84
  data: Final[bytearray] = bytearray(self.read())
77
85
  header: Final[bytes] = (
78
86
  b"\x00\x00\x00\x01\x00\x00\x00\x08"
79
- + len(data).to_bytes(4, "big")
87
+ + len(data).to_bytes(4, byteorder="big")
80
88
  + b"\x00\x00\x00\x01"
81
89
  )
82
90
  i: int = 0
koro/item/save.py CHANGED
@@ -5,6 +5,8 @@ from typing import Generic, TypeGuard, TypeVar
5
5
 
6
6
  from .group import Group
7
7
 
8
+ __all__ = ["Save", "Page"]
9
+
8
10
  _G = TypeVar("_G", bound=Group)
9
11
 
10
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: koro
3
- Version: 1.0.8
3
+ Version: 1.1.0
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
@@ -0,0 +1,15 @@
1
+ koro/__init__.py,sha256=clmcgzd33gY_tOC3U5U-010NYVZZrC-9Og6AYAovIjo,176
2
+ koro/file/__init__.py,sha256=rrsPxeKSG6WXprbO-xDZR1E9GdQ2xuzPvoZ08C3QSQc,1038
3
+ koro/file/bin.py,sha256=2rV6BQN-6HH-ilV8WpInI5xWjds9Mo3_4kzQLEDPmqU,7482
4
+ koro/file/dir.py,sha256=pnMQJAQRlzcm_MC14goDeKlQ3llKhywM1FdADHM8xds,6632
5
+ koro/file/lvl.py,sha256=lrVpQyZFj1JlVPYx-OkcCne8VdjHCWQIuOX0pls1Dto,1130
6
+ koro/file/zip.py,sha256=hFV3rnro0pTeg7mpoKEvQnO-Oo93_DgnxJQCJupkVI8,7332
7
+ koro/item/__init__.py,sha256=3RXQS-HSN6EDGLk6sY4xip-oy8JiV31XshsCWVCgie8,65
8
+ koro/item/group.py,sha256=lwmx_1FMD4LdOxxuqPDN2Dl_8MZD9OJcy259SS8Apzk,1538
9
+ koro/item/level.py,sha256=KXiOK3RGC72WK4lduf3RO9Cs1zXaJRk0EfJCDndA4bs,3068
10
+ koro/item/save.py,sha256=LUV79vW3gY__jVVVG1kcYW-wzsGCRlYq126nulbT37Y,607
11
+ koro-1.1.0.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
12
+ koro-1.1.0.dist-info/METADATA,sha256=5Tt71Gav-Itj_GMxWkdYMaMCxssxWLpHmqiRxdfojY0,628
13
+ koro-1.1.0.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
14
+ koro-1.1.0.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
15
+ koro-1.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.41.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- koro/__init__.py,sha256=clmcgzd33gY_tOC3U5U-010NYVZZrC-9Og6AYAovIjo,176
2
- koro/file/__init__.py,sha256=_UIx5h1YBoheqQeorBV5XrbZXusAOipbFsesqUeLJwk,948
3
- koro/file/dir.py,sha256=sxNM_COlADES3oVnlutApXPd2SvWuANRnxMC6RdQ448,6348
4
- koro/file/lvl.py,sha256=cxOJGuzzs3yVTAdi-clhXPn1A7-TU3p-FJW3JLZBTyY,752
5
- koro/file/zip.py,sha256=hK8XemvmM3dhyNPevCt9lXm0p24HYbzyHp40yA5O5cg,5472
6
- koro/item/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- koro/item/group.py,sha256=TfiC2okh-fIVPics7VxOWEIP3-8K0ixDBIOl9yTjBkE,1515
8
- koro/item/level.py,sha256=opGy-JnytnsXGYEZDQky9bTwAr_oJFq1mrQsOAr6pOs,2723
9
- koro/item/save.py,sha256=vwI2b1iUs976ZgTIt3hFxpJajGrGZugDhUxkS7Zl4SE,577
10
- koro-1.0.8.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
11
- koro-1.0.8.dist-info/METADATA,sha256=Vg519VHrxWxDCrFoTmEnBLtQbTXeflX7UvIvmmS-qfA,628
12
- koro-1.0.8.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
13
- koro-1.0.8.dist-info/top_level.txt,sha256=Msq6ssMwv56hnBQqwaTdJJkxM7x7BZ-3JfQbkepQAEY,5
14
- koro-1.0.8.dist-info/RECORD,,
File without changes