sonolus.py 0.3.0__py3-none-any.whl → 0.3.2__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/script/runtime.py CHANGED
@@ -259,6 +259,382 @@ class _TutorialRuntimeUi:
259
259
  instruction: BasicRuntimeUiLayout
260
260
 
261
261
 
262
+ class UiLayout[T](Record):
263
+ """The layout of a UI element."""
264
+
265
+ _underlying: T
266
+
267
+ def update(
268
+ self,
269
+ anchor: Vec2 | None = None,
270
+ pivot: Vec2 | None = None,
271
+ dimensions: Vec2 | None = None,
272
+ rotation: float | None = None,
273
+ alpha: float | None = None,
274
+ horizontal_align: HorizontalAlign | None = None,
275
+ background: bool | None = None,
276
+ ):
277
+ """Update the layout properties if it's available in the current mode and do nothing otherwise."""
278
+ match self._underlying:
279
+ case RuntimeUiLayout():
280
+ self._underlying.update(
281
+ anchor=anchor,
282
+ pivot=pivot,
283
+ dimensions=dimensions,
284
+ rotation=rotation,
285
+ alpha=alpha,
286
+ horizontal_align=horizontal_align,
287
+ background=background,
288
+ )
289
+ case BasicRuntimeUiLayout():
290
+ self._underlying.update(
291
+ anchor=anchor,
292
+ pivot=pivot,
293
+ dimensions=dimensions,
294
+ rotation=rotation,
295
+ alpha=alpha,
296
+ background=background,
297
+ )
298
+ case _:
299
+ pass # do nothing
300
+
301
+ @property
302
+ def is_available(self) -> bool:
303
+ """Check if the layout is available in the current mode."""
304
+ return self._underlying is not None
305
+
306
+
307
+ class UiConfig[T](Record):
308
+ """The user configuration for a UI element."""
309
+
310
+ _underlying: T
311
+
312
+ @property
313
+ def scale(self) -> float:
314
+ """The scale of the UI element."""
315
+ match self._underlying:
316
+ case RuntimeUiConfig():
317
+ return self._underlying.scale
318
+ case _:
319
+ return 1.0 # Default scale if not available
320
+
321
+ @property
322
+ def alpha(self) -> float:
323
+ """The alpha (opacity) of the UI element."""
324
+ match self._underlying:
325
+ case RuntimeUiConfig():
326
+ return self._underlying.alpha
327
+ case _:
328
+ return 1.0 # Default alpha if not available
329
+
330
+ @property
331
+ def is_available(self) -> bool:
332
+ """Check if the config is available in the current mode."""
333
+ return self._underlying is not None
334
+
335
+
336
+ class RuntimeUi(Record):
337
+ """Holds the layouts for different UI elements across all modes."""
338
+
339
+ @property
340
+ @meta_fn
341
+ def menu(self) -> UiLayout:
342
+ """The configuration for the menu UI element.
343
+
344
+ Available in play, watch, preview, and tutorial mode.
345
+ """
346
+ match ctx().global_state.mode:
347
+ case Mode.PLAY:
348
+ return UiLayout(_PlayRuntimeUi.menu)
349
+ case Mode.WATCH:
350
+ return UiLayout(_WatchRuntimeUi.menu)
351
+ case Mode.PREVIEW:
352
+ return UiLayout(_PreviewRuntimeUi.menu)
353
+ case Mode.TUTORIAL:
354
+ return UiLayout(_TutorialRuntimeUi.menu)
355
+ case _:
356
+ raise RuntimeError("Unsupported mode for menu UI layout")
357
+
358
+ @property
359
+ @meta_fn
360
+ def menu_config(self) -> UiConfig:
361
+ """The configuration for the menu UI element.
362
+
363
+ Available in play, watch, preview, and tutorial mode.
364
+ """
365
+ match ctx().global_state.mode:
366
+ case Mode.PLAY:
367
+ return UiConfig(_PlayRuntimeUiConfigs.menu)
368
+ case Mode.WATCH:
369
+ return UiConfig(_WatchRuntimeUiConfigs.menu)
370
+ case Mode.PREVIEW:
371
+ return UiConfig(_PreviewRuntimeUiConfigs.menu)
372
+ case Mode.TUTORIAL:
373
+ return UiConfig(_TutorialRuntimeUiConfigs.menu)
374
+ case _:
375
+ raise RuntimeError("Unsupported mode for menu UI configuration")
376
+
377
+ @property
378
+ @meta_fn
379
+ def judgment(self) -> UiLayout:
380
+ """The configuration for the judgment UI element.
381
+
382
+ Available in play and watch mode.
383
+ """
384
+ match ctx().global_state.mode:
385
+ case Mode.PLAY:
386
+ return UiLayout(_PlayRuntimeUi.judgment)
387
+ case Mode.WATCH:
388
+ return UiLayout(_WatchRuntimeUi.judgment)
389
+ case _:
390
+ return UiLayout(None)
391
+
392
+ @property
393
+ @meta_fn
394
+ def judgment_config(self) -> UiConfig:
395
+ """The configuration for the judgment UI element.
396
+
397
+ Available in play and watch mode.
398
+ """
399
+ match ctx().global_state.mode:
400
+ case Mode.PLAY:
401
+ return UiConfig(_PlayRuntimeUiConfigs.judgment)
402
+ case Mode.WATCH:
403
+ return UiConfig(_WatchRuntimeUiConfigs.judgment)
404
+ case _:
405
+ return UiConfig(None)
406
+
407
+ @property
408
+ @meta_fn
409
+ def combo_value(self) -> UiLayout:
410
+ """The configuration for the combo value UI element.
411
+
412
+ Available in play and watch mode.
413
+ """
414
+ match ctx().global_state.mode:
415
+ case Mode.PLAY:
416
+ return UiLayout(_PlayRuntimeUi.combo_value)
417
+ case Mode.WATCH:
418
+ return UiLayout(_WatchRuntimeUi.combo_value)
419
+ case _:
420
+ return UiLayout(None)
421
+
422
+ @property
423
+ @meta_fn
424
+ def combo_text(self) -> UiLayout:
425
+ """The configuration for the combo text UI element.
426
+
427
+ Available in play and watch mode.
428
+ """
429
+ match ctx().global_state.mode:
430
+ case Mode.PLAY:
431
+ return UiLayout(_PlayRuntimeUi.combo_text)
432
+ case Mode.WATCH:
433
+ return UiLayout(_WatchRuntimeUi.combo_text)
434
+ case _:
435
+ return UiLayout(None)
436
+
437
+ @property
438
+ @meta_fn
439
+ def combo_config(self) -> UiConfig:
440
+ """The configuration for the combo UI element.
441
+
442
+ Available in play and watch mode.
443
+ """
444
+ match ctx().global_state.mode:
445
+ case Mode.PLAY:
446
+ return UiConfig(_PlayRuntimeUiConfigs.combo)
447
+ case Mode.WATCH:
448
+ return UiConfig(_WatchRuntimeUiConfigs.combo)
449
+ case _:
450
+ return UiConfig(None)
451
+
452
+ @property
453
+ @meta_fn
454
+ def primary_metric_bar(self) -> UiLayout:
455
+ """The configuration for the primary metric bar UI element.
456
+
457
+ Available in play and watch mode.
458
+ """
459
+ match ctx().global_state.mode:
460
+ case Mode.PLAY:
461
+ return UiLayout(_PlayRuntimeUi.primary_metric_bar)
462
+ case Mode.WATCH:
463
+ return UiLayout(_WatchRuntimeUi.primary_metric_bar)
464
+ case _:
465
+ return UiLayout(None)
466
+
467
+ @property
468
+ @meta_fn
469
+ def primary_metric_value(self) -> UiLayout:
470
+ """The configuration for the primary metric value UI element.
471
+
472
+ Available in play and watch mode.
473
+ """
474
+ match ctx().global_state.mode:
475
+ case Mode.PLAY:
476
+ return UiLayout(_PlayRuntimeUi.primary_metric_value)
477
+ case Mode.WATCH:
478
+ return UiLayout(_WatchRuntimeUi.primary_metric_value)
479
+ case _:
480
+ return UiLayout(None)
481
+
482
+ @property
483
+ @meta_fn
484
+ def primary_metric_config(self) -> UiConfig:
485
+ """The configuration for the primary metric UI element.
486
+
487
+ Available in play and watch mode.
488
+ """
489
+ match ctx().global_state.mode:
490
+ case Mode.PLAY:
491
+ return UiConfig(_PlayRuntimeUiConfigs.primary_metric)
492
+ case Mode.WATCH:
493
+ return UiConfig(_WatchRuntimeUiConfigs.primary_metric)
494
+ case _:
495
+ return UiConfig(None)
496
+
497
+ @property
498
+ @meta_fn
499
+ def secondary_metric_bar(self) -> UiLayout:
500
+ """The configuration for the secondary metric bar UI element.
501
+
502
+ Available in play and watch mode.
503
+ """
504
+ match ctx().global_state.mode:
505
+ case Mode.PLAY:
506
+ return UiLayout(_PlayRuntimeUi.secondary_metric_bar)
507
+ case Mode.WATCH:
508
+ return UiLayout(_WatchRuntimeUi.secondary_metric_bar)
509
+ case _:
510
+ return UiLayout(None)
511
+
512
+ @property
513
+ @meta_fn
514
+ def secondary_metric_value(self) -> UiLayout:
515
+ """The configuration for the secondary metric value UI element.
516
+
517
+ Available in play and watch mode.
518
+ """
519
+ match ctx().global_state.mode:
520
+ case Mode.PLAY:
521
+ return UiLayout(_PlayRuntimeUi.secondary_metric_value)
522
+ case Mode.WATCH:
523
+ return UiLayout(_WatchRuntimeUi.secondary_metric_value)
524
+ case _:
525
+ return UiLayout(None)
526
+
527
+ @property
528
+ @meta_fn
529
+ def secondary_metric_config(self) -> UiConfig:
530
+ """The configuration for the secondary metric UI element.
531
+
532
+ Available in play and watch mode.
533
+ """
534
+ match ctx().global_state.mode:
535
+ case Mode.PLAY:
536
+ return UiConfig(_PlayRuntimeUiConfigs.secondary_metric)
537
+ case Mode.WATCH:
538
+ return UiConfig(_WatchRuntimeUiConfigs.secondary_metric)
539
+ case _:
540
+ return UiConfig(None)
541
+
542
+ @property
543
+ @meta_fn
544
+ def progress(self) -> UiLayout:
545
+ """The configuration for the progress UI element.
546
+
547
+ Available in watch and preview mode.
548
+ """
549
+ match ctx().global_state.mode:
550
+ case Mode.WATCH:
551
+ return UiLayout(_WatchRuntimeUi.progress)
552
+ case Mode.PREVIEW:
553
+ return UiLayout(_PreviewRuntimeUi.progress)
554
+ case _:
555
+ return UiLayout(None)
556
+
557
+ @property
558
+ @meta_fn
559
+ def progress_config(self) -> UiConfig:
560
+ """The configuration for the progress UI element.
561
+
562
+ Available in watch and preview mode.
563
+ """
564
+ match ctx().global_state.mode:
565
+ case Mode.WATCH:
566
+ return UiConfig(_WatchRuntimeUiConfigs.progress)
567
+ case Mode.PREVIEW:
568
+ return UiConfig(_PreviewRuntimeUiConfigs.progress)
569
+ case _:
570
+ return UiConfig(None)
571
+
572
+ @property
573
+ @meta_fn
574
+ def previous(self) -> UiLayout:
575
+ """The configuration for the previous navigation UI element.
576
+
577
+ Available in tutorial mode.
578
+ """
579
+ match ctx().global_state.mode:
580
+ case Mode.TUTORIAL:
581
+ return UiLayout(_TutorialRuntimeUi.previous)
582
+ case _:
583
+ return UiLayout(None)
584
+
585
+ @property
586
+ @meta_fn
587
+ def next(self) -> UiLayout:
588
+ """The configuration for the next navigation UI element.
589
+
590
+ Available in tutorial mode.
591
+ """
592
+ match ctx().global_state.mode:
593
+ case Mode.TUTORIAL:
594
+ return UiLayout(_TutorialRuntimeUi.next)
595
+ case _:
596
+ return UiLayout(None)
597
+
598
+ @property
599
+ @meta_fn
600
+ def navigation_config(self) -> UiConfig:
601
+ """The configuration for the navigation UI element.
602
+
603
+ Available in tutorial mode.
604
+ """
605
+ match ctx().global_state.mode:
606
+ case Mode.TUTORIAL:
607
+ return UiConfig(_TutorialRuntimeUiConfigs.navigation)
608
+ case _:
609
+ return UiConfig(None)
610
+
611
+ @property
612
+ @meta_fn
613
+ def instruction(self) -> UiLayout:
614
+ """The configuration for the instruction UI element.
615
+
616
+ Available in tutorial mode.
617
+ """
618
+ match ctx().global_state.mode:
619
+ case Mode.TUTORIAL:
620
+ return UiLayout(_TutorialRuntimeUi.instruction)
621
+ case _:
622
+ return UiLayout(None)
623
+
624
+ @property
625
+ @meta_fn
626
+ def instruction_config(self) -> UiConfig:
627
+ """The configuration for the instruction UI element.
628
+
629
+ Available in tutorial mode.
630
+ """
631
+ match ctx().global_state.mode:
632
+ case Mode.TUTORIAL:
633
+ return UiConfig(_TutorialRuntimeUiConfigs.instruction)
634
+ case _:
635
+ return UiConfig(None)
636
+
637
+
262
638
  class Touch(Record):
