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

@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  from abc import abstractmethod
5
- from collections.abc import Callable
5
+ from collections.abc import Callable, Sequence
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum, StrEnum
8
8
  from types import FunctionType
@@ -11,7 +11,9 @@ from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
11
11
  from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr, IRStmt
12
12
  from sonolus.backend.mode import Mode
13
13
  from sonolus.backend.ops import Op
14
+ from sonolus.backend.visitor import compile_and_call
14
15
  from sonolus.script.bucket import Bucket, Judgment
16
+ from sonolus.script.debug import runtime_checks_enabled, static_error
15
17
  from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
16
18
  from sonolus.script.internal.context import ctx
17
19
  from sonolus.script.internal.descriptor import SonolusDescriptor
@@ -207,7 +209,7 @@ class _IsScoredDescriptor(SonolusDescriptor):
207
209
  if instance is None:
208
210
  return self.value
209
211
  elif ctx():
210
- return ctx().mode_state.is_scored_by_archetype_id[instance.id]
212
+ return ctx().mode_state.is_scored_by_archetype_id.get_unchecked(instance.id)
211
213
  else:
212
214
  return self.value
213
215
 
@@ -221,7 +223,7 @@ class _IdDescriptor(SonolusDescriptor):
221
223
  raise RuntimeError("Archetype id is only available during compilation")
222
224
  if instance is None:
223
225
  result = ctx().mode_state.archetypes.get(owner)
224
- if result is None:
226
+ if result is None or owner in ctx().mode_state.compile_time_only_archetypes:
225
227
  raise RuntimeError("Archetype is not registered")
226
228
  return result
227
229
  else:
@@ -237,7 +239,7 @@ class _KeyDescriptor(SonolusDescriptor):
237
239
 
238
240
  def __get__(self, instance, owner):
239
241
  if instance is not None and ctx():
240
- return ctx().mode_state.keys_by_archetype_id[instance.id]
242
+ return ctx().mode_state.keys_by_archetype_id.get_unchecked(instance.id)
241
243
  else:
242
244
  return self.value
243
245
 
@@ -427,7 +429,7 @@ class _BaseArchetype:
427
429
 
428
430
  _imported_keys_: ClassVar[dict[str, int]]
429
431
  _exported_keys_: ClassVar[dict[str, int]]
430
- _callbacks_: ClassVar[list[Callable]]
432
+ _callbacks_: ClassVar[dict[str, Callable]]
431
433
  _data_constructor_signature_: ClassVar[inspect.Signature]
432
434
  _spawn_signature_: ClassVar[inspect.Signature]
433
435
 
@@ -491,17 +493,67 @@ class _BaseArchetype:
491
493
 
492
494
  @classmethod
493
495
  @meta_fn
494
- def at(cls, index: int) -> Self:
496
+ def _compile_time_id(cls):
497
+ if not ctx():
498
+ raise RuntimeError("Archetype id is only available during compilation")
499
+ result = ctx().mode_state.archetypes.get(cls)
500
+ if result is None:
501
+ raise RuntimeError("Archetype is not registered")
502
+ return result
503
+
504
+ @classmethod
505
+ @meta_fn
506
+ def at(cls, index: int, check: bool = True) -> Self:
507
+ """Access the entity of this archetype at the given index.
508
+
509
+ Args:
510
+ index: The index of the entity to reference.
511
+ check: If true, raises an error if the entity at the index is not of this archetype or of a subclass of
512
+ this archetype. If false, no validation is performed.
513
+
514
+ Returns:
515
+ The entity at the given index.
516
+ """
517
+ if not ctx():
518
+ raise RuntimeError("Archetype.at is only available during compilation")
519
+ compile_and_call(cls._check_is_at, index, check=check)
495
520
  result = cls._new()
496
521
  result._data_ = _ArchetypeReferenceData(index=Num._accept_(index))
497
522
  return result
498
523
 
499
524
  @classmethod
