sonolus.py 0.1.4__py3-none-any.whl → 0.1.6__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.

Files changed (79) hide show
  1. sonolus/backend/finalize.py +18 -10
  2. sonolus/backend/interpret.py +7 -7
  3. sonolus/backend/ir.py +24 -0
  4. sonolus/backend/optimize/__init__.py +0 -0
  5. sonolus/backend/{allocate.py → optimize/allocate.py} +4 -3
  6. sonolus/backend/{constant_evaluation.py → optimize/constant_evaluation.py} +7 -7
  7. sonolus/backend/{coalesce.py → optimize/copy_coalesce.py} +3 -3
  8. sonolus/backend/optimize/dead_code.py +185 -0
  9. sonolus/backend/{dominance.py → optimize/dominance.py} +2 -17
  10. sonolus/backend/{flow.py → optimize/flow.py} +6 -5
  11. sonolus/backend/{inlining.py → optimize/inlining.py} +4 -17
  12. sonolus/backend/{liveness.py → optimize/liveness.py} +69 -65
  13. sonolus/backend/optimize/optimize.py +44 -0
  14. sonolus/backend/{passes.py → optimize/passes.py} +1 -1
  15. sonolus/backend/optimize/simplify.py +191 -0
  16. sonolus/backend/{ssa.py → optimize/ssa.py} +31 -18
  17. sonolus/backend/place.py +17 -25
  18. sonolus/backend/utils.py +10 -0
  19. sonolus/backend/visitor.py +360 -101
  20. sonolus/build/cli.py +14 -3
  21. sonolus/build/compile.py +8 -8
  22. sonolus/build/engine.py +10 -5
  23. sonolus/build/project.py +30 -1
  24. sonolus/script/archetype.py +429 -138
  25. sonolus/script/array.py +25 -8
  26. sonolus/script/array_like.py +297 -0
  27. sonolus/script/bucket.py +73 -11
  28. sonolus/script/containers.py +234 -51
  29. sonolus/script/debug.py +8 -8
  30. sonolus/script/easing.py +147 -105
  31. sonolus/script/effect.py +60 -0
  32. sonolus/script/engine.py +71 -4
  33. sonolus/script/globals.py +66 -32
  34. sonolus/script/instruction.py +79 -25
  35. sonolus/script/internal/builtin_impls.py +138 -27
  36. sonolus/script/internal/constant.py +139 -0
  37. sonolus/script/internal/context.py +14 -5
  38. sonolus/script/internal/dict_impl.py +65 -0
  39. sonolus/script/internal/generic.py +6 -9
  40. sonolus/script/internal/impl.py +38 -13
  41. sonolus/script/internal/introspection.py +5 -2
  42. sonolus/script/{math.py → internal/math_impls.py} +28 -28
  43. sonolus/script/internal/native.py +3 -3
  44. sonolus/script/internal/random.py +67 -0
  45. sonolus/script/internal/range.py +81 -0
  46. sonolus/script/internal/transient.py +51 -0
  47. sonolus/script/internal/tuple_impl.py +113 -0
  48. sonolus/script/interval.py +234 -16
  49. sonolus/script/iterator.py +120 -167
  50. sonolus/script/level.py +24 -0
  51. sonolus/script/num.py +79 -47
  52. sonolus/script/options.py +78 -12
  53. sonolus/script/particle.py +37 -4
  54. sonolus/script/pointer.py +4 -4
  55. sonolus/script/print.py +22 -1
  56. sonolus/script/project.py +59 -0
  57. sonolus/script/{graphics.py → quad.py} +75 -12
  58. sonolus/script/record.py +44 -13
  59. sonolus/script/runtime.py +50 -1
  60. sonolus/script/sprite.py +198 -115
  61. sonolus/script/text.py +2 -0
  62. sonolus/script/timing.py +72 -0
  63. sonolus/script/transform.py +296 -66
  64. sonolus/script/ui.py +134 -78
  65. sonolus/script/values.py +6 -13
  66. sonolus/script/vec.py +118 -3
  67. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/METADATA +1 -1
  68. sonolus_py-0.1.6.dist-info/RECORD +89 -0
  69. sonolus/backend/dead_code.py +0 -80
  70. sonolus/backend/optimize.py +0 -37
  71. sonolus/backend/simplify.py +0 -47
  72. sonolus/script/comptime.py +0 -160
  73. sonolus/script/random.py +0 -14
  74. sonolus/script/range.py +0 -58
  75. sonolus_py-0.1.4.dist-info/RECORD +0 -84
  76. /sonolus/script/{callbacks.py → internal/callbacks.py} +0 -0
  77. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/WHEEL +0 -0
  78. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/entry_points.txt +0 -0
  79. {sonolus_py-0.1.4.dist-info → sonolus_py-0.1.6.dist-info}/licenses/LICENSE +0 -0
@@ -5,14 +5,14 @@ from collections.abc import Callable
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum, StrEnum
7
7
  from types import FunctionType
8
- from typing import Annotated, Any, ClassVar, Self, get_origin
8
+ from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
9
9
 
10
10
  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.backend.place import BlockPlace
14
14
  from sonolus.script.bucket import Bucket, Judgment
15
- from sonolus.script.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
15
+ from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
16
16
  from sonolus.script.internal.context import ctx
17
17
  from sonolus.script.internal.descriptor import SonolusDescriptor
18
18
  from sonolus.script.internal.generic import validate_concrete_type
@@ -21,16 +21,16 @@ from sonolus.script.internal.introspection import get_field_specifiers
21
21
  from sonolus.script.internal.native import native_call
22
22
  from sonolus.script.internal.value import Value
23
23
  from sonolus.script.num import Num
24
- from sonolus.script.pointer import deref
24
+ from sonolus.script.pointer import _deref
25
25
  from sonolus.script.record import Record
26
26
  from sonolus.script.values import zeros
27
27
 