263
639
  """Data of a touch event."""
264
640
 
@@ -733,6 +1109,13 @@ preview_ui_configs = _PreviewRuntimeUiConfigs
733
1109
  tutorial_ui = _TutorialRuntimeUi
734
1110
  tutorial_ui_configs = _TutorialRuntimeUiConfigs
735
1111
 
1112
+ _runtime_ui = RuntimeUi()
1113
+
1114
+
1115
+ def runtime_ui() -> RuntimeUi:
1116
+ """Get the runtime UI configuration."""
1117
+ return _runtime_ui
1118
+
736
1119
 
737
1120
  def canvas() -> _PreviewRuntimeCanvas:
738
1121
  """Get the preview canvas."""
sonolus/script/stream.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import cast, dataclass_transform
4
4
 
5
- from sonolus.backend.ir import IRExpr, IRInstr, IRPureInstr
5
+ from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr
6
6
  from sonolus.backend.mode import Mode
7
7
  from sonolus.backend.ops import Op
8
8
  from sonolus.script.internal.context import ctx
@@ -14,6 +14,7 @@ from sonolus.script.internal.value import BackingValue, Value
14
14
  from sonolus.script.iterator import SonolusIterator
15
15
  from sonolus.script.num import Num
16
16
  from sonolus.script.record import Record
