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.
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
4
  from enum import Enum
5
- from types import FunctionType, MethodType, ModuleType, NoneType, NotImplementedType, UnionType
5
+ from types import EllipsisType, FunctionType, MethodType, ModuleType, NoneType, NotImplementedType, UnionType
6
6
  from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar, get_origin, overload
7
7
 
8
8
  if TYPE_CHECKING:
@@ -42,7 +42,7 @@ def validate_value(value: Any) -> Value:
42
42
 
43
43
  def try_validate_value(value: Any) -> Value | None:
44
44
  from sonolus.script.globals import _GlobalPlaceholder
45
- from sonolus.script.internal.constant import MiscConstantValue
45
+ from sonolus.script.internal.constant import BasicConstantValue
46
46
  from sonolus.script.internal.dict_impl import DictImpl
47
47
  from sonolus.script.internal.generic import PartialGeneric
48
48
  from sonolus.script.internal.tuple_impl import TupleImpl
@@ -65,8 +65,8 @@ def try_validate_value(value: Any) -> Value | None:
65
65
  return value
66
66
  case type():
67
67
  if value in {int, float, bool}:
68
- return MiscConstantValue.of(Num)
69
- return MiscConstantValue.of(value)
68
+ return BasicConstantValue.of(Num)
69
+ return BasicConstantValue.of(value)
70
70
  case int() | float() | bool():
71
71
  return Num._accept_(value)
72
72
  case tuple():
@@ -78,17 +78,18 @@ def try_validate_value(value: Any) -> Value | None:
78
78
  | TypeVar()
79
79
  | FunctionType()
80
80
  | MethodType()
81
- | NotImplementedType()
82
81
  | str()
83
- | NoneType()
84
82
  | ModuleType()
83
+ | NoneType()
84
+ | NotImplementedType()
85
+ | EllipsisType()
85
86
  ):
86
- return MiscConstantValue.of(value)
87
+ return BasicConstantValue.of(value)
87
88
  case other_type if get_origin(value) in {Literal, Annotated, UnionType, tuple}:
88
- return MiscConstantValue.of(other_type)
89
+ return BasicConstantValue.of(other_type)
89
90
  case _GlobalPlaceholder():
90
91
  return value.get()
91
92
  case comptime_value if getattr(comptime_value, "_is_comptime_value_", False):
92
- return MiscConstantValue.of(comptime_value)
93
+ return BasicConstantValue.of(comptime_value)
93
94
  case _:
94
95
  return None
@@ -2,7 +2,7 @@ from collections.abc import Iterable
2
2
  from typing import Any, Self
3
3
 
4
4
  from sonolus.backend.place import BlockPlace
5
- from sonolus.script.internal.value import Value
5
+ from sonolus.script.internal.value import DataValue, Value
6
6
 
7
7
 
8
8
  class TransientValue(Value):
@@ -23,10 +23,10 @@ class TransientValue(Value):
23
23
  raise TypeError(f"{cls.__name__} cannot be dereferenced")
24
24
 
25
25
  @classmethod
26
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
26
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
27
27
  raise TypeError(f"{cls.__name__} cannot be constructed from list")
28
28
 
29
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
29
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
30
30
  raise TypeError(f"{type(self).__name__} cannot be deconstructed to list")
31
31
 
32
32
  @classmethod
@@ -1,10 +1,36 @@
1
1
  from abc import abstractmethod
2
- from collections.abc import Iterable
2
+ from collections.abc import Callable, Iterable
3
3
  from typing import Any, Self
4
4
 
5
+ from sonolus.backend.ir import IRConst, IRExpr, IRStmt
5
6
  from sonolus.backend.place import BlockPlace
6
7
 
7
8
 
