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

Potentially problematic release.


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

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
 
@@ -410,6 +410,11 @@ def package_rom(rom: ReadOnlyMemory) -> bytes:
410
410
  return gzip.compress(bytes(output))
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
415
  return gzip.compress(json_data)
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,29 @@ 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]
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
+ packaged_new_data = package_level_data(new_data)
60
+ new_data_srl = self.add_asset(packaged_new_data)
61
+ level["data"] = new_data_srl
62
+
63
+
32
64
  def add_engine_to_collection(collection: Collection, project: Project, engine: Engine, config: BuildConfig | None):
33
65
  packaged_engine = package_engine(engine.data, config)
34
66
  item = {
@@ -1227,6 +1227,12 @@ 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
+
1230
1236
  @meta_fn
1231
1237
  def get(self) -> A:
1232
1238
  """Get the entity."""
@@ -1365,7 +1371,7 @@ class StandardImport:
1365
1371
  TIMESCALE_EASE = Annotated[TimescaleEase, imported(name=StandardImportName.TIMESCALE_EASE)]
1366
1372
  """The timescale ease type, for timescale change markers."""
1367
1373
 
1368
- JUDGMENT = Annotated[int, imported(name=StandardImportName.JUDGMENT)]
1374
+ JUDGMENT = Annotated[Judgment, imported(name=StandardImportName.JUDGMENT)]
1369
1375
  """The judgment of the entity.
1370
1376
 
1371
1377
  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
@@ -4,7 +4,7 @@ import json
4
4
  from collections.abc import Iterator
5
5
  from os import PathLike
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, NamedTuple, NotRequired, TypedDict
8
8
 
9
9
  from sonolus.build.collection import Asset, load_asset
10
10
  from sonolus.script.archetype import PlayArchetype, StandardArchetypeName, StandardImport
@@ -196,3 +196,51 @@ class TimescaleChange(PlayArchetype):
196
196
 
197
197
  beat: StandardImport.BEAT
198
198
  timescale: StandardImport.TIMESCALE
199
+
200
+
201
+ class ExternalLevelDataDict(TypedDict):
202
+ bgmOffset: float
203
+ entities: list[ExternalEntityDataDict]
204
+
205
+
206
+ class ExternalEntityDataDict(TypedDict):
207
+ name: NotRequired[str]
208
+ archetype: str
209
+ data: NotRequired[list[ExternalEntityDataValueDict]]
210
+
211
+
212
+ class ExternalEntityDataValueDict(TypedDict):
213
+ name: str
214
+ value: NotRequired[int | float]
215
+ ref: NotRequired[str]
216
+
217
+
218
+ class ExternalLevelData(NamedTuple):
219
+ """Level data parsed from an external source."""
220
+
221
+ bgm_offset: float
222
+ entities: list[ExternalEntityData]
223
+
224
+
225
+ class ExternalEntityData(NamedTuple):
226
+ """Entity data parsed from an external source."""
227
+
228
+ archetype: str
229
+ data: dict[str, Any]
230
+
231
+
232
+ def parse_external_level_data(raw_data: ExternalLevelDataDict) -> ExternalLevelData:
233
+ bgm_offset = raw_data["bgmOffset"]
234
+ raw_entities = raw_data["entities"]
235
+ entity_name_to_index = {e["name"]: i for i, e in enumerate(raw_entities) if "name" in e}
236
+ entities = []
237
+ for raw_entity in raw_entities:
238
+ archetype = raw_entity["archetype"]
239
+ data = {}
240
+ for entry in raw_entity.get("data", []):
241
+ if "value" in entry:
242
+ data[entry["name"]] = entry["value"]
243
+ elif "ref" in entry:
244
+ data[entry["name"]] = entity_name_to_index.get(entry["ref"], 0)
245
+ entities.append(ExternalEntityData(archetype=archetype, data=data))
246
+ 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,
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.0
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -29,12 +29,12 @@ sonolus/build/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  sonolus/build/cli.py,sha256=-_lTN8zT7nQB2lySM8itAEPVutcEQI-TJ13BcPIfGb4,10113
30
30
  sonolus/build/collection.py,sha256=sMYLfEIVn6UydLy7iEnNwrpIXDs7bGG32uQQgHXyhSI,12289
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=xPhNnaFkfAuflp7CYb2NrXTgh9xCNN2h_hT_uWAYl1g,13445
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=h-9tcC60KqrpUOxGHFCK9trGVhGPUkciXiibasBEzu4,8097
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=xoBVt4sQcC0e1ikyaM4NfcDZrMrJ0lHHUrxznl6dsU4,49492
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=5M_XTzvXpFZTY-ODlS6M3GOiCIYN3F8rFuUgOiGxVBI,7824
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=a9bRY5T8deRMR35iC-6cuqn4HiKwZHvVy9K-np-rAfI,4432
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.0.dist-info/METADATA,sha256=aTrZv-zipIgHI36FFgSO_bmIjPOAixQa4-CdHcq7Cvc,302
90
+ sonolus_py-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
+ sonolus_py-0.7.0.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
92
+ sonolus_py-0.7.0.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
93
+ sonolus_py-0.7.0.dist-info/RECORD,,