sonolus.py 0.2.0__py3-none-any.whl → 0.3.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.
- sonolus/backend/ops.py +6 -0
- sonolus/script/archetype.py +45 -7
- sonolus/script/array.py +58 -29
- sonolus/script/array_like.py +7 -2
- sonolus/script/bucket.py +5 -3
- sonolus/script/containers.py +166 -0
- sonolus/script/debug.py +20 -2
- sonolus/script/engine.py +2 -2
- sonolus/script/internal/constant.py +6 -4
- sonolus/script/internal/generic.py +7 -0
- sonolus/script/internal/impl.py +10 -9
- sonolus/script/internal/transient.py +3 -3
- sonolus/script/internal/value.py +40 -11
- sonolus/script/interval.py +3 -14
- sonolus/script/num.py +29 -16
- sonolus/script/options.py +18 -3
- sonolus/script/particle.py +1 -0
- sonolus/script/pointer.py +9 -1
- sonolus/script/quad.py +58 -4
- sonolus/script/record.py +15 -4
- sonolus/script/runtime.py +17 -6
- sonolus/script/stream.py +529 -0
- sonolus/script/text.py +9 -0
- sonolus/script/transform.py +3 -3
- sonolus/script/ui.py +13 -4
- sonolus/script/values.py +12 -1
- {sonolus_py-0.2.0.dist-info → sonolus_py-0.3.0.dist-info}/METADATA +2 -2
- {sonolus_py-0.2.0.dist-info → sonolus_py-0.3.0.dist-info}/RECORD +31 -30
- {sonolus_py-0.2.0.dist-info → sonolus_py-0.3.0.dist-info}/WHEEL +0 -0
- {sonolus_py-0.2.0.dist-info → sonolus_py-0.3.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.2.0.dist-info → sonolus_py-0.3.0.dist-info}/licenses/LICENSE +0 -0
sonolus/backend/ops.py
CHANGED
|
@@ -180,6 +180,12 @@ class Op(StrEnum):
|
|
|
180
180
|
StackSetPointer = ("StackSetPointer", True, False, False)
|
|
181
181
|
StopLooped = ("StopLooped", True, False, False)
|
|
182
182
|
StopLoopedScheduled = ("StopLoopedScheduled", True, False, False)
|
|
183
|
+
# Streams are immutable when they're readable, so we can treat read operations as pure.
|
|
184
|
+
StreamGetNextKey = ("StreamGetNextKey", False, True, False)
|
|
185
|
+
StreamGetPreviousKey = ("StreamGetPreviousKey", False, True, False)
|
|
186
|
+
StreamGetValue = ("StreamGetValue", False, True, False)
|
|
187
|
+
StreamHas = ("StreamHas", False, True, False)
|
|
188
|
+
StreamSet = ("StreamSet", True, False, False)
|
|
183
189
|
Subtract = ("Subtract", False, True, False)
|
|
184
190
|
Switch = ("Switch", False, True, True)
|
|
185
191
|
SwitchInteger = ("SwitchInteger", False, True, True)
|
sonolus/script/archetype.py
CHANGED
|
@@ -8,10 +8,9 @@ from enum import Enum, StrEnum
|
|
|
8
8
|
from types import FunctionType
|
|
9
9
|
from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
|
|
10
10
|
|
|
11
|
-
from sonolus.backend.ir import IRConst, IRInstr
|
|
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.place import BlockPlace
|
|
15
14
|
from sonolus.script.bucket import Bucket, Judgment
|
|
16
15
|
from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
|
|
17
16
|
from sonolus.script.internal.context import ctx
|
|
@@ -20,9 +19,9 @@ from sonolus.script.internal.generic import validate_concrete_type
|
|
|
20
19
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
21
20
|
from sonolus.script.internal.introspection import get_field_specifiers
|
|
22
21
|
from sonolus.script.internal.native import native_call
|
|
23
|
-
from sonolus.script.internal.value import Value
|
|
22
|
+
from sonolus.script.internal.value import BackingValue, DataValue, Value
|
|
24
23
|
from sonolus.script.num import Num
|
|
25
|
-
from sonolus.script.pointer import _deref
|
|
24
|
+
from sonolus.script.pointer import _backing_deref, _deref
|
|
26
25
|
from sonolus.script.record import Record
|
|
27
26
|
from sonolus.script.values import zeros
|
|
28
27
|
|
|
@@ -44,6 +43,17 @@ class _ArchetypeFieldInfo:
|
|
|
44
43
|
storage: _StorageType
|
|
45
44
|
|
|
46
45
|
|
|
46
|
+
class _ExportBackingValue(BackingValue):
|
|
47
|
+
def __init__(self, index: IRExpr):
|
|
48
|
+
self.index = index
|
|
49
|
+
|
|
50
|
+
def read(self) -> IRExpr:
|
|
51
|
+
raise NotImplementedError("Exported fields are write-only")
|
|
52
|
+
|
|
53
|
+
def write(self, value: IRExpr) -> IRStmt:
|
|
54
|
+
return IRInstr(Op.ExportValue, [self.index, value])
|
|
55
|
+
|
|
56
|
+
|
|
47
57
|
class _ArchetypeField(SonolusDescriptor):
|
|
48
58
|
def __init__(self, name: str, data_name: str, storage: _StorageType, offset: int, type_: type[Value]):
|
|
49
59
|
self.name = name
|
|
@@ -70,7 +80,20 @@ class _ArchetypeField(SonolusDescriptor):
|
|
|
70
80
|
case _ArchetypeLevelData(values=values):
|
|
71
81
|
result = values[self.name]
|
|
72
82
|
case _StorageType.EXPORTED:
|
|
73
|
-
|
|
83
|
+
match instance._data_:
|
|
84
|
+
case _ArchetypeSelfData():
|
|
85
|
+
|
|
86
|
+
def backing_source(i: IRExpr):
|
|
87
|
+
return _ExportBackingValue(IRPureInstr(Op.Add, [i, IRConst(self.offset)]))
|
|
88
|
+
|
|
89
|
+
result = _backing_deref(
|
|
90
|
+
backing_source,
|
|
91
|
+
self.type,
|
|
92
|
+
)
|
|
93
|
+
case _ArchetypeReferenceData():
|
|
94
|
+
raise RuntimeError("Exported fields of other entities are not accessible")
|
|
95
|
+
case _ArchetypeLevelData():
|
|
96
|
+
raise RuntimeError("Exported fields are not available in level data")
|
|
74
97
|
case _StorageType.MEMORY:
|
|
75
98
|
match instance._data_:
|
|
76
99
|
case _ArchetypeSelfData():
|
|
@@ -584,6 +607,7 @@ class _BaseArchetype:
|
|
|
584
607
|
cls._spawn_signature_ = inspect.Signature(
|
|
585
608
|
[inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in cls._memory_fields_]
|
|
586
609
|
)
|
|
610
|
+
cls._post_init_fields()
|
|
587
611
|
|
|
588
612
|
@property
|
|
589
613
|
@abstractmethod
|
|
@@ -609,6 +633,10 @@ class _BaseArchetype:
|
|
|
609
633
|
case _:
|
|
610
634
|
raise RuntimeError("Invalid entity data")
|
|
611
635
|
|
|
636
|
+
@classmethod
|
|
637
|
+
def _post_init_fields(cls):
|
|
638
|
+
pass
|
|
639
|
+
|
|
612
640
|
|
|
613
641
|
class PlayArchetype(_BaseArchetype):
|
|
614
642
|
"""Base class for play mode archetypes.
|
|
@@ -651,7 +679,7 @@ class PlayArchetype(_BaseArchetype):
|
|
|
651
679
|
def should_spawn(self) -> bool:
|
|
652
680
|
"""Return whether the entity should be spawned.
|
|
653
681
|
|
|
654
|
-
Runs
|
|
682
|
+
Runs each frame while the entity is the first entity in the spawn queue.
|
|
655
683
|
"""
|
|
656
684
|
|
|
657
685
|
def initialize(self):
|
|
@@ -885,6 +913,11 @@ class WatchArchetype(_BaseArchetype):
|
|
|
885
913
|
case _:
|
|
886
914
|
raise RuntimeError("Result is only accessible from the entity itself")
|
|
887
915
|
|
|
916
|
+
@classmethod
|
|
917
|
+
def _post_init_fields(cls):
|
|
918
|
+
if cls._exported_fields_:
|
|
919
|
+
raise RuntimeError("Watch archetypes cannot have exported fields")
|
|
920
|
+
|
|
888
921
|
|
|
889
922
|
class PreviewArchetype(_BaseArchetype):
|
|
890
923
|
"""Base class for preview mode archetypes.
|
|
@@ -934,6 +967,11 @@ class PreviewArchetype(_BaseArchetype):
|
|
|
934
967
|
"""The index of this entity."""
|
|
935
968
|
return self._info.index
|
|
936
969
|
|
|
970
|
+
@classmethod
|
|
971
|
+
def _post_init_fields(cls):
|
|
972
|
+
if cls._exported_fields_:
|
|
973
|
+
raise RuntimeError("Preview archetypes cannot have exported fields")
|
|
974
|
+
|
|
937
975
|
|
|
938
976
|
@meta_fn
|
|
939
977
|
def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
|
|
@@ -1066,7 +1104,7 @@ class EntityRef[A: _BaseArchetype](Record):
|
|
|
1066
1104
|
"""Check if entity at the index is precisely of the archetype."""
|
|
1067
1105
|
return self.index >= 0 and self.archetype().is_at(self.index)
|
|
1068
1106
|
|
|
1069
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
1107
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
1070
1108
|
ref = getattr(self, "_ref_", None)
|
|
1071
1109
|
if ref is None:
|
|
1072
1110
|
return [self.index]
|
sonolus/script/array.py
CHANGED
|
@@ -12,7 +12,7 @@ from sonolus.script.internal.context import ctx
|
|
|
12
12
|
from sonolus.script.internal.error import InternalError
|
|
13
13
|
from sonolus.script.internal.generic import GenericValue
|
|
14
14
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
15
|
-
from sonolus.script.internal.value import Value
|
|
15
|
+
from sonolus.script.internal.value import BackingSource, DataValue, Value
|
|
16
16
|
from sonolus.script.num import Num
|
|
17
17
|
|
|
18
18
|
|
|
@@ -27,16 +27,14 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
27
27
|
```
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
_value: list[T] | BlockPlace
|
|
30
|
+
_value: list[T] | BlockPlace | BackingSource
|
|
31
31
|
|
|
32
32
|
@classmethod
|
|
33
|
-
@meta_fn
|
|
34
33
|
def element_type(cls) -> type[T] | type[Value]:
|
|
35
34
|
"""Return the type of elements in this array type."""
|
|
36
35
|
return cls.type_var_value(T)
|
|
37
36
|
|
|
38
37
|
@classmethod
|
|
39
|
-
@meta_fn
|
|
40
38
|
def size(cls) -> int:
|
|
41
39
|
"""Return the size of this array type.
|
|
42
40
|
|
|
@@ -85,6 +83,10 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
85
83
|
def _is_value_type_(cls) -> bool:
|
|
86
84
|
return False
|
|
87
85
|
|
|
86
|
+
@classmethod
|
|
87
|
+
def _from_backing_source_(cls, source: BackingSource) -> Self:
|
|
88
|
+
return cls._with_value(source)
|
|
89
|
+
|
|
88
90
|
@classmethod
|
|
89
91
|
def _from_place_(cls, place: BlockPlace) -> Self:
|
|
90
92
|
return cls._with_value(place)
|
|
@@ -108,11 +110,11 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
108
110
|
return self
|
|
109
111
|
|
|
110
112
|
@classmethod
|
|
111
|
-
def _from_list_(cls, values: Iterable[
|
|
113
|
+
def _from_list_(cls, values: Iterable[DataValue]) -> Self:
|
|
112
114
|
iterator = iter(values)
|
|
113
115
|
return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
|
|
114
116
|
|
|
115
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
117
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
116
118
|
match self._value:
|
|
117
119
|
case list():
|
|
118
120
|
return [entry for value in self._value for entry in value._to_list_(level_refs)]
|
|
@@ -124,6 +126,8 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
124
126
|
._from_place_(self._value.add_offset(i * self.element_type()._size_()))
|
|
125
127
|
._to_list_()
|
|
126
128
|
]
|
|
129
|
+
case backing_source if callable(backing_source):
|
|
130
|
+
return [backing_source(IRConst(i)) for i in range(self.size() * self.element_type()._size_())]
|
|
127
131
|
case _:
|
|
128
132
|
assert_unreachable()
|
|
129
133
|
|
|
@@ -135,7 +139,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
135
139
|
return self
|
|
136
140
|
|
|
137
141
|
def _set_(self, value: Self):
|
|
138
|
-
raise TypeError("Array does not support
|
|
142
|
+
raise TypeError("Array does not support _set_")
|
|
139
143
|
|
|
140
144
|
def _copy_from_(self, value: Self):
|
|
141
145
|
if not isinstance(value, type(self)):
|
|
@@ -150,6 +154,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
150
154
|
result._copy_from_(self)
|
|
151
155
|
return result
|
|
152
156
|
else:
|
|
157
|
+
assert isinstance(self._value, list)
|
|
153
158
|
return self._with_value([value._copy_() for value in self._value])
|
|
154
159
|
|
|
155
160
|
@classmethod
|
|
@@ -186,22 +191,36 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
186
191
|
return self._value[const_index]._get_()
|
|
187
192
|
else:
|
|
188
193
|
return self._value[const_index]._get_()._as_py_()
|
|
189
|
-
|
|
194
|
+
elif isinstance(self._value, BlockPlace):
|
|
190
195
|
return (
|
|
191
196
|
self.element_type()
|
|
192
197
|
._from_place_(self._value.add_offset(const_index * self.element_type()._size_()))
|
|
193
198
|
._get_()
|
|
194
199
|
)
|
|
200
|
+
elif callable(self._value):
|
|
201
|
+
return self.element_type()._from_backing_source_(
|
|
202
|
+
lambda offset: self._value((Num(offset) + Num(const_index * self.element_type()._size_())).ir())
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
raise InternalError("Unexpected array value")
|
|
195
206
|
else:
|
|
196
207
|
if not ctx():
|
|
197
208
|
raise InternalError("Unexpected non-constant index")
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
if isinstance(self._value, list | BlockPlace):
|
|
210
|
+
base = ctx().rom[tuple(self._to_list_())] if isinstance(self._value, list) else self._value
|
|
211
|
+
place = BlockPlace(
|
|
212
|
+
block=base.block,
|
|
213
|
+
index=(Num(base.index) + index * self.element_type()._size_()).index(),
|
|
214
|
+
offset=base.offset,
|
|
215
|
+
)
|
|
216
|
+
return self.element_type()._from_place_(place)._get_()
|
|
217
|
+
elif callable(self._value):
|
|
218
|
+
base_offset = index * Num(self.element_type()._size_())
|
|
219
|
+
return self.element_type()._from_backing_source_(
|
|
220
|
+
lambda offset: self._value((Num(offset) + base_offset).ir())
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
raise InternalError("Unexpected array value")
|
|
205
224
|
|
|
206
225
|
@meta_fn
|
|
207
226
|
def __setitem__(self, index: Num, value: T):
|
|
@@ -210,17 +229,25 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
210
229
|
if ctx():
|
|
211
230
|
if isinstance(self._value, list):
|
|
212
231
|
raise ValueError("Cannot mutate a compile time constant array")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
232
|
+
elif isinstance(self._value, BlockPlace):
|
|
233
|
+
base = self._value
|
|
234
|
+
place = (
|
|
235
|
+
base.add_offset(int(index._as_py_()) * self.element_type()._size_())
|
|
236
|
+
if index._is_py_()
|
|
237
|
+
else BlockPlace(
|
|
238
|
+
block=base.block,
|
|
239
|
+
index=(Num(base.index) + index * self.element_type()._size_()).index(),
|
|
240
|
+
offset=base.offset,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
dst = self.element_type()._from_place_(place)
|
|
244
|
+
elif callable(self._value):
|
|
245
|
+
base_offset = index * Num(self.element_type()._size_())
|
|
246
|
+
dst = self.element_type()._from_backing_source_(
|
|
247
|
+
lambda offset: self._value((Num(offset) + base_offset).ir())
|
|
221
248
|
)
|
|
222
|
-
|
|
223
|
-
|
|
249
|
+
else:
|
|
250
|
+
raise InternalError("Unexpected array value")
|
|
224
251
|
if self.element_type()._is_value_type_():
|
|
225
252
|
dst._set_(value)
|
|
226
253
|
else:
|
|
@@ -259,11 +286,13 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
|
259
286
|
return hash(tuple(self[i] for i in range(self.size())))
|
|
260
287
|
|
|
261
288
|
def __str__(self):
|
|
262
|
-
if isinstance(self._value, BlockPlace):
|
|
289
|
+
if isinstance(self._value, BlockPlace) or callable(self._value):
|
|
263
290
|
return f"{type(self).__name__}({self._value}...)"
|
|
264
|
-
|
|
291
|
+
else:
|
|
292
|
+
return f"{type(self).__name__}({", ".join(str(self[i]) for i in range(self.size()))})"
|
|
265
293
|
|
|
266
294
|
def __repr__(self):
|
|
267
|
-
if isinstance(self._value, BlockPlace):
|
|
295
|
+
if isinstance(self._value, BlockPlace) or callable(self._value):
|
|
268
296
|
return f"{type(self).__name__}({self._value}...)"
|
|
269
|
-
|
|
297
|
+
else:
|
|
298
|
+
return f"{type(self).__name__}({", ".join(repr(self[i]) for i in range(self.size()))})"
|
sonolus/script/array_like.py
CHANGED
|
@@ -158,10 +158,14 @@ class ArrayLike[T](Sequence, ABC):
|
|
|
158
158
|
return min_index
|
|
159
159
|
|
|
160
160
|
def _max_(self, key: Callable[T, Any] | None = None) -> T:
|
|
161
|
-
|
|
161
|
+
index = self.index_of_max(key=key)
|
|
162
|
+
assert index != -1
|
|
163
|
+
return self[index]
|
|
162
164
|
|
|
163
165
|
def _min_(self, key: Callable[T, Any] | None = None) -> T:
|
|
164
|
-
|
|
166
|
+
index = self.index_of_min(key=key)
|
|
167
|
+
assert index != -1
|
|
168
|
+
return self[index]
|
|
165
169
|
|
|
166
170
|
def swap(self, i: Num, j: Num, /):
|
|
167
171
|
"""Swap the values at the given indices.
|
|
@@ -184,6 +188,7 @@ class ArrayLike[T](Sequence, ABC):
|
|
|
184
188
|
if len(self) < 15 or key is not None:
|
|
185
189
|
if key is None:
|
|
186
190
|
key = _identity
|
|
191
|
+
# May be worth adding a block sort variant for better performance on large arrays in the future
|
|
187
192
|
_insertion_sort(self, 0, len(self), key, reverse)
|
|
188
193
|
else:
|
|
189
194
|
# Heap sort is unstable, so if there's a key, we can't rely on it
|
sonolus/script/bucket.py
CHANGED
|
@@ -20,7 +20,7 @@ class JudgmentWindow(Record):
|
|
|
20
20
|
"""The window for judging the accuracy of a hit.
|
|
21
21
|
|
|
22
22
|
Usage:
|
|
23
|
-
```
|
|
23
|
+
```python
|
|
24
24
|
JudgmentWindow(perfect: Interval, great: Interval, good: Interval)
|
|
25
25
|
```
|
|
26
26
|
"""
|
|
@@ -62,7 +62,7 @@ class JudgmentWindow(Record):
|
|
|
62
62
|
target: The target time of the hit.
|
|
63
63
|
|
|
64
64
|
Returns:
|
|
65
|
-
The
|
|
65
|
+
The [`Judgment`][sonolus.script.bucket.Judgment] of the hit.
|
|
66
66
|
"""
|
|
67
67
|
return _judge(
|
|
68
68
|
actual,
|
|
@@ -133,7 +133,9 @@ class Bucket(Record):
|
|
|
133
133
|
"""A bucket for entity judgment results.
|
|
134
134
|
|
|
135
135
|
Usage:
|
|
136
|
-
```
|
|
136
|
+
```python
|
|
137
|
+
Bucket(id: int)
|
|
138
|
+
```
|
|
137
139
|
"""
|
|
138
140
|
|
|
139
141
|
id: int
|
sonolus/script/containers.py
CHANGED
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from sonolus.backend.visitor import compile_and_call
|
|
3
4
|
from sonolus.script.array import Array
|
|
4
5
|
from sonolus.script.array_like import ArrayLike
|
|
5
6
|
from sonolus.script.debug import error
|
|
7
|
+
from sonolus.script.internal.context import ctx
|
|
8
|
+
from sonolus.script.internal.impl import meta_fn
|
|
6
9
|
from sonolus.script.iterator import SonolusIterator
|
|
10
|
+
from sonolus.script.num import Num
|
|
11
|
+
from sonolus.script.pointer import _deref
|
|
7
12
|
from sonolus.script.record import Record
|
|
8
13
|
from sonolus.script.values import alloc, copy
|
|
9
14
|
|
|
10
15
|
|
|
16
|
+
class Box[T](Record):
|
|
17
|
+
"""A box that contains a value.
|
|
18
|
+
|
|
19
|
+
This can be helpful for generic code that can handle both Num and non-Num types.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
```python
|
|
23
|
+
Box[T](value: T)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
```python
|
|
28
|
+
box = Box(1)
|
|
29
|
+
box = Box[int](2)
|
|
30
|
+
|
|
31
|
+
x: T = ...
|
|
32
|
+
y: T = ...
|
|
33
|
+
box = Box(x)
|
|
34
|
+
box.value = y # Works regardless of whether x is a Num or not
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
value: T
|
|
39
|
+
"""The value contained in the box."""
|
|
40
|
+
|
|
41
|
+
|
|
11
42
|
class Pair[T, U](Record):
|
|
12
43
|
"""A generic pair of values.
|
|
13
44
|
|
|
@@ -129,6 +160,17 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
|
|
|
129
160
|
self._array[self._size] = value
|
|
130
161
|
self._size += 1
|
|
131
162
|
|
|
163
|
+
def append_unchecked(self, value: T):
|
|
164
|
+
"""Append the given value to the end of the array without checking the capacity.
|
|
165
|
+
|
|
166
|
+
Use with caution as this may cause hard to debug issues if the array is full.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
value: The value to append.
|
|
170
|
+
"""
|
|
171
|
+
self._array[self._size] = value
|
|
172
|
+
self._size += 1
|
|
173
|
+
|
|
132
174
|
def extend(self, values: ArrayLike[T]):
|
|
133
175
|
"""Appends copies of the values in the given array to the end of the array.
|
|
134
176
|
|
|
@@ -258,6 +300,130 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
|
|
|
258
300
|
raise TypeError("unhashable type: 'VarArray'")
|
|
259
301
|
|
|
260
302
|
|
|
303
|
+
class ArrayPointer[T](Record, ArrayLike[T]):
|
|
304
|
+
"""An array defined by a size and pointer to the first element.
|
|
305
|
+
|
|
306
|
+
This is intended to be created internally and improper use may result in hard to debug issues.
|
|
307
|
+
|
|
308
|
+
Usage:
|
|
309
|
+
```python
|
|
310
|
+
ArrayPointer[T](size: int, block: int, offset: int)
|
|
311
|
+
```
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
size: int
|
|
315
|
+
block: int
|
|
316
|
+
offset: int
|
|
317
|
+
|
|
318
|
+
def __len__(self) -> int:
|
|
319
|
+
"""Return the number of elements in the array."""
|
|
320
|
+
return self.size
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def element_type(cls) -> type[T]:
|
|
324
|
+
"""Return the type of the elements in the array."""
|
|
325
|
+
return cls.type_var_value(T)
|
|
326
|
+
|
|
327
|
+
def _check_index(self, index: int):
|
|
328
|
+
assert 0 <= index < self.size
|
|
329
|
+
|
|
330
|
+
@meta_fn
|
|
331
|
+
def _get_item(self, item: int) -> T:
|
|
332
|
+
if not ctx():
|
|
333
|
+
raise TypeError("ArrayPointer values cannot be accessed outside of a context")
|
|
334
|
+
return _deref(
|
|
335
|
+
self.block,
|
|
336
|
+
self.offset + Num._accept_(item) * Num._accept_(self.element_type()._size_()),
|
|
337
|
+
self.element_type(),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@meta_fn
|
|
341
|
+
def __getitem__(self, item: int) -> T:
|
|
342
|
+
compile_and_call(self._check_index, item)
|
|
343
|
+
return self._get_item(item)._get_()
|
|
344
|
+
|
|
345
|
+
@meta_fn
|
|
346
|
+
def __setitem__(self, key: int, value: T):
|
|
347
|
+
compile_and_call(self._check_index, key)
|
|
348
|
+
dst = self._get_item(key)
|
|
349
|
+
if self.element_type()._is_value_type_():
|
|
350
|
+
dst._set_(value)
|
|
351
|
+
else:
|
|
352
|
+
dst._copy_from__(value)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class ArraySet[T, Capacity](Record):
|
|
356
|
+
"""A set implemented as an array with a fixed maximum capacity.
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
```python
|
|
360
|
+
ArraySet[T, Capacity].new() # Create a new empty set
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Examples:
|
|
364
|
+
```python
|
|
365
|
+
s = ArraySet[int, 10].new()
|
|
366
|
+
s.add(1)
|
|
367
|
+
s.add(2)
|
|
368
|
+
assert 1 in s
|
|
369
|
+
assert 3 not in s
|
|
370
|
+
s.remove(1)
|
|
371
|
+
assert 1 not in s
|
|
372
|
+
```
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
_values: VarArray[T, Capacity]
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def new(cls):
|
|
379
|
+
"""Create a new empty set."""
|
|
380
|
+
element_type = cls.type_var_value(T)
|
|
381
|
+
capacity = cls.type_var_value(Capacity)
|
|
382
|
+
return cls(VarArray[element_type, capacity].new())
|
|
383
|
+
|
|
384
|
+
def __len__(self):
|
|
385
|
+
"""Return the number of elements in the set."""
|
|
386
|
+
return len(self._values)
|
|
387
|
+
|
|
388
|
+
def __contains__(self, value):
|
|
389
|
+
"""Return whether the given value is present in the set."""
|
|
390
|
+
return value in self._values
|
|
391
|
+
|
|
392
|
+
def __iter__(self):
|
|
393
|
+
"""Return an iterator over the values in the set."""
|
|
394
|
+
return self._values.__iter__()
|
|
395
|
+
|
|
396
|
+
def add(self, value: T) -> bool:
|
|
397
|
+
"""Add a copy of the given value to the set.
|
|
398
|
+
|
|
399
|
+
This has no effect and returns False if the value is already present or if the set is full.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
value: The value to add.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
True if the value was added, False otherwise.
|
|
406
|
+
"""
|
|
407
|
+
return self._values.set_add(value)
|
|
408
|
+
|
|
409
|
+
def remove(self, value: T) -> bool:
|
|
410
|
+
"""Remove the given value from the set.
|
|
411
|
+
|
|
412
|
+
This has no effect and returns False if the value is not present.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
value: The value to remove.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if the value was removed, False otherwise.
|
|
419
|
+
"""
|
|
420
|
+
return self._values.set_remove(value)
|
|
421
|
+
|
|
422
|
+
def clear(self):
|
|
423
|
+
"""Clear the set, removing all elements."""
|
|
424
|
+
self._values.clear()
|
|
425
|
+
|
|
426
|
+
|
|
261
427
|
class _ArrayMapEntry[K, V](Record):
|
|
262
428
|
key: K
|
|
263
429
|
value: V
|
sonolus/script/debug.py
CHANGED
|
@@ -16,8 +16,13 @@ debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@meta_fn
|
|
19
|
-
def error(message: str | None = None) ->
|
|
20
|
-
|
|
19
|
+
def error(message: str | None = None) -> Never:
|
|
20
|
+
"""Raise an error.
|
|
21
|
+
|
|
22
|
+
This function is used to raise an error during runtime.
|
|
23
|
+
When this happens, the game will pause in debug mode. The current callback will also immediately return 0.
|
|
24
|
+
"""
|
|
25
|
+
message = validate_value(message)._as_py_() if message is not None else "Error"
|
|
21
26
|
if not isinstance(message, str):
|
|
22
27
|
raise ValueError("Expected a string")
|
|
23
28
|
if ctx():
|
|
@@ -28,6 +33,19 @@ def error(message: str | None = None) -> None:
|
|
|
28
33
|
raise RuntimeError(message)
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
@meta_fn
|
|
37
|
+
def static_error(message: str | None = None) -> Never:
|
|
38
|
+
"""Raise a static error.
|
|
39
|
+
|
|
40
|
+
This function is used to raise an error during compile-time if the compiler cannot guarantee that
|
|
41
|
+
this function will not be called during runtime.
|
|
42
|
+
"""
|
|
43
|
+
message = validate_value(message)._as_py_() if message is not None else "Error"
|
|
44
|
+
if not isinstance(message, str):
|
|
45
|
+
raise ValueError("Expected a string")
|
|
46
|
+
raise RuntimeError(message)
|
|
47
|
+
|
|
48
|
+
|
|
31
49
|
@meta_fn
|
|
32
50
|
def debug_log(value: Num):
|
|
33
51
|
"""Log a value in debug mode."""
|
sonolus/script/engine.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from os import PathLike
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, Literal
|
|
8
8
|
|
|
9
9
|
from sonolus.build.collection import Asset, load_asset
|
|
10
10
|
from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
|
|
@@ -81,7 +81,7 @@ class Engine:
|
|
|
81
81
|
meta: Additional metadata of the engine.
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
|
-
version =
|
|
84
|
+
version: Literal[13] = 13
|
|
85
85
|
|
|
86
86
|
def __init__(
|
|
87
87
|
self,
|
|
@@ -3,7 +3,7 @@ from typing import Any, ClassVar, Self
|
|
|
3
3
|
|
|
4
4
|
from sonolus.backend.place import BlockPlace
|
|
5
5
|
from sonolus.script.internal.impl import meta_fn
|
|
6
|
-
from sonolus.script.internal.value import Value
|
|
6
|
+
from sonolus.script.internal.value import DataValue, Value
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class _Missing:
|
|
@@ -18,6 +18,8 @@ _MISSING = _Missing()
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class ConstantValue(Value):
|
|
21
|
+
"""Wraps a python constant value usable in Sonolus scripts."""
|
|
22
|
+
|
|
21
23
|
_parameterized_: ClassVar[dict[Any, type[Self]]] = {}
|
|
22
24
|
_value: ClassVar[Any] = _MISSING
|
|
23
25
|
instance: ClassVar[Self | _Missing] = _MISSING
|
|
@@ -94,10 +96,10 @@ class ConstantValue(Value):
|
|
|
94
96
|
return self.value()
|
|
95
97
|
|
|
96
98
|
@classmethod
|
|
97
|
-
def _from_list_(cls, values: Iterable[
|
|
99
|
+
def _from_list_(cls, values: Iterable[DataValue]) -> Self:
|
|
98
100
|
return cls()
|
|
99
101
|
|
|
100
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
102
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
101
103
|
return []
|
|
102
104
|
|
|
103
105
|
@classmethod
|
|
@@ -139,5 +141,5 @@ class ConstantValue(Value):
|
|
|
139
141
|
return hash(self.value())
|
|
140
142
|
|
|
141
143
|
|
|
142
|
-
class
|
|
144
|
+
class BasicConstantValue(ConstantValue):
|
|
143
145
|
"""For constants without any special behavior."""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import typing
|
|
3
4
|
from enum import Enum
|
|
5
|
+
from types import UnionType
|
|
4
6
|
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, get_origin
|
|
5
7
|
|
|
6
8
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
@@ -31,6 +33,11 @@ def validate_type_spec(spec: Any) -> PartialGeneric | TypeVar | type[Value]:
|
|
|
31
33
|
spec = validate_type_spec(spec.__mro__[1])
|
|
32
34
|
if isinstance(spec, PartialGeneric | TypeVar) or (isinstance(spec, type) and issubclass(spec, Value)):
|
|
33
35
|
return spec
|
|
36
|
+
if typing.get_origin(spec) is UnionType:
|
|
37
|
+
args = typing.get_args(spec)
|
|
38
|
+
validated_args = {validate_type_arg(arg) for arg in args}
|
|
39
|
+
if len(validated_args) == 1:
|
|
40
|
+
return validated_args.pop()
|
|
34
41
|
raise TypeError(f"Invalid type spec: {spec}")
|
|
35
42
|
|
|
36
43
|
|