17
+ from sonolus.script.runtime import delta_time, time
17
18
  from sonolus.script.values import sizeof
18
19
 
19
20
 
@@ -42,7 +43,7 @@ class _StreamDataField(SonolusDescriptor):
42
43
  self.type_ = type_
43
44
 
44
45
  def _get(self):
45
- return self.type_._from_backing_source_(lambda offset: _StreamBacking(self.offset, Num(offset)))
46
+ return self.type_._from_backing_source_(lambda offset: _SparseStreamBacking(self.offset, Num(offset)))
46
47
 
47
48
  def __get__(self, instance, owner):
48
49
  _check_can_read_or_write_stream()
@@ -150,10 +151,35 @@ class _StreamBacking(BackingValue):
150
151
  ctx().add_statement(IRInstr(Op.StreamSet, [self.id.ir(), self.index.ir(), value]))
151
152
 
152
153
 
153
- class Stream[T](Record, BackingValue):
154
+ class _SparseStreamBacking(BackingValue):
155
+ id: Num
156
+ index: Num
157
+
158
+ def __init__(self, stream_id: int, index: Num):
159
+ super().__init__()
160
+ self.id = Num._accept_(stream_id)
161
+ self.index = Num._accept_(index)
162
+
163
+ def read(self) -> IRExpr:
164
+ """Read the value from the stream."""
165
+ _check_can_read_stream()
166
+ return IRPureInstr(Op.StreamGetValue, [self.id.ir(), self.index.ir()])
167
+
168
+ def write(self, value: IRExpr) -> None:
169
+ """Write the value to the stream."""
170
+ _check_can_write_stream()
171
+ ctx().add_statements(
172
+ IRInstr(Op.StreamSet, [self.id.ir(), self.index.ir(), value]),
173
+ IRInstr(Op.StreamSet, [self.id.ir(), (self.index - 0.5).ir(), IRConst(0)]),
174
+ IRInstr(Op.StreamSet, [self.id.ir(), (self.index + 0.5).ir(), IRConst(0)]),
175
+ )
176
+
177
+
178
+ class Stream[T](Record):
154
179
  """Represents a stream.
155
180
 
156
- Most users should use `@stream` to declare streams and stream groups rather than using this class directly.
181
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare streams and stream groups rather than
182
+ using this class directly.
157
183
 
158
184
  If used directly, it is important that streams do not overlap. No other streams should have an offset in
159
185
  `range(self.offset, self.offset + max(1, sizeof(self.element_type())))`, or they will overlap and interfere
@@ -224,6 +250,12 @@ class Stream[T](Record, BackingValue):
224
250
  _check_can_read_stream()
225
251
  return _stream_get_next_key(self.offset, key)
226
252
 
253
+ def next_key_or_default(self, key: int | float, default: int | float) -> int:
254
+ """Get the next key, or the default value if there is no next key."""
255
+ _check_can_read_stream()
256
+ next_key = self.next_key(key)
257
+ return next_key if next_key > key else default
258
+
227
259
  def previous_key(self, key: int | float) -> int:
228
260
  """Get the previous key, or the key unchanged if it is the first key or the stream is empty.