525
+ def is_at(cls, index: int, strict: bool = False) -> bool:
526
+ """Return whether the entity at the given index is of this archetype.
527
+
528
+ Args:
529
+ index: The index of the entity to check.
530
+ strict: If true, only returns true if the entity is exactly of this archetype. If false, also returns true
531
+ if the entity is of a subclass of this archetype.
532
+
533
+ Returns:
534
+ Whether the entity at the given index is of this archetype.
535
+ """
536
+ if strict:
537
+ return index >= 0 and cls._compile_time_id() == entity_info_at(index).archetype_id
538
+ else:
539
+ mro_ids = cls._get_mro_id_array(entity_info_at(index).archetype_id)
540
+ return index >= 0 and cls._compile_time_id() in mro_ids
541
+
542
+ @classmethod
543
+ def _check_is_at(cls, index: int, check: bool):
544
+ if not check or not runtime_checks_enabled():
545
+ return
546
+ assert index >= 0, "Entity index must be non-negative"
547
+ assert cls._compile_time_id() in cls._get_mro_id_array(entity_info_at(index).archetype_id), (
548
+ "Entity at index is not of the expected archetype"
549
+ )
550
+
551
+ @staticmethod
500
552
  @meta_fn
501
- def is_at(cls, index: int) -> bool:
553
+ def _get_mro_id_array(archetype_id: int) -> Sequence[int]:
502
554
  if not ctx():
503
- raise RuntimeError("is_at is only available during compilation")
504
- return entity_info_at(index).archetype_id == cls.id
555
+ raise RuntimeError("Archetype._get_mro_id_array is only available during compilation")
556
+ return ctx().get_archetype_mro_id_array(archetype_id)
505
557
 
506
558
  @classmethod
507
559
  @meta_fn
@@ -562,13 +614,16 @@ class _BaseArchetype:
562
614
  return
563
615
  if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
564
616
  cls.name = cls.__name__.removeprefix(cls._removable_prefix)
565
- cls._callbacks_ = []
617
+ cls._callbacks_ = {}
566
618
  for name in cls._supported_callbacks_:
567
- cb = getattr(cls, name)
568
- if cb in cls._default_callbacks_:
569
- continue
570
- cls._callbacks_.append(cb)
619
+ for mro_entry in cls.mro():
620
+ if name in mro_entry.__dict__:
621
+ cb = mro_entry.__dict__[name]
622
+ if cb not in cls._default_callbacks_:
623
+ cls._callbacks_[name] = cb
624
+ break
571
625
  cls._field_init_done = False
626
+ cls._is_concrete_archetype_ = True
572
627
  cls.id = _IdDescriptor()
573
628
  cls._key_ = cls.key
574
629
  cls.key = _KeyDescriptor(cls.key)
@@ -581,27 +636,39 @@ class _BaseArchetype:
581
636
  def _init_fields(cls):
582
637
  if cls._field_init_done:
583
638
  return
584
- cls._field_init_done = True
585
639
  for mro_entry in cls.mro()[1:]:
586
640
  if hasattr(mro_entry, "_field_init_done"):
587
641
  mro_entry._init_fields()
588
- field_specifiers = get_field_specifiers(
589
- cls,
590
- skip={
591
- "id",
592
- "key",
593
- "name",
594
- "life",
595
- "is_scored",
596
- "_key_",
597
- "_is_scored_",
598
- "_derived_base_",
599
- "_is_derived_",
600
- "_default_callbacks_",
601
- "_callbacks_",
602
- "_field_init_done",
603
- },
604
- ).items()
642
+ if sum(issubclass(base, _BaseArchetype) for base in cls.__bases__) > 1:
643
+ raise TypeError("Multiple inheritance of Archetypes is not supported")
644
+ mro_from_archetype_parents = set(
645
+ type("Dummy", tuple(base for base in cls.__bases__ if issubclass(base, _BaseArchetype)), {}).mro()
646
+ )
647
+ # Archetype parents would have already initialized relevant fields, so only consider the current class
648
+ # and mixins that were not already included via an archetype parent
649
+ mro_excluding_archetype_parents = [entry for entry in cls.mro() if entry not in mro_from_archetype_parents]
650
+ try:
651
+ field_specifiers = get_field_specifiers(
652
+ cls,
653
+ skip={
654
+ "id",
655
+ "key",
656
+ "name",
657
+ "life",
658
+ "is_scored",
659
+ "_key_",
660
+ "_is_scored_",
661
+ "_derived_base_",
662
+ "_is_derived_",
663
+ "_default_callbacks_",
664
+ "_callbacks_",
665
+ "_field_init_done",
666
+ "_is_concrete_archetype_",
667
+ },
668
+ included_classes=mro_excluding_archetype_parents,
669
+ ).items()
670
+ except Exception as e:
671
+ raise TypeError(f"Error while processing fields of {cls.__name__}: {e}") from e
605
672
  if not hasattr(cls, "_imported_fields_"):
