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/array.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# ruff: noqa: A005
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any, Self, final
|
|
6
|
+
|
|
7
|
+
from sonolus.backend.place import BlockPlace
|
|
8
|
+
from sonolus.script.debug import assert_unreachable
|
|
9
|
+
from sonolus.script.internal.context import ctx
|
|
10
|
+
from sonolus.script.internal.error import InternalError
|
|
11
|
+
from sonolus.script.internal.generic import GenericValue
|
|
12
|
+
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
13
|
+
from sonolus.script.internal.value import Value
|
|
14
|
+
from sonolus.script.iterator import ArrayLike
|
|
15
|
+
from sonolus.script.num import Num
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class Array[T, Size](GenericValue, ArrayLike[T]):
|
|
20
|
+
_value: list[T] | BlockPlace
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@meta_fn
|
|
24
|
+
def element_type(cls) -> type[T] | type[Value]:
|
|
25
|
+
return cls._get_type_arg_(T)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
@meta_fn
|
|
29
|
+
def size(cls) -> int:
|
|
30
|
+
return cls._get_type_arg_(Size)
|
|
31
|
+
|
|
32
|
+
def __new__(cls, *args: T) -> Array[T, Any]:
|
|
33
|
+
if cls._type_args_ is None:
|
|
34
|
+
values = [validate_value(arg) for arg in args]
|
|
35
|
+
types = {type(value) for value in values}
|
|
36
|
+
if len(types) == 0:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"{cls.__name__} constructor should be used with at least one value if type is not specified"
|
|
39
|
+
)
|
|
40
|
+
if len(types) > 1:
|
|
41
|
+
raise TypeError(f"{cls.__name__} constructor should be used with values of the same type, got {types}")
|
|
42
|
+
parameterized_cls = cls[types.pop(), len(args)]
|
|
43
|
+
else:
|
|
44
|
+
values = [cls.element_type()._accept_(arg) for arg in args]
|
|
45
|
+
if len(args) != cls.size():
|
|
46
|
+
raise ValueError(f"{cls.__name__} constructor should be used with {cls.size()} values, got {len(args)}")
|
|
47
|
+
parameterized_cls = cls
|
|
48
|
+
if ctx():
|
|
49
|
+
place = ctx().alloc(size=parameterized_cls.size())
|
|
50
|
+
result: parameterized_cls = parameterized_cls._from_place_(place)
|
|
51
|
+
result._copy_from_(parameterized_cls._with_value(values))
|
|
52
|
+
return result
|
|
53
|
+
else:
|
|
54
|
+
return parameterized_cls._with_value([value._copy_() for value in values])
|
|
55
|
+
|
|
56
|
+
def __init__(self, *_args: T):
|
|
57
|
+
super().__init__()
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def _with_value(cls, value) -> Self:
|
|
61
|
+
result = object.__new__(cls)
|
|
62
|
+
result._value = value
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _size_(cls) -> int:
|
|
67
|
+
return cls.size() * cls.element_type()._size_()
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _is_value_type_(cls) -> bool:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def _from_place_(cls, place: BlockPlace) -> Self:
|
|
75
|
+
return cls._with_value(place)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _accepts_(cls, value: Any) -> bool:
|
|
79
|
+
return isinstance(value, cls)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def _accept_(cls, value: Any) -> Self:
|
|
83
|
+
if not cls._accepts_(value):
|
|
84
|
+
raise TypeError(f"Cannot accept value {value} as {cls.__name__}")
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
def _is_py_(self) -> bool:
|
|
88
|
+
return isinstance(self._value, list)
|
|
89
|
+
|
|
90
|
+
def _as_py_(self) -> Any:
|
|
91
|
+
if not self._is_py_():
|
|
92
|
+
raise ValueError("Not a python value")
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
97
|
+
iterator = iter(values)
|
|
98
|
+
return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
|
|
99
|
+
|
|
100
|
+
def _to_list_(self) -> list[float | BlockPlace]:
|
|
101
|
+
match self._value:
|
|
102
|
+
case list():
|
|
103
|
+
return [entry for value in self._value for entry in value._to_list_()]
|
|
104
|
+
case BlockPlace():
|
|
105
|
+
return [
|
|
106
|
+
entry
|
|
107
|
+
for i in range(self.size())
|
|
108
|
+
for entry in self.element_type()
|
|
109
|
+
._from_place_(self._value.add_offset(i * self.element_type()._size_()))
|
|
110
|
+
._to_list_()
|
|
111
|
+
]
|
|
112
|
+
case _:
|
|
113
|
+
assert_unreachable()
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def _flat_keys_(cls, prefix: str) -> list[str]:
|
|
117
|
+
return [entry for i in range(cls.size()) for entry in cls.element_type()._flat_keys_(f"{prefix}[{i}]")]
|
|
118
|
+
|
|
119
|
+
def _get_(self) -> Self:
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def _set_(self, value: Self):
|
|
123
|
+
raise TypeError("Array does not support set_")
|
|
124
|
+
|
|
125
|
+
def _copy_from_(self, value: Self):
|
|
126
|
+
if not isinstance(value, type(self)):
|
|
127
|
+
raise TypeError("Cannot copy from different type")
|
|
128
|
+
for i in range(self.size()):
|
|
129
|
+
self[i] = value[i]
|
|
130
|
+
|
|
131
|
+
def _copy_(self) -> Self:
|
|
132
|
+
if ctx():
|
|
133
|
+
place = ctx().alloc(size=self.size())
|
|
134
|
+
result: Self = self._from_place_(place)
|
|
135
|
+
result._copy_from_(self)
|
|
136
|
+
return result
|
|
137
|
+
else:
|
|
138
|
+
return self._with_value([value._copy_() for value in self._value])
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _alloc_(cls) -> Self:
|
|
142
|
+
if ctx():
|
|
143
|
+
place = ctx().alloc(size=cls._size_())
|
|
144
|
+
return cls._from_place_(place)
|
|
145
|
+
else:
|
|
146
|
+
return cls._with_value([cls.element_type()._alloc_() for _ in range(cls.size())])
|
|
147
|
+
|
|
148
|
+
@meta_fn
|
|
149
|
+
def __getitem__(self, index: Num) -> T:
|
|
150
|
+
index: Num = Num._accept_(index)
|
|
151
|
+
if index._is_py_():
|
|
152
|
+
const_index = index._as_py_()
|
|
153
|
+
if isinstance(const_index, float) and not const_index.is_integer():
|
|
154
|
+
raise ValueError("Array index must be an integer")
|
|
155
|
+
const_index = int(const_index)
|
|
156
|
+
if not 0 <= const_index < self.size():
|
|
157
|
+
raise IndexError("Array index out of range")
|
|
158
|
+
if isinstance(self._value, list):
|
|
159
|
+
if ctx():
|
|
160
|
+
return self._value[const_index]._get_()
|
|
161
|
+
else:
|
|
162
|
+
return self._value[const_index]._get_()._as_py_()
|
|
163
|
+
else:
|
|
164
|
+
return (
|
|
165
|
+
self.element_type()
|
|
166
|
+
._from_place_(self._value.add_offset(const_index * self.element_type()._size_()))
|
|
167
|
+
._get_()
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
if not ctx():
|
|
171
|
+
raise InternalError("Unexpected non-constant index")
|
|
172
|
+
base = ctx().rom[tuple(self._to_list_())] if isinstance(self._value, list) else self._value
|
|
173
|
+
place = BlockPlace(
|
|
174
|
+
block=base.block,
|
|
175
|
+
index=(Num(base.index) + index * self.element_type()._size_()).index(),
|
|
176
|
+
offset=base.offset,
|
|
177
|
+
)
|
|
178
|
+
return self.element_type()._from_place_(place)._get_()
|
|
179
|
+
|
|
180
|
+
@meta_fn
|
|
181
|
+
def __setitem__(self, index: Num, value: T):
|
|
182
|
+
index: Num = Num._accept_(index)
|
|
183
|
+
value = self.element_type()._accept_(value)
|
|
184
|
+
if ctx():
|
|
185
|
+
if isinstance(self._value, list):
|
|
186
|
+
raise ValueError("Cannot mutate a compile time constant array")
|
|
187
|
+
base = self._value
|
|
188
|
+
place = (
|
|
189
|
+
base.add_offset(int(index._as_py_()) * self.element_type()._size_())
|
|
190
|
+
if index._is_py_()
|
|
191
|
+
else BlockPlace(
|
|
192
|
+
block=base.block,
|
|
193
|
+
index=(Num(base.index) + index * self.element_type()._size_()).index(),
|
|
194
|
+
offset=base.offset,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
dst = self.element_type()._from_place_(place)
|
|
198
|
+
if self.element_type()._is_value_type_():
|
|
199
|
+
dst._set_(value)
|
|
200
|
+
else:
|
|
201
|
+
dst._copy_from_(value)
|
|
202
|
+
else:
|
|
203
|
+
if not isinstance(self._value, list):
|
|
204
|
+
raise InternalError("Unexpected mutation of non compile time constant array")
|
|
205
|
+
const_index = index._as_py_()
|
|
206
|
+
if isinstance(const_index, float) and not const_index.is_integer():
|
|
207
|
+
raise ValueError("Array index must be an integer")
|
|
208
|
+
const_index = int(const_index)
|
|
209
|
+
if not 0 <= const_index < self.size():
|
|
210
|
+
raise IndexError("Array index out of range")
|
|
211
|
+
dst = self._value[const_index]
|
|
212
|
+
if self.element_type()._is_value_type_():
|
|
213
|
+
dst._set_(value)
|
|
214
|
+
else:
|
|
215
|
+
dst._copy_from_(value)
|
|
216
|
+
|
|
217
|
+
def __eq__(self, other):
|
|
218
|
+
if self.size() != other.size():
|
|
219
|
+
return False
|
|
220
|
+
i = 0
|
|
221
|
+
while i < self.size():
|
|
222
|
+
if self[i] != other[i]:
|
|
223
|
+
return False
|
|
224
|
+
i += 1
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
def __ne__(self, other):
|
|
228
|
+
return not self == other
|
|
229
|
+
|
|
230
|
+
def __hash__(self):
|
|
231
|
+
return hash(tuple(self[i] for i in range(self.size())))
|
|
232
|
+
|
|
233
|
+
def __str__(self):
|
|
234
|
+
if isinstance(self._value, BlockPlace):
|
|
235
|
+
return f"{type(self).__name__}({self._value}...)"
|
|
236
|
+
return f"{type(self).__name__}({", ".join(str(self[i]) for i in range(self.size()))})"
|
|
237
|
+
|
|
238
|
+
def __repr__(self):
|
|
239
|
+
if isinstance(self._value, BlockPlace):
|
|
240
|
+
return f"{type(self).__name__}({self._value}...)"
|
|
241
|
+
return f"{type(self).__name__}({", ".join(repr(self[i]) for i in range(self.size()))})"
|
sonolus/script/bucket.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import Annotated, Any, Protocol, dataclass_transform, get_origin
|
|
6
|
+
|
|
7
|
+
from sonolus.backend.mode import Mode
|
|
8
|
+
from sonolus.backend.ops import Op
|
|
9
|
+
from sonolus.script.internal.context import ctx
|
|
10
|
+
from sonolus.script.internal.impl import meta_fn
|
|
11
|
+
from sonolus.script.internal.introspection import get_field_specifiers
|
|
12
|
+
from sonolus.script.internal.native import native_function
|
|
13
|
+
from sonolus.script.interval import Interval
|
|
14
|
+
from sonolus.script.pointer import deref
|
|
15
|
+
from sonolus.script.record import Record
|
|
16
|
+
from sonolus.script.sprite import Sprite
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JudgmentWindow(Record):
|
|
20
|
+
perfect: Interval
|
|
21
|
+
great: Interval
|
|
22
|
+
good: Interval
|
|
23
|
+
|
|
24
|
+
def update(
|
|
25
|
+
self,
|
|
26
|
+
perfect: Interval | None = None,
|
|
27
|
+
great: Interval | None = None,
|
|
28
|
+
good: Interval | None = None,
|
|
29
|
+
):
|
|
30
|
+
if perfect is not None:
|
|
31
|
+
self.perfect = perfect
|
|
32
|
+
if great is not None:
|
|
33
|
+
self.great = great
|
|
34
|
+
if good is not None:
|
|
35
|
+
self.good = good
|
|
36
|
+
|
|
37
|
+
def judge(self, actual: float, target: float) -> Judgment:
|
|
38
|
+
return _judge(
|
|
39
|
+
actual,
|
|
40
|
+
target,
|
|
41
|
+
*self.perfect.tuple,
|
|
42
|
+
*self.great.tuple,
|
|
43
|
+
*self.good.tuple,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __mul__(self, other: float | int) -> JudgmentWindow:
|
|
47
|
+
return JudgmentWindow(
|
|
48
|
+
self.perfect * other,
|
|
49
|
+
self.great * other,
|
|
50
|
+
self.good * other,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Judgment(IntEnum):
|
|
55
|
+
MISS = 0
|
|
56
|
+
PERFECT = 1
|
|
57
|
+
GREAT = 2
|
|
58
|
+
GOOD = 3
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@native_function(Op.Judge)
|
|
62
|
+
def _judge(
|
|
63
|
+
actual: float,
|
|
64
|
+
target: float,
|
|
65
|
+
perfect_min: float,
|
|
66
|
+
perfect_max: float,
|
|
67
|
+
great_min: float,
|
|
68
|
+
great_max: float,
|
|
69
|
+
good_min: float,
|
|
70
|
+
good_max: float,
|
|
71
|
+
) -> Judgment:
|
|
72
|
+
diff = actual - target
|
|
73
|
+
if perfect_min <= diff <= perfect_max:
|
|
74
|
+
return Judgment.PERFECT
|
|
75
|
+
if great_min <= diff <= great_max:
|
|
76
|
+
return Judgment.GREAT
|
|
77
|
+
if good_min <= diff <= good_max:
|
|
78
|
+
return Judgment.GOOD
|
|
79
|
+
return Judgment.MISS
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Bucket(Record):
|
|
83
|
+
id: int
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@meta_fn
|
|
87
|
+
def window(self) -> JudgmentWindow:
|
|
88
|
+
if not ctx():
|
|
89
|
+
raise RuntimeError("Bucket window access outside of compilation")
|
|
90
|
+
match ctx().global_state.mode:
|
|
91
|
+
case Mode.Play:
|
|
92
|
+
return deref(ctx().blocks.LevelBucket, self.id * JudgmentWindow._size_(), JudgmentWindow)
|
|
93
|
+
case Mode.Watch:
|
|
94
|
+
return deref(ctx().blocks.LevelBucket, self.id * JudgmentWindow._size_(), JudgmentWindow)
|
|
95
|
+
case _:
|
|
96
|
+
raise RuntimeError("Invalid mode for bucket window access")
|
|
97
|
+
|
|
98
|
+
@window.setter
|
|
99
|
+
@meta_fn
|
|
100
|
+
def window(self, value: JudgmentWindow):
|
|
101
|
+
if not ctx():
|
|
102
|
+
raise RuntimeError("Bucket window access outside of compilation")
|
|
103
|
+
self.window.update(value.perfect, value.great, value.good)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class BucketSprite:
|
|
108
|
+
id: int
|
|
109
|
+
fallback_id: int | None
|
|
110
|
+
x: int
|
|
111
|
+
y: int
|
|
112
|
+
w: int
|
|
113
|
+
h: int
|
|
114
|
+
rotation: float
|
|
115
|
+
|
|
116
|
+
def to_dict(self):
|
|
117
|
+
results = {
|
|
118
|
+
"id": self.id,
|
|
119
|
+
"x": self.x,
|
|
120
|
+
"y": self.y,
|
|
121
|
+
"w": self.w,
|
|
122
|
+
"h": self.h,
|
|
123
|
+
"rotation": self.rotation,
|
|
124
|
+
}
|
|
125
|
+
if self.fallback_id is not None:
|
|
126
|
+
results["fallbackId"] = self.fallback_id
|
|
127
|
+
return results
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class BucketInfo:
|
|
132
|
+
sprites: list[BucketSprite]
|
|
133
|
+
unit: str | None = None
|
|
134
|
+
|
|
135
|
+
def to_dict(self):
|
|
136
|
+
results = {
|
|
137
|
+
"sprites": [sprite.to_dict() for sprite in self.sprites],
|
|
138
|
+
}
|
|
139
|
+
if self.unit is not None:
|
|
140
|
+
results["unit"] = self.unit
|
|
141
|
+
return results
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def bucket_sprite(
|
|
145
|
+
*,
|
|
146
|
+
sprite: Sprite,
|
|
147
|
+
fallback_sprite: Sprite | None = None,
|
|
148
|
+
x: int,
|
|
149
|
+
y: int,
|
|
150
|
+
w: int,
|
|
151
|
+
h: int,
|
|
152
|
+
rotation: float = 0,
|
|
153
|
+
) -> BucketSprite:
|
|
154
|
+
return BucketSprite(sprite.id, fallback_sprite.id if fallback_sprite else None, x, y, w, h, rotation)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def bucket(*, sprites: list[BucketSprite], unit: str | None = None) -> Any:
|
|
158
|
+
return BucketInfo(sprites, unit)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Buckets(Protocol):
|
|
162
|
+
_buckets_: list[BucketInfo]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass_transform()
|
|
166
|
+
def buckets[T](cls: type[T]) -> T | Buckets:
|
|
167
|
+
if len(cls.__bases__) != 1:
|
|
168
|
+
raise ValueError("Buckets class must not inherit from any class (except object)")
|
|
169
|
+
instance = cls()
|
|
170
|
+
bucket_info = []
|
|
171
|
+
for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
|
|
172
|
+
if get_origin(annotation) is not Annotated:
|
|
173
|
+
raise TypeError(f"Invalid annotation for buckets: {annotation}")
|
|
174
|
+
annotation_type = annotation.__args__[0]
|
|
175
|
+
annotation_values = annotation.__metadata__
|
|
176
|
+
if annotation_type is not Bucket:
|
|
177
|
+
raise TypeError(f"Invalid annotation for buckets: {annotation}, expected annotation of type Bucket")
|
|
178
|
+
if len(annotation_values) != 1 or not isinstance(annotation_values[0], BucketInfo):
|
|
179
|
+
raise TypeError(
|
|
180
|
+
f"Invalid annotation for buckets: {annotation}, expected a single BucketInfo annotation value"
|
|
181
|
+
)
|
|
182
|
+
info = annotation_values[0]
|
|
183
|
+
bucket_info.append(info)
|
|
184
|
+
setattr(instance, name, Bucket(i))
|
|
185
|
+
instance._buckets_ = bucket_info
|
|
186
|
+
instance._is_comptime_value_ = True
|
|
187
|
+
return instance
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@buckets
|
|
191
|
+
class EmptyBuckets:
|
|
192
|
+
pass
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class CallbackInfo:
|
|
6
|
+
name: str
|
|
7
|
+
py_name: str
|
|
8
|
+
supports_order: bool
|
|
9
|
+
returns_value: bool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
preprocess_callback = CallbackInfo(
|
|
13
|
+
name="preprocess",
|
|
14
|
+
py_name="preprocess",
|
|
15
|
+
supports_order=True,
|
|
16
|
+
returns_value=False,
|
|
17
|
+
)
|
|
18
|
+
spawn_order_callback = CallbackInfo(
|
|
19
|
+
name="spawnOrder",
|
|
20
|
+
py_name="spawn_order",
|
|
21
|
+
supports_order=True,
|
|
22
|
+
returns_value=True,
|
|
23
|
+
)
|
|
24
|
+
should_spawn_callback = CallbackInfo(
|
|
25
|
+
name="shouldSpawn",
|
|
26
|
+
py_name="should_spawn",
|
|
27
|
+
supports_order=False,
|
|
28
|
+
returns_value=True,
|
|
29
|
+
)
|
|
30
|
+
initialize_callback = CallbackInfo(
|
|
31
|
+
name="initialize",
|
|
32
|
+
py_name="initialize",
|
|
33
|
+
supports_order=False,
|
|
34
|
+
returns_value=False,
|
|
35
|
+
)
|
|
36
|
+
update_sequential_callback = CallbackInfo(
|
|
37
|
+
name="updateSequential",
|
|
38
|
+
py_name="update_sequential",
|
|
39
|
+
supports_order=True,
|
|
40
|
+
returns_value=False,
|
|
41
|
+
)
|
|
42
|
+
touch_callback = CallbackInfo(
|
|
43
|
+
name="touch",
|
|
44
|
+
py_name="touch",
|
|
45
|
+
supports_order=True,
|
|
46
|
+
returns_value=False,
|
|
47
|
+
)
|
|
48
|
+
update_parallel_callback = CallbackInfo(
|
|
49
|
+
name="updateParallel",
|
|
50
|
+
py_name="update_parallel",
|
|
51
|
+
supports_order=False,
|
|
52
|
+
returns_value=False,
|
|
53
|
+
)
|
|
54
|
+
terminate_callback = CallbackInfo(
|
|
55
|
+
name="terminate",
|
|
56
|
+
py_name="terminate",
|
|
57
|
+
supports_order=False,
|
|
58
|
+
returns_value=False,
|
|
59
|
+
)
|
|
60
|
+
spawn_time_callback = CallbackInfo(
|
|
61
|
+
name="spawnTime",
|
|
62
|
+
py_name="spawn_time",
|
|
63
|
+
supports_order=False,
|
|
64
|
+
returns_value=True,
|
|
65
|
+
)
|
|
66
|
+
despawn_time_callback = CallbackInfo(
|
|
67
|
+
name="despawnTime",
|
|
68
|
+
py_name="despawn_time",
|
|
69
|
+
supports_order=False,
|
|
70
|
+
returns_value=True,
|
|
71
|
+
)
|
|
72
|
+
update_spawn_callback = CallbackInfo(
|
|
73
|
+
name="updateSpawn",
|
|
74
|
+
py_name="update_spawn",
|
|
75
|
+
supports_order=False,
|
|
76
|
+
returns_value=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _by_name(*callbacks: CallbackInfo) -> dict[str, CallbackInfo]:
|
|
81
|
+
return {cb.py_name: cb for cb in callbacks}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
PLAY_CALLBACKS = _by_name(
|
|
85
|
+
preprocess_callback,
|
|
86
|
+
spawn_order_callback,
|
|
87
|
+
should_spawn_callback,
|
|
88
|
+
initialize_callback,
|
|
89
|
+
update_sequential_callback,
|
|
90
|
+
touch_callback,
|
|
91
|
+
update_parallel_callback,
|
|
92
|
+
terminate_callback,
|
|
93
|
+
)
|
|
94
|
+
WATCH_ARCHETYPE_CALLBACKS = _by_name(
|
|
95
|
+
preprocess_callback,
|
|
96
|
+
spawn_time_callback,
|
|
97
|
+
despawn_time_callback,
|
|
98
|
+
initialize_callback,
|
|
99
|
+
update_sequential_callback,
|
|
100
|
+
update_parallel_callback,
|
|
101
|
+
terminate_callback,
|
|
102
|
+
)
|
|
103
|
+
WATCH_GLOBAL_CALLBACKS = _by_name(
|
|
104
|
+
update_spawn_callback,
|
|
105
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Self, TypeVar, final
|
|
3
|
+
|
|
4
|
+
from sonolus.backend.place import BlockPlace
|
|
5
|
+
from sonolus.script.internal.generic import GenericValue
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@final
|
|
9
|
+
class _Comptime[T, V](GenericValue):
|
|
10
|
+
_instance: Self | None = None
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
raise TypeError("Comptime cannot be instantiated")
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def value(cls):
|
|
18
|
+
_, value = cls._type_args_
|
|
19
|
+
if isinstance(value, Identity):
|
|
20
|
+
return value.value
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _get_parameterized(cls, args: tuple[Any, ...]) -> type[Self]:
|
|
25
|
+
result = super()._get_parameterized(args)
|
|
26
|
+
result._instance = object.__new__(result)
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _size_(cls) -> int:
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _is_value_type_(cls) -> bool:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _from_place_(cls, place: BlockPlace) -> Self:
|
|
39
|
+
return cls._instance
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _accepts_(cls, value: Any) -> bool:
|
|
43
|
+
from sonolus.script.internal.impl import validate_value
|
|
44
|
+
|
|
45
|
+
value = validate_value(value)
|
|
46
|
+
if not value._is_py_():
|
|
47
|
+
return False
|
|
48
|
+
if cls._type_args_ is None:
|
|
49
|
+
return True
|
|
50
|
+
return value._as_py_() == cls.value()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _accept_(cls, value: Any) -> Self:
|
|
54
|
+
from sonolus.script.internal.impl import validate_value
|
|
55
|
+
|
|
56
|
+
if not cls._accepts_(value):
|
|
57
|
+
raise TypeError("Value does not match this Comptime instance")
|
|
58
|
+
# This might not actually return a Comptime instance, but it will be a compile-time constant
|
|
59
|
+
return validate_value(value)
|
|
60
|
+
|
|
61
|
+
def _is_py_(self) -> bool:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def _as_py_(self) -> Any:
|
|
65
|
+
return self.value()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
69
|
+
return cls._instance
|
|
70
|
+
|
|
71
|
+
def _to_list_(self) -> list[float | BlockPlace]:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _flat_keys_(cls, prefix: str) -> list[str]:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def _get_(self) -> Self:
|
|
79
|
+
from sonolus.script.internal.impl import validate_value
|
|
80
|
+
|
|
81
|
+
# Converts numbers out of comptime, although _accept_ may end up returning a non-comptime instance anyway
|
|
82
|
+
return validate_value(self.value())
|
|
83
|
+
|
|
84
|
+
def _set_(self, value: Self):
|
|
85
|
+
if value is not self:
|
|
86
|
+
raise TypeError("Comptime value cannot be changed")
|
|
87
|
+
|
|
88
|
+
def _copy_from_(self, value: Self):
|
|
89
|
+
if value is not self:
|
|
90
|
+
raise TypeError("Comptime value cannot be changed")
|
|
91
|
+
|
|
92
|
+
def _copy_(self) -> Self:
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _alloc_(cls) -> Self:
|
|
97
|
+
return cls._instance
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def _validate__type_args_(cls, args: tuple[Any, ...]) -> tuple[Any, ...]:
|
|
101
|
+
if len(args) == 2:
|
|
102
|
+
_, value = args
|
|
103
|
+
# We want the type to be there for documentation,
|
|
104
|
+
# but not enforced since they might not match up, e.g. a Callable is really FunctionType
|
|
105
|
+
if isinstance(value, TypeVar):
|
|
106
|
+
args = Any, value
|
|
107
|
+
else:
|
|
108
|
+
args = type(value), value
|
|
109
|
+
return super()._validate__type_args_(args)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def accept_unchecked(cls, value: Any) -> Self:
|
|
113
|
+
if isinstance(value, dict | tuple):
|
|
114
|
+
args = type(value), Identity(value)
|
|
115
|
+
else:
|
|
116
|
+
args = type(value), value
|
|
117
|
+
if args not in cls._parameterized_:
|
|
118
|
+
cls._parameterized_[args] = cls._get_parameterized(args)
|
|
119
|
+
return cls._parameterized_[args]._instance
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Identity[T]: # This is to allow accepting potentially unhashable values by using identity comparison
|
|
123
|
+
value: T
|
|
124
|
+
|
|
125
|
+
def __init__(self, value: T):
|
|
126
|
+
self.value = value
|
|
127
|
+
|
|
128
|
+
def __eq__(self, other):
|
|
129
|
+
return self is other
|
|
130
|
+
|
|
131
|
+
def __hash__(self):
|
|
132
|
+
return id(self)
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return f"{type(self).__name__}({self.value})"
|
|
136
|
+
|
|
137
|
+
def __repr__(self):
|
|
138
|
+
return f"{type(self).__name__}({self.value!r})"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if TYPE_CHECKING:
|
|
142
|
+
type Comptime[T, V] = T | V
|
|
143
|
+
else:
|
|
144
|
+
_Comptime.__name__ = "Comptime"
|
|
145
|
+
_Comptime.__qualname__ = "Comptime"
|
|
146
|
+
globals()["Comptime"] = _Comptime
|