sonolus.py 0.1.9__py3-none-any.whl → 0.2.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/backend/optimize/constant_evaluation.py +2 -2
- sonolus/backend/optimize/optimize.py +12 -4
- sonolus/backend/optimize/passes.py +2 -1
- sonolus/backend/place.py +95 -14
- sonolus/backend/visitor.py +51 -3
- sonolus/build/cli.py +60 -9
- sonolus/build/collection.py +68 -26
- sonolus/build/compile.py +85 -27
- sonolus/build/engine.py +166 -40
- sonolus/build/node.py +8 -1
- sonolus/build/project.py +30 -11
- sonolus/script/archetype.py +110 -26
- sonolus/script/array.py +11 -0
- sonolus/script/debug.py +2 -2
- sonolus/script/effect.py +2 -2
- sonolus/script/engine.py +123 -15
- sonolus/script/internal/builtin_impls.py +21 -2
- sonolus/script/internal/constant.py +5 -1
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +4 -0
- sonolus/script/internal/value.py +6 -0
- sonolus/script/interval.py +16 -0
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +113 -10
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +9 -0
- sonolus/script/options.py +5 -3
- sonolus/script/pointer.py +2 -0
- sonolus/script/project.py +41 -5
- sonolus/script/record.py +7 -2
- sonolus/script/runtime.py +61 -10
- sonolus/script/sprite.py +18 -1
- sonolus/script/ui.py +7 -3
- sonolus/script/values.py +8 -5
- sonolus/script/vec.py +28 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/RECORD +42 -41
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
sonolus/script/archetype.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
from abc import abstractmethod
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from enum import Enum, StrEnum
|
|
@@ -177,6 +178,23 @@ def imported(*, name: str | None = None) -> Any:
|
|
|
177
178
|
return _ArchetypeFieldInfo(name, _StorageType.IMPORTED)
|
|
178
179
|
|
|
179
180
|
|
|
181
|
+
def entity_data() -> Any:
|
|
182
|
+
"""Declare a field as entity data.
|
|
183
|
+
|
|
184
|
+
Entity data is accessible from other entities, but may only be updated in the `preprocess` callback
|
|
185
|
+
and is read-only in other callbacks.
|
|
186
|
+
|
|
187
|
+
It functions like `imported`, except that it is not loaded from the level data.
|
|
188
|
+
|
|
189
|
+
Usage:
|
|
190
|
+
```
|
|
191
|
+
class MyArchetype(PlayArchetype):
|
|
192
|
+
field: int = entity_data()
|
|
193
|
+
```
|
|
194
|
+
"""
|
|
195
|
+
return _ArchetypeFieldInfo(None, _StorageType.IMPORTED)
|
|
196
|
+
|
|
197
|
+
|
|
180
198
|
def exported(*, name: str | None = None) -> Any:
|
|
181
199
|
"""Declare a field as exported.
|
|
182
200
|
|
|
@@ -418,7 +436,11 @@ class _BaseArchetype:
|
|
|
418
436
|
bound.apply_defaults()
|
|
419
437
|
data = []
|
|
420
438
|
for field in cls._memory_fields_.values():
|
|
421
|
-
data.extend(
|
|
439
|
+
data.extend(
|
|
440
|
+
field.type._accept_(
|
|
441
|
+
bound.arguments[field.name] if field.name in bound.arguments else zeros(field.type)
|
|
442
|
+
)._to_list_()
|
|
443
|
+
)
|
|
422
444
|
native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
|
|
423
445
|
|
|
424
446
|
@classmethod
|
|
@@ -446,9 +468,7 @@ class _BaseArchetype:
|
|
|
446
468
|
raise TypeError("Cannot directly subclass Archetype, use the Archetype subclass for your mode")
|
|
447
469
|
cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
|
|
448
470
|
return
|
|
449
|
-
if getattr(
|
|
450
|
-
raise TypeError("Cannot subclass Archetypes")
|
|
451
|
-
if cls.name is None:
|
|
471
|
+
if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
|
|
452
472
|
cls.name = cls.__name__
|
|
453
473
|
cls._callbacks_ = []
|
|
454
474
|
for name in cls._supported_callbacks_:
|
|
@@ -463,17 +483,32 @@ class _BaseArchetype:
|
|
|
463
483
|
if cls._field_init_done:
|
|
464
484
|
return
|
|
465
485
|
cls._field_init_done = True
|
|
486
|
+
for mro_entry in cls.mro()[1:]:
|
|
487
|
+
if hasattr(mro_entry, "_field_init_done"):
|
|
488
|
+
mro_entry._init_fields()
|
|
466
489
|
field_specifiers = get_field_specifiers(
|
|
467
490
|
cls, skip={"name", "is_scored", "_callbacks_", "_field_init_done"}
|
|
468
491
|
).items()
|
|
469
|
-
cls
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
492
|
+
if not hasattr(cls, "_imported_fields_"):
|
|
493
|
+
cls._imported_fields_ = {}
|
|
494
|
+
else:
|
|
495
|
+
cls._imported_fields_ = {**cls._imported_fields_}
|
|
496
|
+
if not hasattr(cls, "_exported_fields_"):
|
|
497
|
+
cls._exported_fields_ = {}
|
|
498
|
+
else:
|
|
499
|
+
cls._exported_fields_ = {**cls._exported_fields_}
|
|
500
|
+
if not hasattr(cls, "_memory_fields_"):
|
|
501
|
+
cls._memory_fields_ = {}
|
|
502
|
+
else:
|
|
503
|
+
cls._memory_fields_ = {**cls._memory_fields_}
|
|
504
|
+
if not hasattr(cls, "_shared_memory_fields_"):
|
|
505
|
+
cls._shared_memory_fields_ = {}
|
|
506
|
+
else:
|
|
507
|
+
cls._shared_memory_fields_ = {**cls._shared_memory_fields_}
|
|
508
|
+
imported_offset = sum(field.type._size_() for field in cls._imported_fields_.values())
|
|
509
|
+
exported_offset = sum(field.type._size_() for field in cls._exported_fields_.values())
|
|
510
|
+
memory_offset = sum(field.type._size_() for field in cls._memory_fields_.values())
|
|
511
|
+
shared_memory_offset = sum(field.type._size_() for field in cls._shared_memory_fields_.values())
|
|
477
512
|
for name, value in field_specifiers:
|
|
478
513
|
if value is ClassVar or get_origin(value) is ClassVar:
|
|
479
514
|
continue
|
|
@@ -504,24 +539,32 @@ class _BaseArchetype:
|
|
|
504
539
|
name, field_info.name or name, field_info.storage, imported_offset, field_type
|
|
505
540
|
)
|
|
506
541
|
imported_offset += field_type._size_()
|
|
542
|
+
if imported_offset > _ENTITY_DATA_SIZE:
|
|
543
|
+
raise ValueError("Imported fields exceed entity data size")
|
|
507
544
|
setattr(cls, name, cls._imported_fields_[name])
|
|
508
545
|
case _StorageType.EXPORTED:
|
|
509
546
|
cls._exported_fields_[name] = _ArchetypeField(
|
|
510
547
|
name, field_info.name or name, field_info.storage, exported_offset, field_type
|
|
511
548
|
)
|
|
512
549
|
exported_offset += field_type._size_()
|
|
550
|
+
if exported_offset > _ENTITY_DATA_SIZE:
|
|
551
|
+
raise ValueError("Exported fields exceed entity data size")
|
|
513
552
|
setattr(cls, name, cls._exported_fields_[name])
|
|
514
553
|
case _StorageType.MEMORY:
|
|
515
554
|
cls._memory_fields_[name] = _ArchetypeField(
|
|
516
555
|
name, field_info.name or name, field_info.storage, memory_offset, field_type
|
|
517
556
|
)
|
|
518
557
|
memory_offset += field_type._size_()
|
|
558
|
+
if memory_offset > _ENTITY_MEMORY_SIZE:
|
|
559
|
+
raise ValueError("Memory fields exceed entity memory size")
|
|
519
560
|
setattr(cls, name, cls._memory_fields_[name])
|
|
520
561
|
case _StorageType.SHARED:
|
|
521
562
|
cls._shared_memory_fields_[name] = _ArchetypeField(
|
|
522
563
|
name, field_info.name or name, field_info.storage, shared_memory_offset, field_type
|
|
523
564
|
)
|
|
524
565
|
shared_memory_offset += field_type._size_()
|
|
566
|
+
if shared_memory_offset > _ENTITY_SHARED_MEMORY_SIZE:
|
|
567
|
+
raise ValueError("Shared memory fields exceed entity shared memory size")
|
|
525
568
|
setattr(cls, name, cls._shared_memory_fields_[name])
|
|
526
569
|
cls._imported_keys_ = {
|
|
527
570
|
name: i
|
|
@@ -542,6 +585,30 @@ class _BaseArchetype:
|
|
|
542
585
|
[inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
|
|
543
586
|
)
|
|
544
587
|
|
|
588
|
+
@property
|
|
589
|
+
@abstractmethod
|
|
590
|
+
def index(self) -> int:
|
|
591
|
+
"""The index of this entity."""
|
|
592
|
+
raise NotImplementedError
|
|
593
|
+
|
|
594
|
+
@meta_fn
|
|
595
|
+
def ref(self) -> EntityRef[Self]:
|
|
596
|
+
"""Get a reference to this entity.
|
|
597
|
+
|
|
598
|
+
Valid both in level data and in callbacks.
|
|
599
|
+
"""
|
|
600
|
+
match self._data_:
|
|
601
|
+
case _ArchetypeSelfData():
|
|
602
|
+
return EntityRef[type(self)](index=self.index)
|
|
603
|
+
case _ArchetypeReferenceData(index=index):
|
|
604
|
+
return EntityRef[type(self)](index=index)
|
|
605
|
+
case _ArchetypeLevelData():
|
|
606
|
+
result = EntityRef[type(self)](index=-1)
|
|
607
|
+
result._ref_ = self
|
|
608
|
+
return result
|
|
609
|
+
case _:
|
|
610
|
+
raise RuntimeError("Invalid entity data")
|
|
611
|
+
|
|
545
612
|
|
|
546
613
|
class PlayArchetype(_BaseArchetype):
|
|
547
614
|
"""Base class for play mode archetypes.
|
|
@@ -682,6 +749,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
682
749
|
return self._info.state == 2
|
|
683
750
|
|
|
684
751
|
@property
|
|
752
|
+
@meta_fn
|
|
685
753
|
def life(self) -> ArchetypeLife:
|
|
686
754
|
"""How this entity contributes to life."""
|
|
687
755
|
if not ctx():
|
|
@@ -693,6 +761,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
693
761
|
raise RuntimeError("Life is not available in level data")
|
|
694
762
|
|
|
695
763
|
@property
|
|
764
|
+
@meta_fn
|
|
696
765
|
def result(self) -> PlayEntityInput:
|
|
697
766
|
"""The result of this entity.
|
|
698
767
|
|
|
@@ -706,17 +775,6 @@ class PlayArchetype(_BaseArchetype):
|
|
|
706
775
|
case _:
|
|
707
776
|
raise RuntimeError("Result is only accessible from the entity itself")
|
|
708
777
|
|
|
709
|
-
def ref(self):
|
|
710
|
-
"""Get a reference to this entity for creating level data.
|
|
711
|
-
|
|
712
|
-
Not valid elsewhere.
|
|
713
|
-
"""
|
|
714
|
-
if not isinstance(self._data_, _ArchetypeLevelData):
|
|
715
|
-
raise RuntimeError("Entity is not level data")
|
|
716
|
-
result = EntityRef[type(self)](index=-1)
|
|
717
|
-
result._ref_ = self
|
|
718
|
-
return result
|
|
719
|
-
|
|
720
778
|
|
|
721
779
|
class WatchArchetype(_BaseArchetype):
|
|
722
780
|
"""Base class for watch mode archetypes.
|
|
@@ -801,6 +859,7 @@ class WatchArchetype(_BaseArchetype):
|
|
|
801
859
|
return self._info.state == 1
|
|
802
860
|
|
|
803
861
|
@property
|
|
862
|
+
@meta_fn
|
|
804
863
|
def life(self) -> ArchetypeLife:
|
|
805
864
|
"""How this entity contributes to life."""
|
|
806
865
|
if not ctx():
|
|
@@ -812,6 +871,7 @@ class WatchArchetype(_BaseArchetype):
|
|
|
812
871
|
raise RuntimeError("Life is not available in level data")
|
|
813
872
|
|
|
814
873
|
@property
|
|
874
|
+
@meta_fn
|
|
815
875
|
def result(self) -> WatchEntityInput:
|
|
816
876
|
"""The result of this entity.
|
|
817
877
|
|
|
@@ -857,6 +917,7 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
857
917
|
"""
|
|
858
918
|
|
|
859
919
|
@property
|
|
920
|
+
@meta_fn
|
|
860
921
|
def _info(self) -> PreviewEntityInfo:
|
|
861
922
|
if not ctx():
|
|
862
923
|
raise RuntimeError("Calling info is only allowed within a callback")
|
|
@@ -974,21 +1035,35 @@ class WatchEntityInput(Record):
|
|
|
974
1035
|
|
|
975
1036
|
|
|
976
1037
|
class EntityRef[A: _BaseArchetype](Record):
|
|
977
|
-
"""Reference to another entity.
|
|
1038
|
+
"""Reference to another entity.
|
|
1039
|
+
|
|
1040
|
+
May be used with `Any` to reference an unknown archetype.
|
|
1041
|
+
|
|
1042
|
+
Usage:
|
|
1043
|
+
```
|
|
1044
|
+
class MyArchetype(PlayArchetype):
|
|
1045
|
+
ref_1: EntityRef[OtherArchetype] = imported()
|
|
1046
|
+
ref_2: EntityRef[Any] = imported()
|
|
1047
|
+
```
|
|
1048
|
+
"""
|
|
978
1049
|
|
|
979
1050
|
index: int
|
|
980
1051
|
|
|
981
1052
|
@classmethod
|
|
982
1053
|
def archetype(cls) -> type[A]:
|
|
1054
|
+
"""Get the archetype type."""
|
|
983
1055
|
return cls.type_var_value(A)
|
|
984
1056
|
|
|
985
1057
|
def with_archetype(self, archetype: type[A]) -> EntityRef[A]:
|
|
1058
|
+
"""Return a new reference with the given archetype type."""
|
|
986
1059
|
return EntityRef[archetype](index=self.index)
|
|
987
1060
|
|
|
988
1061
|
def get(self) -> A:
|
|
1062
|
+
"""Get the entity."""
|
|
989
1063
|
return self.archetype().at(self.index)
|
|
990
1064
|
|
|
991
|
-
def
|
|
1065
|
+
def archetype_matches(self) -> bool:
|
|
1066
|
+
"""Check if entity at the index is precisely of the archetype."""
|
|
992
1067
|
return self.index >= 0 and self.archetype().is_at(self.index)
|
|
993
1068
|
|
|
994
1069
|
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
|
|
@@ -1000,9 +1075,18 @@ class EntityRef[A: _BaseArchetype](Record):
|
|
|
1000
1075
|
raise KeyError("Reference to entity not in level data")
|
|
1001
1076
|
return [level_refs[ref]]
|
|
1002
1077
|
|
|
1078
|
+
def _copy_from_(self, value: Self):
|
|
1079
|
+
super()._copy_from_(value)
|
|
1080
|
+
if hasattr(value, "_ref_"):
|
|
1081
|
+
self._ref_ = value._ref_
|
|
1082
|
+
|
|
1003
1083
|
@classmethod
|
|
1004
1084
|
def _accepts_(cls, value: Any) -> bool:
|
|
1005
|
-
return
|
|
1085
|
+
return (
|
|
1086
|
+
super()._accepts_(value)
|
|
1087
|
+
or (cls._type_args_ and cls.archetype() is Any and isinstance(value, EntityRef))
|
|
1088
|
+
or (issubclass(type(value), EntityRef) and issubclass(value.archetype(), cls.archetype()))
|
|
1089
|
+
)
|
|
1006
1090
|
|
|
1007
1091
|
@classmethod
|
|
1008
1092
|
def _accept_(cls, value: Any) -> Self:
|
sonolus/script/array.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from typing import Any, Self, final
|
|
6
6
|
|
|
7
|
+
from sonolus.backend.ir import IRConst, IRSet
|
|
7
8
|
from sonolus.backend.place import BlockPlace
|
|
8
9
|
from sonolus.script.array_like import ArrayLike
|
|
9
10
|
from sonolus.script.debug import assert_unreachable
|
|
@@ -159,6 +160,16 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
159
160
|
else:
|
|
160
161
|
return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
|
|
161
162
|
|
|
163
|
+
@classmethod
|
|
164
|
+
def _zero_(cls) -> Self:
|
|
165
|
+
if ctx():
|
|
166
|
+
place = ctx().alloc(size=cls._size_())
|
|
167
|
+
result: Self = cls._from_place_(place)
|
|
168
|
+
ctx().add_statements(*[IRSet(place.add_offset(i), IRConst(0)) for i in range(cls._size_())])
|
|
169
|
+
return result
|
|
170
|
+
else:
|
|
171
|
+
return cls._with_value([cls.element_type()._zero_() for _ in range(cls.size())])
|
|
172
|
+
|
|
162
173
|
def __len__(self):
|
|
163
174
|
return self.size()
|
|
164
175
|
|
sonolus/script/debug.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
2
|
from contextvars import ContextVar
|
|
3
3
|
from typing import Any, Never
|
|
4
4
|
|
|
@@ -75,7 +75,7 @@ def terminate():
|
|
|
75
75
|
raise RuntimeError("Terminated")
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def visualize_cfg(fn: Callable[[], Any], passes:
|
|
78
|
+
def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
|
|
79
79
|
from sonolus.build.compile import callback_to_cfg
|
|
80
80
|
|
|
81
81
|
if passes is None:
|
sonolus/script/effect.py
CHANGED
|
@@ -25,7 +25,7 @@ class Effect(Record):
|
|
|
25
25
|
"""Return whether the effect clip is available."""
|
|
26
26
|
return _has_effect_clip(self.id)
|
|
27
27
|
|
|
28
|
-
def play(self, distance: float) -> None:
|
|
28
|
+
def play(self, distance: float = 0) -> None:
|
|
29
29
|
"""Play the effect clip.
|
|
30
30
|
|
|
31
31
|
If the clip was already played within the specified distance, it will be skipped.
|
|
@@ -35,7 +35,7 @@ class Effect(Record):
|
|
|
35
35
|
"""
|
|
36
36
|
_play(self.id, distance)
|
|
37
37
|
|
|
38
|
-
def schedule(self, time: float, distance: float) -> None:
|
|
38
|
+
def schedule(self, time: float, distance: float = 0) -> None:
|
|
39
39
|
"""Schedule the effect clip to play at a specific time.
|
|
40
40
|
|
|
41
41
|
This is not suitable for real-time effects such as responses to user input. Use `play` instead.
|
sonolus/script/engine.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from collections.abc import Callable
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
from typing import Any
|
|
5
8
|
|
|
6
|
-
from sonolus.build.collection import Asset
|
|
9
|
+
from sonolus.build.collection import Asset, load_asset
|
|
7
10
|
from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
|
|
8
11
|
from sonolus.script.bucket import Buckets, EmptyBuckets
|
|
9
12
|
from sonolus.script.effect import Effects, EmptyEffects
|
|
@@ -13,12 +16,52 @@ from sonolus.script.instruction import (
|
|
|
13
16
|
TutorialInstructionIcons,
|
|
14
17
|
TutorialInstructions,
|
|
15
18
|
)
|
|
19
|
+
from sonolus.script.metadata import AnyText, Tag, as_localization_text
|
|
16
20
|
from sonolus.script.options import EmptyOptions, Options
|
|
17
21
|
from sonolus.script.particle import EmptyParticles, Particles
|
|
18
22
|
from sonolus.script.sprite import EmptySkin, Skin
|
|
19
23
|
from sonolus.script.ui import UiConfig
|
|
20
24
|
|
|
21
25
|
|
|
26
|
+
class ExportedEngine:
|
|
27
|
+
"""An exported Sonolus.py engine."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
item: dict,
|
|
33
|
+
thumbnail: bytes,
|
|
34
|
+
play_data: bytes,
|
|
35
|
+
watch_data: bytes,
|
|
36
|
+
preview_data: bytes,
|
|
37
|
+
tutorial_data: bytes,
|
|
38
|
+
rom: bytes | None = None,
|
|
39
|
+
configuration: bytes,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.item = item
|
|
42
|
+
self.thumbnail = thumbnail
|
|
43
|
+
self.play_data = play_data
|
|
44
|
+
self.watch_data = watch_data
|
|
45
|
+
self.preview_data = preview_data
|
|
46
|
+
self.tutorial_data = tutorial_data
|
|
47
|
+
self.rom = rom
|
|
48
|
+
self.configuration = configuration
|
|
49
|
+
|
|
50
|
+
def write_to_dir(self, path: PathLike):
|
|
51
|
+
"""Write the exported engine to a directory."""
|
|
52
|
+
path = Path(path)
|
|
53
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
(path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
|
|
55
|
+
(path / "thumbnail").write_bytes(self.thumbnail)
|
|
56
|
+
(path / "playData").write_bytes(self.play_data)
|
|
57
|
+
(path / "watchData").write_bytes(self.watch_data)
|
|
58
|
+
(path / "previewData").write_bytes(self.preview_data)
|
|
59
|
+
(path / "tutorialData").write_bytes(self.tutorial_data)
|
|
60
|
+
if self.rom is not None:
|
|
61
|
+
(path / "rom").write_bytes(self.rom)
|
|
62
|
+
(path / "configuration").write_bytes(self.configuration)
|
|
63
|
+
|
|
64
|
+
|
|
22
65
|
class Engine:
|
|
23
66
|
"""A Sonolus.py engine.
|
|
24
67
|
|
|
@@ -33,6 +76,9 @@ class Engine:
|
|
|
33
76
|
particle: The default particle for the engine.
|
|
34
77
|
thumbnail: The thumbnail for the engine.
|
|
35
78
|
data: The engine's modes and configurations.
|
|
79
|
+
tags: The tags of the engine.
|
|
80
|
+
description: The description of the engine.
|
|
81
|
+
meta: Additional metadata of the engine.
|
|
36
82
|
"""
|
|
37
83
|
|
|
38
84
|
version = 12
|
|
@@ -41,26 +87,68 @@ class Engine:
|
|
|
41
87
|
self,
|
|
42
88
|
*,
|
|
43
89
|
name: str,
|
|
44
|
-
title:
|
|
45
|
-
subtitle:
|
|
46
|
-
author:
|
|
90
|
+
title: AnyText | None = None,
|
|
91
|
+
subtitle: AnyText = "Sonolus.py Engine",
|
|
92
|
+
author: AnyText = "Unknown",
|
|
47
93
|
skin: str | None = None,
|
|
48
94
|
background: str | None = None,
|
|
49
95
|
effect: str | None = None,
|
|
50
96
|
particle: str | None = None,
|
|
51
97
|
thumbnail: Asset | None = None,
|
|
52
98
|
data: EngineData,
|
|
99
|
+
tags: list[Tag] | None = None,
|
|
100
|
+
description: AnyText | None = None,
|
|
101
|
+
meta: Any = None,
|
|
53
102
|
) -> None:
|
|
54
103
|
self.name = name
|
|
55
|
-
self.title = title or name
|
|
56
|
-
self.subtitle = subtitle
|
|
57
|
-
self.author = author
|
|
104
|
+
self.title = as_localization_text(title or name)
|
|
105
|
+
self.subtitle = as_localization_text(subtitle)
|
|
106
|
+
self.author = as_localization_text(author)
|
|
58
107
|
self.skin = skin
|
|
59
108
|
self.background = background
|
|
60
109
|
self.effect = effect
|
|
61
110
|
self.particle = particle
|
|
62
111
|
self.thumbnail = thumbnail
|
|
63
112
|
self.data = data
|
|
113
|
+
self.tags = tags or []
|
|
114
|
+
self.description = as_localization_text(description) if description is not None else None
|
|
115
|
+
self.meta = meta
|
|
116
|
+
|
|
117
|
+
def export(self) -> ExportedEngine:
|
|
118
|
+
"""Export the engine in a sonolus-pack compatible format.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
An exported engine.
|
|
122
|
+
"""
|
|
123
|
+
from sonolus.build.engine import package_engine
|
|
124
|
+
from sonolus.build.project import BLANK_PNG
|
|
125
|
+
|
|
126
|
+
item = {
|
|
127
|
+
"version": self.version,
|
|
128
|
+
"title": self.title,
|
|
129
|
+
"subtitle": self.subtitle,
|
|
130
|
+
"author": self.author,
|
|
131
|
+
"tags": [tag.as_dict() for tag in self.tags],
|
|
132
|
+
"skin": self.skin,
|
|
133
|
+
"background": self.background,
|
|
134
|
+
"effect": self.effect,
|
|
135
|
+
"particle": self.particle,
|
|
136
|
+
}
|
|
137
|
+
packaged = package_engine(self.data)
|
|
138
|
+
if self.description is not None:
|
|
139
|
+
item["description"] = self.description
|
|
140
|
+
if self.meta is not None:
|
|
141
|
+
item["meta"] = self.meta
|
|
142
|
+
return ExportedEngine(
|
|
143
|
+
item=item,
|
|
144
|
+
thumbnail=load_asset(self.thumbnail) if self.thumbnail is not None else BLANK_PNG,
|
|
145
|
+
play_data=packaged.play_data,
|
|
146
|
+
watch_data=packaged.watch_data,
|
|
147
|
+
preview_data=packaged.preview_data,
|
|
148
|
+
tutorial_data=packaged.tutorial_data,
|
|
149
|
+
rom=packaged.rom,
|
|
150
|
+
configuration=packaged.configuration,
|
|
151
|
+
)
|
|
64
152
|
|
|
65
153
|
|
|
66
154
|
def default_callback() -> Any:
|
|
@@ -190,6 +278,30 @@ class TutorialMode:
|
|
|
190
278
|
self.update = update
|
|
191
279
|
|
|
192
280
|
|
|
281
|
+
def empty_play_mode() -> PlayMode:
|
|
282
|
+
"""Create an empty play mode."""
|
|
283
|
+
return PlayMode()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def empty_watch_mode() -> WatchMode:
|
|
287
|
+
"""Create an empty watch mode."""
|
|
288
|
+
return WatchMode(update_spawn=default_callback)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def empty_preview_mode() -> PreviewMode:
|
|
292
|
+
"""Create an empty preview mode."""
|
|
293
|
+
return PreviewMode()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def empty_tutorial_mode() -> TutorialMode:
|
|
297
|
+
"""Create an empty tutorial mode."""
|
|
298
|
+
return TutorialMode(
|
|
299
|
+
preprocess=default_callback,
|
|
300
|
+
navigate=default_callback,
|
|
301
|
+
update=default_callback,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
193
305
|
class EngineData:
|
|
194
306
|
"""A Sonolus.py engine's modes and configurations.
|
|
195
307
|
|
|
@@ -214,11 +326,7 @@ class EngineData:
|
|
|
214
326
|
) -> None:
|
|
215
327
|
self.ui = ui or UiConfig()
|
|
216
328
|
self.options = options
|
|
217
|
-
self.play = play or
|
|
218
|
-
self.watch = watch or
|
|
219
|
-
self.preview = preview or
|
|
220
|
-
self.tutorial = tutorial or
|
|
221
|
-
preprocess=default_callback,
|
|
222
|
-
navigate=default_callback,
|
|
223
|
-
update=default_callback,
|
|
224
|
-
)
|
|
329
|
+
self.play = play or empty_play_mode()
|
|
330
|
+
self.watch = watch or empty_watch_mode()
|
|
331
|
+
self.preview = preview or empty_preview_mode()
|
|
332
|
+
self.tutorial = tutorial or empty_tutorial_mode()
|
|
@@ -52,9 +52,11 @@ def _enumerate(iterable, start=0):
|
|
|
52
52
|
from sonolus.backend.visitor import compile_and_call
|
|
53
53
|
|
|
54
54
|
iterable = validate_value(iterable)
|
|
55
|
-
if
|
|
55
|
+
if isinstance(iterable, TupleImpl):
|
|
56
|
+
return TupleImpl._accept_(tuple((start + i, value) for i, value in enumerate(iterable._as_py_(), start=start)))
|
|
57
|
+
elif not hasattr(iterable, "__iter__"):
|
|
56
58
|
raise TypeError(f"'{type(iterable).__name__}' object is not iterable")
|
|
57
|
-
|
|
59
|
+
elif isinstance(iterable, ArrayLike):
|
|
58
60
|
return compile_and_call(iterable._enumerate_, start)
|
|
59
61
|
else:
|
|
60
62
|
iterator = compile_and_call(iterable.__iter__)
|
|
@@ -232,10 +234,27 @@ _int._type_mapping_ = Num
|
|
|
232
234
|
_float._type_mapping_ = Num
|
|
233
235
|
_bool._type_mapping_ = Num
|
|
234
236
|
|
|
237
|
+
|
|
238
|
+
def _any(iterable):
|
|
239
|
+
for value in iterable: # noqa: SIM110
|
|
240
|
+
if value:
|
|
241
|
+
return True
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _all(iterable):
|
|
246
|
+
for value in iterable: # noqa: SIM110
|
|
247
|
+
if not value:
|
|
248
|
+
return False
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
|
|
235
252
|
# classmethod, property, staticmethod are supported as decorators, but not within functions
|
|
236
253
|
|
|
237
254
|
BUILTIN_IMPLS = {
|
|
238
255
|
id(abs): _abs,
|
|
256
|
+
id(all): _all,
|
|
257
|
+
id(any): _any,
|
|
239
258
|
id(bool): _bool,
|
|
240
259
|
id(callable): _callable,
|
|
241
260
|
id(enumerate): _enumerate,
|
|
@@ -107,7 +107,7 @@ class ConstantValue(Value):
|
|
|
107
107
|
def _get_(self) -> Self:
|
|
108
108
|
return self
|
|
109
109
|
|
|
110
|
-
def _set_(self, value: Any)
|
|
110
|
+
def _set_(self, value: Any):
|
|
111
111
|
if value is not self:
|
|
112
112
|
raise ValueError(f"{type(self).__name__} is immutable")
|
|
113
113
|
|
|
@@ -122,6 +122,10 @@ class ConstantValue(Value):
|
|
|
122
122
|
def _alloc_(cls) -> Self:
|
|
123
123
|
return cls()
|
|
124
124
|
|
|
125
|
+
@classmethod
|
|
126
|
+
def _zero_(cls) -> Self:
|
|
127
|
+
return cls()
|
|
128
|
+
|
|
125
129
|
@meta_fn
|
|
126
130
|
def __eq__(self, other):
|
|
127
131
|
return self is other
|