sonolus.py 0.1.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/__init__.py +0 -0
- sonolus/backend/__init__.py +0 -0
- sonolus/backend/allocate.py +51 -0
- sonolus/backend/blocks.py +756 -0
- sonolus/backend/excepthook.py +37 -0
- sonolus/backend/finalize.py +69 -0
- sonolus/backend/flow.py +92 -0
- sonolus/backend/interpret.py +333 -0
- sonolus/backend/ir.py +89 -0
- sonolus/backend/mode.py +24 -0
- sonolus/backend/node.py +40 -0
- sonolus/backend/ops.py +197 -0
- sonolus/backend/optimize.py +9 -0
- sonolus/backend/passes.py +6 -0
- sonolus/backend/place.py +90 -0
- sonolus/backend/simplify.py +30 -0
- sonolus/backend/utils.py +48 -0
- sonolus/backend/visitor.py +880 -0
- sonolus/build/__init__.py +0 -0
- sonolus/build/cli.py +170 -0
- sonolus/build/collection.py +293 -0
- sonolus/build/compile.py +90 -0
- sonolus/build/defaults.py +32 -0
- sonolus/build/engine.py +149 -0
- sonolus/build/level.py +23 -0
- sonolus/build/node.py +43 -0
- sonolus/build/project.py +94 -0
- sonolus/py.typed +0 -0
- sonolus/script/__init__.py +0 -0
- sonolus/script/archetype.py +651 -0
- sonolus/script/array.py +241 -0
- sonolus/script/bucket.py +192 -0
- sonolus/script/callbacks.py +105 -0
- sonolus/script/comptime.py +146 -0
- sonolus/script/containers.py +247 -0
- sonolus/script/debug.py +70 -0
- sonolus/script/effect.py +132 -0
- sonolus/script/engine.py +101 -0
- sonolus/script/globals.py +234 -0
- sonolus/script/graphics.py +141 -0
- sonolus/script/icon.py +73 -0
- sonolus/script/internal/__init__.py +5 -0
- sonolus/script/internal/builtin_impls.py +144 -0
- sonolus/script/internal/context.py +365 -0
- sonolus/script/internal/descriptor.py +17 -0
- sonolus/script/internal/error.py +15 -0
- sonolus/script/internal/generic.py +197 -0
- sonolus/script/internal/impl.py +69 -0
- sonolus/script/internal/introspection.py +14 -0
- sonolus/script/internal/native.py +38 -0
- sonolus/script/internal/value.py +144 -0
- sonolus/script/interval.py +98 -0
- sonolus/script/iterator.py +211 -0
- sonolus/script/level.py +52 -0
- sonolus/script/math.py +92 -0
- sonolus/script/num.py +382 -0
- sonolus/script/options.py +194 -0
- sonolus/script/particle.py +158 -0
- sonolus/script/pointer.py +30 -0
- sonolus/script/project.py +17 -0
- sonolus/script/range.py +58 -0
- sonolus/script/record.py +293 -0
- sonolus/script/runtime.py +526 -0
- sonolus/script/sprite.py +332 -0
- sonolus/script/text.py +404 -0
- sonolus/script/timing.py +42 -0
- sonolus/script/transform.py +118 -0
- sonolus/script/ui.py +160 -0
- sonolus/script/values.py +43 -0
- sonolus/script/vec.py +48 -0
- sonolus_py-0.1.0.dist-info/METADATA +10 -0
- sonolus_py-0.1.0.dist-info/RECORD +75 -0
- sonolus_py-0.1.0.dist-info/WHEEL +4 -0
- sonolus_py-0.1.0.dist-info/entry_points.txt +2 -0
- sonolus_py-0.1.0.dist-info/licenses/LICENSE +21 -0
sonolus/script/num.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# ruff: noqa: N801
|
|
2
|
+
import operator
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self, final
|
|
5
|
+
|
|
6
|
+
from sonolus.backend.ir import IRConst, IRGet, IRPureInstr, IRSet
|
|
7
|
+
from sonolus.backend.ops import Op
|
|
8
|
+
from sonolus.backend.place import BlockPlace, Place
|
|
9
|
+
from sonolus.script.internal.context import ctx
|
|
10
|
+
from sonolus.script.internal.error import InternalError
|
|
11
|
+
from sonolus.script.internal.impl import meta_fn
|
|
12
|
+
from sonolus.script.internal.value import Value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@final
|
|
16
|
+
class _Num(Value):
|
|
17
|
+
data: BlockPlace | float | int | bool
|
|
18
|
+
|
|
19
|
+
def __init__(self, data: Place | float | int | bool):
|
|
20
|
+
if isinstance(data, int):
|
|
21
|
+
data = float(data)
|
|
22
|
+
if isinstance(data, _Num):
|
|
23
|
+
raise InternalError("Cannot create a Num from a Num")
|
|
24
|
+
self.data = data
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
if isinstance(self.data, float) and self.data.is_integer():
|
|
28
|
+
return str(int(self.data))
|
|
29
|
+
return str(self.data)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"Num({self.data})"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _is_concrete_(cls) -> bool:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _size_(cls) -> int:
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _is_value_type_(cls) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def _from_place_(cls, place: BlockPlace) -> Self:
|
|
48
|
+
return cls(place)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _accepts_(cls, value: Value) -> bool:
|
|
52
|
+
return isinstance(value, Num | float | int | bool)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def _accept_(cls, value: Any) -> Self:
|
|
56
|
+
if not cls._accepts_(value):
|
|
57
|
+
raise TypeError(f"Cannot accept {value}")
|
|
58
|
+
if isinstance(value, Num):
|
|
59
|
+
return value
|
|
60
|
+
return cls(value)
|
|
61
|
+
|
|
62
|
+
def _is_py_(self) -> bool:
|
|
63
|
+
return not isinstance(self.data, BlockPlace)
|
|
64
|
+
|
|
65
|
+
def _as_py_(self) -> Any:
|
|
66
|
+
if not self._is_py_():
|
|
67
|
+
raise ValueError("Not a compile time constant Num")
|
|
68
|
+
if self.data.is_integer():
|
|
69
|
+
return int(self.data)
|
|
70
|
+
return self.data
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
74
|
+
value = next(iter(values))
|
|
75
|
+
return Num(value)
|
|
76
|
+
|
|
77
|
+
def _to_list_(self) -> list[float | BlockPlace]:
|
|
78
|
+
return [self.data]
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def _flat_keys_(cls, prefix: str) -> list[str]:
|
|
82
|
+
return [prefix]
|
|
83
|
+
|
|
84
|
+
def _get_(self) -> Self:
|
|
85
|
+
if ctx():
|
|
86
|
+
place = ctx().alloc(size=1)
|
|
87
|
+
if isinstance(self.data, BlockPlace):
|
|
88
|
+
ctx().check_readable(self.data)
|
|
89
|
+
ctx().add_statements(IRSet(place, self.ir()))
|
|
90
|
+
return Num(place)
|
|
91
|
+
else:
|
|
92
|
+
return Num(self.data)
|
|
93
|
+
|
|
94
|
+
def _set_(self, value: Self):
|
|
95
|
+
if ctx():
|
|
96
|
+
if not isinstance(self.data, BlockPlace):
|
|
97
|
+
raise ValueError("Cannot set a compile time constant value")
|
|
98
|
+
ctx().check_writable(self.data)
|
|
99
|
+
ctx().add_statements(IRSet(self.data, value.ir()))
|
|
100
|
+
else:
|
|
101
|
+
self.data = value.data
|
|
102
|
+
|
|
103
|
+
def _copy_from_(self, value: Self):
|
|
104
|
+
raise ValueError("Cannot assign to a number")
|
|
105
|
+
|
|
106
|
+
def _copy_(self) -> Self:
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _alloc_(cls) -> Self:
|
|
111
|
+
if ctx():
|
|
112
|
+
return Num(ctx().alloc(size=1))
|
|
113
|
+
else:
|
|
114
|
+
return Num(-1)
|
|
115
|
+
|
|
116
|
+
def ir(self):
|
|
117
|
+
if isinstance(self.data, BlockPlace):
|
|
118
|
+
return IRGet(self.data)
|
|
119
|
+
else:
|
|
120
|
+
return IRConst(self.data)
|
|
121
|
+
|
|
122
|
+
def index(self) -> int | BlockPlace:
|
|
123
|
+
return self.data
|
|
124
|
+
|
|
125
|
+
def _bin_op(self, other: Self, const_fn: Callable[[Self, Self], Self | None], ir_op: Op) -> Self:
|
|
126
|
+
if not Num._accepts_(other):
|
|
127
|
+
return NotImplemented
|
|
128
|
+
other = Num._accept_(other)
|
|
129
|
+
const_value = const_fn(self, other)
|
|
130
|
+
if const_value is not None:
|
|
131
|
+
return const_value
|
|
132
|
+
if ctx():
|
|
133
|
+
result_place = ctx().alloc(size=1)
|
|
134
|
+
ctx().add_statements(IRSet(result_place, IRPureInstr(ir_op, [self.ir(), other.ir()])))
|
|
135
|
+
return Num(result_place)
|
|
136
|
+
else:
|
|
137
|
+
raise InternalError("Unexpected call on non-comptime Num instance outside a compilation context")
|
|
138
|
+
|
|
139
|
+
def _unary_op(self, py_fn: Callable[[float], float], ir_op: Op) -> Self:
|
|
140
|
+
if self._is_py_():
|
|
141
|
+
return Num(py_fn(self._as_py_()))
|
|
142
|
+
elif ctx():
|
|
143
|
+
result_place = ctx().alloc(size=1)
|
|
144
|
+
ctx().add_statements(IRSet(result_place, IRPureInstr(ir_op, [self.ir()])))
|
|
145
|
+
return Num(result_place)
|
|
146
|
+
else:
|
|
147
|
+
raise InternalError("Unexpected call on non-comptime Num instance outside a compilation context")
|
|
148
|
+
|
|
149
|
+
@meta_fn
|
|
150
|
+
def __eq__(self, other) -> Self:
|
|
151
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
152
|
+
if a._is_py_() and b._is_py_():
|
|
153
|
+
return Num(a.data == b.data)
|
|
154
|
+
if a.data == b.data:
|
|
155
|
+
return Num(True)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
return self._bin_op(other, const_fn, Op.Equal)
|
|
159
|
+
|
|
160
|
+
def __hash__(self):
|
|
161
|
+
if self._is_py_():
|
|
162
|
+
return hash(self._as_py_())
|
|
163
|
+
raise ValueError("Cannot hash non compile time constant Num")
|
|
164
|
+
|
|
165
|
+
@meta_fn
|
|
166
|
+
def __ne__(self, other) -> Self:
|
|
167
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
168
|
+
if a._is_py_() and b._is_py_():
|
|
169
|
+
return Num(a.data != b.data)
|
|
170
|
+
if a.data == b.data:
|
|
171
|
+
return Num(False)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
return self._bin_op(other, const_fn, Op.NotEqual)
|
|
175
|
+
|
|
176
|
+
@meta_fn
|
|
177
|
+
def __lt__(self, other) -> Self:
|
|
178
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
179
|
+
if a._is_py_() and b._is_py_():
|
|
180
|
+
return Num(a.data < b.data)
|
|
181
|
+
if a.data == b.data:
|
|
182
|
+
return Num(False)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
return self._bin_op(other, const_fn, Op.Less)
|
|
186
|
+
|
|
187
|
+
@meta_fn
|
|
188
|
+
def __le__(self, other) -> Self:
|
|
189
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
190
|
+
if a._is_py_() and b._is_py_():
|
|
191
|
+
return Num(a.data <= b.data)
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return self._bin_op(other, const_fn, Op.LessOr)
|
|
195
|
+
|
|
196
|
+
@meta_fn
|
|
197
|
+
def __gt__(self, other) -> Self:
|
|
198
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
199
|
+
if a._is_py_() and b._is_py_():
|
|
200
|
+
return Num(a.data > b.data)
|
|
201
|
+
if a.data == b.data:
|
|
202
|
+
return Num(False)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
return self._bin_op(other, const_fn, Op.Greater)
|
|
206
|
+
|
|
207
|
+
@meta_fn
|
|
208
|
+
def __ge__(self, other) -> Self:
|
|
209
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
210
|
+
if a._is_py_() and b._is_py_():
|
|
211
|
+
return Num(a.data >= b.data)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
return self._bin_op(other, const_fn, Op.GreaterOr)
|
|
215
|
+
|
|
216
|
+
@meta_fn
|
|
217
|
+
def __add__(self, other) -> Self:
|
|
218
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
219
|
+
if a._is_py_() and b._is_py_():
|
|
220
|
+
return Num(a.data + b.data)
|
|
221
|
+
if a._is_py_() and a.data == 0:
|
|
222
|
+
return b
|
|
223
|
+
if b._is_py_() and b.data == 0:
|
|
224
|
+
return a
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
return self._bin_op(other, const_fn, Op.Add)
|
|
228
|
+
|
|
229
|
+
@meta_fn
|
|
230
|
+
def __sub__(self, other) -> Self:
|
|
231
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
232
|
+
if a._is_py_() and b._is_py_():
|
|
233
|
+
return Num(a.data - b.data)
|
|
234
|
+
if a._is_py_() and a.data == 0:
|
|
235
|
+
return -b
|
|
236
|
+
if b._is_py_() and b.data == 0:
|
|
237
|
+
return a
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
return self._bin_op(other, const_fn, Op.Subtract)
|
|
241
|
+
|
|
242
|
+
@meta_fn
|
|
243
|
+
def __mul__(self, other) -> Self:
|
|
244
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
245
|
+
if a._is_py_() and b._is_py_():
|
|
246
|
+
return Num(a.data * b.data)
|
|
247
|
+
if a._is_py_():
|
|
248
|
+
if a.data == 0:
|
|
249
|
+
return Num(0)
|
|
250
|
+
if a.data == 1:
|
|
251
|
+
return b
|
|
252
|
+
if b._is_py_():
|
|
253
|
+
if b.data == 0:
|
|
254
|
+
return Num(0)
|
|
255
|
+
if b.data == 1:
|
|
256
|
+
return a
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
return self._bin_op(other, const_fn, Op.Multiply)
|
|
260
|
+
|
|
261
|
+
@meta_fn
|
|
262
|
+
def __truediv__(self, other) -> Self:
|
|
263
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
264
|
+
if a._is_py_() and b._is_py_():
|
|
265
|
+
if b.data == 0:
|
|
266
|
+
return None
|
|
267
|
+
return Num(a.data / b.data)
|
|
268
|
+
if b._is_py_():
|
|
269
|
+
if b.data == 1:
|
|
270
|
+
return a
|
|
271
|
+
if b.data == -1:
|
|
272
|
+
return -a
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
return self._bin_op(other, const_fn, Op.Divide)
|
|
276
|
+
|
|
277
|
+
@meta_fn
|
|
278
|
+
def __floordiv__(self, other) -> Self:
|
|
279
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
280
|
+
if a._is_py_() and b._is_py_():
|
|
281
|
+
if b.data == 0:
|
|
282
|
+
return None
|
|
283
|
+
return Num(a.data // b.data)
|
|
284
|
+
if b._is_py_():
|
|
285
|
+
if b.data == 1:
|
|
286
|
+
return a
|
|
287
|
+
if b.data == -1:
|
|
288
|
+
return -a
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
return self._bin_op(other, const_fn, Op.Divide)._unary_op(lambda x: x, Op.Floor)
|
|
292
|
+
|
|
293
|
+
@meta_fn
|
|
294
|
+
def __mod__(self, other) -> Self:
|
|
295
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
296
|
+
if a._is_py_() and b._is_py_():
|
|
297
|
+
if b.data == 0:
|
|
298
|
+
return None
|
|
299
|
+
return Num(a.data % b.data)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
return self._bin_op(other, const_fn, Op.Mod)
|
|
303
|
+
|
|
304
|
+
@meta_fn
|
|
305
|
+
def __pow__(self, other) -> Self:
|
|
306
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
307
|
+
if a._is_py_() and b._is_py_():
|
|
308
|
+
try:
|
|
309
|
+
return Num(a.data**b.data)
|
|
310
|
+
except OverflowError:
|
|
311
|
+
return None
|
|
312
|
+
if b._is_py_():
|
|
313
|
+
if b.data == 0:
|
|
314
|
+
return Num(1)
|
|
315
|
+
if b.data == 1:
|
|
316
|
+
return a
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
return self._bin_op(other, const_fn, Op.Power)
|
|
320
|
+
|
|
321
|
+
@meta_fn
|
|
322
|
+
def __neg__(self) -> Self:
|
|
323
|
+
return self._unary_op(operator.neg, Op.Negate)
|
|
324
|
+
|
|
325
|
+
@meta_fn
|
|
326
|
+
def __pos__(self) -> Self:
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
@meta_fn
|
|
330
|
+
def __abs__(self) -> Self:
|
|
331
|
+
return self._unary_op(abs, Op.Abs)
|
|
332
|
+
|
|
333
|
+
@meta_fn
|
|
334
|
+
def __bool__(self):
|
|
335
|
+
if ctx():
|
|
336
|
+
return self != 0
|
|
337
|
+
else:
|
|
338
|
+
if self._is_py_():
|
|
339
|
+
return bool(self._as_py_())
|
|
340
|
+
raise ValueError("Cannot convert non compile time constant Num to bool")
|
|
341
|
+
|
|
342
|
+
def and_(self, other) -> Self:
|
|
343
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
344
|
+
if a._is_py_() and b._is_py_():
|
|
345
|
+
return Num(a.data and b.data)
|
|
346
|
+
if a._is_py_():
|
|
347
|
+
if a.data == 0:
|
|
348
|
+
return a
|
|
349
|
+
else:
|
|
350
|
+
return b
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
return self._bin_op(other, const_fn, Op.And)
|
|
354
|
+
|
|
355
|
+
def or_(self, other) -> Self:
|
|
356
|
+
def const_fn(a: Self, b: Self) -> Num | None:
|
|
357
|
+
if a._is_py_() and b._is_py_():
|
|
358
|
+
return Num(a.data or b.data)
|
|
359
|
+
if a._is_py_():
|
|
360
|
+
if a.data == 0:
|
|
361
|
+
return b
|
|
362
|
+
else:
|
|
363
|
+
return a
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
return self._bin_op(other, const_fn, Op.Or)
|
|
367
|
+
|
|
368
|
+
def not_(self) -> Self:
|
|
369
|
+
return self._unary_op(operator.not_, Op.Not)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if TYPE_CHECKING:
|
|
373
|
+
|
|
374
|
+
class __Num(float, int, bool, _Num): # type: ignore
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
Num = __Num | _Num | float | int | bool
|
|
378
|
+
else:
|
|
379
|
+
# Need to do this to satisfy type checkers (especially Pycharm)
|
|
380
|
+
_Num.__name__ = "Num"
|
|
381
|
+
_Num.__qualname__ = "Num"
|
|
382
|
+
globals()["Num"] = _Num
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# ruff: noqa: A002
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Annotated, Any, ClassVar, Protocol, dataclass_transform, get_origin
|
|
4
|
+
|
|
5
|
+
from sonolus.backend.mode import Mode
|
|
6
|
+
from sonolus.backend.place import BlockPlace
|
|
7
|
+
from sonolus.script.debug import assert_unreachable
|
|
8
|
+
from sonolus.script.internal.context import ctx
|
|
9
|
+
from sonolus.script.internal.descriptor import SonolusDescriptor
|
|
10
|
+
from sonolus.script.internal.generic import validate_concrete_type
|
|
11
|
+
from sonolus.script.internal.introspection import get_field_specifiers
|
|
12
|
+
from sonolus.script.num import Num
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SliderOption:
|
|
17
|
+
name: str | None
|
|
18
|
+
standard: bool
|
|
19
|
+
advanced: bool
|
|
20
|
+
scope: str | None
|
|
21
|
+
default: float
|
|
22
|
+
min: float
|
|
23
|
+
max: float
|
|
24
|
+
step: float
|
|
25
|
+
unit: str | None
|
|
26
|
+
|
|
27
|
+
def to_dict(self):
|
|
28
|
+
result = {
|
|
29
|
+
"type": "slider",
|
|
30
|
+
"name": self.name,
|
|
31
|
+
"standard": self.standard,
|
|
32
|
+
"advanced": self.advanced,
|
|
33
|
+
"def": self.default,
|
|
34
|
+
"min": self.min,
|
|
35
|
+
"max": self.max,
|
|
36
|
+
"step": self.step,
|
|
37
|
+
}
|
|
38
|
+
if self.scope is not None:
|
|
39
|
+
result["scope"] = self.scope
|
|
40
|
+
if self.unit is not None:
|
|
41
|
+
result["unit"] = self.unit
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ToggleOption:
|
|
47
|
+
name: str | None
|
|
48
|
+
standard: bool
|
|
49
|
+
advanced: bool
|
|
50
|
+
scope: str | None
|
|
51
|
+
default: bool
|
|
52
|
+
|
|
53
|
+
def to_dict(self):
|
|
54
|
+
result = {
|
|
55
|
+
"type": "toggle",
|
|
56
|
+
"name": self.name,
|
|
57
|
+
"standard": self.standard,
|
|
58
|
+
"advanced": self.advanced,
|
|
59
|
+
"def": int(self.default),
|
|
60
|
+
}
|
|
61
|
+
if self.scope is not None:
|
|
62
|
+
result["scope"] = self.scope
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class SelectOption:
|
|
68
|
+
name: str | None
|
|
69
|
+
standard: bool
|
|
70
|
+
advanced: bool
|
|
71
|
+
scope: str | None
|
|
72
|
+
default: str
|
|
73
|
+
values: list[str]
|
|
74
|
+
|
|
75
|
+
def to_dict(self):
|
|
76
|
+
result = {
|
|
77
|
+
"type": "select",
|
|
78
|
+
"name": self.name,
|
|
79
|
+
"standard": self.standard,
|
|
80
|
+
"advanced": self.advanced,
|
|
81
|
+
"def": self.values.index(self.default),
|
|
82
|
+
"values": self.values,
|
|
83
|
+
}
|
|
84
|
+
if self.scope is not None:
|
|
85
|
+
result["scope"] = self.scope
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def slider_option(
|
|
90
|
+
*,
|
|
91
|
+
name: str | None = None,
|
|
92
|
+
standard: bool = False,
|
|
93
|
+
advanced: bool = False,
|
|
94
|
+
default: float,
|
|
95
|
+
min: float,
|
|
96
|
+
max: float,
|
|
97
|
+
step: float,
|
|
98
|
+
unit: str | None = None,
|
|
99
|
+
scope: str | None = None,
|
|
100
|
+
) -> Any:
|
|
101
|
+
return SliderOption(name, standard, advanced, scope, default, min, max, step, unit)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def toggle_option(
|
|
105
|
+
*,
|
|
106
|
+
name: str | None = None,
|
|
107
|
+
standard: bool = False,
|
|
108
|
+
advanced: bool = False,
|
|
109
|
+
default: bool,
|
|
110
|
+
scope: str | None = None,
|
|
111
|
+
) -> Any:
|
|
112
|
+
return ToggleOption(name, standard, advanced, scope, default)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def select_option(
|
|
116
|
+
*,
|
|
117
|
+
name: str | None = None,
|
|
118
|
+
standard: bool = False,
|
|
119
|
+
advanced: bool = False,
|
|
120
|
+
default: str,
|
|
121
|
+
values: list[str],
|
|
122
|
+
scope: str | None = None,
|
|
123
|
+
) -> Any:
|
|
124
|
+
return SelectOption(name, standard, advanced, scope, default, values)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Options(Protocol):
|
|
128
|
+
_options_: ClassVar[list[SliderOption | ToggleOption | SelectOption]]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
type OptionInfo = SliderOption | ToggleOption | SelectOption
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class OptionField(SonolusDescriptor):
|
|
135
|
+
info: OptionInfo
|
|
136
|
+
index: int
|
|
137
|
+
|
|
138
|
+
def __init__(self, info: OptionInfo, index: int):
|
|
139
|
+
self.info = info
|
|
140
|
+
self.index = index
|
|
141
|
+
|
|
142
|
+
def __get__(self, instance, owner):
|
|
143
|
+
if ctx():
|
|
144
|
+
match ctx().global_state.mode:
|
|
145
|
+
case Mode.Play:
|
|
146
|
+
block = ctx().blocks.LevelOption
|
|
147
|
+
case Mode.Watch:
|
|
148
|
+
block = ctx().blocks.LevelOption
|
|
149
|
+
case Mode.Preview:
|
|
150
|
+
block = ctx().blocks.PreviewOption
|
|
151
|
+
case Mode.Tutorial:
|
|
152
|
+
block = None
|
|
153
|
+
case _:
|
|
154
|
+
assert_unreachable()
|
|
155
|
+
if block is not None:
|
|
156
|
+
return Num._from_place_(BlockPlace(block, self.index))
|
|
157
|
+
else:
|
|
158
|
+
return Num._accept_(self.info.default)
|
|
159
|
+
|
|
160
|
+
def __set__(self, instance, value):
|
|
161
|
+
raise AttributeError("Options are read-only")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass_transform()
|
|
165
|
+
def options[T](cls: type[T]) -> T | Options:
|
|
166
|
+
if len(cls.__bases__) != 1:
|
|
167
|
+
raise ValueError("Options class must not inherit from any class (except object)")
|
|
168
|
+
instance = cls()
|
|
169
|
+
entries = []
|
|
170
|
+
for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
|
|
171
|
+
if get_origin(annotation) is not Annotated:
|
|
172
|
+
raise TypeError(f"Invalid annotation for options: {annotation}")
|
|
173
|
+
annotation_type = annotation.__args__[0]
|
|
174
|
+
annotation_values = annotation.__metadata__
|
|
175
|
+
if len(annotation_values) != 1:
|
|
176
|
+
raise ValueError("Invalid annotation values for options")
|
|
177
|
+
annotation_type = validate_concrete_type(annotation_type)
|
|
178
|
+
if annotation_type is not Num:
|
|
179
|
+
raise TypeError(f"Invalid annotation type for options: {annotation_type}")
|
|
180
|
+
annotation_value = annotation_values[0]
|
|
181
|
+
if not isinstance(annotation_value, SliderOption | ToggleOption | SelectOption):
|
|
182
|
+
raise TypeError(f"Invalid annotation value for options: {annotation_value}")
|
|
183
|
+
if annotation_value.name is None:
|
|
184
|
+
annotation_value.name = name
|
|
185
|
+
entries.append(annotation_value)
|
|
186
|
+
setattr(cls, name, OptionField(annotation_value, i))
|
|
187
|
+
instance._options_ = entries
|
|
188
|
+
instance._is_comptime_value_ = True
|
|
189
|
+
return instance
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@options
|
|
193
|
+
class EmptyOptions:
|
|
194
|
+
pass
|