28
- ENTITY_MEMORY_SIZE = 64
29
- ENTITY_DATA_SIZE = 32
30
- ENTITY_SHARED_MEMORY_SIZE = 32
28
+ _ENTITY_MEMORY_SIZE = 64
29
+ _ENTITY_DATA_SIZE = 32
30
+ _ENTITY_SHARED_MEMORY_SIZE = 32
31
31
 
32
32
 
33
- class StorageType(Enum):
33
+ class _StorageType(Enum):
34
34
  IMPORTED = "imported"
35
35
  EXPORTED = "exported"
36
36
  MEMORY = "memory"
@@ -38,53 +38,55 @@ class StorageType(Enum):
38
38
 
39
39
 
40
40
  @dataclass
41
- class ArchetypeFieldInfo:
41
+ class _ArchetypeFieldInfo:
42
42
  name: str | None
43
- storage: StorageType
43
+ storage: _StorageType
44
44
 
45
45
 
46
- class ArchetypeField(SonolusDescriptor):
47
- def __init__(self, name: str, data_name: str, storage: StorageType, offset: int, type_: type[Value]):
46
+ class _ArchetypeField(SonolusDescriptor):
47
+ def __init__(self, name: str, data_name: str, storage: _StorageType, offset: int, type_: type[Value]):
48
48
  self.name = name
49
49
  self.data_name = data_name # name used in level data
50
50
  self.storage = storage
51
51
  self.offset = offset
52
52
  self.type = type_
53
53
 
54
- def __get__(self, instance: BaseArchetype, owner):
54
+ def __get__(self, instance: _BaseArchetype, owner):
55
55
  if instance is None:
56
56
  return self
57
57
  result = None
58
58
  match self.storage:
59
- case StorageType.IMPORTED:
59
+ case _StorageType.IMPORTED:
60
60
  match instance._data_:
61
- case ArchetypeSelfData():
62
- result = deref(ctx().blocks.EntityData, self.offset, self.type)
63
- case ArchetypeReferenceData(index=index):
64
- result = deref(ctx().blocks.EntityDataArray, self.offset + index * ENTITY_DATA_SIZE, self.type)
65
- case ArchetypeLevelData(values=values):
61
+ case _ArchetypeSelfData():
62
+ result = _deref(ctx().blocks.EntityData, self.offset, self.type)
63
+ case _ArchetypeReferenceData(index=index):
64
+ result = _deref(
65
+ ctx().blocks.EntityDataArray, self.offset + index * _ENTITY_DATA_SIZE, self.type
66
+ )
67
+ case _ArchetypeLevelData(values=values):
66
68
  result = values[self.name]
67
- case StorageType.EXPORTED:
69
+ case _StorageType.EXPORTED:
68
70
  raise RuntimeError("Exported fields are write-only")
69
- case StorageType.MEMORY:
71
+ case _StorageType.MEMORY:
70
72
  match instance._data_:
71
- case ArchetypeSelfData():
72
- result = deref(ctx().blocks.EntityMemory, self.offset, self.type)
73
- case ArchetypeReferenceData():
73
+ case _ArchetypeSelfData():
74
+ result = _deref(ctx().blocks.EntityMemory, self.offset, self.type)
75
+ case _ArchetypeReferenceData():
74
76
  raise RuntimeError("Entity memory of other entities is not accessible")
75
- case ArchetypeLevelData():
77
+ case _ArchetypeLevelData():
76
78
  raise RuntimeError("Entity memory is not available in level data")
77
- case StorageType.SHARED:
79
+ case _StorageType.SHARED:
78
80
  match instance._data_:
