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.
@@ -0,0 +1,529 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import cast, dataclass_transform
4
+
5
+ from sonolus.backend.ir import IRExpr, IRInstr, IRPureInstr
6
+ from sonolus.backend.mode import Mode
7
+ from sonolus.backend.ops import Op
8
+ from sonolus.script.internal.context import ctx
9
+ from sonolus.script.internal.descriptor import SonolusDescriptor
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.internal.value import BackingValue, Value
14
+ from sonolus.script.iterator import SonolusIterator
15
+ from sonolus.script.num import Num
16
+ from sonolus.script.record import Record
17
+ from sonolus.script.values import sizeof
18
+
19
+
20
+ class _StreamField(SonolusDescriptor):
21
+ offset: int
22
+ type_: type[Stream] | type[StreamGroup]
23
+
24
+ def __init__(self, offset: int, type_: type[Stream] | type[StreamGroup]):
25
+ self.offset = offset
26
+ self.type_ = type_
27
+
28
+ def __get__(self, instance, owner):
29
+ _check_can_read_or_write_stream()
30
+ return self.type_(self.offset)
31
+
32
+ def __set__(self, instance, value):
33
+ raise AttributeError("Cannot set attribute")
34
+
35
+
36
+ class _StreamDataField(SonolusDescriptor):
37
+ offset: int
38
+ type_: type[Value]
39
+
40
+ def __init__(self, offset: int, type_: type[Value]):
41
+ self.offset = offset
42
+ self.type_ = type_
43
+
44
+ def _get(self):
45
+ return self.type_._from_backing_source_(lambda offset: _StreamBacking(self.offset, Num(offset)))
46
+
47
+ def __get__(self, instance, owner):
48
+ _check_can_read_or_write_stream()
49
+ return self._get()._get_()
50
+
51
+ def __set__(self, instance, value):
52
+ _check_can_write_stream()
53
+ if self.type_._is_value_type_():
54
+ self._get()._set_(value)
55
+ else:
56
+ self._get()._copy_from_(value)
57
+
58
+
59
+ @dataclass_transform()
60
+ def streams[T](cls: type[T]) -> T:
61
+ """Decorator to define streams and stream groups.
62
+
63
+ Streams and stream groups are declared by annotating class attributes with `Stream` or `StreamGroup`.
64
+
65
+ Other types are also supported in the form of data fields. They may be used to store additional data to export from
66
+ Play to Watch mode.
67
+
68
+ In either case, data is write-only in Play mode and read-only in Watch mode.
69
+
70
+ This should only be used once in most projects, as multiple decorated classes will overlap with each other and
71
+ interfere when both are used at the same time.
72
+
73
+ For backwards compatibility, new streams and stream groups should be added to the end of existing ones, and
74
+ lengths and element types of existing streams and stream groups should not be changed. Otherwise, old replays may
75
+ not work on new versions of the engine.
76
+
77
+ Usage:
78
+ ```python
79
+ @streams
80
+ class Streams:
81
+ stream_1: Stream[Num] # A stream of Num values
82
+ stream_2: Stream[Vec2] # A stream of Vec2 values
83
+ group_1: StreamGroup[Num, 10] # A group of 10 Num streams
84
+ group_2: StreamGroup[Vec2, 5] # A group of 5 Vec2 streams
85
+
86
+ data_field_1: Num # A data field of type Num
87
+ data_field_2: Vec2 # A data field of type Vec2
88
+ ```
89
+ """
90
+ if len(cls.__bases__) != 1:
91
+ raise ValueError("Options class must not inherit from any class (except object)")
92
+ instance = cls()
93
+ entries = []
94
+ # Offset 0 is unused so we can tell when a stream object is uninitialized since it'll have offset 0.
95
+ offset = 1
96
+ for name, annotation in get_field_specifiers(cls).items():
97
+ if issubclass(annotation, Stream | StreamGroup):
98
+ annotation = cast(type[Stream | StreamGroup], annotation)
99
+ if annotation is Stream or annotation is StreamGroup:
100
+ raise TypeError(f"Invalid annotation for streams: {annotation}. Must have type arguments.")
101
+ setattr(cls, name, _StreamField(offset, annotation))
102
+ # Streams store their data across several backing streams
103
+ entries.append((name, offset, annotation))
104
+ offset += annotation.backing_size()
105
+ elif issubclass(annotation, Value) and annotation._is_concrete_():
106
+ setattr(cls, name, _StreamDataField(offset, annotation))
107
+ # Data fields store their data in a single backing stream at different offsets in the same stream
108
+ entries.append((name, offset, annotation))
109
+ offset += 1
110
+ instance._streams_ = entries
111
+ instance._is_comptime_value_ = True
112
+ return instance
113
+
114
+
115
+ @meta_fn
116
+ def _check_can_read_stream() -> None:
117
+ if not ctx() or ctx().global_state.mode != Mode.WATCH:
118
+ raise RuntimeError("Stream read operations are only allowed in watch mode.")
119
+
120
+
121
+ @meta_fn
122
+ def _check_can_write_stream() -> None:
123
+ if not ctx() or ctx().global_state.mode != Mode.PLAY:
124
+ raise RuntimeError("Stream write operations are only allowed in play mode.")
125
+
126
+
127
+ @meta_fn
128
+ def _check_can_read_or_write_stream() -> None:
129
+ if not ctx() or ctx().global_state.mode not in {Mode.PLAY, Mode.WATCH}:
130
+ raise RuntimeError("Stream operations are only allowed in play and watch modes.")
131
+
132
+
133
+ class _StreamBacking(BackingValue):
134
+ id: Num
135
+ index: Num
136
+
137
+ def __init__(self, stream_id: int, index: Num):
138
+ super().__init__()
139
+ self.id = Num._accept_(stream_id)
140
+ self.index = Num._accept_(index)
141
+
142
+ def read(self) -> IRExpr:
143
+ """Read the value from the stream."""
144
+ _check_can_read_stream()
145
+ return IRPureInstr(Op.StreamGetValue, [self.id.ir(), self.index.ir()])
146
+
147
+ def write(self, value: IRExpr) -> None:
148
+ """Write the value to the stream."""
149
+ _check_can_write_stream()
150
+ ctx().add_statement(IRInstr(Op.StreamSet, [self.id.ir(), self.index.ir(), value]))
151
+
152
+
153
+ class Stream[T](Record, BackingValue):
154
+ """Represents a stream.
155
+
156
+ Most users should use `@stream` to declare streams and stream groups rather than using this class directly.
157
+
158
+ If used directly, it is important that streams do not overlap. No other streams should have an offset in
159
+ `range(self.offset, self.offset + max(1, sizeof(self.element_type())))`, or they will overlap and interfere
160
+ with each other.
161
+
162
+ Usage:
163
+ Declaring a stream:
164
+ ```python
165
+ @streams
166
+ class Streams:
167
+ my_stream_1: Stream[Num] # A stream of Num values
168
+ my_stream_2: Stream[Vec2] # A stream of Vec2 values
169
+ ```
170
+
171
+ Directly creating a stream (advanced usage):
172
+ ```python
173
+ stream = Stream[Num](offset=0)
174
+ ```
175
+ """
176
+
177
+ offset: int
178
+
179
+ @classmethod
180
+ def element_type(cls) -> type[T] | type[Value]:
181
+ """Return the type of elements in this array type."""
182
+ return cls.type_var_value(T)
183
+
184
+ @classmethod
185
+ @meta_fn
186
+ def backing_size(cls) -> int:
187
+ """Return the number of underlying single-value streams backing this stream."""
188
+ return max(1, sizeof(cls.element_type()))
189
+
190
+ def __contains__(self, item: int | float) -> bool:
191
+ """Check if the stream contains the key."""
192
+ _check_can_read_stream()
193
+ return _stream_has(self.offset, item)
194
+
195
+ @meta_fn
196
+ def __getitem__(self, key: int | float) -> T:
197
+ """Get the value corresponding to the key.
198
+
199
+ If the key is not in the stream, interpolates linearly between surrounding values.
200
+ If the stream is empty, returns the zero value of the element type.
201
+ """
202
+ # This is allowed in Play mode since a stream value may be accessed just to write to it without reading.
203
+ _check_can_read_or_write_stream()
204
+ return self.element_type()._from_backing_source_(lambda offset: _StreamBacking(self.offset + Num(offset), key))
205
+
206
+ @meta_fn
207
+ def __setitem__(self, key: int | float, value: T) -> None:
208
+ """Set the value corresponding to the key."""
209
+ _check_can_write_stream()
210
+ if not self.element_type()._accepts_(value):
211
+ raise TypeError(f"Cannot set value of type {type(value)} to stream of type {self.element_type()}.")
212
+ if self.element_type()._size_() == 0:
213
+ # We still need to store something to preserve the key, so this is a special case.
214
+ _stream_set(self.offset, key, 0)
215
+ else:
216
+ for i, v in enumerate(value._to_list_()):
217
+ _stream_set(self.offset + i, key, Num(v))
218
+
219
+ def next_key(self, key: int | float) -> int:
220
+ """Get the next key, or the key unchanged if it is the last key or the stream is empty.
221
+
222
+ If the key is in the stream and there is a next key, returns the next key.
223
+ """
224
+ _check_can_read_stream()
225
+ return _stream_get_next_key(self.offset, key)
226
+
227
+ def previous_key(self, key: int | float) -> int:
228
+ """Get the previous key, or the key unchanged if it is the first key or the stream is empty.
229
+
230
+ If the key is in the stream and there is a previous key, returns the previous key.
231
+ """
232
+ _check_can_read_stream()
233
+ return _stream_get_previous_key(self.offset, key)
234
+
235
+ def has_next_key(self, key: int | float) -> bool:
236
+ """Check if there is a next key after the given key in the stream."""
237
+ _check_can_read_stream()
238
+ next_key = self.next_key(key)
239
+ return next_key > key
240
+
241
+ def has_previous_key(self, key: int | float) -> bool:
242
+ """Check if there is a previous key before the given key in the stream."""
243
+ _check_can_read_stream()
244
+ previous_key = self.previous_key(key)
245
+ return previous_key < key
246
+
247
+ def next_key_inclusive(self, key: int | float) -> int:
248
+ """Like `next_key`, but returns the key itself if it is in the stream."""
249
+ _check_can_read_stream()
250
+ return key if key in self else self.next_key(key)
251
+
252
+ def previous_key_inclusive(self, key: int | float) -> int:
253
+ """Like `previous_key`, but returns the key itself if it is in the stream."""
254
+ _check_can_read_stream()
255
+ return key if key in self else self.previous_key(key)
256
+
257
+ def get_next(self, key: int | float) -> T:
258
+ """Get the value corresponding to the next key.
259
+
260
+ If there is no next key, returns the value at the given key. Equivalent to `self[self.next_key(key)]`.
261
+ """
262
+ _check_can_read_stream()
263
+ return self[self.next_key(key)]
264
+
265
+ def get_previous(self, key: int | float) -> T:
266
+ """Get the value corresponding to the previous key.
267
+
268
+ If there is no previous key, returns the value at the given key. Equivalent to `self[self.previous_key(key)]`.
269
+ """
270
+ _check_can_read_stream()
271
+ return self[self.previous_key(key)]
272
+
273
+ def get_next_inclusive(self, key: int | float) -> T:
274
+ """Get the value corresponding to the next key, or the value at the given key if it is in the stream.
275
+
276
+ Equivalent to `self[self.next_key_inclusive(key)]`.
277
+ """
278
+ _check_can_read_stream()
279
+ return self[self.next_key_inclusive(key)]
280
+
281
+ def iter_items_from(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
282
+ """Iterate over the items in the stream in ascending order starting from the given key.
283
+
284
+ If the key is in the stream, it will be included in the iteration.
285
+
286
+ Usage:
287
+ ```python
288
+ stream = ...
289
+ for key, value in stream.iter_items_from(0):
290
+ do_something(key, value)
291
+ ```
292
+ """
293
+ _check_can_read_stream()
294
+ return _StreamAscIterator(self, self.next_key_inclusive(start))
295
+
296
+ def iter_items_from_desc(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
297
+ """Iterate over the items in the stream in descending order starting from the given key.
298
+
299
+ If the key is in the stream, it will be included in the iteration.
300
+
301
+ Usage:
302
+ ```python
303
+ stream = ...
304
+ for key, value in stream.iter_items_from_desc(0):
305
+ do_something(key, value)
306
+ ```
307
+ """
308
+ _check_can_read_stream()
309
+ return _StreamDescIterator(self, self.previous_key_inclusive(start))
310
+
311
+ def iter_keys_from(self, start: int | float, /) -> SonolusIterator[int | float]:
312
+ """Iterate over the keys in the stream in ascending order starting from the given key.
313
+
314
+ If the key is in the stream, it will be included in the iteration.
315
+
316
+ Usage:
317
+ ```python
318
+ stream = ...
319
+ for key in stream.iter_keys_from(0):
320
+ do_something(key)
321
+ ```
322
+ """
323
+ _check_can_read_stream()
324
+ return _StreamAscKeyIterator(self, self.next_key_inclusive(start))
325
+
326
+ def iter_keys_from_desc(self, start: int | float, /) -> SonolusIterator[int | float]:
327
+ """Iterate over the keys in the stream in descending order starting from the given key.
328
+
329
+ If the key is in the stream, it will be included in the iteration.
330
+
331
+ Usage:
332
+ ```python
333
+ stream = ...
334
+ for key in stream.iter_keys_from_desc(0):
335
+ do_something(key)
336
+ ```
337
+ """
338
+ _check_can_read_stream()
339
+ return _StreamDescKeyIterator(self, self.previous_key_inclusive(start))
340
+
341
+ def iter_values_from(self, start: int | float, /) -> SonolusIterator[T]:
342
+ """Iterate over the values in the stream in ascending order starting from the given key.
343
+
344
+ If the key is in the stream, it will be included in the iteration.
345
+
346
+ Usage:
347
+ ```python
348
+ stream = ...
349
+ for value in stream.iter_values_from(0):
350
+ do_something(value)
351
+ ```
352
+ """
353
+ _check_can_read_stream()
354
+ return _StreamAscValueIterator(self, self.next_key_inclusive(start))
355
+
356
+ def iter_values_from_desc(self, start: int | float, /) -> SonolusIterator[T]:
357
+ """Iterate over the values in the stream in descending order starting from the given key.
358
+
359
+ If the key is in the stream, it will be included in the iteration.
360
+
361
+ Usage:
362
+ ```python
363
+ stream = ...
364
+ for value in stream.iter_values_from_desc(0):
365
+ do_something(value)
366
+ ```
367
+ """
368
+ _check_can_read_stream()
369
+ return _StreamDescValueIterator(self, self.previous_key_inclusive(start))
370
+
371
+
372
+ class StreamGroup[T, Size](Record):
373
+ """Represents a group of streams.
374
+
375
+ Most users should use `@stream` to declare stream groups rather than using this class directly.
376
+
377
+ Usage:
378
+ Declaring a stream group:
379
+ ```python
380
+ @streams
381
+ class Streams:
382
+ my_group_1: StreamGroup[Num, 10] # A group of 10 Num streams
383
+ my_group_2: StreamGroup[Vec2, 5] # A group of 5 Vec2 streams
384
+ ```
385
+ """
386
+
387
+ offset: int
388
+
389
+ @classmethod
390
+ def size(cls) -> Size:
391
+ """Return the size of the group."""
392
+ return cls.type_var_value(Size)
393
+
394
+ @classmethod
395
+ def element_type(cls) -> type[T] | type[Value]:
396
+ """Return the type of elements in this group."""
397
+ return cls.type_var_value(T)
398
+
399
+ @classmethod
400
+ @meta_fn
401
+ def backing_size(cls) -> int:
402
+ """Return the number of underlying single-value streams backing this stream."""
403
+ return max(1, sizeof(cls.element_type())) * cls.size()
404
+
405
+ def __contains__(self, item: int) -> bool:
406
+ """Check if the group contains the stream with the given index."""
407
+ _check_can_read_or_write_stream()
408
+ return 0 <= item < self.size()
409
+
410
+ def __getitem__(self, index: int) -> Stream[T]:
411
+ """Get the stream at the given index."""
412
+ _check_can_read_or_write_stream()
413
+ assert index in self
414
+ # Size 0 elements still need 1 stream to preserve the key.
415
+ return Stream[self.type_var_value(T)](max(1, sizeof(self.element_type())) * index + self.offset)
416
+
417
+
418
+ class _StreamAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
419
+ stream: Stream[T]
420
+ current_key: int | float
421
+
422
+ def has_next(self) -> bool:
423
+ return self.stream.next_key(self.current_key) > self.current_key
424
+
425
+ def get(self) -> tuple[int | float, T]:
426
+ return self.current_key, self.stream[self.current_key]
427
+
428
+ def advance(self):
429
+ self.current_key = self.stream.next_key(self.current_key)
430
+
431
+
432
+ class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
433
+ stream: Stream[T]
434
+ current_key: int | float
435
+
436
+ def has_next(self) -> bool:
437
+ return self.stream.previous_key(self.current_key) < self.current_key
438
+
439
+ def get(self) -> tuple[int | float, T]:
440
+ return self.current_key, self.stream[self.current_key]
441
+
442
+ def advance(self):
443
+ self.current_key = self.stream.previous_key(self.current_key)
444
+
445
+
446
+ class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
447
+ stream: Stream[T]
448
+ current_key: int | float
449
+
450
+ def has_next(self) -> bool:
451
+ return self.stream.next_key(self.current_key) > self.current_key
452
+
453
+ def get(self) -> int | float:
454
+ return self.current_key
455
+
456
+ def advance(self):
457
+ self.current_key = self.stream.next_key(self.current_key)
458
+
459
+
460
+ class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
461
+ stream: Stream[T]
462
+ current_key: int | float
463
+
464
+ def has_next(self) -> bool:
465
+ return self.stream.previous_key(self.current_key) < self.current_key
466
+
467
+ def get(self) -> int | float:
468
+ return self.current_key
469
+
470
+ def advance(self):
471
+ self.current_key = self.stream.previous_key(self.current_key)
472
+
473
+
474
+ class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
475
+ stream: Stream[T]
476
+ current_key: int | float
477
+
478
+ def has_next(self) -> bool:
479
+ return self.stream.next_key(self.current_key) > self.current_key
480
+
481
+ def get(self) -> T:
482
+ return self.stream[self.current_key]
483
+
484
+ def advance(self):
485
+ self.current_key = self.stream.next_key(self.current_key)
486
+
487
+
488
+ class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
489
+ stream: Stream[T]
490
+ current_key: int | float
491
+
492
+ def has_next(self) -> bool:
493
+ return self.stream.previous_key(self.current_key) < self.current_key
494
+
495
+ def get(self) -> T:
496
+ return self.stream[self.current_key]
497
+
498
+ def advance(self):
499
+ self.current_key = self.stream.previous_key(self.current_key)
500
+
501
+
502
+ @native_function(Op.StreamGetNextKey)
503
+ def _stream_get_next_key(stream_id: int, key: int | float) -> int:
504
+ """Get the next key in the stream, or the key unchanged if it is the last key or the stream is empty."""
505
+ raise NotImplementedError
506
+
507
+
508
+ @native_function(Op.StreamGetPreviousKey)
509
+ def _stream_get_previous_key(stream_id: int, key: int | float) -> int:
510
+ """Get the previous key in the stream, or the key unchanged if it is the first key or the stream is empty."""
511
+ raise NotImplementedError
512
+
513
+
514
+ @native_function(Op.StreamGetValue)
515
+ def _stream_get_value(stream_id: int, key: int | float) -> float:
516
+ """Get the value of the key in the stream."""
517
+ raise NotImplementedError
518
+
519
+
520
+ @native_function(Op.StreamHas)
521
+ def _stream_has(stream_id: int, key: int | float) -> bool:
522
+ """Check if the stream has the key."""
523
+ raise NotImplementedError
524
+
525
+
526
+ @native_function(Op.StreamSet)
527
+ def _stream_set(stream_id: int, key: int | float, value: float) -> None:
528
+ """Set the value of the key in the stream."""
529
+ raise NotImplementedError
sonolus/script/text.py CHANGED
@@ -157,6 +157,15 @@ class StandardText(StrEnum):
157
157
  SIMLINE_COLOR = "#SIMLINE_COLOR"
158
158
  SIMLINE_ALPHA = "#SIMLINE_ALPHA"
159
159
  SIMLINE_ANIMATION = "#SIMLINE_ANIMATION"
160
+ PREVIEW_SCALE_VERTICAL = "#PREVIEW_SCALE_VERTICAL"
161
+ PREVIEW_SCALE_HORIZONTAL = "#PREVIEW_SCALE_HORIZONTAL"
162
+ PREVIEW_TIME = "#PREVIEW_TIME"
163
+ PREVIEW_SCORE = "#PREVIEW_SCORE"
164
+ PREVIEW_BPM = "#PREVIEW_BPM"
165
+ PREVIEW_TIMESCALE = "#PREVIEW_TIMESCALE"
166
+ PREVIEW_BEAT = "#PREVIEW_BEAT"
167
+ PREVIEW_MEASURE = "#PREVIEW_MEASURE"
168
+ PREVIEW_COMBO = "#PREVIEW_COMBO"
160
169
  NONE = "#NONE"
161
170
  ANY = "#ANY"
162
171
  ALL = "#ALL"
@@ -10,7 +10,7 @@ class Transform2d(Record):
10
10
  """A transformation matrix for 2D points.
11
11
 
12
12
  Usage:
13
- ```
13
+ ```python
14
14
  Transform2d.new()
15
15
  ```
16
16
  """
@@ -368,7 +368,7 @@ class Transform2d(Record):
368
368
  return other.compose(self)
369
369
 
370
370
  def transform_vec(self, v: Vec2) -> Vec2:
371
- """Transform a Vec2 and return a new Vec2.
371
+ """Transform a [`Vec2`][sonolus.script.vec.Vec2] and return a new [`Vec2`][sonolus.script.vec.Vec2].
372
372
 
373
373
  Args:
374
374
  v: The vector to transform.
@@ -382,7 +382,7 @@ class Transform2d(Record):
382
382
  return Vec2(x / w, y / w)
383
383
 
384
384
  def transform_quad(self, quad: QuadLike) -> Quad:
385
- """Transform a Quad and return a new Quad.
385
+ """Transform a [`Quad`][sonolus.script.quad.Quad] and return a new [`Quad`][sonolus.script.quad.Quad].
386
386
 
387
387
  Args:
388
388
  quad: The quad to transform.
sonolus/script/ui.py CHANGED
@@ -20,9 +20,14 @@ class UiMetric(StrEnum):
20
20
 
21
21
 
22
22
  class UiJudgmentErrorStyle(StrEnum):
23
- """The style of the judgment error."""
23
+ """The style of the judgment error.
24
+
25
+ The name of each member refers to what's used for positive (late) judgment errors.
26
+ """
24
27
 
25
28
  NONE = "none"
29
+ LATE = "late"
30
+ EARLY = "early" # Not really useful
26
31
  PLUS = "plus"
27
32
  MINUS = "minus"
28
33
  ARROW_UP = "arrowUp"
@@ -38,9 +43,13 @@ class UiJudgmentErrorStyle(StrEnum):
38
43
  class UiJudgmentErrorPlacement(StrEnum):
39
44
  """The placement of the judgment error."""
40
45
 
41
- BOTH = "both"
42
46
  LEFT = "left"
43
47
  RIGHT = "right"
48
+ LEFT_RIGHT = "leftRight"
49
+ TOP = "top"
50
+ BOTTOM = "bottom"
51
+ TOP_BOTTOM = "topBottom"
52
+ CENTER = "center"
44
53
 
45
54
 
46
55
  class EaseType(StrEnum):
@@ -193,8 +202,8 @@ class UiConfig:
193
202
  scale=UiAnimationTween(1.2, 1, 0.2, EaseType.IN_CUBIC), alpha=UiAnimationTween(1, 1, 0, EaseType.NONE)
194
203
  )
195
204
  )