9
+ class BackingValue:
10
+ def read(self) -> IRExpr:
11
+ raise NotImplementedError()
12
+
13
+ def write(self, value: IRExpr) -> IRStmt:
14
+ raise NotImplementedError()
15
+
16
+
17
+ class ExprBackingValue(BackingValue):
18
+ """A backing value that is backed by an expression."""
19
+
20
+ def __init__(self, expr: IRExpr):
21
+ self._expr = expr
22
+
23
+ def read(self) -> IRExpr:
24
+ return self._expr
25
+
26
+ def write(self, value: IRExpr) -> IRStmt:
27
+ raise RuntimeError("Value is read-only, cannot write to it")
28
+
29
+
30
+ type DataValue = BlockPlace | BackingValue | float | int | bool
31
+ type BackingSource = Callable[[IRExpr], BackingValue]
32
+
33
+
8
34
  class Value:
9
35
  """Base class for values."""
10
36
 
@@ -59,15 +85,20 @@ class Value:
59
85
  """
60
86
  raise NotImplementedError
61
87
 
88
+ @classmethod
89
+ def _from_backing_source_(cls, source: BackingSource) -> Self:
90
+ """Creates a value from a backing source."""
91
+ return cls._from_list_(source(IRConst(i)) for i in range(cls._size_()))
92
+
62
93
  @classmethod
63
94
  @abstractmethod
64
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
65
- """Creates a value from a list of floats."""
95
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
96
+ """Creates a value from a list of data values."""
66
97
  raise NotImplementedError
67
98
 
68
99
  @abstractmethod
69
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
70
- """Converts this value to a list of floats."""
100
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
101
+ """Converts this value to a list of data values."""
71
102
  raise NotImplementedError
72
103
 
73
104
  @classmethod
@@ -76,9 +107,7 @@ class Value:
76
107
  """Returns the keys to a flat representation of this value."""
77
108
  raise NotImplementedError
78
109
 
79
- def _to_flat_dict_(
80
- self, prefix: str, level_refs: dict[Any, str] | None = None
81
- ) -> dict[str, float | str | BlockPlace]:
110
+ def _to_flat_dict_(self, prefix: str, level_refs: dict[Any, str] | None = None) -> dict[str, DataValue | str]:
82
111
  """Converts this value to a flat dictionary."""
83
112
  return dict(zip(self._flat_keys_(prefix), self._to_list_(level_refs), strict=False))
84
113
 
@@ -96,12 +125,12 @@ class Value:
96
125
  v: Num
97
126
 
98
127
  a = 1
99
- b = X(a) # (1) _get() is called on a
100
- c = b.v # (2) _get() is called on the value for v
128
+ b = X(a) # (1) _get_() is called on a
129
+ c = b.v # (2) _get_() is called on the value for v
101
130
 
102
131
  # (1) prevents this from changing the value of a
103
132
  # (2) prevents this from changing the value of c
104
- # Thus, both calls to _get() are necessary to ensure values behave immutably.
133
+ # Thus, both calls to _get_() are necessary to ensure values behave immutably.
105
134
  b.v = 2
106
135
  ```
107
136
  """
@@ -1,7 +1,7 @@
1
1
  from typing import Self
2
2
 
3
3
  from sonolus.backend.ops import Op
4
- from sonolus.script.debug import error
4
+ from sonolus.script.debug import static_error
5
5
  from sonolus.script.internal.native import native_function
6
6
  from sonolus.script.num import Num
7
7
  from sonolus.script.record import Record
@@ -24,17 +24,6 @@ class Interval(Record):
24
24
  """Get an empty interval."""
25
25
  return cls(0, 0)
26
26
 
27
- def then(self, length: float) -> Self:
28
- """Get the interval after this one with a given length.
29
-
30
- Args:
31
- length: The length of the interval.
32
-
33
- Returns:
34
- An interval that has the end of this interval as the start and has the given length.
35
- """
36
- return Interval(self.end, self.end + length)
37
-
38
27
  @property
39
28
  def length(self) -> float:
40
29
  """The length of the interval.
@@ -45,7 +34,7 @@ class Interval(Record):
45
34
 
46
35
  @property
47
36
  def is_empty(self) -> bool:
48
- """Whether the interval has length of zero or less."""
37
+ """Whether the has a start greater than its end."""
49
38
  return self.start > self.end
50
39
 
51
40
  @property
@@ -73,7 +62,7 @@ class Interval(Record):
73
62
  case Num(value):
74
63
  return self.start <= value <= self.end
75
64
  case _:
76
- error("Invalid type for interval check")
65
+ static_error("Invalid type for interval check")
77
66
 
78
67
  def __add__(self, other: float | int) -> Self:
79
68
  """Add a value to both ends of the interval.
sonolus/script/num.py CHANGED
@@ -5,13 +5,13 @@ import operator
5
5
  from collections.abc import Callable, Iterable
6
6
  from typing import TYPE_CHECKING, Any, Self, TypeGuard, final, runtime_checkable
7
7
 
8
- from sonolus.backend.ir import IRConst, IRGet, IRPureInstr, IRSet
8
+ from sonolus.backend.ir import IRConst, IRExpr, IRGet, IRPureInstr, IRSet
9
9
  from sonolus.backend.ops import Op
10
- from sonolus.backend.place import BlockPlace, Place
10
+ from sonolus.backend.place import BlockPlace
11
11
  from sonolus.script.internal.context import ctx
12
12
  from sonolus.script.internal.error import InternalError
13
13
  from sonolus.script.internal.impl import meta_fn
14
- from sonolus.script.internal.value import Value
14
+ from sonolus.script.internal.value import BackingValue, DataValue, ExprBackingValue, Value
15
15
 
16
16
 
17
17
  class _NumMeta(type):
@@ -30,15 +30,19 @@ class _Num(Value, metaclass=_NumMeta):
30
30
  # Since we don't support complex numbers, real is equal to the original number
31
31
  __match_args__ = ("real",)
32
32
 
33
- data: BlockPlace | float | int | bool
33
+ data: DataValue
34
34
 
35
- def __init__(self, data: Place | float | int | bool):
35
+ def __init__(self, data: DataValue | IRExpr):
36
36
  if isinstance(data, complex):
37
37
  raise TypeError("Cannot create a Num from a complex number")
38
38
  if isinstance(data, int):
39
39
  data = float(data)
40
+ if isinstance(data, IRConst | IRPureInstr | IRGet):
41
+ data = ExprBackingValue(data)
40
42
  if _is_num(data):
41
43
  raise InternalError("Cannot create a Num from a Num")
44
+ if not isinstance(data, BlockPlace | BackingValue | float | int | bool):
45
+ raise TypeError(f"Cannot create a Num from {type(data)}")
42
46
  self.data = data
43
47
 
44
48
  def __str__(self) -> str:
@@ -78,7 +82,7 @@ class _Num(Value, metaclass=_NumMeta):
78
82
  return cls(value)
79
83
 
80
84
  def _is_py_(self) -> bool:
81
- return not isinstance(self.data, BlockPlace)
85
+ return isinstance(self.data, float | int | bool)
82
86
 
83
87
  def _as_py_(self) -> Any:
84
88
  if not self._is_py_():
@@ -88,11 +92,11 @@ class _Num(Value, metaclass=_NumMeta):
88
92
  return self.data
89
93
 
90
94
  @classmethod
91
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
95
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
92
96
  value = next(iter(values))
93
97
  return Num(value)
94
98
 
95
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
99
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue]:
96
100
  return [self.data]
97
101
 
98
102
  @classmethod
@@ -111,10 +115,14 @@ class _Num(Value, metaclass=_NumMeta):
111
115
 
112
116
  def _set_(self, value: Self):
113
117
  if ctx():
114
- if not isinstance(self.data, BlockPlace):
115
- raise ValueError("Cannot set a compile time constant value")
116
- ctx().check_writable(self.data)
117
- ctx().add_statements(IRSet(self.data, value.ir()))
118
+ match self.data:
119
+ case BackingValue():
120
+ ctx().add_statements(self.data.write(value))
121
+ case BlockPlace():
122
+ ctx().check_writable(self.data)
123
+ ctx().add_statements(IRSet(self.data, value.ir()))
124
+ case _:
125
+ raise ValueError("Cannot set a read-only value")
118
126
  else:
119
127
  self.data = value.data
120
128
 
@@ -144,12 +152,17 @@ class _Num(Value, metaclass=_NumMeta):
144
152
  return cls(0)
145
153
 
146
154
  def ir(self):
147
- if isinstance(self.data, BlockPlace):
148
- return IRGet(self.data)
149
- else:
150
- return IRConst(self.data)
155
+ match self.data:
156
+ case BlockPlace():
157
+ return IRGet(self.data)
158
+ case BackingValue():
159
+ return self.data.read()
160
+ case _:
161
+ return IRConst(self.data)
151
162
 
152
163
  def index(self) -> int | BlockPlace:
164
+ if isinstance(self.data, BlockPlace):
165
+ return self._get_().data
153
166
  return self.data
154
167
 
155
168
  def _bin_op(self, other: Self, const_fn: Callable[[Self, Self], Self | None], ir_op: Op) -> Self:
sonolus/script/options.py CHANGED
@@ -15,6 +15,7 @@ from sonolus.script.num import Num
15
15
  @dataclass
16
16
  class _SliderOption:
17
17
  name: str | None
18
+ description: str | None
18
19
  standard: bool
19
20
  advanced: bool
20
21
  scope: str | None
@@ -35,6 +36,8 @@ class _SliderOption:
35
36
  "max": self.max,
36
37
  "step": self.step,
37
38
  }
39
+ if self.description is not None:
40
+ result["description"] = self.description
38
41
  if self.scope is not None:
39
42
  result["scope"] = self.scope
40
43
  if self.unit is not None:
@@ -45,6 +48,7 @@ class _SliderOption:
45
48
  @dataclass
46
49
  class _ToggleOption:
47
50
  name: str | None
51
+ description: str | None
48
52
  standard: bool
49
53
  advanced: bool
50
54
  scope: str | None
@@ -58,6 +62,8 @@ class _ToggleOption:
58
62
  "advanced": self.advanced,
59
63
  "def": int(self.default),
60
64
  }
65
+ if self.description is not None:
66
+ result["description"] = self.description
61
67
  if self.scope is not None:
62
68
  result["scope"] = self.scope
63
69
  return result
@@ -66,6 +72,7 @@ class _ToggleOption:
66
72
  @dataclass
67
73
  class _SelectOption:
68
74
  name: str | None
75
+ description: str | None
69
76
  standard: bool
70
77
  advanced: bool
71
78
  scope: str | None
@@ -81,6 +88,8 @@ class _SelectOption:
81
88
  "def": self.default,
82
89
  "values": self.values,
83
90
  }
91
+ if self.description is not None:
92
+ result["description"] = self.description
84
93
  if self.scope is not None:
85
94
  result["scope"] = self.scope
86
95
  return result
