sonolus.py 0.6.7__py3-none-any.whl → 0.7.1__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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

@@ -129,7 +129,7 @@ class Collection:
129
129
  resource_data = resource_path.read_bytes()
130
130
 
131
131
  if resource_path.suffix.lower() in {".json", ".bin"}:
132
- resource_data = gzip.compress(resource_data)
132
+ resource_data = gzip.compress(resource_data, mtime=0)
133
133
 
134
134
  srl = self.add_asset(resource_data)
135
135
  item_data[resource_path.stem] = srl
sonolus/build/engine.py CHANGED
@@ -182,11 +182,11 @@ def package_engine(
182
182
  )
183
183
 
184
184
  return PackagedEngine(
185
- configuration=package_output(configuration),
186
- play_data=package_output(play_data),
187
- watch_data=package_output(watch_data),
188
- preview_data=package_output(preview_data),
189
- tutorial_data=package_output(tutorial_data),
185
+ configuration=package_data(configuration),
186
+ play_data=package_data(play_data),
187
+ watch_data=package_data(watch_data),
188
+ preview_data=package_data(preview_data),
189
+ tutorial_data=package_data(tutorial_data),
190
190
  rom=package_rom(rom),
191
191
  )
192
192
 
@@ -407,9 +407,14 @@ def package_rom(rom: ReadOnlyMemory) -> bytes:
407
407
  for value in values:
408
408
  output.extend(struct.pack("<f", value))
409
409
 
410
- return gzip.compress(bytes(output))
410
+ return gzip.compress(bytes(output), mtime=0)
411
411
 
412
412
 
413
- def package_output(value: JsonValue) -> bytes:
413
+ def package_data(value: JsonValue) -> bytes:
414
414
  json_data = json.dumps(value, separators=(",", ":")).encode("utf-8")
415
- return gzip.compress(json_data)
415
+ return gzip.compress(json_data, mtime=0)
416
+
417
+
418
+ def unpackage_data(data: bytes) -> JsonValue:
419
+ json_data = gzip.decompress(data)
420
+ return json.loads(json_data)
sonolus/build/level.py CHANGED
@@ -1,11 +1,11 @@
1
- from sonolus.build.engine import JsonValue, package_output
1
+ from sonolus.build.engine import JsonValue, package_data
2
2
  from sonolus.script.level import LevelData
3
3
 
4
4
 
5
5
  def package_level_data(
6
6
  level_data: LevelData,
7
7
  ) -> bytes:
8
- return package_output(build_level_data(level_data))
8
+ return package_data(build_level_data(level_data))
9
9
 
10
10
 