196
- judgment_error_style: UiJudgmentErrorStyle = UiJudgmentErrorStyle.NONE
197
- judgment_error_placement: UiJudgmentErrorPlacement = UiJudgmentErrorPlacement.BOTH
205
+ judgment_error_style: UiJudgmentErrorStyle = UiJudgmentErrorStyle.LATE
206
+ judgment_error_placement: UiJudgmentErrorPlacement = UiJudgmentErrorPlacement.TOP
198
207
  judgment_error_min: float = 0.0
199
208
 
200
209
  def to_dict(self):
sonolus/script/values.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from sonolus.script.internal.context import ctx
2
2
  from sonolus.script.internal.generic import validate_concrete_type
3
3
  from sonolus.script.internal.impl import meta_fn, validate_value
4
+ from sonolus.script.num import Num
4
5
 
5
6
 
6
7
  @meta_fn
@@ -33,7 +34,17 @@ def copy[T](value: T) -> T:
33
34
 
34
35
 
35
36
  def swap[T](a: T, b: T):
36
- """Swap the values of the given variables."""
37
+ """Swap the values of the two given arguments."""
37
38
  temp = copy(a)
38
39
  a @= b
39
40
  b @= temp
41
+
42
+
43
+ @meta_fn
44
+ def sizeof(type_: type, /) -> int:
45
+ """Return the size of the given type."""
46
+ type_ = validate_concrete_type(type_)
47
+ if ctx():
48
+ return Num(type_._size_())
49
+ else:
50
+ return type_._size_()
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
8
 
9
9
  # Sonolus.py
10
- Sonolus engine development in Python.
10
+ Sonolus engine development in Python. See [docs](https://sonolus.py.qwewqa.xyz) for more information.