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