sonolus.py 0.1.1__tar.gz → 0.1.2__tar.gz

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.

Files changed (99) hide show
  1. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/PKG-INFO +1 -1
  2. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/pyproject.toml +1 -1
  3. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/mode.py +4 -4
  4. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/visitor.py +2 -2
  5. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/compile.py +3 -3
  6. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/engine.py +20 -4
  7. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/archetype.py +42 -11
  8. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/bucket.py +2 -2
  9. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/callbacks.py +10 -0
  10. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/comptime.py +14 -0
  11. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/debug.py +1 -1
  12. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/effect.py +11 -11
  13. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/engine.py +26 -6
  14. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/globals.py +79 -44
  15. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/graphics.py +37 -28
  16. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/builtin_impls.py +3 -3
  17. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/native.py +2 -2
  18. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/interval.py +14 -0
  19. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/level.py +7 -7
  20. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/num.py +30 -4
  21. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/options.py +4 -4
  22. sonolus_py-0.1.2/sonolus/script/particle.py +157 -0
  23. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/pointer.py +3 -3
  24. sonolus_py-0.1.2/sonolus/script/preview.py +81 -0
  25. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/runtime.py +113 -35
  26. sonolus_py-0.1.2/sonolus/script/sprite.py +333 -0
  27. sonolus_py-0.1.2/sonolus/script/text.py +407 -0
  28. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/transform.py +13 -17
  29. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/vec.py +24 -0
  30. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/conftest.py +1 -1
  31. sonolus_py-0.1.1/sonolus/script/particle.py +0 -157
  32. sonolus_py-0.1.1/sonolus/script/sprite.py +0 -331
  33. sonolus_py-0.1.1/sonolus/script/text.py +0 -404
  34. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/.gitignore +0 -0
  35. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/.python-version +0 -0
  36. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/LICENSE +0 -0
  37. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/README.md +0 -0
  38. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/generate.py +0 -0
  39. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/runtimes/Engine/Tutorial/Blocks.json +0 -0
  40. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/runtimes/Functions.json +0 -0
  41. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/runtimes/Level/Play/Blocks.json +0 -0
  42. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/runtimes/Level/Preview/Blocks.json +0 -0
  43. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/scripts/runtimes/Level/Watch/Blocks.json +0 -0
  44. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/__init__.py +0 -0
  45. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/__init__.py +0 -0
  46. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/allocate.py +0 -0
  47. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/blocks.py +0 -0
  48. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/excepthook.py +0 -0
  49. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/finalize.py +0 -0
  50. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/flow.py +0 -0
  51. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/interpret.py +0 -0
  52. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/ir.py +0 -0
  53. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/node.py +0 -0
  54. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/ops.py +0 -0
  55. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/optimize.py +0 -0
  56. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/passes.py +0 -0
  57. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/place.py +0 -0
  58. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/simplify.py +0 -0
  59. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/backend/utils.py +0 -0
  60. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/__init__.py +0 -0
  61. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/cli.py +0 -0
  62. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/collection.py +0 -0
  63. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/defaults.py +0 -0
  64. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/level.py +0 -0
  65. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/node.py +0 -0
  66. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/build/project.py +0 -0
  67. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/py.typed +0 -0
  68. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/__init__.py +0 -0
  69. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/array.py +0 -0
  70. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/containers.py +0 -0
  71. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/icon.py +0 -0
  72. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/__init__.py +0 -0
  73. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/context.py +0 -0
  74. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/descriptor.py +0 -0
  75. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/error.py +0 -0
  76. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/generic.py +0 -0
  77. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/impl.py +0 -0
  78. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/introspection.py +0 -0
  79. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/internal/value.py +0 -0
  80. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/iterator.py +0 -0
  81. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/math.py +0 -0
  82. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/project.py +0 -0
  83. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/range.py +0 -0
  84. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/record.py +0 -0
  85. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/timing.py +0 -0
  86. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/ui.py +0 -0
  87. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/sonolus/script/values.py +0 -0
  88. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/__init__.py +0 -0
  89. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/__init__.py +0 -0
  90. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_array.py +0 -0
  91. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_array_map.py +0 -0
  92. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_assert.py +0 -0
  93. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_helpers.py +0 -0
  94. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_interval.py +0 -0
  95. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_num.py +0 -0
  96. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_range.py +0 -0
  97. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_record.py +0 -0
  98. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/tests/script/test_var_array.py +0 -0
  99. {sonolus_py-0.1.1 → sonolus_py-0.1.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sonolus.py
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sonolus.py"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Sonolus engine development in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -7,10 +7,10 @@ from sonolus.backend.blocks import Block, PlayBlock, PreviewBlock, TutorialBlock
7
7
  class Mode(Enum):
8
8
  blocks: type[Block]
9
9
 
10
- Play = (PlayBlock,)
11
- Watch = (WatchBlock,)
12
- Preview = (PreviewBlock,)
13
- Tutorial = (TutorialBlock,)
10
+ PLAY = (PlayBlock,)
11
+ WATCH = (WatchBlock,)
12
+ PREVIEW = (PreviewBlock,)
13
+ TUTORIAL = (TutorialBlock,)
14
14
 
15
15
  def __init__(self, blocks: type[Block]):
16
16
  self.blocks = blocks
@@ -17,7 +17,7 @@ from sonolus.script.internal.error import CompilationError
17
17
  from sonolus.script.internal.impl import try_validate_value, validate_value
18
18
  from sonolus.script.internal.value import Value
19
19
  from sonolus.script.iterator import SonolusIterator
20
- from sonolus.script.num import Num
20
+ from sonolus.script.num import Num, is_num
21
21
 
22
22
  _compiler_internal_ = True
23
23
 
@@ -833,7 +833,7 @@ class Visitor(ast.NodeVisitor):
833
833
 
834
834
  def ensure_boolean_num(self, value) -> Num:
835
835
  # This just checks the type for now, although we could support custom __bool__ implementations in the future
836
- if not isinstance(value, Num):
836
+ if not is_num(value):
837
837
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
838
838
  return value
839
839
 
@@ -19,7 +19,7 @@ from sonolus.script.internal.context import (
19
19
  ctx,
20
20
  using_ctx,
21
21
  )
22
- from sonolus.script.num import Num
22
+ from sonolus.script.num import is_num
23
23
 
24
24
 
25
25
  def compile_mode(
@@ -43,7 +43,7 @@ def compile_mode(
43
43
  archetype_data["imports"] = [
44
44
  {"name": name, "index": index} for name, index in archetype._imported_keys_.items()
45
45
  ]
46
- if mode == Mode.Play:
46
+ if mode == Mode.PLAY:
47
47
  archetype_data["exports"] = [
48
48
  {"name": name, "index": index} for name, index in archetype._exported_keys_.items()
49
49
  ]
@@ -85,6 +85,6 @@ def callback_to_cfg(
85
85
  result = compile_and_call(callback, archetype._for_compilation())
86
86
  else:
87
87
  result = compile_and_call(callback)
88
- if isinstance(result, Num):
88
+ if is_num(result):
89
89
  ctx().add_statements(IRInstr(Op.Break, [IRConst(1), result.ir()]))
90
90
  return context_to_cfg(context)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from sonolus.backend.mode import Mode
9
9
  from sonolus.build.compile import compile_mode
10
- from sonolus.build.defaults import EMPTY_ENGINE_PREVIEW_DATA, EMPTY_ENGINE_TUTORIAL_DATA
10
+ from sonolus.build.defaults import EMPTY_ENGINE_TUTORIAL_DATA
11
11
  from sonolus.script.archetype import BaseArchetype
12
12
  from sonolus.script.bucket import Buckets
13
13
  from sonolus.script.callbacks import update_spawn_callback
@@ -61,11 +61,16 @@ def package_engine(engine: EngineData):
61
61
  rom=rom,
62
62
  update_spawn=engine.watch.update_spawn,
63
63
  )
64
+ preview_mode = build_preview_mode(
65
+ archetypes=engine.preview.archetypes,
66
+ skin=engine.preview.skin,
67
+ rom=rom,
68
+ )
64
69
  return PackagedEngine(
65
70
  configuration=package_output(configuration),
66
71
  play_data=package_output(play_data),
67
72
  watch_data=package_output(watch_data),
68
- preview_data=package_output(EMPTY_ENGINE_PREVIEW_DATA),
73
+ preview_data=package_output(preview_mode),
69
74
  tutorial_data=package_output(EMPTY_ENGINE_TUTORIAL_DATA),
70
75
  rom=package_rom(rom),
71
76
  )
@@ -90,7 +95,7 @@ def build_play_mode(
90
95
  rom: ReadOnlyMemory,
91
96
  ):
92
97
  return {
93
- **compile_mode(mode=Mode.Play, rom=rom, archetypes=archetypes, global_callbacks=None),
98
+ **compile_mode(mode=Mode.PLAY, rom=rom, archetypes=archetypes, global_callbacks=None),
94
99
  "skin": build_skin(skin),
95
100
  "effect": build_effects(effects),
96
101
  "particle": build_particles(particles),
@@ -109,7 +114,7 @@ def build_watch_mode(
109
114
  ):
110
115
  return {
111
116
  **compile_mode(
112
- mode=Mode.Watch, rom=rom, archetypes=archetypes, global_callbacks=[(update_spawn_callback, update_spawn)]
117
+ mode=Mode.WATCH, rom=rom, archetypes=archetypes, global_callbacks=[(update_spawn_callback, update_spawn)]
113
118
  ),
114
119
  "skin": build_skin(skin),
115
120
  "effect": build_effects(effects),
@@ -118,6 +123,17 @@ def build_watch_mode(
118
123
  }
119
124
 
120
125
 
126
+ def build_preview_mode(
127
+ archetypes: list[type[BaseArchetype]],
128
+ skin: Skin,
129
+ rom: ReadOnlyMemory,
130
+ ):
131
+ return {
132
+ **compile_mode(mode=Mode.PREVIEW, rom=rom, archetypes=archetypes, global_callbacks=None),
133
+ "skin": build_skin(skin),
134
+ }
135
+
136
+
121
137
  def build_skin(skin: Skin) -> JsonValue:
122
138
  return {"sprites": [{"name": name, "id": i} for i, name in enumerate(skin._sprites_)]}
123
139
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import inspect
4
4
  from collections.abc import Callable
5
5
  from dataclasses import dataclass
6
- from enum import Enum
6
+ from enum import Enum, StrEnum
7
7
  from types import FunctionType
8
8
  from typing import Annotated, Any, ClassVar, Self, get_origin
9
9
 
@@ -11,7 +11,7 @@ from sonolus.backend.ir import IRConst, IRInstr
11
11
  from sonolus.backend.mode import Mode
12
12
  from sonolus.backend.ops import Op
13
13
  from sonolus.script.bucket import Bucket, Judgment
14
- from sonolus.script.callbacks import PLAY_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
14
+ from sonolus.script.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
15
15
  from sonolus.script.internal.context import ctx
16
16
  from sonolus.script.internal.descriptor import SonolusDescriptor
17
17
  from sonolus.script.internal.generic import validate_concrete_type
@@ -174,11 +174,11 @@ _annotation_defaults: dict[Callable, ArchetypeFieldInfo] = {
174
174
 
175
175
 
176
176
  class StandardImport:
177
- Beat = Annotated[float, imported(name="#BEAT")]
178
- Bpm = Annotated[float, imported(name="#BPM")]
179
- Timescale = Annotated[float, imported(name="#TIMESCALE")]
180
- Judgment = Annotated[int, imported(name="#JUDGMENT")]
181
- Accuracy = Annotated[float, imported(name="#ACCURACY")]
177
+ BEAT = Annotated[float, imported(name="#BEAT")]
178
+ BPM = Annotated[float, imported(name="#BPM")]
179
+ TIMESCALE = Annotated[float, imported(name="#TIMESCALE")]
180
+ JUDGMENT = Annotated[int, imported(name="#JUDGMENT")]
181
+ ACCURACY = Annotated[float, imported(name="#ACCURACY")]
182
182
 
183
183
 
184
184
  def callback[T: Callable](order: int) -> Callable[[T], T]:
@@ -559,16 +559,42 @@ class WatchArchetype(BaseArchetype):
559
559
  self.result.target_time = value
560
560
 
561
561
 
562
+ class PreviewArchetype(BaseArchetype):
563
+ _supported_callbacks_ = PREVIEW_CALLBACKS
564
+
565
+ def preprocess(self):
566
+ pass
567
+
568
+ def render(self):
569
+ pass
570
+
571
+ @property
572
+ def _info(self) -> PreviewEntityInfo:
573
+ if not ctx():
574
+ raise RuntimeError("Calling info is only allowed within a callback")
575
+ match self._data_:
576
+ case ArchetypeSelfData():
577
+ return deref(ctx().blocks.EntityInfo, 0, PreviewEntityInfo)
578
+ case ArchetypeReferenceData(index=index):
579
+ return deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
580
+ case _:
581
+ raise RuntimeError("Info is only accessible from the entity itself")
582
+
583
+ @property
584
+ def index(self) -> int:
585
+ return self._info.index
586
+
587
+
562
588
  @meta_fn
563
589
  def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
564
590
  if not ctx():
565
591
  raise RuntimeError("Calling entity_info_at is only allowed within a callback")
566
592
  match ctx().global_state.mode:
567
- case Mode.Play:
593
+ case Mode.PLAY:
568
594
  return deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
569
- case Mode.Watch:
595
+ case Mode.WATCH:
570
596
  return deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
571
- case Mode.Preview:
597
+ case Mode.PREVIEW:
572
598
  return deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
573
599
  case _:
574
600
  raise RuntimeError(f"Entity info is not available in mode '{ctx().global_state.mode}'")
@@ -581,7 +607,7 @@ def archetype_life_of(archetype: type[BaseArchetype] | BaseArchetype) -> Archety
581
607
  if not ctx():
582
608
  raise RuntimeError("Calling archetype_life_of is only allowed within a callback")
583
609
  match ctx().global_state.mode:
584
- case Mode.Play | Mode.Watch:
610
+ case Mode.PLAY | Mode.WATCH:
585
611
  return deref(ctx().blocks.ArchetypeLife, archetype.id() * ArchetypeLife._size_(), ArchetypeLife)
586
612
  case _:
587
613
  raise RuntimeError(f"Archetype life is not available in mode '{ctx().global_state.mode}'")
@@ -649,3 +675,8 @@ class EntityRef[A: BaseArchetype](Record):
649
675
 
650
676
  def get(self) -> A:
651
677
  return self.archetype().at(Num(self.index))
678
+
679
+
680
+ class StandardArchetypeName(StrEnum):
681
+ BPM_CHANGE = "#BPM_CHANGE"
682
+ TIMESCALE_CHANGE = "#TIMESCALE_CHANGE"
@@ -88,9 +88,9 @@ class Bucket(Record):
88
88
  if not ctx():
89
89
  raise RuntimeError("Bucket window access outside of compilation")
90
90
  match ctx().global_state.mode:
91
- case Mode.Play:
91
+ case Mode.PLAY:
92
92
  return deref(ctx().blocks.LevelBucket, self.id * JudgmentWindow._size_(), JudgmentWindow)
93
- case Mode.Watch:
93
+ case Mode.WATCH:
94
94
  return deref(ctx().blocks.LevelBucket, self.id * JudgmentWindow._size_(), JudgmentWindow)
95
95
  case _:
96
96
  raise RuntimeError("Invalid mode for bucket window access")
@@ -75,6 +75,12 @@ update_spawn_callback = CallbackInfo(
75
75
  supports_order=False,
76
76
  returns_value=True,
77
77
  )
78
+ render_callback = CallbackInfo(
79
+ name="render",
80
+ py_name="render",
81
+ supports_order=False,
82
+ returns_value=False,
83
+ )
78
84
 
79
85
 
80
86
  def _by_name(*callbacks: CallbackInfo) -> dict[str, CallbackInfo]:
@@ -103,3 +109,7 @@ WATCH_ARCHETYPE_CALLBACKS = _by_name(
103
109
  WATCH_GLOBAL_CALLBACKS = _by_name(
104
110
  update_spawn_callback,
105
111
  )
112
+ PREVIEW_CALLBACKS = _by_name(
113
+ preprocess_callback,
114
+ render_callback,
115
+ )
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Self, TypeVar, final
3
3
 
4
4
  from sonolus.backend.place import BlockPlace
5
5
  from sonolus.script.internal.generic import GenericValue
6
+ from sonolus.script.internal.impl import meta_fn, validate_value
6
7
 
7
8
 
8
9
  @final
@@ -92,6 +93,19 @@ class _Comptime[T, V](GenericValue):
92
93
  def _copy_(self) -> Self:
93
94
  return self
94
95
 
96
+ @meta_fn
97
+ def __eq__(self, other):
98
+ other = validate_value(other)
99
+ match self.value():
100
+ case str():
101
+ return other._is_py_() and other._as_py_() == self.value()
102
+ case _:
103
+ raise TypeError("Unsupported comparison with comptime value")
104
+
105
+ @meta_fn
106
+ def __hash__(self):
107
+ return hash(self.value())
108
+
95
109
  @classmethod
96
110
  def _alloc_(cls) -> Self:
97
111
  return cls._instance
@@ -65,6 +65,6 @@ def terminate():
65
65
  def visualize_cfg(fn: Callable[[], Any]) -> str:
66
66
  from sonolus.build.compile import callback_to_cfg
67
67
 
68
- cfg = callback_to_cfg(GlobalContextState(Mode.Play), fn, "")
68
+ cfg = callback_to_cfg(GlobalContextState(Mode.PLAY), fn, "")
69
69
  cfg = CoalesceFlow().run(cfg)
70
70
  return cfg_to_mermaid(cfg)
@@ -113,17 +113,17 @@ def effects[T](cls: type[T]) -> T | Effects:
113
113
 
114
114
 
115
115
  class StandardEffect:
116
- Miss = Annotated[Effect, effect("#MISS")]
117
- Perfect = Annotated[Effect, effect("#PERFECT")]
118
- Great = Annotated[Effect, effect("#GREAT")]
119
- Good = Annotated[Effect, effect("#GOOD")]
120
- Hold = Annotated[Effect, effect("#HOLD")]
121
- MissAlternative = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
122
- PerfectAlternative = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
123
- GreatAlternative = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
124
- GoodAlternative = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
125
- HoldAlternative = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
126
- Stage = Annotated[Effect, effect("#STAGE")]
116
+ MISS = Annotated[Effect, effect("#MISS")]
117
+ PERFECT = Annotated[Effect, effect("#PERFECT")]
118
+ GREAT = Annotated[Effect, effect("#GREAT")]
119
+ GOOD = Annotated[Effect, effect("#GOOD")]
120
+ HOLD = Annotated[Effect, effect("#HOLD")]
121
+ MISS_ALTERNATIVE = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
122
+ PERFECT_ALTERNATIVE = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
123
+ GREAT_ALTERNATIVE = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
124
+ GOOD_ALTERNATIVE = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
125
+ HOLD_ALTERNATIVE = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
126
+ STAGE = Annotated[Effect, effect("#STAGE")]
127
127
 
128
128
 
129
129
  @effects
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
 
5
5
  from sonolus.build.collection import Asset
6
- from sonolus.script.archetype import BaseArchetype
6
+ from sonolus.script.archetype import BaseArchetype, PlayArchetype, PreviewArchetype, WatchArchetype
7
7
  from sonolus.script.bucket import Buckets, EmptyBuckets
8
8
  from sonolus.script.effect import Effects, EmptyEffects
9
9
  from sonolus.script.options import EmptyOptions, Options
@@ -61,6 +61,10 @@ class PlayMode:
61
61
  self.particles = particles
62
62
  self.buckets = buckets
63
63
 
64
+ for archetype in self.archetypes:
65
+ if not issubclass(archetype, PlayArchetype):
66
+ raise ValueError(f"archetype {archetype} is not a PlayArchetype")
67
+
64
68
 
65
69
  class WatchMode:
66
70
  def __init__(
@@ -80,13 +84,27 @@ class WatchMode:
80
84
  self.buckets = buckets
81
85
  self.update_spawn = update_spawn
82
86
 
87
+ for archetype in self.archetypes:
88
+ if not issubclass(archetype, WatchArchetype):
89
+ raise ValueError(f"archetype {archetype} is not a PlayArchetype")
83
90
 
84
- class EngineData:
85
- ui: UiConfig
86
- options: Options
87
- play: PlayMode
88
- watch: WatchMode
89
91
 
92
+ class PreviewMode:
93
+ def __init__(
94
+ self,
95
+ *,
96
+ archetypes: list[type[BaseArchetype]] | None = None,
97
+ skin: Skin = EmptySkin,
98
+ ) -> None:
99
+ self.archetypes = archetypes or []
100
+ self.skin = skin
101
+
102
+ for archetype in self.archetypes:
103
+ if not issubclass(archetype, PreviewArchetype):
104
+ raise ValueError(f"archetype {archetype} is not a BaseArchetype")
105
+
106
+
107
+ class EngineData:
90
108
  def __init__(
91
109
  self,
92
110
  *,
@@ -94,8 +112,10 @@ class EngineData:
94
112
  options: Options = EmptyOptions,
95
113
  play: PlayMode | None = None,
96
114
  watch: WatchMode | None = None,
115
+ preview: PreviewMode | None = None,
97
116
  ) -> None:
98
117
  self.ui = ui or UiConfig()
99
118
  self.options = options
100
119
  self.play = play or PlayMode()
101
120
  self.watch = watch or WatchMode(update_spawn=default_callback)
121
+ self.preview = preview or PreviewMode()
@@ -86,42 +86,47 @@ def create_global(cls: type, blocks: dict[Mode, Block], offset: int | None):
86
86
 
87
87
  @dataclass_transform()
88
88
  def _play_runtime_environment[T](cls: type[T]) -> T:
89
- return create_global(cls, {Mode.Play: PlayBlock.RuntimeEnvironment}, 0)
89
+ return create_global(cls, {Mode.PLAY: PlayBlock.RuntimeEnvironment}, 0)
90
90
 
91
91
 
92
92
  @dataclass_transform()
93
93
  def _watch_runtime_environment[T](cls: type[T]) -> T:
94
- return create_global(cls, {Mode.Watch: WatchBlock.RuntimeEnvironment}, 0)
94
+ return create_global(cls, {Mode.WATCH: WatchBlock.RuntimeEnvironment}, 0)
95
95
 
96
96
 
97
97
  @dataclass_transform()
98
98
  def _tutorial_runtime_environment[T](cls: type[T]) -> T:
99
- return create_global(cls, {Mode.Tutorial: TutorialBlock.RuntimeEnvironment}, 0)
99
+ return create_global(cls, {Mode.TUTORIAL: TutorialBlock.RuntimeEnvironment}, 0)
100
100
 
101
101
 
102
102
  @dataclass_transform()
103
103
  def _preview_runtime_environment[T](cls: type[T]) -> T:
104
- return create_global(cls, {Mode.Preview: PreviewBlock.RuntimeEnvironment}, 0)
104
+ return create_global(cls, {Mode.PREVIEW: PreviewBlock.RuntimeEnvironment}, 0)
105
105
 
106
106
 
107
107
  @dataclass_transform()
108
108
  def _play_runtime_update[T](cls: type[T]) -> T:
109
- return create_global(cls, {Mode.Play: PlayBlock.RuntimeUpdate}, 0)
109
+ return create_global(cls, {Mode.PLAY: PlayBlock.RuntimeUpdate}, 0)
110
110
 
111
111
 
112
112
  @dataclass_transform()
113
113
  def _watch_runtime_update[T](cls: type[T]) -> T:
114
- return create_global(cls, {Mode.Watch: WatchBlock.RuntimeUpdate}, 0)
114
+ return create_global(cls, {Mode.WATCH: WatchBlock.RuntimeUpdate}, 0)
115
+
116
+
117
+ @dataclass_transform()
118
+ def _preview_runtime_canvas[T](cls: type[T]) -> T:
119
+ return create_global(cls, {Mode.PREVIEW: PreviewBlock.RuntimeCanvas}, 0)
115
120
 
116
121
 
117
122
  @dataclass_transform()
118
123
  def _tutorial_runtime_update[T](cls: type[T]) -> T:
119
- return create_global(cls, {Mode.Tutorial: TutorialBlock.RuntimeUpdate}, 0)
124
+ return create_global(cls, {Mode.TUTORIAL: TutorialBlock.RuntimeUpdate}, 0)
120
125
 
121
126
 
122
127
  @dataclass_transform()
123
128
  def _runtime_touch_array[T](cls: type[T]) -> T:
124
- return create_global(cls, {Mode.Play: PlayBlock.RuntimeTouchArray}, 0)
129
+ return create_global(cls, {Mode.PLAY: PlayBlock.RuntimeTouchArray}, 0)
125
130
 
126
131
 
127
132
  @dataclass_transform()
@@ -129,9 +134,10 @@ def _runtime_skin_transform[T](cls: type[T]) -> T:
129
134
  return create_global(
130
135
  cls,
131
136
  {
132
- Mode.Play: PlayBlock.RuntimeSkinTransform,
133
- Mode.Watch: WatchBlock.RuntimeSkinTransform,
134
- Mode.Tutorial: TutorialBlock.RuntimeSkinTransform,
137
+ Mode.PLAY: PlayBlock.RuntimeSkinTransform,
138
+ Mode.WATCH: WatchBlock.RuntimeSkinTransform,
139
+ Mode.PREVIEW: PreviewBlock.RuntimeSkinTransform,
140
+ Mode.TUTORIAL: TutorialBlock.RuntimeSkinTransform,
135
141
  },
136
142
  0,
137
143
  )
@@ -142,9 +148,9 @@ def _runtime_particle_transform[T](cls: type[T]) -> T:
142
148
  return create_global(
143
149
  cls,
144
150
  {
145
- Mode.Play: PlayBlock.RuntimeParticleTransform,
146
- Mode.Watch: WatchBlock.RuntimeParticleTransform,
147
- Mode.Tutorial: TutorialBlock.RuntimeParticleTransform,
151
+ Mode.PLAY: PlayBlock.RuntimeParticleTransform,
152
+ Mode.WATCH: WatchBlock.RuntimeParticleTransform,
153
+ Mode.TUTORIAL: TutorialBlock.RuntimeParticleTransform,
148
154
  },
149
155
  0,
150
156
  )
@@ -155,55 +161,84 @@ def _runtime_background[T](cls: type[T]) -> T:
155
161
  return create_global(
156
162
  cls,
157
163
  {
158
- Mode.Play: PlayBlock.RuntimeBackground,
159
- Mode.Watch: WatchBlock.RuntimeBackground,
160
- Mode.Tutorial: TutorialBlock.RuntimeBackground,
164
+ Mode.PLAY: PlayBlock.RuntimeBackground,
165
+ Mode.WATCH: WatchBlock.RuntimeBackground,
166
+ Mode.TUTORIAL: TutorialBlock.RuntimeBackground,
161
167
  },
162
168
  0,
163
169
  )
164
170
 
165
171
 
166
172
  @dataclass_transform()
167
- def _runtime_ui[T](cls: type[T]) -> T:
168
- return create_global(
169
- cls,
170
- {
171
- Mode.Play: PlayBlock.RuntimeUI,
172
- Mode.Watch: WatchBlock.RuntimeUI,
173
- Mode.Tutorial: TutorialBlock.RuntimeUI,
174
- Mode.Preview: PreviewBlock.RuntimeUI,
175
- },
176
- 0,
177
- )
173
+ def _play_runtime_ui[T](cls: type[T]) -> T:
174
+ return create_global(cls, {Mode.PLAY: PlayBlock.RuntimeUI}, 0)
178
175
 
179
176
 
180
177
  @dataclass_transform()
181
- def _runtime_ui_configuration[T](cls: type[T]) -> T:
182
- return create_global(
183
- cls,
184
- {
185
- Mode.Play: PlayBlock.RuntimeUIConfiguration,
186
- Mode.Watch: WatchBlock.RuntimeUIConfiguration,
187
- Mode.Tutorial: TutorialBlock.RuntimeUIConfiguration,
188
- Mode.Preview: PreviewBlock.RuntimeUIConfiguration,
189
- },
190
- 0,
191
- )
178
+ def _watch_runtime_ui[T](cls: type[T]) -> T:
179
+ return create_global(cls, {Mode.WATCH: WatchBlock.RuntimeUI}, 0)
180
+
181
+
182
+ @dataclass_transform()
183
+ def _tutorial_runtime_ui[T](cls: type[T]) -> T:
184
+ return create_global(cls, {Mode.TUTORIAL: TutorialBlock.RuntimeUI}, 0)
185
+
186
+
187
+ @dataclass_transform()
188
+ def _preview_runtime_ui[T](cls: type[T]) -> T:
189
+ return create_global(cls, {Mode.PREVIEW: PreviewBlock.RuntimeUI}, 0)
190
+
191
+
192
+ @dataclass_transform()
193
+ def _play_runtime_ui_configuration[T](cls: type[T]) -> T:
194
+ return create_global(cls, {Mode.PLAY: PlayBlock.RuntimeUIConfiguration}, 0)
195
+
196
+
197
+ @dataclass_transform()
198
+ def _watch_runtime_ui_configuration[T](cls: type[T]) -> T:
199
+ return create_global(cls, {Mode.WATCH: WatchBlock.RuntimeUIConfiguration}, 0)
200
+
201
+
202
+ @dataclass_transform()
203
+ def _tutorial_runtime_ui_configuration[T](cls: type[T]) -> T:
204
+ return create_global(cls, {Mode.TUTORIAL: TutorialBlock.RuntimeUIConfiguration}, 0)
205
+
206
+
207
+ @dataclass_transform()
208
+ def _preview_runtime_ui_configuration[T](cls: type[T]) -> T:
209
+ return create_global(cls, {Mode.PREVIEW: PreviewBlock.RuntimeUIConfiguration}, 0)
192
210
 
193
211
 
194
212
  @dataclass_transform()
195
213
  def _tutorial_instruction[T](cls: type[T]) -> T:
196
- return create_global(cls, {Mode.Tutorial: TutorialBlock.TutorialInstruction}, 0)
214
+ return create_global(cls, {Mode.TUTORIAL: TutorialBlock.TutorialInstruction}, 0)
197
215
 
198
216
 
199
217
  @dataclass_transform()
200
218
  def level_memory[T](cls: type[T]) -> T:
201
- return create_global(cls, {Mode.Play: PlayBlock.LevelMemory, Mode.Watch: WatchBlock.LevelMemory}, None)
219
+ return create_global(
220
+ cls,
221
+ {
222
+ Mode.PLAY: PlayBlock.LevelMemory,
223
+ Mode.WATCH: WatchBlock.LevelMemory,
224
+ Mode.TUTORIAL: TutorialBlock.TutorialMemory,
225
+ },
226
+ None,
227
+ )
202
228
 
203
229
 
204
230
  @dataclass_transform()
205
231
  def level_data[T](cls: type[T]) -> T:
206
- return create_global(cls, {Mode.Play: PlayBlock.LevelData, Mode.Watch: WatchBlock.LevelData}, None)
232
+ return create_global(
233
+ cls,
234
+ {
235
+ Mode.PLAY: PlayBlock.LevelData,
236
+ Mode.WATCH: WatchBlock.LevelData,
237
+ Mode.PREVIEW: PreviewBlock.PreviewData,
238
+ Mode.TUTORIAL: TutorialBlock.TutorialData,
239
+ },
240
+ None,
241
+ )
207
242
 
208
243
 
209
244
  # level_option is handled by the options decorator
@@ -212,12 +247,12 @@ def level_data[T](cls: type[T]) -> T:
212
247
 
213
248
  @dataclass_transform()
214
249
  def _level_score[T](cls: type[T]) -> T:
215
- return create_global(cls, {Mode.Play: PlayBlock.LevelScore, Mode.Watch: WatchBlock.LevelScore}, 0)
250
+ return create_global(cls, {Mode.PLAY: PlayBlock.LevelScore, Mode.WATCH: WatchBlock.LevelScore}, 0)
216
251
 
217
252
 
218
253
  @dataclass_transform()
219
254
  def _level_life[T](cls: type[T]) -> T:
220
- return create_global(cls, {Mode.Play: PlayBlock.LevelLife, Mode.Watch: WatchBlock.LevelLife}, 0)
255
+ return create_global(cls, {Mode.PLAY: PlayBlock.LevelLife, Mode.WATCH: WatchBlock.LevelLife}, 0)
221
256
 
222
257
 
223
258
  # engine_rom is handled by the compiler