229
261
 
@@ -232,6 +264,12 @@ class Stream[T](Record, BackingValue):
232
264
  _check_can_read_stream()
233
265
  return _stream_get_previous_key(self.offset, key)
234
266
 
267
+ def previous_key_or_default(self, key: int | float, default: int | float) -> int:
268
+ """Get the previous key, or the default value if there is no previous key."""
269
+ _check_can_read_stream()
270
+ previous_key = self.previous_key(key)
271
+ return previous_key if previous_key < key else default
272
+
235
273
  def has_next_key(self, key: int | float) -> bool:
236
274
  """Check if there is a next key after the given key in the stream."""
237
275
  _check_can_read_stream()
@@ -293,6 +331,22 @@ class Stream[T](Record, BackingValue):
293
331
  _check_can_read_stream()
294
332
  return _StreamAscIterator(self, self.next_key_inclusive(start))
295
333
 
334
+ def iter_items_since_previous_frame(self) -> SonolusIterator[tuple[int | float, T]]:
335
+ """Iterate over the items in the stream since the last frame.
336
+
337
+ This is a convenience method that iterates over the items in the stream occurring after the time of the
338
+ previous frame and up to and including the current time.
339
+
340
+ Usage:
341
+ ```python
342
+ stream = ...
343
+ for key, value in stream.iter_items_since_previous_frame():
344
+ do_something(key, value)
345
+ ```
346
+ """
347
+ _check_can_read_stream()
348
+ return _StreamBoundedAscIterator(self, self.next_key(time() - delta_time()), time())
349
+
296
350
  def iter_items_from_desc(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
297
351
  """Iterate over the items in the stream in descending order starting from the given key.
298
352
 
@@ -323,6 +377,22 @@ class Stream[T](Record, BackingValue):
323
377
  _check_can_read_stream()
324
378
  return _StreamAscKeyIterator(self, self.next_key_inclusive(start))
325
379
 
380
+ def iter_keys_since_previous_frame(self) -> SonolusIterator[int | float]:
381
+ """Iterate over the keys in the stream since the last frame.
382
+
383
+ This is a convenience method that iterates over the keys in the stream occurring after the time of the
384
+ previous frame and up to and including the current time.
385
+
386
+ Usage:
387
+ ```python
388
+ stream = ...
389
+ for key in stream.iter_keys_since_previous_frame():
390
+ do_something(key)
391
+ ```
392
+ """
393
+ _check_can_read_stream()
394
+ return _StreamBoundedAscKeyIterator(self, self.next_key(time() - delta_time()), time())
395
+
326
396
  def iter_keys_from_desc(self, start: int | float, /) -> SonolusIterator[int | float]:
327
397
  """Iterate over the keys in the stream in descending order starting from the given key.
328
398
 
@@ -353,6 +423,22 @@ class Stream[T](Record, BackingValue):
353
423
  _check_can_read_stream()
354
424
  return _StreamAscValueIterator(self, self.next_key_inclusive(start))
355
425
 
426
+ def iter_values_since_previous_frame(self) -> SonolusIterator[T]:
427
+ """Iterate over the values in the stream since the last frame.
428
+
429
+ This is a convenience method that iterates over the values in the stream occurring after the time of the
430
+ previous frame and up to and including the current time.
431
+
432
+ Usage:
433
+ ```python
434
+ stream = ...
435
+ for value in stream.iter_values_since_previous_frame():
436
+ do_something(value)
437
+ ```
438
+ """
439
+ _check_can_read_stream()
440
+ return _StreamBoundedAscValueIterator(self, self.next_key(time() - delta_time()), time())
441
+
356
442
  def iter_values_from_desc(self, start: int | float, /) -> SonolusIterator[T]:
357
443
  """Iterate over the values in the stream in descending order starting from the given key.
358
444
 
@@ -372,7 +458,8 @@ class Stream[T](Record, BackingValue):
372
458
  class StreamGroup[T, Size](Record):
373
459
  """Represents a group of streams.
374
460
 
375
- Most users should use `@stream` to declare stream groups rather than using this class directly.
461
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare stream groups rather than using this
462
+ class directly.
376
463
 
377
464
  Usage:
378
465
  Declaring a stream group:
@@ -420,13 +507,28 @@ class _StreamAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
420
507
  current_key: int | float
421
508
 
422
509
  def has_next(self) -> bool:
423
- return self.stream.next_key(self.current_key) > self.current_key
510
+ return self.current_key in self.stream
511
+
512
+ def get(self) -> tuple[int | float, T]:
513
+ return self.current_key, self.stream[self.current_key]
514
+
515
+ def advance(self):
516
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
517
+
518
+
519
+ class _StreamBoundedAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
520
+ stream: Stream[T]
521
+ current_key: int | float
522
+ end_key: int | float
523
+
524
+ def has_next(self) -> bool:
525
+ return self.current_key in self.stream and self.current_key <= self.end_key
424
526
 
425
527
  def get(self) -> tuple[int | float, T]:
426
528
  return self.current_key, self.stream[self.current_key]
427
529
 
428
530
  def advance(self):
429
- self.current_key = self.stream.next_key(self.current_key)
531
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
430
532
 
431
533
 
432
534
  class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
@@ -434,13 +536,13 @@ class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
434
536
  current_key: int | float
435
537
 
436
538
  def has_next(self) -> bool:
437
- return self.stream.previous_key(self.current_key) < self.current_key
539
+ return self.current_key in self.stream
438
540
 
439
541
  def get(self) -> tuple[int | float, T]:
440
542
  return self.current_key, self.stream[self.current_key]
441
543
 
442
544
  def advance(self):
443
- self.current_key = self.stream.previous_key(self.current_key)
545
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
444
546
 
445
547
 
446
548
  class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -448,13 +550,28 @@ class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
448
550
  current_key: int | float
449
551
 
450
552
  def has_next(self) -> bool:
451
- return self.stream.next_key(self.current_key) > self.current_key
553
+ return self.current_key in self.stream
452
554
 
453
555
  def get(self) -> int | float:
454
556
  return self.current_key
455
557
 
456
558
  def advance(self):
457
- self.current_key = self.stream.next_key(self.current_key)
559
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
560
+
561
+
562
+ class _StreamBoundedAscKeyIterator[T](Record, SonolusIterator[int | float]):
563
+ stream: Stream[T]
564
+ current_key: int | float
565
+ end_key: int | float
566
+
567
+ def has_next(self) -> bool:
568
+ return self.current_key in self.stream and self.current_key <= self.end_key
569
+
570
+ def get(self) -> int | float:
571
+ return self.current_key
572
+
573
+ def advance(self):
574
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
458
575
 
459
576
 
460
577
  class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -462,13 +579,13 @@ class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
462
579
  current_key: int | float
463
580
 
464
581
  def has_next(self) -> bool:
465
- return self.stream.previous_key(self.current_key) < self.current_key
582
+ return self.current_key in self.stream
466
583
 
467
584
  def get(self) -> int | float:
468
585
  return self.current_key
469
586
 
470
587
  def advance(self):
471
- self.current_key = self.stream.previous_key(self.current_key)
588
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
472
589
 
473
590
 
474
591
  class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
@@ -476,13 +593,28 @@ class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
476
593
  current_key: int | float
477
594
 
478
595
  def has_next(self) -> bool:
479
- return self.stream.next_key(self.current_key) > self.current_key
596
+ return self.current_key in self.stream
597
+
598
+ def get(self) -> T:
599
+ return self.stream[self.current_key]
600
+
601
+ def advance(self):
602
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
603
+
604
+
605
+ class _StreamBoundedAscValueIterator[T](Record, SonolusIterator[T]):
606
+ stream: Stream[T]
607
+ current_key: int | float
608
+ end_key: int | float
609
+
610
+ def has_next(self) -> bool:
611
+ return self.current_key in self.stream and self.current_key <= self.end_key
480
612
 
481
613
  def get(self) -> T:
482
614
  return self.stream[self.current_key]
483
615
 
484
616
  def advance(self):
485
- self.current_key = self.stream.next_key(self.current_key)
617
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
486
618
 
487
619
 
488
620
  class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
@@ -490,13 +622,13 @@ class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
490
622
  current_key: int | float
491
623
 
492
624
  def has_next(self) -> bool:
493
- return self.stream.previous_key(self.current_key) < self.current_key
625
+ return self.current_key in self.stream
494
626
 
495
627
  def get(self) -> T:
496
628
  return self.stream[self.current_key]
497
629
 
498
630
  def advance(self):
499
- self.current_key = self.stream.previous_key(self.current_key)
631
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
500
632
 
501
633
 
502
634
  @native_function(Op.StreamGetNextKey)