@@ -89,6 +98,7 @@ class _SelectOption:
89
98
  def slider_option(
90
99
  *,
91
100
  name: str | None = None,
101
+ description: str | None = None,
92
102
  standard: bool = False,
93
103
  advanced: bool = False,
94
104
  default: float,
@@ -102,6 +112,7 @@ def slider_option(
102
112
 
103
113
  Args:
104
114
  name: The name of the option.
115
+ description: The description of the option.
105
116
  standard: Whether the option is standard.
106
117
  advanced: Whether the option is advanced.
107
118
  default: The default value of the option.
@@ -111,12 +122,13 @@ def slider_option(
111
122
  unit: The unit of the option.
112
123
  scope: The scope of the option.
113
124
  """
114
- return _SliderOption(name, standard, advanced, scope, default, min, max, step, unit)
125
+ return _SliderOption(name, description, standard, advanced, scope, default, min, max, step, unit)
115
126
 
116
127
 
117
128
  def toggle_option(
118
129
  *,
119
130
  name: str | None = None,
131
+ description: str | None = None,
120
132
  standard: bool = False,
121
133
  advanced: bool = False,
122
134
  default: bool,
@@ -126,17 +138,19 @@ def toggle_option(
126
138
 
127
139
  Args:
128
140
  name: The name of the option.
141
+ description: The description of the option.
129
142
  standard: Whether the option is standard.
130
143
  advanced: Whether the option is advanced.
131
144
  default: The default value of the option.
132
145
  scope: The scope of the option.
133
146
  """
134
- return _ToggleOption(name, standard, advanced, scope, default)
147
+ return _ToggleOption(name, description, standard, advanced, scope, default)
135
148
 
136
149
 
137
150
  def select_option(
138
151
  *,
139
152
  name: str | None = None,
153
+ description: str | None = None,
140
154
  standard: bool = False,
141
155
  advanced: bool = False,
142
156
  default: str | int,
@@ -147,6 +161,7 @@ def select_option(
147
161
 
148
162
  Args:
149
163
  name: The name of the option.
164
+ description: The description of the option.
150
165
  standard: Whether the option is standard.
151
166
  advanced: Whether the option is advanced.
152
167
  default: The default value of the option.
@@ -155,7 +170,7 @@ def select_option(
155
170
  """
156
171
  if isinstance(default, str):
157
172
  default = values.index(default)
158
- return _SelectOption(name, standard, advanced, scope, default, values)
173
+ return _SelectOption(name, description, standard, advanced, scope, default, values)
159
174
 
160
175
 
161
176
  type Options = NewType("Options", Any)
@@ -91,6 +91,7 @@ class _ParticleInfo:
91
91
 
92
92
 
93
93
  def particle(name: str) -> Any:
94
+ """Define a particle with the given name."""
94
95
  return _ParticleInfo(name)
95
96
 
96
97
 
sonolus/script/pointer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from sonolus.backend.place import BlockPlace
2
2
  from sonolus.script.internal.context import ctx
3
3
  from sonolus.script.internal.impl import meta_fn, validate_value
4
- from sonolus.script.internal.value import Value
4
+ from sonolus.script.internal.value import BackingSource, Value
5
5
  from sonolus.script.num import Num, _is_num
6
6
 
7
7
 
@@ -30,3 +30,11 @@ def _deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
30
30
  if not (isinstance(type_, type) and issubclass(type_, Value)):
31
31
  raise TypeError("type_ must be a Value")
32
32
  return type_._from_place_(BlockPlace(block, offset))
33
+
34
+
35
+ @meta_fn
36
+ def _backing_deref[T: Value](source: BackingSource, type_: type[T]) -> T:
37
+ type_ = validate_value(type_)._as_py_()
38
+ if not isinstance(type_, type) or not issubclass(type_, Value):
39
+ raise TypeError("type_ must be a Value")
40
+ return type_._from_backing_source_(source)
sonolus/script/quad.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import Protocol, Self
4
4
 
5
5
  from sonolus.script.record import Record
6
+ from sonolus.script.values import zeros
6
7
  from sonolus.script.vec import Vec2, pnpoly
7
8
 
8
9
 
@@ -10,7 +11,7 @@ class Quad(Record):
10
11
  """A quad defined by its four corners.
11
12
 
12
13
  Usage:
13
- ```
14
+ ```python
14
15
  Quad(bl: Vec2, tl: Vec2, tr: Vec2, br: Vec2)
15
16
  ```
16
17
  """
@@ -27,6 +28,16 @@ class Quad(Record):
27
28
  br: Vec2
28
29
  """The bottom-right corner of the quad."""
29
30
 
31
+ @classmethod
32
+ def from_quad(cls, value: QuadLike, /) -> Quad:
33
+ """Create a quad from a quad-like value."""
34
+ return cls(
35
+ bl=value.bl,
36
+ tl=value.tl,
37
+ tr=value.tr,
38
+ br=value.br,
39
+ )
40
+
30
41
  @property
31
42
  def center(self) -> Vec2:
32
43
  """The center of the quad."""
@@ -95,6 +106,44 @@ class Quad(Record):
95
106
  """Rotate the quad by the given angle about its center and return a new quad."""
96
107
  return self.rotate_about(angle, self.center)
97
108
 
109
+ def permute(self, count: int = 1, /) -> Self:
110
+ """Perform a cyclic permutation of the quad's vertices and return a new quad.
111
+
112
+ On a square, this operation is equivalent to rotating the square counterclockwise 90 degrees `count` times.
113
+
114
+ Negative values of `count` are allowed and will rotate the quad clockwise.
115
+
116
+ Args:
117
+ count: The number of vertices to shift. Defaults to 1.
118
+
119
+ Returns:
120
+ The permuted quad.
121
+ """
122
+ count = int(count % 4)
123
+ result = zeros(Quad)
124
+ match count:
125
+ case 0:
126
+ result.bl @= self.bl
127
+ result.tl @= self.tl
128
+ result.tr @= self.tr
129
+ result.br @= self.br
130
+ case 1:
131
+ result.bl @= self.br
132
+ result.tl @= self.bl
133
+ result.tr @= self.tl
134
+ result.br @= self.tr
135
+ case 2:
136
+ result.bl @= self.tr
137
+ result.tl @= self.br
138
+ result.tr @= self.bl
139
+ result.br @= self.tl
140
+ case 3:
141
+ result.bl @= self.tl
142
+ result.tl @= self.tr
143
+ result.tr @= self.br
144
+ result.br @= self.bl
145
+ return result
146
+
98
147
  def contains_point(self, point: Vec2, /) -> bool:
99
148
  """Check if the quad contains the given point.
100
149
 
@@ -111,7 +160,7 @@ class Rect(Record):
111
160
  """A rectangle defined by its top, right, bottom, and left edges.
112
161
 
113
162
  Usage:
114
- ```
163
+ ```python
115
164
  Rect(t: float, r: float, b: float, l: float)
116
165
  ```
117
166
  """
@@ -174,7 +223,7 @@ class Rect(Record):
174
223
  return Vec2((self.l + self.r) / 2, (self.t + self.b) / 2)
175
224
 
176
225
  def as_quad(self) -> Quad:
177
- """Convert the rectangle to a quad."""
226
+ """Convert the rectangle to a [`Quad`][sonolus.script.quad.Quad]."""
178
227
  return Quad(
179
228
  bl=self.bl,
180
229
  tl=self.tl,
@@ -248,7 +297,7 @@ class Rect(Record):
248
297
  return self.l <= point.x <= self.r and self.b <= point.y <= self.t
249
298
 
250
299
 
251
- class QuadLike(Protocol):
300
+ class _QuadLike(Protocol):
252
301
  """A protocol for types that can be used as quads."""
253
302
 
254
303
  @property
@@ -268,6 +317,11 @@ class QuadLike(Protocol):
268
317
  """The bottom-right corner of the quad."""
269
318
 
270
319
 
320
+ # PyCharm doesn't recognize attributes as satisfying the protocol.
321
+ type QuadLike = _QuadLike | Quad
322
+ """A type that can be used as a quad."""
323
+
324
+
271
325
  def flatten_quad(quad: QuadLike) -> tuple[float, float, float, float, float, float, float, float]:
272
326
  bl = quad.bl
273
327
  tl = quad.tl
sonolus/script/record.py CHANGED
@@ -16,7 +16,7 @@ from sonolus.script.internal.generic import (
16
16
  validate_type_spec,
17
17
  )
18
18
  from sonolus.script.internal.impl import meta_fn
19
- from sonolus.script.internal.value import Value
19
+ from sonolus.script.internal.value import BackingSource, DataValue, Value
20
20
  from sonolus.script.num import Num
21
21
 
22
22
 
@@ -157,6 +157,17 @@ class Record(GenericValue):
157
157
  def _is_value_type_(cls) -> bool:
158
158
  return False
159
159
 
160
+ @classmethod
161
+ def _from_backing_source_(cls, source: BackingSource) -> Self:
162
+ result = object.__new__(cls)
163
+ result._value = {
164
+ field.name: field.type._from_backing_source_(
165
+ lambda offset, field_offset=field.offset: source((Num(offset) + Num(field_offset)).ir())
166
+ )
167
+ for field in cls._fields
168
+ }
169
+ return result
170
+
160
171
  @classmethod
161
172
  def _from_place_(cls, place: BlockPlace) -> Self:
162
173
  result = object.__new__(cls)
@@ -182,11 +193,11 @@ class Record(GenericValue):
182
193
  return self
183
194
 
184
195
  @classmethod
185
- def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
196
+ def _from_list_(cls, values: Iterable[DataValue]) -> Self:
186
197
  iterator = iter(values)
187
198
  return cls(**{field.name: field.type._from_list_(iterator) for field in cls._fields})
188
199
 
189
- def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
200
+ def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
190
201
  result = []
191
202
  for field in self._fields:
192
203
  result.extend(self._value[field.name]._to_list_(level_refs))
@@ -203,7 +214,7 @@ class Record(GenericValue):
203
214
  return self
204
215
 
205
216
  def _set_(self, value: Self):
206
- raise TypeError("Record does not support set_")
217
+ raise TypeError("Record does not support _set_")
207
218
 
208
219
  def _copy_from_(self, value: Self):
209
220
  value = self._accept_(value)
sonolus/script/runtime.py CHANGED
@@ -2,7 +2,8 @@ from enum import IntEnum
2
2
 
3
3
  from sonolus.backend.mode import Mode
4
4
  from sonolus.script.array import Array
5
- from sonolus.script.containers import VarArray
5
+ from sonolus.script.array_like import ArrayLike
6
+ from sonolus.script.containers import ArrayPointer
6
7
  from sonolus.script.globals import (
7
8
  _level_life,
8
9
  _level_score,
@@ -315,7 +316,8 @@ class Touch(Record):
315
316
 
316
317
  @_runtime_touch_array
317
318
  class _TouchArray:
318
- touches: Array[Touch, 999]
319
+ # Handled specially, see touches()
320
+ pass
319
321
 
320
322
 
321
323
  @_runtime_skin_transform
@@ -500,6 +502,15 @@ def is_tutorial() -> bool:
500
502
  return ctx() and ctx().global_state.mode == Mode.TUTORIAL
501
503
 
502
504
 
505
+ @meta_fn
506
+ def is_preprocessing() -> bool:
507
+ """Check if the game is in the preprocessing stage.
508
+
509
+ Returns True if the current callback is one of preprocess, spawn_order, spawn_time, or despawn_time.
510
+ """
511
+ return ctx() and ctx().callback in {"preprocess", "spawnOrder", "spawnTime", "despawnTime"}
512
+
513
+
503
514
  @meta_fn
504
515
  def aspect_ratio() -> float:
505
516
  """Get the aspect ratio of the game."""
@@ -640,15 +651,15 @@ def scaled_time() -> float:
640
651
 
641
652
 
642
653
  @meta_fn
643
- def touches() -> VarArray[Touch, 999]:
654
+ def touches() -> ArrayLike[Touch]:
644
655
  """Get the current touches of the game."""
645
656
  if not ctx():
646
- return VarArray(0, Array[Touch, 0]())
657
+ return Array[Touch, 0]()
647
658
  match ctx().global_state.mode:
648
659
  case Mode.PLAY:
649
- return VarArray(_PlayRuntimeUpdate.touch_count, _TouchArray.touches)
660
+ return ArrayPointer[Touch](_PlayRuntimeUpdate.touch_count, ctx().blocks.RuntimeTouchArray, 0)
650
661
  case _:
651
- return VarArray(0, Array[Touch, 0]())
662
+ return Array[Touch, 0]()
652
663
 
653
664
 
654
665
  @meta_fn