sonolus.py 0.1.1__py3-none-any.whl → 0.1.3__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/backend/mode.py CHANGED
@@ -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
 
@@ -622,6 +622,8 @@ class Visitor(ast.NodeVisitor):
622
622
 
623
623
  def visit_Call(self, node):
624
624
  fn = self.visit(node.func)
625
+ if fn is Num:
626
+ raise ValueError("Calling int/bool/float is not supported")
625
627
  args = []
626
628
  kwargs = {}
627
629
  for arg in node.args:
@@ -833,7 +835,7 @@ class Visitor(ast.NodeVisitor):
833
835
 
834
836
  def ensure_boolean_num(self, value) -> Num:
835
837
  # This just checks the type for now, although we could support custom __bool__ implementations in the future
836
- if not isinstance(value, Num):
838
+ if not is_num(value):
837
839
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
838
840
  return value
839
841
 
sonolus/build/compile.py CHANGED
@@ -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)
sonolus/build/engine.py CHANGED
@@ -7,12 +7,15 @@ 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
11
10
  from sonolus.script.archetype import BaseArchetype
12
11
  from sonolus.script.bucket import Buckets
13
- from sonolus.script.callbacks import update_spawn_callback
12
+ from sonolus.script.callbacks import navigate_callback, preprocess_callback, update_callback, update_spawn_callback
14
13
  from sonolus.script.effect import Effects
15
14
  from sonolus.script.engine import EngineData
15
+ from sonolus.script.instruction import (
16
+ TutorialInstructionIcons,
17
+ TutorialInstructions,
18
+ )
16
19
  from sonolus.script.internal.context import ReadOnlyMemory
17
20
  from sonolus.script.options import Options
18
21
  from sonolus.script.particle import Particles
@@ -61,12 +64,28 @@ def package_engine(engine: EngineData):
61
64
  rom=rom,
62
65
  update_spawn=engine.watch.update_spawn,
63
66
  )
67
+ preview_data = build_preview_mode(
68
+ archetypes=engine.preview.archetypes,
69
+ skin=engine.preview.skin,
70
+ rom=rom,
71
+ )
72
+ tutorial_data = build_tutorial_mode(
73
+ skin=engine.tutorial.skin,
74
+ effects=engine.tutorial.effects,
75
+ particles=engine.tutorial.particles,
76
+ instructions=engine.tutorial.instructions,
77
+ instruction_icons=engine.tutorial.instruction_icons,
78
+ preprocess=engine.tutorial.preprocess,
79
+ navigate=engine.tutorial.navigate,
80
+ update=engine.tutorial.update,
81
+ rom=rom,
82
+ )
64
83
  return PackagedEngine(
65
84
  configuration=package_output(configuration),
66
85
  play_data=package_output(play_data),
67
86
  watch_data=package_output(watch_data),
68
- preview_data=package_output(EMPTY_ENGINE_PREVIEW_DATA),
69
- tutorial_data=package_output(EMPTY_ENGINE_TUTORIAL_DATA),
87
+ preview_data=package_output(preview_data),
88
+ tutorial_data=package_output(tutorial_data),
70
89
  rom=package_rom(rom),
71
90
  )
72
91
 
@@ -90,7 +109,7 @@ def build_play_mode(
90
109
  rom: ReadOnlyMemory,
91
110
  ):