11
11
  def build_level_data(
sonolus/build/project.py CHANGED
@@ -1,10 +1,12 @@
1
+ from collections.abc import Callable
1
2
  from pathlib import Path
3
+ from typing import cast
2
4
 
3
5
  from sonolus.build.collection import Asset, Collection, Srl
4
- from sonolus.build.engine import package_engine
6
+ from sonolus.build.engine import package_engine, unpackage_data
5
7
  from sonolus.build.level import package_level_data
6
8
  from sonolus.script.engine import Engine
7
- from sonolus.script.level import Level
9
+ from sonolus.script.level import ExternalLevelData, ExternalLevelDataDict, Level, LevelData, parse_external_level_data
8
10
  from sonolus.script.project import BuildConfig, Project, ProjectSchema
9
11
 
10
12
  BLANK_PNG = (
@@ -19,6 +21,13 @@ BLANK_AUDIO = (
19
21
 
20
22
  def build_project_to_collection(project: Project, config: BuildConfig | None):
21
23
  collection = load_resources_files_to_collection(project.resources)
24
+ for src_engine, converter in project.converters.items():
25
+ if src_engine is None:
26
+ continue
27
+ apply_converter_to_collection(collection, src_engine, converter)
28
+ fallback_converter = project.converters.get(None)
29
+ if fallback_converter is not None:
30
+ apply_converter_to_collection(collection, None, fallback_converter)
22
31
  if config.override_resource_level_engines:
23
32
  for level in collection.categories.get("levels", {}).values():
24
33
  level["item"]["engine"] = project.engine.name
@@ -29,6 +38,31 @@ def build_project_to_collection(project: Project, config: BuildConfig | None):
29
38
  return collection
30
39
 
31
40
 
41
+ def apply_converter_to_collection(
42
+ self, src_engine: str | None, converter: Callable[[ExternalLevelData], LevelData | None]
43
+ ) -> None:
44
+ for level_details in self.categories.get("levels", {}).values():
45
+ level = level_details["item"]
46
+ if (
47
+ src_engine is not None
48
+ and level["engine"] != src_engine
49
+ and not (isinstance(level["engine"], dict) and level["engine"].get("name") == src_engine)
50
+ ):
51
+ continue
52
+ packaged_data_srl = level.get("data")
53
+ packaged_data = self.repository.get(packaged_data_srl["hash"])
54
+ data = unpackage_data(packaged_data)
55
+ if not (isinstance(data, dict) and "bgmOffset" in data and "entities" in data):
56
+ raise ValueError(f"Level data for level '{level['name']}' is not valid")
57
+ parsed_data = parse_external_level_data(cast(ExternalLevelDataDict, data))
58
+ new_data = converter(parsed_data)
59
+ if new_data is None:
60
+ continue
61
+ packaged_new_data = package_level_data(new_data)
62
+ new_data_srl = self.add_asset(packaged_new_data)
63
+ level["data"] = new_data_srl
64
+
65
+
32
66
  def add_engine_to_collection(collection: Collection, project: Project, engine: Engine, config: BuildConfig | None):
33
67
  packaged_engine = package_engine(engine.data, config)
34
68
  item = {
@@ -1227,6 +1227,17 @@ class EntityRef[A: _BaseArchetype](Record):
1227
1227
  """Return a new reference with the given archetype type."""
1228
1228
  return EntityRef[archetype](index=self.index)
1229
1229
 
1230
+ @meta_fn
1231
+ def __eq__(self, other: Any) -> bool:
1232
+ if not ctx() and hasattr(self, "_ref_") and hasattr(other, "_ref_"):
1233
+ return self._ref_ is other._ref_
1234
+ return super().__eq__(other)
1235
+
1236
+ def __hash__(self) -> int:
1237
+ if not ctx() and hasattr(self, "_ref_"):
1238
+ return hash(id(self._ref_))
1239
+ return super().__hash__()
1240
+
1230
1241
  @meta_fn
1231
1242
  def get(self) -> A:
1232
1243
  """Get the entity."""
@@ -1365,7 +1376,7 @@ class StandardImport:
1365
1376
  TIMESCALE_EASE = Annotated[TimescaleEase, imported(name=StandardImportName.TIMESCALE_EASE)]
1366
1377
  """The timescale ease type, for timescale change markers."""
1367
1378
 
1368
- JUDGMENT = Annotated[int, imported(name=StandardImportName.JUDGMENT)]
1379
+ JUDGMENT = Annotated[Judgment, imported(name=StandardImportName.JUDGMENT)]
1369
1380
  """The judgment of the entity.
1370
1381
 
1371
1382
  Automatically set in watch mode for archetypes with a corresponding scored play mode archetype.
@@ -16,6 +16,9 @@ def validate_type_arg(arg: Any) -> Any:
16
16
  if not arg._is_py_():
17
17
  raise TypeError(f"Expected a compile-time constant type argument, got {arg}")
18
18
  result = arg._as_py_()
19
+ if isinstance(result, type) and issubclass(result, Enum):
20
+ # E.g. if this is an IntEnum subclass, we call it on IntEnum, and then int, which gets us the result we want
21
+ result = validate_type_arg(result.__mro__[1])
19
22
  if hasattr(result, "_type_mapping_"):
20
23
  return result._type_mapping_
21
24
  if get_origin(result) is Annotated:
@@ -27,10 +30,6 @@ def validate_type_arg(arg: Any) -> Any:
27
30
 
28
31
  def validate_type_spec(spec: Any) -> PartialGeneric | TypeVar | type[Value]:
29
32
  spec = validate_type_arg(spec)
30
- if isinstance(spec, type) and issubclass(spec, Enum):
31
- # For values like IntEnum subclasses, this will call validate_type_spec(IntEnum),
32
- # which in turn will call it on int, so this works.
33
- spec = validate_type_spec(spec.__mro__[1])
34
33
  if isinstance(spec, PartialGeneric | TypeVar) or (isinstance(spec, type) and issubclass(spec, Value)):
35
34
  return spec
36
35
  if typing.get_origin(spec) is UnionType:
sonolus/script/level.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import gzip
3
4
  import json
4
5
  from collections.abc import Iterator
5
6
  from os import PathLike
6
7
  from pathlib import Path
7
- from typing import Any
8
+ from typing import Any, NamedTuple, NotRequired, TypedDict
8
9
 
9
10
  from sonolus.build.collection import Asset, load_asset
10
11
  from sonolus.script.archetype import PlayArchetype, StandardArchetypeName, StandardImport
@@ -196,3 +197,65 @@ class TimescaleChange(PlayArchetype):
196
197
 
197
198
  beat: StandardImport.BEAT
198
199
  timescale: StandardImport.TIMESCALE
200
+
201
+
202
+ class ExternalLevelDataDict(TypedDict):
203
+ bgmOffset: float
204
+ entities: list[ExternalEntityDataDict]
205
+
206
+
207
+ class ExternalEntityDataDict(TypedDict):
208
+ name: NotRequired[str]
209
+ archetype: str
210
+ data: NotRequired[list[ExternalEntityDataValueDict]]
211
+
212
+
213
+ class ExternalEntityDataValueDict(TypedDict):
214
+ name: str
215
+ value: NotRequired[int | float]
216
+ ref: NotRequired[str]
217
+
218
+
219
+ class ExternalLevelData(NamedTuple):
220
+ """Level data parsed from an external source."""
221
+
222
+ bgm_offset: float
223
+ entities: list[ExternalEntityData]
224
+
225
+
226
+ class ExternalEntityData(NamedTuple):
227
+ """Entity data parsed from an external source."""
228
+
229
+ archetype: str
230
+ data: dict[str, Any]
231
+
232
+
233
+ def parse_external_level_data(raw_data: ExternalLevelDataDict | str | bytes, /) -> ExternalLevelData:
234
+ """Parse level data from an external source.
235
+
236
+ If given a string, it is parsed as JSON. If given bytes, it is un-gzipped and then parsed as JSON.
237
+
238
+ Args:
239
+ raw_data: The raw level data to parse.
240
+
241
+ Returns:
242
+ The parsed level data.
243
+ """
244
+ if isinstance(raw_data, bytes):
245
+ raw_data = gzip.decompress(raw_data).decode("utf-8")
246
+ if isinstance(raw_data, str):
247
+ raw_data = json.loads(raw_data)
248
+ bgm_offset = raw_data["bgmOffset"]
249
+ raw_entities = raw_data["entities"]
250
+ entity_name_to_index = {e["name"]: i for i, e in enumerate(raw_entities) if "name" in e}
251
+ entities = []
252
+ for raw_entity in raw_entities:
253
+ archetype = raw_entity["archetype"]
254
+ data = {}
255
+ for entry in raw_entity.get("data", []):
256
+ if "value" in entry:
257
+ data[entry["name"]] = entry["value"]
258
+ elif "ref" in entry:
259
+ data[entry["name"]] = entity_name_to_index.get(entry["ref"], 0)
260
+ entities.append(ExternalEntityData(archetype=archetype, data=data))
261
+ return ExternalLevelData(bgm_offset=bgm_offset, entities=entities)
sonolus/script/project.py CHANGED
@@ -10,7 +10,7 @@ from sonolus.backend.optimize import optimize
10
10
  from sonolus.backend.optimize.passes import CompilerPass
11
11
  from sonolus.script.archetype import ArchetypeSchema
12
12
  from sonolus.script.engine import Engine
13
- from sonolus.script.level import Level
13
+ from sonolus.script.level import ExternalLevelData, Level, LevelData
14
14
 
15
15
 
16
16
  class Project:
@@ -20,6 +20,7 @@ class Project:
20
20
  engine: The engine of the project.
21
21
  levels: The levels of the project.
22
22
  resources: The path to the resources of the project.
23
+ converters: A dictionary mapping engine names to converter functions, for converting loaded levels.
23
24
  """
24
25
 
25
26
  def __init__(
@@ -27,6 +28,7 @@ class Project:
27
28
  engine: Engine,
28
29
  levels: Iterable[Level] | Callable[[], Iterable[Level]] | None = None,
29
30
  resources: PathLike | None = None,
31
+ converters: dict[str | None, Callable[[ExternalLevelData], LevelData | None]] | None = None,
30
32
  ):
31
33
  self.engine = engine
32
34
  match levels:
@@ -40,6 +42,7 @@ class Project:
40
42
  raise TypeError(f"Invalid type for levels: {type(levels)}. Expected Iterable or Callable.")
41
43
  self._levels = None
42
44
  self.resources = Path(resources or "resources")
45
+ self.converters = converters or {}
43
46
 
44
47
  def with_levels(self, levels: Iterable[Level] | Callable[[], Iterable[Level]] | None) -> Project:
45
48
  """Create a new project with the specified levels.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.6.7
3
+ Version: 0.7.1
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -27,14 +27,14 @@ sonolus/backend/optimize/simplify.py,sha256=RDNVTKfC7ByRyxY5z30_ShimOAKth_pKlVFV
27
27
  sonolus/backend/optimize/ssa.py,sha256=raQO0furQQRPYb8iIBKfNrJlj-_5wqtI4EWNfLZ8QFo,10834
28
28
  sonolus/build/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  sonolus/build/cli.py,sha256=-_lTN8zT7nQB2lySM8itAEPVutcEQI-TJ13BcPIfGb4,10113
30
- sonolus/build/collection.py,sha256=sMYLfEIVn6UydLy7iEnNwrpIXDs7bGG32uQQgHXyhSI,12289
30
+ sonolus/build/collection.py,sha256=5TvltBugFmIrvZC5HSP3LRZe6sjO9g6q-DHjBrovHsg,12298
31
31
  sonolus/build/compile.py,sha256=Yex-ZbSZmv9ujyAOVaMcfq-0NnZzFIqtmNYYaplF4Uc,6761
32
- sonolus/build/engine.py,sha256=zUl0KfRygqNhIM8BABNJkKG-0zXFwcYwck-5hJy59yk,13338
33
- sonolus/build/level.py,sha256=yXsQtnabkJK0vuVmZ_Wr1jx37jFLgInCS7lTlXjkv9Q,706
32
+ sonolus/build/engine.py,sha256=YQ2Sc2zC56N2Y5S5zE3QCoUzwKcVHSPl14iQE0tkm6Q,13463
33
+ sonolus/build/level.py,sha256=KLqUAtxIuIqrzeFURJA97rdqjA5pcvYSmwNZQhElaMQ,702
34
34
  sonolus/build/node.py,sha256=gnX71RYDUOK_gYMpinQi-bLWO4csqcfiG5gFmhxzSec,1330
35
- sonolus/build/project.py,sha256=eHh4ioOjaFtt26bcefUuDZhMhFw8NXnjRTYPiEInQV8,6505
35
+ sonolus/build/project.py,sha256=__sOJas3RTwn4aizq16rYCvZgG8aX-JbQaxmfVhRnc4,8154
36
36
  sonolus/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- sonolus/script/archetype.py,sha256=mUBGvD7pMGeIEMLJfs-A5Bgd1CgG0Rce1vTJ7GUAXPE,49272
37
+ sonolus/script/archetype.py,sha256=xhdm1jO_p32V1NtGjgV0SGPm7X9GnJ6Z7hXi2zT69Ww,49647
38
38
  sonolus/script/array.py,sha256=9uOUHZIDMyMT9q3APcXJWXWt97yG-AZoRlxwrvSY6SU,12367
39
39
  sonolus/script/array_like.py,sha256=hUDdDaP306kflVv9YomdHIMXogrVjxsBXCrLvB9QpuE,9681
40
40
  sonolus/script/bucket.py,sha256=SNqnfLGpnO_u-3LFFazdgzNk332OdDUIlmZHsuoVZuE,7674
@@ -47,7 +47,7 @@ sonolus/script/globals.py,sha256=nlXSNS4NRXsgQU2AJImVIs752h1WqsMnShSKgU011c4,102
47
47
  sonolus/script/instruction.py,sha256=Dd-14D5Amo8nhPBr6DNyg2lpYw_rqZkT8Kix3HkfE7k,6793
48
48
  sonolus/script/interval.py,sha256=dj6F2wn5uP6I6_mcZn-wIREgRUQbsLzhvhzB0oEyAdU,11290
49
49
  sonolus/script/iterator.py,sha256=_ICY_yX7FG0Zbgs3NhVnaIBdVDpAeXjxJ_CQtq30l7Y,3774
50
- sonolus/script/level.py,sha256=vnotMbdr_4-MJUsTXMbvWiw2MlMjMHme3q0XRdNFXRg,6349
50
+ sonolus/script/level.py,sha256=X3-V99ihruYYCcPdch66dHi_ydCWXXn7epviLLjxW8w,8288
51
51
  sonolus/script/maybe.py,sha256=VYvTWgEfPzoXqI3i3zXhc4dz0pWBVoHmW8FtWH0GQvM,8194
52
52
  sonolus/script/metadata.py,sha256=ttRK27eojHf3So50KQJ-8yj3udZoN1bli5iD-knaeLw,753
53
53
  sonolus/script/num.py,sha256=924kWWZusW7oaWuvtQzdAMzkb4ZItWSJwNj3W9XrqZU,16041
@@ -55,7 +55,7 @@ sonolus/script/options.py,sha256=XVN-mL7Rwhd2Tu9YysYq9YDGpH_LazdmhqzSYE6nR3Q,945
55
55
  sonolus/script/particle.py,sha256=RTfamg_ZTq7-qJ6j9paduNVUHCkw3hzkBK1QUbwwN7I,8403
56
56
  sonolus/script/pointer.py,sha256=FoOfyD93r0G5d_2BaKfeOT9SqkOP3hq6sqtOs_Rb0c8,1511
57
57
  sonolus/script/printing.py,sha256=mNYu9QWiacBBGZrnePZQMVwbbguoelUps9GiOK_aVRU,2096
58
- sonolus/script/project.py,sha256=BDGaae3lXWQqZgY3lF3_27VSSk_oGEA4sbN-gQFlhAM,4157
58
+ sonolus/script/project.py,sha256=gG6mYsGlIqznY_RODyFMiXVBh0tVbXLAWZ5LHCiBIuU,4439
59
59
  sonolus/script/quad.py,sha256=XoAjaUqR60zIrC_CwheZs7HwS-DRS58yUmlj9GIjX7k,11179
60
60
  sonolus/script/record.py,sha256=igmawc0hqb98YVcZnPTThHvnIP88Qelhoge4cNJx45Q,12770
61
61
  sonolus/script/runtime.py,sha256=rJZM_KbKmnwpjhDEpR0DrM6EMSEu46apIErWA_pfLJA,33321
@@ -75,7 +75,7 @@ sonolus/script/internal/context.py,sha256=Cd9US3GNHxs_96UcVdBAXGD-H4gCNnV9h5OfCW
75
75
  sonolus/script/internal/descriptor.py,sha256=XRFey-EjiAm_--KsNl-8N0Mi_iyQwlPh68gDp0pKf3E,392
76
76
  sonolus/script/internal/dict_impl.py,sha256=alu_wKGSk1kZajNf64qbe7t71shEzD4N5xNIATH8Swo,1885
77
77
  sonolus/script/internal/error.py,sha256=ZNnsvQVQAnFKzcvsm6-sste2lo-tP5pPI8sD7XlAZWc,490
78
- sonolus/script/internal/generic.py,sha256=F0-cCiRNGTaUJvYlpmkiOsU3Xge_XjoBpBwBhH_qS_s,7577
78
+ sonolus/script/internal/generic.py,sha256=_3d5Rn_tn214-77fPE67vdbdqt1PQF8-2WB_XDu5YRg,7551
79
79
  sonolus/script/internal/impl.py,sha256=JzRk2iylXHNk7q7f_H84spsix2gcnoTqo-hLbIegjoI,3261
80
80
  sonolus/script/internal/introspection.py,sha256=guL9_NR2D3OJAnNpeFdyYkO_vVXk-3KQr2-y4YielM0,1133
81
81
  sonolus/script/internal/math_impls.py,sha256=nHSLgA7Tcx7jY1p07mYBCeSRmVx713bwdNayCIcaXSE,2652
@@ -86,8 +86,8 @@ sonolus/script/internal/simulation_context.py,sha256=LGxLTvxbqBIhoe1R-SfwGajNIDw
86
86
  sonolus/script/internal/transient.py,sha256=y2AWABqF1aoaP6H4_2u4MMpNioC4OsZQCtPyNI0txqo,1634
87
87
  sonolus/script/internal/tuple_impl.py,sha256=DPNdmmRmupU8Ah4_XKq6-PdT336l4nt15_uCJKQGkkk,3587
88
88
  sonolus/script/internal/value.py,sha256=OngrCdmY_h6mV2Zgwqhuo4eYFad0kTk6263UAxctZcY,6963
89
- sonolus_py-0.6.7.dist-info/METADATA,sha256=UZzZ0PYBRM0JuGiVVT16UHvO6A5ZQ1Xbplv0t9Yt5yQ,302
90
- sonolus_py-0.6.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
- sonolus_py-0.6.7.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
92
- sonolus_py-0.6.7.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
93
- sonolus_py-0.6.7.dist-info/RECORD,,
89
+ sonolus_py-0.7.1.dist-info/METADATA,sha256=p9fNorfrM-GbAWcBdMkyFjWt5mTc3xVnEXrZLql5AqQ,302
90
+ sonolus_py-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
+ sonolus_py-0.7.1.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
92
+ sonolus_py-0.7.1.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
93
+ sonolus_py-0.7.1.dist-info/RECORD,,