606
673
  cls._imported_fields_ = {}
607
674
  else:
@@ -643,17 +710,27 @@ class _BaseArchetype:
643
710
  pass
644
711
  else:
645
712
  raise TypeError(
646
- f"Unexpected multiple field annotations for '{name}', "
713
+ f"Unexpected multiple annotations for field '{name}' of {cls.__name__}, "
647
714
  f"expected exactly one of imported, exported, entity_memory, or shared_memory"
648
715
  )
649
716
  else:
650
717
  field_info = metadata
651
718
  if field_info is None:
652
719
  raise TypeError(
653
- f"Missing field annotation for '{name}', "
720
+ f"Missing annotation for '{name}' of {cls.__name__}, "
654
721
  f"expected exactly one of imported, exported, entity_memory, or shared_memory"
655
722
  )
656
- field_type = validate_concrete_type(value.__args__[0])
723
+ if (
724
+ name in cls._imported_fields_
725
+ or name in cls._exported_fields_
726
+ or name in cls._memory_fields_
727
+ or name in cls._shared_memory_fields_
728
+ ):
729
+ raise ValueError(f"Field '{name}' is already defined in a superclass")
730
+ try:
731
+ field_type = validate_concrete_type(value.__args__[0])
732
+ except Exception as e:
733
+ raise TypeError(f"Error in field '{name}' of {cls.__name__}: {e}") from e
657
734
  match field_info.storage:
658
735
  case _StorageType.IMPORTED:
659
736
  cls._imported_fields_[name] = _ArchetypeField(
@@ -706,6 +783,7 @@ class _BaseArchetype:
706
783
  [inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
707
784
  )
708
785
  cls._post_init_fields()
786
+ cls._field_init_done = True
709
787
 
710
788
  @property
711
789
  @abstractmethod
@@ -770,6 +848,18 @@ class _BaseArchetype:
770
848
  new_cls = type(name, (cls,), cls_dict)
771
849
  return new_cls
772
850
 
851
+ @meta_fn
852
+ def _delegate(self, name: str, default: int | float | None = None):
853
+ name = validate_value(name)._as_py_()
854
+ if hasattr(super(), name):
855
+ fn = getattr(super(), name)
856
+ if ctx():
857
+ return compile_and_call(fn)
858
+ else:
859
+ return fn()
860
+ else:
861
+ return default
862
+
773
863
 
774
864
  class PlayArchetype(_BaseArchetype):
775
865
  """Base class for play mode archetypes.
@@ -807,26 +897,28 @@ class PlayArchetype(_BaseArchetype):
807
897
 
808
898
  Runs first when the level is loaded.
809
899
  """
900
+ self._delegate("preprocess")
810
901
 
811
902
  def spawn_order(self) -> float:
812
903
  """Return the spawn order of the entity.
813
904
 
814
905
  Runs when the level is loaded after [`preprocess`][sonolus.script.archetype.PlayArchetype.preprocess].
815
906
  """
816
- return 0.0
907
+ return self._delegate("spawn_order", default=0.0)
817
908
 
818
909
  def should_spawn(self) -> bool:
819
910
  """Return whether the entity should be spawned.
820
911
 
821
912
  Runs each frame while the entity is the first entity in the spawn queue.
822
913
  """
823
- return True
914
+ return self._delegate("should_spawn", default=True)
824
915
 
825
916
  def initialize(self):
826
917
  """Initialize this entity.
827
918
 
828
919
  Runs when this entity is spawned.
829
920
  """
921
+ self._delegate("initialize")
830
922
 
831
923
  def update_sequential(self):
832
924
  """Perform non-parallel actions for this frame.
@@ -838,6 +930,7 @@ class PlayArchetype(_BaseArchetype):
838
930
  [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel]
839
931
  for better performance.
840
932
  """
933
+ self._delegate("update_sequential")
841
934
 
842
935
  def update_parallel(self):
843
936
  """Perform parallel actions for this frame.
@@ -846,18 +939,21 @@ class PlayArchetype(_BaseArchetype):
846
939
 
847
940
  This is where most gameplay logic should be placed.
848
941
  """
942
+ self._delegate("update_parallel")
849
943
 
850
944
  def touch(self):
851
945
  """Handle user input.
852
946
 
853
947
  Runs after [`update_sequential`][sonolus.script.archetype.PlayArchetype.update_sequential] each frame.
854
948
  """
949
+ self._delegate("touch")
855
950
 
856
951
  def terminate(self):
857
952
  """Finalize before despawning.
858
953
 
859
954
  Runs when the entity is despawned.
860
955
  """
956
+ self._delegate("terminate")
861
957
 
862
958
  @property
863
959
  @meta_fn
@@ -965,20 +1061,22 @@ class WatchArchetype(_BaseArchetype):
965
1061
 
966
1062
  Runs first when the level is loaded.
967
1063
  """
1064
+ self._delegate("preprocess")
968
1065
 
969
1066
  def spawn_time(self) -> float:
970
1067
  """Return the spawn time of the entity."""
971
- return 0.0
1068
+ return self._delegate("spawn_time", default=0.0)
972
1069
 
973
1070
  def despawn_time(self) -> float:
974
1071
  """Return the despawn time of the entity."""
975
- return 0.0
1072
+ return self._delegate("despawn_time", default=0.0)
976
1073
 
977
1074
  def initialize(self):
978
1075
  """Initialize this entity.
979
1076
 
980
1077
  Runs when this entity is spawned.
981
1078
  """
1079
+ self._delegate("initialize")
982
1080
 
983
1081
  def update_sequential(self):
984
1082
  """Perform non-parallel actions for this frame.
@@ -989,6 +1087,7 @@ class WatchArchetype(_BaseArchetype):
989
1087
  Other logic should typically be placed in
990
1088
  [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel] for better performance.
991
1089
  """
1090
+ self._delegate("update_sequential")
992
1091
 
993
1092
  def update_parallel(self):
994
1093
  """Parallel update callback.
@@ -997,12 +1096,14 @@ class WatchArchetype(_BaseArchetype):
997
1096
 
998
1097
  This is where most gameplay logic should be placed.
999
1098
  """
1099
+ self._delegate("update_parallel")
1000
1100
 
1001
1101
  def terminate(self):
1002
1102
  """Finalize before despawning.
1003
1103
 
1004
1104
  Runs when the entity is despawned.
1005
1105
  """
1106
+ self._delegate("terminate")
1006
1107
 
1007
1108
  @property
1008
1109
  @meta_fn
@@ -1073,12 +1174,14 @@ class PreviewArchetype(_BaseArchetype):
1073
1174
 
1074
1175
  Runs first when the level is loaded.
1075
1176
  """
1177
+ self._delegate("preprocess")
1076
1178
 
1077
1179
  def render(self):
1078
1180
  """Render the entity.
1079
1181
 
1080
1182
  Runs after `preprocess`.
1081
1183
  """
1184
+ self._delegate("render")
1082
1185
 
1083
1186
  @property
1084
1187
  @meta_fn
@@ -1210,6 +1313,8 @@ class EntityRef[A: _BaseArchetype](Record):
1210
1313
 
1211
1314
  Usage:
1212
1315
  ```python
1316
+ ref = EntityRef[MyArchetype](index=123)
1317
+
1213
1318
  class MyArchetype(PlayArchetype):
1214
1319
  ref_1: EntityRef[OtherArchetype] = imported()
1215
1320
  ref_2: EntityRef[Any] = imported()
@@ -1239,11 +1344,28 @@ class EntityRef[A: _BaseArchetype](Record):
1239
1344
  return super().__hash__()
1240
1345
 
1241
1346
  @meta_fn
1242
- def get(self) -> A:
1243
- """Get the entity."""
1347
+ def __bool__(self):
1348
+ if ctx():
1349
+ static_error("EntityRef cannot be used in a boolean context. Check index directly instead.")
1350
+ return True
1351
+
1352
+ @meta_fn
1353
+ def get(self, *, check: bool = True) -> A:
1354
+ """Get the entity this reference points to.
1355
+
1356
+ Args:
1357
+ check: If true, raises an error if the referenced entity is not of this archetype or of a subclass of
1358
+ this archetype. If false, no validation is performed.
1359
+
1360
+ Returns:
1361
+ The entity this reference points to.
1362
+ """
1363
+ assert self.archetype() != Any, (
1364
+ "Cannot get entity of unknown (Any) archetype. Use with_archetype() first or use get_as()."
1365
+ )
1244
1366
  if ref := getattr(self, "_ref_", None):
1245
1367
  return ref
1246
- return self.archetype().at(self.index)
1368
+ return self.archetype().at(self.index, check=check)
1247
1369
 
1248
1370
  @meta_fn
1249
1371
  def get_as(self, archetype: type[_BaseArchetype]) -> _BaseArchetype:
@@ -1252,9 +1374,20 @@ class EntityRef[A: _BaseArchetype](Record):
1252
1374
  raise TypeError("Using get_as in level data is not supported.")
1253
1375
  return self.with_archetype(archetype).get()
1254
1376
 
1255
- def archetype_matches(self) -> bool:
1256
- """Check if entity at the index is precisely of the archetype."""
1257
- return self.index >= 0 and self.archetype().is_at(self.index)
1377
+ def archetype_matches(self, strict: bool = False) -> bool:
1378
+ """Check if entity at the index is of this archetype.
1379
+
1380
+ Args:
1381
+ strict: If true, only returns true if the entity is exactly of this archetype. If false, also returns true
1382
+ if the entity is of a subclass of this archetype.
1383
+
1384
+ Returns:
1385
+ Whether the entity at the given index is of this archetype.
1386
+ """
1387
+ assert self.archetype() != Any, (
1388
+ "Cannot use archetype_matches with unknown (Any) archetype. Use with_archetype() first."
1389
+ )
1390
+ return self.archetype().is_at(self.index, strict=strict)
1258
1391
 
1259
1392
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1260
1393
  ref = getattr(self, "_ref_", None)
@@ -1289,6 +1422,11 @@ class EntityRef[A: _BaseArchetype](Record):
1289
1422
  result._ref_ = value._ref_
1290
1423
  return result
1291
1424
 
1425
+ @classmethod
1426
+ def _validate_parameterized_(cls):
1427
+ if not issubclass(cls.archetype(), _BaseArchetype) and cls.archetype() is not Any:
1428
+ raise TypeError("EntityRef type parameter must be an Archetype or Any")
1429
+
1292
1430
 
1293
1431
  class StandardArchetypeName(StrEnum):
1294
1432
  """Standard archetype names."""
sonolus/script/bucket.py CHANGED
@@ -75,7 +75,7 @@ class JudgmentWindow(Record):
75
75
  @perf_meta_fn
76
76
  def __mul__(self, other: float | int) -> JudgmentWindow:
77
77
  """Multiply the intervals by a scalar."""
78
- return JudgmentWindow._quick_construct(
78
+ return JudgmentWindow._unchecked(
79
79
  perfect=self.perfect * other,
80
80
  great=self.great * other,
81
81
  good=self.good * other,
@@ -84,7 +84,7 @@ class JudgmentWindow(Record):
84
84
  @perf_meta_fn
85
85
  def __add__(self, other: float | int) -> JudgmentWindow:
86
86
  """Add a scalar to the intervals."""
87
- return JudgmentWindow._quick_construct(
87
+ return JudgmentWindow._unchecked(
88
88
  perfect=self.perfect + other,
89
89
  great=self.great + other,
90
90
  good=self.good + other,
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from typing import Any, Protocol, Self
5
5
 
6
+ from sonolus.backend.visitor import compile_and_call
6
7
  from sonolus.script.archetype import AnyArchetype, EntityRef
7
8
  from sonolus.script.array import Array
8
9
  from sonolus.script.array_like import ArrayLike, get_positive_index
@@ -441,6 +442,56 @@ class ArraySet[T, Capacity](Record):
441
442
  self._values.clear()
442
443
 
443
444
 
445
+ class FrozenNumSet[Size](Record):
446
+ _values: Array[Num, Size]
447
+
448
+ @classmethod
449
+ @meta_fn
450
+ def of(cls, *values: Num) -> Self:
451
+ if ctx():
452
+ try:
453
+ num_values = [Num._accept_(v) for v in values]
454
+ except TypeError:
455
+ raise TypeError("Only sets of numeric values are supported") from None
456
+ if all(v._is_py_() for v in num_values):
457
+ const_values = [v._as_py_() for v in num_values]
458
+ arr = Array[Num, len(const_values)]._with_value([Num(v) for v in sorted(const_values)])
459
+ return cls(arr)
460
+ else:
461
+ arr = Array[Num, len(values)](*values)
462
+ compile_and_call(arr.sort)
463
+ else:
464
+ arr = Array[Num, len(values)](*sorted(values))
465
+ return cls(arr)
466
+
467
+ def __len__(self) -> int:
468
+ return len(self._values)
469
+
470
+ def __contains__(self, value: Num) -> bool:
471
+ if len(self) < 8:
472
+ return value in self._as_tuple()
473
+ else:
474
+ left = 0
475
+ right = len(self) - 1
476
+ while left <= right:
477
+ mid = (left + right) // 2
478
+ mid_value = self._values.get_unchecked(mid)
479
+ if mid_value == value:
480
+ return True
481
+ elif mid_value < value:
482
+ left = mid + 1
483
+ else:
484
+ right = mid - 1
485
+ return False
486
+
487
+ def __iter__(self) -> SonolusIterator[Num]:
488
+ return self._values.__iter__()
489
+
490
+ @meta_fn
491
+ def _as_tuple(self) -> tuple[Num, ...]:
492
+ return tuple(self._values.get_unchecked(i) for i in range(Num._accept_(len(self))._as_py_()))
493
+
494
+
444
495
  class _ArrayMapEntry[K, V](Record):
445
496
  key: K
446
497
  value: V
sonolus/script/debug.py CHANGED
@@ -102,6 +102,14 @@ def notify(message: str):
102
102
  print(f"[NOTIFY] {message}")
103
103
 
104
104
 
105
+ @meta_fn
106
+ def runtime_checks_enabled() -> bool:
107
+ if ctx():
108
+ return ctx().project_state.runtime_checks != RuntimeChecks.NONE
109
+ else:
110
+ return True
111
+
112
+
105
113
  @meta_fn
106
114
  def require(value: int | float | bool, message: str | None = None):
107
115
  """Require a condition to be true, or raise an error.
@@ -138,13 +146,22 @@ def require(value: int | float | bool, message: str | None = None):
138
146
 
139
147
  @meta_fn
140
148
  def assert_true(value: int | float | bool, message: str | None = None):
141
- if ctx() and ctx().project_state.runtime_checks == RuntimeChecks.NONE:
149
+ value = validate_value(value)
150
+ if (
151
+ ctx()
152
+ and ctx().project_state.runtime_checks == RuntimeChecks.NONE
153
+ and not (validate_value(value)._is_py_() and validate_value(value)._as_py_() == 0)
154
+ ):
155
+ # Don't do anything if runtime checks are disabled, unless the value is statically known to be false.
156
+ # `assert False` is a somewhat common pattern to indicate unreachable code, and stripping that out can
157
+ # cause compilation errors, so we retain those checks.
142
158
  return
143
159
  require(value, message)
144
160
 
145
161
 
162
+ @meta_fn
146
163
  def assert_false(value: int | float | bool, message: str | None = None):
147
- assert_true(not value, message)
164
+ assert_true(value == 0, message)
148
165
 
149
166
 
150
167
  def static_assert(value: int | float | bool, message: str | None = None):
@@ -211,7 +228,7 @@ def visualize_cfg(
211
228
  project_state = ProjectContextState()
212
229
  mode_state = ModeContextState(
213
230
  mode,
214
- {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
231
+ archetypes,
215
232
  )
216
233
 
217
234
  cfg = callback_to_cfg(project_state, mode_state, fn, callback, archetype=archetype) # type: ignore
sonolus/script/engine.py CHANGED
@@ -155,6 +155,36 @@ def default_callback() -> Any:
155
155
  return 0.0
156
156
 
157
157
 
158
+ def check_skin(skin: Any):
159
+ if not hasattr(skin, "_sprites_"):
160
+ raise ValueError(f"Invalid skin: {skin}. Missing an @skin decorator?")
161
+
162
+
163
+ def check_effects(effects: Any):
164
+ if not hasattr(effects, "_effects_"):
165
+ raise ValueError(f"Invalid effects: {effects}. Missing an @effects decorator?")
166
+
167
+
168
+ def check_particles(particles: Any):
169
+ if not hasattr(particles, "_particles_"):
170
+ raise ValueError(f"Invalid particles: {particles}. Missing an @particles decorator?")
171
+
172
+
173
+ def check_buckets(buckets: Any):
174
+ if not hasattr(buckets, "_buckets_"):
175
+ raise ValueError(f"Invalid buckets: {buckets}. Missing an @buckets decorator?")
176
+
177
+
178
+ def check_instructions(instructions: Any):
179
+ if not hasattr(instructions, "_instructions_"):
180
+ raise ValueError(f"Invalid instructions: {instructions}. Missing an @instructions decorator?")
181
+
182
+
183
+ def check_instruction_icons(instruction_icons: Any):
184
+ if not hasattr(instruction_icons, "_instruction_icons_"):
185
+ raise ValueError(f"Invalid instruction icons: {instruction_icons}. Missing an @instruction_icons decorator?")
186
+
187
+
158
188
  class PlayMode:
159
189
  """A play mode definition.
160
190
 
@@ -185,6 +215,11 @@ class PlayMode:
185
215
  if not issubclass(archetype, PlayArchetype):
186
216
  raise ValueError(f"archetype {archetype} is not a PlayArchetype")
187
217
 
218
+ check_skin(skin)
219
+ check_effects(effects)
220
+ check_particles(particles)
221
+ check_buckets(buckets)
222
+
188
223
 
189
224
  class WatchMode:
190
225
  """A watch mode definition.
@@ -219,6 +254,11 @@ class WatchMode:
219
254
  if not issubclass(archetype, WatchArchetype):
220
255
  raise ValueError(f"archetype {archetype} is not a WatchArchetype")
221
256
 
257
+ check_skin(skin)
258
+ check_effects(effects)
259
+ check_particles(particles)
260
+ check_buckets(buckets)
261
+
222
262
 
223
263
  class PreviewMode:
224
264
  """A preview mode definition.
@@ -241,6 +281,8 @@ class PreviewMode:
241
281
  if not issubclass(archetype, PreviewArchetype):
242
282
  raise ValueError(f"archetype {archetype} is not a PreviewArchetype")
243
283
 
284
+ check_skin(skin)
285
+
244
286
 
245
287
  class TutorialMode:
246
288
  """A tutorial mode definition.
@@ -277,6 +319,12 @@ class TutorialMode:
277
319
  self.navigate = navigate
278
320
  self.update = update
279
321
 
322
+ check_skin(skin)
323
+ check_effects(effects)
324
+ check_particles(particles)
325
+ check_instructions(instructions)
326
+ check_instruction_icons(instruction_icons)
327
+
280
328
 
281
329
  def empty_play_mode() -> PlayMode:
282
330
  """Create an empty play mode."""