92
111
  return {
93
- **compile_mode(mode=Mode.Play, rom=rom, archetypes=archetypes, global_callbacks=None),
112
+ **compile_mode(mode=Mode.PLAY, rom=rom, archetypes=archetypes, global_callbacks=None),
94
113
  "skin": build_skin(skin),
95
114
  "effect": build_effects(effects),
96
115
  "particle": build_particles(particles),
@@ -109,7 +128,7 @@ def build_watch_mode(
109
128
  ):
110
129
  return {
111
130
  **compile_mode(
112
- mode=Mode.Watch, rom=rom, archetypes=archetypes, global_callbacks=[(update_spawn_callback, update_spawn)]
131
+ mode=Mode.WATCH, rom=rom, archetypes=archetypes, global_callbacks=[(update_spawn_callback, update_spawn)]
113
132
  ),
114
133
  "skin": build_skin(skin),
115
134
  "effect": build_effects(effects),
@@ -118,6 +137,46 @@ def build_watch_mode(
118
137
  }
119
138
 
120
139
 
140
+ def build_preview_mode(
141
+ archetypes: list[type[BaseArchetype]],
142
+ skin: Skin,
143
+ rom: ReadOnlyMemory,
144
+ ):
145
+ return {
146
+ **compile_mode(mode=Mode.PREVIEW, rom=rom, archetypes=archetypes, global_callbacks=None),
147
+ "skin": build_skin(skin),
148
+ }
149
+
150
+
151
+ def build_tutorial_mode(
152
+ skin: Skin,
153
+ effects: Effects,
154
+ particles: Particles,
155
+ instructions: TutorialInstructions,
156
+ instruction_icons: TutorialInstructionIcons,
157
+ preprocess: Callable[[], None],
158
+ navigate: Callable[[int], None],
159
+ update: Callable[[], None],
160
+ rom: ReadOnlyMemory,
161
+ ):
162
+ return {
163
+ **compile_mode(
164
+ mode=Mode.TUTORIAL,
165
+ rom=rom,
166
+ archetypes=[],
167
+ global_callbacks=[
168
+ (preprocess_callback, preprocess),
169
+ (navigate_callback, navigate),
170
+ (update_callback, update),
171
+ ],
172
+ ),
173
+ "skin": build_skin(skin),
174
+ "effect": build_effects(effects),
175
+ "particle": build_particles(particles),
176
+ "instruction": build_instructions(instructions, instruction_icons),
177
+ }
178
+
179
+
121
180
  def build_skin(skin: Skin) -> JsonValue:
122
181
  return {"sprites": [{"name": name, "id": i} for i, name in enumerate(skin._sprites_)]}
123
182
 
@@ -134,6 +193,13 @@ def build_buckets(buckets: Buckets) -> JsonValue:
134
193
  return [bucket.to_dict() for bucket in buckets._buckets_]
135
194
 
136
195
 
196
+ def build_instructions(instructions: TutorialInstructions, instruction_icons: TutorialInstructionIcons) -> JsonValue:
197
+ return {
198
+ "texts": [{"name": name, "id": i} for i, name in enumerate(instructions._instructions_)],
199
+ "icons": [{"name": name, "id": i} for i, name in enumerate(instruction_icons._instruction_icons_)],
200
+ }
201
+
202
+
137
203
  def package_rom(rom: ReadOnlyMemory) -> bytes:
138
204
  values = rom.values or [0]
139
205
  output = bytearray()
@@ -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"
sonolus/script/bucket.py CHANGED
@@ -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,24 @@ 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
+ )
84
+ navigate_callback = CallbackInfo(
85
+ name="navigate",
86
+ py_name="navigate",
87
+ supports_order=False,
88
+ returns_value=False,
89
+ )
90
+ update_callback = CallbackInfo(
91
+ name="update",
92
+ py_name="update",
93
+ supports_order=False,
94
+ returns_value=False,
95
+ )
78
96
 
79
97
 
80
98
  def _by_name(*callbacks: CallbackInfo) -> dict[str, CallbackInfo]:
@@ -103,3 +121,7 @@ WATCH_ARCHETYPE_CALLBACKS = _by_name(
103
121
  WATCH_GLOBAL_CALLBACKS = _by_name(
104
122
  update_spawn_callback,
105
123
  )
124
+ PREVIEW_CALLBACKS = _by_name(
125
+ preprocess_callback,
126
+ render_callback,
127
+ )
@@ -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
sonolus/script/debug.py CHANGED
@@ -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)
sonolus/script/effect.py CHANGED
@@ -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
sonolus/script/engine.py CHANGED
@@ -1,11 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
+ from typing import Any
4
5
 
5
6
  from sonolus.build.collection import Asset
6
- from sonolus.script.archetype import BaseArchetype
7
+ from sonolus.script.archetype import BaseArchetype, PlayArchetype, PreviewArchetype, WatchArchetype
7
8
  from sonolus.script.bucket import Buckets, EmptyBuckets
8
9
  from sonolus.script.effect import Effects, EmptyEffects
10
+ from sonolus.script.instruction import (
11
+ EmptyInstructionIcons,
12
+ EmptyInstructions,
13
+ TutorialInstructionIcons,
14
+ TutorialInstructions,
15
+ )
9
16
  from sonolus.script.options import EmptyOptions, Options
10
17
  from sonolus.script.particle import EmptyParticles, Particles
11
18
  from sonolus.script.sprite import EmptySkin, Skin
@@ -41,7 +48,7 @@ class Engine:
41
48
  self.data = data
42
49
 
43
50
 
44
- def default_callback() -> float:
51
+ def default_callback() -> Any:
45
52
  return 0.0
46
53
 
47
54
 
@@ -61,6 +68,10 @@ class PlayMode:
61
68
  self.particles = particles
62
69
  self.buckets = buckets
63
70
 
71
+ for archetype in self.archetypes:
72
+ if not issubclass(archetype, PlayArchetype):
73
+ raise ValueError(f"archetype {archetype} is not a PlayArchetype")
74
+
64
75
 
65
76
  class WatchMode:
66
77
  def __init__(
@@ -80,13 +91,50 @@ class WatchMode:
80
91
  self.buckets = buckets
81
92
  self.update_spawn = update_spawn
82
93
 
94
+ for archetype in self.archetypes:
95
+ if not issubclass(archetype, WatchArchetype):
96
+ raise ValueError(f"archetype {archetype} is not a PlayArchetype")
97
+
98
+
99
+ class PreviewMode:
100
+ def __init__(
101
+ self,
102
+ *,
103
+ archetypes: list[type[BaseArchetype]] | None = None,
104
+ skin: Skin = EmptySkin,
105
+ ) -> None:
106
+ self.archetypes = archetypes or []
107
+ self.skin = skin
108
+
109
+ for archetype in self.archetypes:
110
+ if not issubclass(archetype, PreviewArchetype):
111
+ raise ValueError(f"archetype {archetype} is not a BaseArchetype")
83
112
 
84
- class EngineData:
85
- ui: UiConfig
86
- options: Options
87
- play: PlayMode
88
- watch: WatchMode
89
113
 
114
+ class TutorialMode:
115
+ def __init__(
116
+ self,
117
+ *,
118
+ skin: Skin = EmptySkin,
119
+ effects: Effects = EmptyEffects,
120
+ particles: Particles = EmptyParticles,
121
+ instructions: TutorialInstructions = EmptyInstructions,
122
+ instruction_icons: TutorialInstructionIcons = EmptyInstructionIcons,
123
+ preprocess: Callable[[], None],
124
+ navigate: Callable[[], None],
125
+ update: Callable[[], None],
126
+ ) -> None:
127
+ self.skin = skin
128
+ self.effects = effects
129
+ self.particles = particles
130
+ self.instructions = instructions
131
+ self.instruction_icons = instruction_icons
132
+ self.preprocess = preprocess
133
+ self.navigate = navigate
134
+ self.update = update
135
+
136
+
137
+ class EngineData:
90
138
  def __init__(
91
139
  self,
92
140
  *,
@@ -94,8 +142,16 @@ class EngineData:
94
142
  options: Options = EmptyOptions,
95
143
  play: PlayMode | None = None,
96
144
  watch: WatchMode | None = None,
145
+ preview: PreviewMode | None = None,
146
+ tutorial: TutorialMode | None = None,
97
147
  ) -> None:
98
148
  self.ui = ui or UiConfig()
99
149
  self.options = options
100
150
  self.play = play or PlayMode()
101
151
  self.watch = watch or WatchMode(update_spawn=default_callback)
152
+ self.preview = preview or PreviewMode()
153
+ self.tutorial = tutorial or TutorialMode(
154
+ preprocess=default_callback,
155
+ navigate=default_callback,
156
+ update=default_callback,
157
+ )