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.
- sonolus/backend/finalize.py +38 -32
- sonolus/backend/interpret.py +3 -3
- sonolus/backend/node.py +6 -28
- sonolus/backend/optimize/liveness.py +5 -2
- sonolus/backend/visitor.py +5 -2
- sonolus/build/cli.py +18 -0
- sonolus/build/compile.py +17 -3
- sonolus/build/dev_server.py +9 -1
- sonolus/build/node.py +6 -8
- sonolus/script/archetype.py +183 -45
- sonolus/script/bucket.py +2 -2
- sonolus/script/containers.py +51 -0
- sonolus/script/debug.py +20 -3
- sonolus/script/engine.py +48 -0
- sonolus/script/internal/builtin_impls.py +91 -4
- sonolus/script/internal/context.py +51 -6
- sonolus/script/internal/generic.py +34 -7
- sonolus/script/internal/impl.py +11 -5
- sonolus/script/internal/introspection.py +10 -1
- sonolus/script/internal/tuple_impl.py +6 -0
- sonolus/script/interval.py +5 -5
- sonolus/script/project.py +2 -0
- sonolus/script/record.py +36 -21
- sonolus/script/runtime.py +1 -1
- sonolus/script/stream.py +28 -21
- sonolus/script/transform.py +9 -8
- sonolus/script/values.py +1 -6
- sonolus/script/vec.py +27 -14
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/METADATA +2 -2
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/RECORD +33 -33
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/WHEEL +0 -0
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.10.7.dist-info → sonolus_py-0.12.5.dist-info}/licenses/LICENSE +0 -0
sonolus/script/archetype.py
CHANGED
|
@@ -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
|
|
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
|
|
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[
|
|
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
|
|
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
|
|
553
|
+
def _get_mro_id_array(archetype_id: int) -> Sequence[int]:
|
|
502
554
|
if not ctx():
|
|
503
|
-
raise RuntimeError("
|
|
504
|
-
return
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1243
|
-
|
|
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
|
|
1257
|
-
|
|
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.
|
|
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.
|
|
87
|
+
return JudgmentWindow._unchecked(
|
|
88
88
|
perfect=self.perfect + other,
|
|
89
89
|
great=self.great + other,
|
|
90
90
|
good=self.good + other,
|
sonolus/script/containers.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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."""
|