79
- case ArchetypeSelfData():
80
- result = deref(ctx().blocks.EntitySharedMemory, self.offset, self.type)
81
- case ArchetypeReferenceData(index=index):
82
- result = deref(
81
+ case _ArchetypeSelfData():
82
+ result = _deref(ctx().blocks.EntitySharedMemory, self.offset, self.type)
83
+ case _ArchetypeReferenceData(index=index):
84
+ result = _deref(
83
85
  ctx().blocks.EntitySharedMemoryArray,
84
- Num._accept_(self.offset) + index * ENTITY_SHARED_MEMORY_SIZE,
86
+ Num._accept_(self.offset) + index * _ENTITY_SHARED_MEMORY_SIZE,
85
87
  self.type,
86
88
  )
87
- case ArchetypeLevelData():
89
+ case _ArchetypeLevelData():
88
90
  raise RuntimeError("Entity shared memory is not available in level data")
89
91
  if result is None:
90
92
  raise RuntimeError("Invalid storage type")
@@ -93,53 +95,55 @@ class ArchetypeField(SonolusDescriptor):
93
95
  else:
94
96
  return result._as_py_()
95
97
 
96
- def __set__(self, instance: BaseArchetype, value):
98
+ def __set__(self, instance: _BaseArchetype, value):
97
99
  if instance is None:
98
100
  raise RuntimeError("Cannot set field on class")
99
101
  if not self.type._accepts_(value):
100
102
  raise TypeError(f"Expected {self.type}, got {type(value)}")
101
103
  target = None
102
104
  match self.storage:
103
- case StorageType.IMPORTED:
105
+ case _StorageType.IMPORTED:
104
106
  match instance._data_:
105
- case ArchetypeSelfData():
106
- target = deref(ctx().blocks.EntityData, self.offset, self.type)
107
- case ArchetypeReferenceData(index=index):
108
- target = deref(ctx().blocks.EntityDataArray, self.offset + index * ENTITY_DATA_SIZE, self.type)
109
- case ArchetypeLevelData(values=values):
107
+ case _ArchetypeSelfData():
108
+ target = _deref(ctx().blocks.EntityData, self.offset, self.type)
109
+ case _ArchetypeReferenceData(index=index):
110
+ target = _deref(
111
+ ctx().blocks.EntityDataArray, self.offset + index * _ENTITY_DATA_SIZE, self.type
112
+ )
113
+ case _ArchetypeLevelData(values=values):
110
114
  target = values[self.name]
111
- case StorageType.EXPORTED:
115
+ case _StorageType.EXPORTED:
112
116
  match instance._data_:
113
- case ArchetypeSelfData():
117
+ case _ArchetypeSelfData():
114
118
  if not isinstance(value, self.type):
115
119
  raise TypeError(f"Expected {self.type}, got {type(value)}")
116
120
  for k, v in value._to_flat_dict_(self.data_name).items():
117
121
  index = instance._exported_keys_[k]
118
122
  ctx().add_statements(IRInstr(Op.ExportValue, [IRConst(index), Num._accept_(v).ir()]))
119
123
  return
120
- case ArchetypeReferenceData():
124
+ case _ArchetypeReferenceData():
121
125
  raise RuntimeError("Exported fields of other entities are not accessible")
122
- case ArchetypeLevelData():
126
+ case _ArchetypeLevelData():
123
127
  raise RuntimeError("Exported fields are not available in level data")
124
- case StorageType.MEMORY:
128
+ case _StorageType.MEMORY:
125
129
  match instance._data_:
126
- case ArchetypeSelfData():
127
- target = deref(ctx().blocks.EntityMemory, self.offset, self.type)
128
- case ArchetypeReferenceData():
130
+ case _ArchetypeSelfData():
131
+ target = _deref(ctx().blocks.EntityMemory, self.offset, self.type)
132
+ case _ArchetypeReferenceData():
129
133
  raise RuntimeError("Entity memory of other entities is not accessible")
130
- case ArchetypeLevelData():
134
+ case _ArchetypeLevelData():
131
135
  raise RuntimeError("Entity memory is not available in level data")
132
- case StorageType.SHARED:
136
+ case _StorageType.SHARED:
133
137
  match instance._data_:
134
- case ArchetypeSelfData():
135
- target = deref(ctx().blocks.EntitySharedMemory, self.offset, self.type)
136
- case ArchetypeReferenceData(index=index):
137
- target = deref(
138
+ case _ArchetypeSelfData():
139
+ target = _deref(ctx().blocks.EntitySharedMemory, self.offset, self.type)
140
+ case _ArchetypeReferenceData(index=index):
141
+ target = _deref(
138
142
  ctx().blocks.EntitySharedMemoryArray,
139
- Num._accept_(self.offset) + index * ENTITY_SHARED_MEMORY_SIZE,
143
+ Num._accept_(self.offset) + index * _ENTITY_SHARED_MEMORY_SIZE,
140
144
  self.type,
141
145
  )
142
- case ArchetypeLevelData():
146
+ case _ArchetypeLevelData():
143
147
  raise RuntimeError("Entity shared memory is not available in level data")
144
148
  if target is None:
145
149
  raise RuntimeError("Invalid storage type")
@@ -151,22 +155,75 @@ class ArchetypeField(SonolusDescriptor):
151
155
 
152
156
 
153
157
  def imported(*, name: str | None = None) -> Any:
154
- return ArchetypeFieldInfo(name, StorageType.IMPORTED)
158
+ """Declare a field as imported.
159
+
160
+ Imported fields may be loaded from the level data.
161
+
162
+ In watch mode, data may also be loaded from a corresponding exported field in play mode.
163
+
164
+ Imported fields may only be updated in the `preprocess` callback, and are read-only in other callbacks.
165
+
166
+ Usage:
167
+ ```
168
+ class MyArchetype(PlayArchetype):
169
+ field: int = imported()
170
+ field_with_explicit_name: int = imported(name="field_name")
171
+ ```
172
+ """
173
+ return _ArchetypeFieldInfo(name, _StorageType.IMPORTED)
155
174
 
156
175
 
157
176
  def exported(*, name: str | None = None) -> Any:
158
- return ArchetypeFieldInfo(name, StorageType.EXPORTED)
177
+ """Declare a field as exported.
178
+
179
+ This is only usable in play mode to export data to be loaded in watch mode.
180
+
181
+ Exported fields are write-only.
182
+
183
+ Usage:
184
+ ```
185
+ class MyArchetype(PlayArchetype):
186
+ field: int = exported()
187
+ field_with_explicit_name: int = exported(name="#FIELD")
188
+ ```
189
+ """
190
+ return _ArchetypeFieldInfo(name, _StorageType.EXPORTED)
159
191
 
160
192
 
161
193
  def entity_memory() -> Any:
162
- return ArchetypeFieldInfo(None, StorageType.MEMORY)
194
+ """Declare a field as entity memory.
195
+
196
+ Entity memory is private to the entity and is not accessible from other entities.
197
+
198
+ Entity memory fields may also be set when an entity is spawned using the `spawn()` method.
199
+
200
+ Usage:
201
+ ```
202
+ class MyArchetype(PlayArchetype):
203
+ field: int = entity_memory()
204
+
205
+ ```
206
+ """
207
+ return _ArchetypeFieldInfo(None, _StorageType.MEMORY)
163
208
 
164
209
 
165
210
  def shared_memory() -> Any:
166
- return ArchetypeFieldInfo(None, StorageType.SHARED)
211
+ """Declare a field as shared memory.
212
+
213
+ Shared memory is accessible from other entities.
214
+
215
+ Shared memory may only be updated by sequential callbacks such as `preprocess`, `update_sequential`, and `touch`.
216
+
217
+ Usage:
218
+ ```
219
+ class MyArchetype(PlayArchetype):
220
+ field: int = shared_memory()
221
+ ```
222
+ """
223
+ return _ArchetypeFieldInfo(None, _StorageType.SHARED)
167
224
 
168
225
 
169
- _annotation_defaults: dict[Callable, ArchetypeFieldInfo] = {
226
+ _annotation_defaults: dict[Callable, _ArchetypeFieldInfo] = {
170
227
  imported: imported(),
171
228
  exported: exported(),
172
229
  entity_memory: entity_memory(),
@@ -175,14 +232,53 @@ _annotation_defaults: dict[Callable, ArchetypeFieldInfo] = {
175
232
 
176
233
 
177
234
  class StandardImport:
235
+ """Standard import annotations for Archetype fields.
236
+
237
+ Usage:
238
+ ```
239
+ class MyArchetype(WatchArchetype):
240
+ judgment: StandardImport.JUDGMENT
241
+ ```
242
+ """
243
+
178
244
  BEAT = Annotated[float, imported(name="#BEAT")]
245
+ """The beat of the entity."""
246
+
179
247
  BPM = Annotated[float, imported(name="#BPM")]
248
+ """The bpm, for bpm change markers."""
249
+
180
250
  TIMESCALE = Annotated[float, imported(name="#TIMESCALE")]
251
+ """The timescale, for timescale change markers."""
252
+
181
253
  JUDGMENT = Annotated[int, imported(name="#JUDGMENT")]
254
+ """The judgment of the entity.
255
+
256
+ Automatically supported in watch mode for archetypes with a corresponding scored play mode archetype.
257
+ """
182
258
  ACCURACY = Annotated[float, imported(name="#ACCURACY")]
259
+ """The accuracy of the entity.
260
+
261
+ Automatically supported in watch mode for archetypes with a corresponding scored play mode archetype.
262
+ """
263
+
264
+
265
+ def callback[T: Callable](*, order: int = 0) -> Callable[[T], T]:
266
+ """Annotate a callback with its order.
267
+
268
+ Callbacks are execute from lowest to highest order. By default, callbacks have an order of 0.
269
+
270
+ Usage:
271
+ ```
272
+ class MyArchetype(PlayArchetype):
273
+ @callback(order=1)
274
+ def update_sequential(self):
275
+ pass
276
+ ```
183
277
 
278
+ Args:
279
+ order: The order of the callback. Lower values are executed first.
280
+ """
184
281
 
185
- def callback[T: Callable](order: int) -> Callable[[T], T]:
186
282
  def decorator(func: T) -> T:
187
283
  func._callback_order_ = order
188
284
  return func
@@ -190,37 +286,42 @@ def callback[T: Callable](order: int) -> Callable[[T], T]:
190
286
  return decorator
191
287
 
192
288
 
193
- class ArchetypeSelfData:
289
+ class _ArchetypeSelfData:
194
290
  pass
195
291
 
196
292
 
197
- class ArchetypeReferenceData:
293
+ class _ArchetypeReferenceData:
198
294
  index: Num
199
295
 
200
296
  def __init__(self, index: Num):
201
297
  self.index = index
202
298
 
203
299
 
204
- class ArchetypeLevelData:
300
+ class _ArchetypeLevelData:
205
301
  values: dict[str, Value]
206
302
 
207
303
  def __init__(self, values: dict[str, Value]):
208
304
  self.values = values
209
305
 
210
306
 
211
- type ArchetypeData = ArchetypeSelfData | ArchetypeReferenceData | ArchetypeLevelData
307
+ type _ArchetypeData = _ArchetypeSelfData | _ArchetypeReferenceData | _ArchetypeLevelData
212
308
 
213
309
 
214
- class BaseArchetype:
310
+ class ArchetypeSchema(TypedDict):
311
+ name: str
312
+ fields: list[str]
313
+
314
+
315
+ class _BaseArchetype:
215
316
  _is_comptime_value_ = True
216
317
 
217
318
  _supported_callbacks_: ClassVar[dict[str, CallbackInfo]]
218
319
  _default_callbacks_: ClassVar[set[Callable]]
219
320
 
220
- _imported_fields_: ClassVar[dict[str, ArchetypeField]]
221
- _exported_fields_: ClassVar[dict[str, ArchetypeField]]
222
- _memory_fields_: ClassVar[dict[str, ArchetypeField]]
223
- _shared_memory_fields_: ClassVar[dict[str, ArchetypeField]]
321
+ _imported_fields_: ClassVar[dict[str, _ArchetypeField]]
322
+ _exported_fields_: ClassVar[dict[str, _ArchetypeField]]
323
+ _memory_fields_: ClassVar[dict[str, _ArchetypeField]]
324
+ _shared_memory_fields_: ClassVar[dict[str, _ArchetypeField]]
224
325
 
225
326
  _imported_keys_: ClassVar[dict[str, int]]
226
327
  _exported_keys_: ClassVar[dict[str, int]]
@@ -228,9 +329,16 @@ class BaseArchetype:
228
329
  _data_constructor_signature_: ClassVar[inspect.Signature]
229
330
  _spawn_signature_: ClassVar[inspect.Signature]
230
331
 
231
- _data_: ArchetypeData
332
+ _data_: _ArchetypeData
232
333
 
233
334
  name: ClassVar[str | None] = None
335
+ """The name of the archetype.
336
+
337
+ If not set, the name will be the class name.
338
+
339
+ The name is used in level data.
340
+ """
341
+
234
342
  is_scored: ClassVar[bool] = False
235
343
 
236
344
  def __init__(self, *args, **kwargs):
@@ -242,7 +350,7 @@ class BaseArchetype:
242
350
  field.name: field.type._accept_(bound.arguments.get(field.name) or zeros(field.type))._get_()
243
351
  for field in self._imported_fields_.values()
244
352
  }
245
- self._data_ = ArchetypeLevelData(values=values)
353
+ self._data_ = _ArchetypeLevelData(values=values)
246
354
 
247
355
  @classmethod
248
356
  def _new(cls):
@@ -251,14 +359,14 @@ class BaseArchetype:
251
359
  @classmethod
252
360
  def _for_compilation(cls):
253
361
  result = cls._new()
254
- result._data_ = ArchetypeSelfData()
362
+ result._data_ = _ArchetypeSelfData()
255
363
  return result
256
364
 
257
365
  @classmethod
258
366
  @meta_fn
259
367
  def at(cls, index: Num) -> Self:
260
368
  result = cls._new()
261
- result._data_ = ArchetypeReferenceData(index=Num._accept_(index))
369
+ result._data_ = _ArchetypeReferenceData(index=Num._accept_(index))
262
370
  return result
263
371
 
264
372
  @classmethod
@@ -273,7 +381,21 @@ class BaseArchetype:
273
381
 
274
382
  @classmethod
275
383
  @meta_fn
276
- def spawn(cls, **kwargs):
384
+ def spawn(cls, **kwargs: Any) -> None:
385
+ """Spawn an entity of this archetype, injecting the given values into entity memory.
386
+
387
+ Usage:
388
+ ```
389
+ class MyArchetype(PlayArchetype):
390
+ field: int = entity_memory()
391
+
392
+ def f():
393
+ MyArchetype.spawn(field=123)
394
+ ```
395
+
396
+ Args:
397
+ **kwargs: Entity memory values to inject by field name as defined in the Archetype.
398
+ """
277
399
  if not ctx():
278
400
  raise RuntimeError("Spawn is only allowed within a callback")
279
401
  archetype_id = cls.id()
@@ -284,8 +406,12 @@ class BaseArchetype:
284
406
  data.extend(field.type._accept_(bound.arguments[field.name] or zeros(field.type))._to_list_())
285
407
  native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
286
408
 
409
+ @classmethod
410
+ def schema(cls) -> ArchetypeSchema:
411
+ return {"name": cls.name or "unnamed", "fields": list(cls._imported_fields_)}
412
+
287
413
  def _level_data_entries(self, level_refs: dict[Any, int] | None = None):
288
- if not isinstance(self._data_, ArchetypeLevelData):
414
+ if not isinstance(self._data_, _ArchetypeLevelData):
289
415
  raise RuntimeError("Entity is not level data")
290
416
  entries = []
291
417
  for name, value in self._data_.values.items():
@@ -295,7 +421,7 @@ class BaseArchetype:
295
421
  return entries
296
422
 
297
423
  def __init_subclass__(cls, **kwargs):
298
- if cls.__module__ == BaseArchetype.__module__:
424
+ if cls.__module__ == _BaseArchetype.__module__:
299
425
  if cls._supported_callbacks_ is None:
300
426
  raise TypeError("Cannot directly subclass Archetype, use the Archetype subclass for your mode")
301
427
  cls._default_callbacks_ = {getattr(cls, cb_info.py_name) for cb_info in cls._supported_callbacks_.values()}
@@ -304,6 +430,7 @@ class BaseArchetype:
304
430
  raise TypeError("Cannot subclass Archetypes")
305
431
  if cls.name is None:
306
432
  cls.name = cls.__name__
433
+ field_specifiers = get_field_specifiers(cls, skip={"name", "is_scored"}).items()
307
434
  cls._imported_fields_ = {}
308
435
  cls._exported_fields_ = {}
309
436
  cls._memory_fields_ = {}
@@ -312,7 +439,7 @@ class BaseArchetype:
312
439
  exported_offset = 0
313
440
  memory_offset = 0
314
441
  shared_memory_offset = 0
315
- for name, value in get_field_specifiers(cls).items():
442
+ for name, value in field_specifiers:
316
443
  if value is ClassVar or get_origin(value) is ClassVar:
317
444
  continue
318
445
  if get_origin(value) is not Annotated:
@@ -323,7 +450,7 @@ class BaseArchetype:
323
450
  for metadata in value.__metadata__:
324
451
  if isinstance(metadata, FunctionType):
325
452
  metadata = _annotation_defaults.get(metadata, metadata)
326
- if isinstance(metadata, ArchetypeFieldInfo):
453
+ if isinstance(metadata, _ArchetypeFieldInfo):
327
454
  if field_info is not None:
328
455
  raise TypeError(
329
456
  f"Unexpected multiple field annotations for '{name}', "
@@ -337,26 +464,26 @@ class BaseArchetype:
337
464
  )
338
465
  field_type = validate_concrete_type(value.__args__[0])
339
466
  match field_info.storage:
340
- case StorageType.IMPORTED:
341
- cls._imported_fields_[name] = ArchetypeField(
467
+ case _StorageType.IMPORTED:
468
+ cls._imported_fields_[name] = _ArchetypeField(
342
469
  name, field_info.name or name, field_info.storage, imported_offset, field_type
343
470
  )
344
471
  imported_offset += field_type._size_()
345
472
  setattr(cls, name, cls._imported_fields_[name])
346
- case StorageType.EXPORTED:
347
- cls._exported_fields_[name] = ArchetypeField(
473
+ case _StorageType.EXPORTED:
474
+ cls._exported_fields_[name] = _ArchetypeField(
348
475
  name, field_info.name or name, field_info.storage, exported_offset, field_type
349
476
  )
350
477
  exported_offset += field_type._size_()
351
478
  setattr(cls, name, cls._exported_fields_[name])
352
- case StorageType.MEMORY:
353
- cls._memory_fields_[name] = ArchetypeField(
479
+ case _StorageType.MEMORY:
480
+ cls._memory_fields_[name] = _ArchetypeField(
354
481
  name, field_info.name or name, field_info.storage, memory_offset, field_type
355
482
  )
356
483
  memory_offset += field_type._size_()
357
484
  setattr(cls, name, cls._memory_fields_[name])
358
- case StorageType.SHARED:
359
- cls._shared_memory_fields_[name] = ArchetypeField(
485
+ case _StorageType.SHARED:
486
+ cls._shared_memory_fields_[name] = _ArchetypeField(
360
487
  name, field_info.name or name, field_info.storage, shared_memory_offset, field_type
361
488
  )
362
489
  shared_memory_offset += field_type._size_()
@@ -387,41 +514,97 @@ class BaseArchetype:
387
514
  cls._callbacks_.append(cb)
388
515
 
389
516
 
390
- class PlayArchetype(BaseArchetype):
517
+ class PlayArchetype(_BaseArchetype):
518
+ """Base class for play mode archetypes.
519
+
520
+ Usage:
521
+ ```
522
+ class MyArchetype(PlayArchetype):
523
+ # Set to True if the entity is a note and contributes to combo and score
524
+ # Default is False
525
+ is_scored: bool = True
526
+
527
+ imported_field: int = imported()
528
+ exported_field: int = exported()
529
+ entity_memory_field: int = entity_memory()
530
+ shared_memory_field: int = shared_memory()
531
+
532
+ @callback(order=1)
533
+ def preprocess(self):
534
+ ...
535
+ ```
536
+ """
537
+
391
538
  _supported_callbacks_ = PLAY_CALLBACKS
392
539
 
540
+ is_scored: ClassVar[bool] = False
541
+ """Whether the entity contributes to combo and score."""
542
+
393
543
  def preprocess(self):
394
- pass
544
+ """Perform upfront processing.
545
+
546
+ Runs first when the level is loaded.
547
+ """
395
548
 
396
549
  def spawn_order(self) -> float:
397
- pass
550
+ """Return the spawn order of the entity.
551
+
552
+ Runs when the level is loaded after `preprocess`.
553
+ """
398
554
 
399
555
  def should_spawn(self) -> bool:
400
- pass
556
+ """Return whether the entity should be spawned.
557
+
558
+ Runs when this entity is first in the spawn queue.
559
+ """
401
560
 
402
561
  def initialize(self):
403
- pass
562
+ """Initialize this entity.
563
+
564
+ Runs when this entity is spawned.
565
+ """
404
566
 
405
567
  def update_sequential(self):
406
- pass
568
+ """Perform non-parallel actions for this frame.
569
+
570
+ Runs first each frame.
571
+
572
+ This is where logic affecting shared memory should be placed.
573
+ Other logic should be placed in `update_parallel` for better performance.
574
+ """
407
575
 
408
576
  def update_parallel(self):
409
- pass
577
+ """Perform parallel actions for this frame.
578
+
579
+ Runs after `touch` each frame.
580
+
581
+ This is where most gameplay logic should be placed.
582
+ """
410
583
 
411
584
  def touch(self):
412
- pass
585
+ """Handle user input.
586
+
587
+ Runs after `update_sequential` each frame.
588
+ """
413
589
 
414
590
  def terminate(self):
415
- pass
591
+ """Finalize before despawning.
592
+
593
+ Runs when the entity is despawned.
594
+ """
416
595
 
417
596
  @property
418
597
  @meta_fn
419
598
  def despawn(self):
599
+ """Whether the entity should be despawned after this frame.
600
+
601
+ Setting this to True will despawn the entity.
602
+ """
420
603
  if not ctx():
421
604
  raise RuntimeError("Calling despawn is only allowed within a callback")
422
605
  match self._data_:
423
- case ArchetypeSelfData():
424
- return deref(ctx().blocks.EntityDespawn, 0, Num)
606
+ case _ArchetypeSelfData():
607
+ return _deref(ctx().blocks.EntityDespawn, 0, Num)
425
608
  case _:
426
609
  raise RuntimeError("Despawn is only accessible from the entity itself")
427
610
 
@@ -431,8 +614,8 @@ class PlayArchetype(BaseArchetype):
431
614
  if not ctx():
432
615
  raise RuntimeError("Calling despawn is only allowed within a callback")
433
616
  match self._data_:
434
- case ArchetypeSelfData():
435
- deref(ctx().blocks.EntityDespawn, 0, Num)._set_(value)
617
+ case _ArchetypeSelfData():
618
+ _deref(ctx().blocks.EntityDespawn, 0, Num)._set_(value)
436
619
  case _:
437
620
  raise RuntimeError("Despawn is only accessible from the entity itself")
438
621
 
@@ -442,80 +625,128 @@ class PlayArchetype(BaseArchetype):
442
625
  if not ctx():
443
626
  raise RuntimeError("Calling info is only allowed within a callback")
444
627
  match self._data_:
445
- case ArchetypeSelfData():
446
- return deref(ctx().blocks.EntityInfo, 0, PlayEntityInfo)
447
- case ArchetypeReferenceData(index=index):
448
- return deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
628
+ case _ArchetypeSelfData():
629
+ return _deref(ctx().blocks.EntityInfo, 0, PlayEntityInfo)
630
+ case _ArchetypeReferenceData(index=index):
631
+ return _deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
449
632
  case _:
450
633
  raise RuntimeError("Info is only accessible from the entity itself")
451
634
 
452
635
  @property
453
636
  def index(self) -> int:
637
+ """The index of this entity."""
454
638
  return self._info.index
455
639
 
456
640
  @property
457
641
  def is_waiting(self) -> bool:
642
+ """Whether this entity is waiting to be spawned."""
458
643
  return self._info.state == 0
459
644
 
460
645
  @property
461
646
  def is_active(self) -> bool:
647
+ """Whether this entity is active."""
462
648
  return self._info.state == 1
463
649
 
464
650
  @property
465
651
  def is_despawned(self) -> bool:
652
+ """Whether this entity is despawned."""
466
653
  return self._info.state == 2
467
654
 
468
655
  @property
469
656
  def life(self) -> ArchetypeLife:
657
+ """How this entity contributes to life."""
470
658
  if not ctx():
471
659
  raise RuntimeError("Calling life is only allowed within a callback")
472
660
  match self._data_:
473
- case ArchetypeSelfData() | ArchetypeReferenceData():
474
- return deref(ctx().blocks.ArchetypeLife, self.id() * ArchetypeLife._size_(), ArchetypeLife)
661
+ case _ArchetypeSelfData() | _ArchetypeReferenceData():
662
+ return _deref(ctx().blocks.ArchetypeLife, self.id() * ArchetypeLife._size_(), ArchetypeLife)
475
663
  case _:
476
664
  raise RuntimeError("Life is not available in level data")
477
665
 
478
666
  @property
479
667
  def result(self) -> PlayEntityInput:
668
+ """The result of this entity.
669
+
670
+ Only meaningful for scored entities.
671
+ """
480
672
  if not ctx():
481
673
  raise RuntimeError("Calling result is only allowed within a callback")
482
674
  match self._data_:
483
- case ArchetypeSelfData():
484
- return deref(ctx().blocks.EntityInput, 0, PlayEntityInput)
675
+ case _ArchetypeSelfData():
676
+ return _deref(ctx().blocks.EntityInput, 0, PlayEntityInput)
485
677
  case _:
486
678
  raise RuntimeError("Result is only accessible from the entity itself")
487
679
 
488
680
  def ref(self):
489
- if not isinstance(self._data_, ArchetypeLevelData):
681
+ """Get a reference to this entity for creating level data.
682
+
683
+ Not valid elsewhere.
684
+ """
685
+ if not isinstance(self._data_, _ArchetypeLevelData):
490
686
  raise RuntimeError("Entity is not level data")
491
687
  result = EntityRef[type(self)](index=-1)
492
688
  result._ref_ = self
493
689
  return result
494
690
 
495
691
 
496
- class WatchArchetype(BaseArchetype):
692
+ class WatchArchetype(_BaseArchetype):
693
+ """Base class for watch mode archetypes.
694
+
695
+ Usage:
696
+ ```
697
+ class MyArchetype(WatchArchetype):
698
+ imported_field: int = imported()
699
+ entity_memory_field: int = entity_memory()
700
+ shared_memory_field: int = shared_memory()
701
+
702
+ @callback(order=1)
703
+ def update_sequential(self):
704
+ ...
705
+ ```
706
+ """
707
+
497
708
  _supported_callbacks_ = WATCH_ARCHETYPE_CALLBACKS
498
709
 
499
710
  def preprocess(self):
500
- pass
711
+ """Perform upfront processing.
712
+
713
+ Runs first when the level is loaded.
714
+ """
501
715
 
502
716
  def spawn_time(self) -> float:
503
- pass
717
+ """Return the spawn time of the entity."""
504
718
 
505
719
  def despawn_time(self) -> float:
506
- pass
720
+ """Return the despawn time of the entity."""
507
721
 
508
722
  def initialize(self):
509
- pass
723
+ """Initialize this entity.
724
+
725
+ Runs when this entity is spawned.
726
+ """
510
727
 
511
728
  def update_sequential(self):
512
- pass
729
+ """Perform non-parallel actions for this frame.
730
+
731
+ Runs first each frame.
732
+
733
+ This is where logic affecting shared memory should be placed.
734
+ Other logic should be placed in `update_parallel` for better performance.
735
+ """
513
736
 
514
737
  def update_parallel(self):
515
- pass
738
+ """Parallel update callback.
739
+
740
+ Runs after `touch` each frame.
741
+
742
+ This is where most gameplay logic should be placed.
743
+ """
516
744
 
517
745
  def terminate(self):
518
- pass
746
+ """Finalize before despawning.
747
+
748
+ Runs when the entity is despawned.
749
+ """
519
750
 
520
751
  @property
521
752
  @meta_fn
@@ -523,43 +754,56 @@ class WatchArchetype(BaseArchetype):
523
754
  if not ctx():
524
755
  raise RuntimeError("Calling info is only allowed within a callback")
525
756
  match self._data_:
526
- case ArchetypeSelfData():
527
- return deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
528
- case ArchetypeReferenceData(index=index):
529
- return deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
757
+ case _ArchetypeSelfData():
758
+ return _deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
759
+ case _ArchetypeReferenceData(index=index):
760
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
530
761
  case _:
531
762
  raise RuntimeError("Info is only accessible from the entity itself")
532
763
 
533
764
  @property
534
765
  def index(self) -> int:
766
+ """The index of this entity."""
535
767
  return self._info.index
536
768
 
537
769
  @property
538
770
  def is_active(self) -> bool:
771
+ """Whether this entity is active."""
539
772
  return self._info.state == 1
540
773
 
541
774
  @property
542
775
  def life(self) -> ArchetypeLife:
776
+ """How this entity contributes to life."""
543
777
  if not ctx():
544
778
  raise RuntimeError("Calling life is only allowed within a callback")
545
779
  match self._data_:
546
- case ArchetypeSelfData() | ArchetypeReferenceData():
547
- return deref(ctx().blocks.ArchetypeLife, self.id() * ArchetypeLife._size_(), ArchetypeLife)
780
+ case _ArchetypeSelfData() | _ArchetypeReferenceData():
781
+ return _deref(ctx().blocks.ArchetypeLife, self.id() * ArchetypeLife._size_(), ArchetypeLife)
548
782
  case _:
549
783
  raise RuntimeError("Life is not available in level data")
550
784
 
551
785
  @property
552
786
  def result(self) -> WatchEntityInput:
787
+ """The result of this entity.
788
+
789
+ Only meaningful for scored entities.
790
+ """
553
791
  if not ctx():
554
792
  raise RuntimeError("Calling result is only allowed within a callback")
555
793
  match self._data_:
556
- case ArchetypeSelfData():
557
- return deref(ctx().blocks.EntityInput, 0, WatchEntityInput)
794
+ case _ArchetypeSelfData():
795
+ return _deref(ctx().blocks.EntityInput, 0, WatchEntityInput)
558
796
  case _:
559
797
  raise RuntimeError("Result is only accessible from the entity itself")
560
798
 
561
799
  @property
562
800
  def target_time(self) -> float:
801
+ """The target time of this entity.
802
+
803
+ Only meaningful for scored entities. Determines when combo and score are updated.
804
+
805
+ Alias of `result.target_time`.
806
+ """
563
807
  return self.result.target_time
564
808
 
565
809
  @target_time.setter
@@ -567,56 +811,86 @@ class WatchArchetype(BaseArchetype):
567
811
  self.result.target_time = value
568
812
 
569
813
 
570
- class PreviewArchetype(BaseArchetype):
814
+ class PreviewArchetype(_BaseArchetype):
815
+ """Base class for preview mode archetypes.
816
+
817
+ Usage:
818
+ ```
819
+ class MyArchetype(PreviewArchetype):
820
+ imported_field: int = imported()
821
+ entity_memory_field: int = entity_memory()
822
+ shared_memory_field: int = shared_memory()
823
+
824
+ @callback(order=1)
825
+ def preprocess(self):
826
+ ...
827
+ ```
828
+ """
829
+
571
830
  _supported_callbacks_ = PREVIEW_CALLBACKS
572
831
 
573
832
  def preprocess(self):
574
- pass
833
+ """Perform upfront processing.
834
+
835
+ Runs first when the level is loaded.
836
+ """
575
837
 
576
838
  def render(self):
577
- pass
839
+ """Render the entity.
840
+
841
+ Runs after `preprocess`.
842
+ """
578
843
 
579
844
  @property
580
845
  def _info(self) -> PreviewEntityInfo:
581
846
  if not ctx():
582
847
  raise RuntimeError("Calling info is only allowed within a callback")
583
848
  match self._data_:
584
- case ArchetypeSelfData():
585
- return deref(ctx().blocks.EntityInfo, 0, PreviewEntityInfo)
586
- case ArchetypeReferenceData(index=index):
587
- return deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
849
+ case _ArchetypeSelfData():
850
+ return _deref(ctx().blocks.EntityInfo, 0, PreviewEntityInfo)
851
+ case _ArchetypeReferenceData(index=index):
852
+ return _deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
588
853
  case _:
589
854
  raise RuntimeError("Info is only accessible from the entity itself")
590
855
 
591
856
  @property
592
857
  def index(self) -> int:
858
+ """The index of this entity."""
593
859
  return self._info.index
594
860
 
595
861
 
596
862
  @meta_fn
597
863
  def entity_info_at(index: Num) -> PlayEntityInfo | WatchEntityInfo | PreviewEntityInfo:
864
+ """Retrieve entity info of the entity at the given index.
865
+
866
+ Available in play, watch, and preview mode.
867
+ """
598
868
  if not ctx():
599
869
  raise RuntimeError("Calling entity_info_at is only allowed within a callback")
600
870
  match ctx().global_state.mode:
601
871
  case Mode.PLAY:
602
- return deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
872
+ return _deref(ctx().blocks.EntityInfoArray, index * PlayEntityInfo._size_(), PlayEntityInfo)
603
873
  case Mode.WATCH:
604
- return deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
874
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
605
875
  case Mode.PREVIEW:
606
- return deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
876
+ return _deref(ctx().blocks.EntityInfoArray, index * PreviewEntityInfo._size_(), PreviewEntityInfo)
607
877
  case _:
608
878
  raise RuntimeError(f"Entity info is not available in mode '{ctx().global_state.mode}'")
609
879
 
610
880
 
611
881
  @meta_fn
612
- def archetype_life_of(archetype: type[BaseArchetype] | BaseArchetype) -> ArchetypeLife:
882
+ def archetype_life_of(archetype: type[_BaseArchetype] | _BaseArchetype) -> ArchetypeLife:
883
+ """Retrieve the archetype life of the given archetype.
884
+
885
+ Available in play and watch mode.
886
+ """
613
887
  archetype = validate_value(archetype)
614
888
  archetype = archetype._as_py_()
615
889
  if not ctx():
616
890
  raise RuntimeError("Calling archetype_life_of is only allowed within a callback")
617
891
  match ctx().global_state.mode:
618
892
  case Mode.PLAY | Mode.WATCH:
619
- return deref(ctx().blocks.ArchetypeLife, archetype.id() * ArchetypeLife._size_(), ArchetypeLife)
893
+ return _deref(ctx().blocks.ArchetypeLife, archetype.id() * ArchetypeLife._size_(), ArchetypeLife)
620
894
  case _:
621
895
  raise RuntimeError(f"Archetype life is not available in mode '{ctx().global_state.mode}'")
622
896
 
@@ -639,10 +913,19 @@ class PreviewEntityInfo(Record):
639
913
 
640
914
 
641
915
  class ArchetypeLife(Record):
916
+ """How an entity contributes to life."""
917
+
642
918
  perfect_increment: Num
919
+ """Life increment for a perfect judgment."""
920
+
643
921
  great_increment: Num
922
+ """Life increment for a great judgment."""
923
+
644
924
  good_increment: Num
925
+ """Life increment for a good judgment."""
926
+
645
927
  miss_increment: Num
928
+ """Life increment for a miss judgment."""
646
929
 
647
930
  def update(
648
931
  self,
@@ -651,6 +934,7 @@ class ArchetypeLife(Record):
651
934
  good_increment: Num | None = None,
652
935
  miss_increment: Num | None = None,
653
936
  ):
937
+ """Update the life increments."""
654
938
  if perfect_increment is not None:
655
939
  self.perfect_increment = perfect_increment
656
940
  if great_increment is not None:
@@ -674,12 +958,14 @@ class WatchEntityInput(Record):
674
958
  bucket_value: float
675
959
 
676
960
 
677
- class EntityRef[A: BaseArchetype](Record):
961
+ class EntityRef[A: _BaseArchetype](Record):
962
+ """Reference to another entity."""
963
+
678
964
  index: int
679
965
 
680
966
  @classmethod
681
967
  def archetype(cls) -> type[A]:
682
- return cls._get_type_arg_(A)
968
+ return cls.type_var_value(A)
683
969
 
684
970
  def get(self) -> A:
685
971
  return self.archetype().at(self.index)
@@ -695,5 +981,10 @@ class EntityRef[A: BaseArchetype](Record):
695
981
 
696
982
 
697
983
  class StandardArchetypeName(StrEnum):
984
+ """Standard archetype names."""
985
+
698
986
  BPM_CHANGE = "#BPM_CHANGE"
987
+ """Bpm change marker"""
988
+
699
989
  TIMESCALE_CHANGE = "#TIMESCALE_CHANGE"
990
+ """Timescale change marker"""