sonolus.py 0.1.2__py3-none-any.whl → 0.1.4__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 (72) hide show
  1. sonolus/backend/allocate.py +125 -51
  2. sonolus/backend/blocks.py +756 -756
  3. sonolus/backend/coalesce.py +85 -0
  4. sonolus/backend/constant_evaluation.py +374 -0
  5. sonolus/backend/dead_code.py +80 -0
  6. sonolus/backend/dominance.py +111 -0
  7. sonolus/backend/excepthook.py +37 -37
  8. sonolus/backend/finalize.py +69 -69
  9. sonolus/backend/flow.py +121 -92
  10. sonolus/backend/inlining.py +150 -0
  11. sonolus/backend/ir.py +5 -3
  12. sonolus/backend/liveness.py +173 -0
  13. sonolus/backend/mode.py +24 -24
  14. sonolus/backend/node.py +40 -40
  15. sonolus/backend/ops.py +197 -197
  16. sonolus/backend/optimize.py +37 -9
  17. sonolus/backend/passes.py +52 -6
  18. sonolus/backend/simplify.py +47 -30
  19. sonolus/backend/ssa.py +187 -0
  20. sonolus/backend/utils.py +48 -48
  21. sonolus/backend/visitor.py +892 -880
  22. sonolus/build/cli.py +7 -1
  23. sonolus/build/compile.py +88 -90
  24. sonolus/build/engine.py +55 -5
  25. sonolus/build/level.py +24 -23
  26. sonolus/build/node.py +43 -43
  27. sonolus/script/archetype.py +23 -6
  28. sonolus/script/array.py +2 -2
  29. sonolus/script/bucket.py +191 -191
  30. sonolus/script/callbacks.py +127 -115
  31. sonolus/script/comptime.py +1 -1
  32. sonolus/script/containers.py +23 -0
  33. sonolus/script/debug.py +19 -3
  34. sonolus/script/easing.py +323 -0
  35. sonolus/script/effect.py +131 -131
  36. sonolus/script/engine.py +37 -1
  37. sonolus/script/globals.py +269 -269
  38. sonolus/script/graphics.py +200 -150
  39. sonolus/script/instruction.py +151 -0
  40. sonolus/script/internal/__init__.py +5 -5
  41. sonolus/script/internal/builtin_impls.py +144 -144
  42. sonolus/script/internal/context.py +12 -4
  43. sonolus/script/internal/descriptor.py +17 -17
  44. sonolus/script/internal/introspection.py +14 -14
  45. sonolus/script/internal/native.py +40 -38
  46. sonolus/script/internal/value.py +3 -3
  47. sonolus/script/interval.py +120 -112
  48. sonolus/script/iterator.py +214 -211
  49. sonolus/script/math.py +30 -1
  50. sonolus/script/num.py +1 -1
  51. sonolus/script/options.py +191 -191
  52. sonolus/script/particle.py +157 -157
  53. sonolus/script/pointer.py +30 -30
  54. sonolus/script/{preview.py → print.py} +81 -81
  55. sonolus/script/random.py +14 -0
  56. sonolus/script/range.py +58 -58
  57. sonolus/script/record.py +3 -3
  58. sonolus/script/runtime.py +45 -6
  59. sonolus/script/sprite.py +333 -333
  60. sonolus/script/text.py +407 -407
  61. sonolus/script/timing.py +42 -42
  62. sonolus/script/transform.py +77 -23
  63. sonolus/script/ui.py +160 -160
  64. sonolus/script/vec.py +81 -72
  65. {sonolus_py-0.1.2.dist-info → sonolus_py-0.1.4.dist-info}/METADATA +1 -2
  66. sonolus_py-0.1.4.dist-info/RECORD +84 -0
  67. {sonolus_py-0.1.2.dist-info → sonolus_py-0.1.4.dist-info}/WHEEL +1 -1
  68. {sonolus_py-0.1.2.dist-info → sonolus_py-0.1.4.dist-info}/licenses/LICENSE +21 -21
  69. sonolus/build/defaults.py +0 -32
  70. sonolus/script/icon.py +0 -73
  71. sonolus_py-0.1.2.dist-info/RECORD +0 -76
  72. {sonolus_py-0.1.2.dist-info → sonolus_py-0.1.4.dist-info}/entry_points.txt +0 -0
sonolus/build/cli.py CHANGED
@@ -7,6 +7,7 @@ import socket
7
7
  import socketserver
8
8
  import sys
9
9
  from pathlib import Path
10
+ from time import perf_counter
10
11
 
11
12
  from sonolus.build.engine import package_engine
12
13
  from sonolus.build.level import package_level_data
@@ -163,8 +164,13 @@ def main():
163
164
  build_dir = Path(args.build_dir)
164
165
 
165
166
  if args.command == "build":
167
+ start_time = perf_counter()
166
168
  build_project(project, build_dir)
167
- print(f"Project built successfully to '{build_dir.resolve()}'")
169
+ end_time = perf_counter()
170
+ print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
168
171
  elif args.command == "dev":
172
+ start_time = perf_counter()
169
173
  build_collection(project, build_dir)
174
+ end_time = perf_counter()
175
+ print(f"Build finished in {end_time - start_time:.2f}s")
170
176
  run_server(build_dir / "site", port=args.port)
sonolus/build/compile.py CHANGED
@@ -1,90 +1,88 @@
1
- from collections.abc import Callable
2
-
3
- from sonolus.backend.finalize import cfg_to_engine_node
4
- from sonolus.backend.flow import BasicBlock
5
- from sonolus.backend.ir import IRConst, IRInstr
6
- from sonolus.backend.mode import Mode
7
- from sonolus.backend.ops import Op
8
- from sonolus.backend.optimize import optimize_and_allocate
9
- from sonolus.backend.visitor import compile_and_call
10
- from sonolus.build.node import OutputNodeGenerator
11
- from sonolus.script.archetype import BaseArchetype
12
- from sonolus.script.callbacks import CallbackInfo
13
- from sonolus.script.internal.context import (
14
- CallbackContextState,
15
- Context,
16
- GlobalContextState,
17
- ReadOnlyMemory,
18
- context_to_cfg,
19
- ctx,
20
- using_ctx,
21
- )
22
- from sonolus.script.num import is_num
23
-
24
-
25
- def compile_mode(
26
- mode: Mode,
27
- rom: ReadOnlyMemory,
28
- archetypes: list[type[BaseArchetype]] | None,
29
- global_callbacks: list[tuple[CallbackInfo, Callable]] | None,
30
- ) -> dict:
31
- global_state = GlobalContextState(
32
- mode, {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None, rom
33
- )
34
- nodes = OutputNodeGenerator()
35
- results = {}
36
- if archetypes is not None:
37
- archetype_entries = []
38
- for archetype in archetypes:
39
- archetype_data = {
40
- "name": archetype.name,
41
- "hasInput": archetype.is_scored,
42
- }
43
- archetype_data["imports"] = [
44
- {"name": name, "index": index} for name, index in archetype._imported_keys_.items()
45
- ]
46
- if mode == Mode.PLAY:
47
- archetype_data["exports"] = [
48
- {"name": name, "index": index} for name, index in archetype._exported_keys_.items()
49
- ]
50
- for cb_name, cb_info in archetype._supported_callbacks_.items():
51
- cb = getattr(archetype, cb_name)
52
- if cb in archetype._default_callbacks_:
53
- continue
54
- cb_order = getattr(cb, "_callback_order_", 0)
55
- if not cb_info.supports_order and cb_order != 0:
56
- raise ValueError(f"Callback '{cb_name}' does not support a non-zero order")
57
- cfg = callback_to_cfg(global_state, cb, cb_info.name, archetype)
58
- cfg = optimize_and_allocate(cfg)
59
- node = cfg_to_engine_node(cfg)
60
- node_index = nodes.add(node)
61
- archetype_data[cb_info.name] = {
62
- "index": node_index,
63
- "order": cb_order,
64
- }
65
- archetype_entries.append(archetype_data)
66
- results["archetypes"] = archetype_entries
67
- if global_callbacks is not None:
68
- for cb_info, cb in global_callbacks:
69
- cfg = callback_to_cfg(global_state, cb, cb_info.name)
70
- cfg = optimize_and_allocate(cfg)
71
- node = cfg_to_engine_node(cfg)
72
- node_index = nodes.add(node)
73
- results[cb_info.name] = node_index
74
- results["nodes"] = nodes.get()
75
- return results
76
-
77
-
78
- def callback_to_cfg(
79
- global_state: GlobalContextState, callback: Callable, name: str, archetype: type[BaseArchetype] | None = None
80
- ) -> BasicBlock:
81
- callback_state = CallbackContextState(name)
82
- context = Context(global_state, callback_state)
83
- with using_ctx(context):
84
- if archetype is not None:
85
- result = compile_and_call(callback, archetype._for_compilation())
86
- else:
87
- result = compile_and_call(callback)
88
- if is_num(result):
89
- ctx().add_statements(IRInstr(Op.Break, [IRConst(1), result.ir()]))
90
- return context_to_cfg(context)
1
+ from collections.abc import Callable
2
+
3
+ from sonolus.backend.finalize import cfg_to_engine_node
4
+ from sonolus.backend.flow import BasicBlock
5
+ from sonolus.backend.ir import IRConst, IRInstr
6
+ from sonolus.backend.mode import Mode
7
+ from sonolus.backend.ops import Op
8
+ from sonolus.backend.optimize import optimize_and_allocate
9
+ from sonolus.backend.visitor import compile_and_call
10
+ from sonolus.build.node import OutputNodeGenerator
11
+ from sonolus.script.archetype import BaseArchetype
12
+ from sonolus.script.callbacks import CallbackInfo
13
+ from sonolus.script.internal.context import (
14
+ CallbackContextState,
15
+ Context,
16
+ GlobalContextState,
17
+ ReadOnlyMemory,
18
+ context_to_cfg,
19
+ ctx,
20
+ using_ctx,
21
+ )
22
+ from sonolus.script.num import is_num
23
+
24
+
25
+ def compile_mode(
26
+ mode: Mode,
27
+ rom: ReadOnlyMemory,
28
+ archetypes: list[type[BaseArchetype]] | None,
29
+ global_callbacks: list[tuple[CallbackInfo, Callable]] | None,
30
+ ) -> dict:
31
+ global_state = GlobalContextState(
32
+ mode, {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None, rom
33
+ )
34
+ nodes = OutputNodeGenerator()
35
+ results = {}
36
+ if archetypes is not None:
37
+ archetype_entries = []
38
+ for archetype in archetypes:
39
+ archetype_data = {
40
+ "name": archetype.name,
41
+ "hasInput": archetype.is_scored,
42
+ "imports": [{"name": name, "index": index} for name, index in archetype._imported_keys_.items()],
43
+ }
44
+ if mode == Mode.PLAY:
45
+ archetype_data["exports"] = [
46
+ {"name": name, "index": index} for name, index in archetype._exported_keys_.items()
47
+ ]
48
+ for cb_name, cb_info in archetype._supported_callbacks_.items():
49
+ cb = getattr(archetype, cb_name)
50
+ if cb in archetype._default_callbacks_:
51
+ continue
52
+ cb_order = getattr(cb, "_callback_order_", 0)
53
+ if not cb_info.supports_order and cb_order != 0:
54
+ raise ValueError(f"Callback '{cb_name}' does not support a non-zero order")
55
+ cfg = callback_to_cfg(global_state, cb, cb_info.name, archetype)
56
+ cfg = optimize_and_allocate(cfg)
57
+ node = cfg_to_engine_node(cfg)
58
+ node_index = nodes.add(node)
59
+ archetype_data[cb_info.name] = {
60
+ "index": node_index,
61
+ "order": cb_order,
62
+ }
63
+ archetype_entries.append(archetype_data)
64
+ results["archetypes"] = archetype_entries
65
+ if global_callbacks is not None:
66
+ for cb_info, cb in global_callbacks:
67
+ cfg = callback_to_cfg(global_state, cb, cb_info.name)
68
+ cfg = optimize_and_allocate(cfg)
69
+ node = cfg_to_engine_node(cfg)
70
+ node_index = nodes.add(node)
71
+ results[cb_info.name] = node_index
72
+ results["nodes"] = nodes.get()
73
+ return results
74
+
75
+
76
+ def callback_to_cfg(
77
+ global_state: GlobalContextState, callback: Callable, name: str, archetype: type[BaseArchetype] | None = None
78
+ ) -> BasicBlock:
79
+ callback_state = CallbackContextState(name)
80
+ context = Context(global_state, callback_state)
81
+ with using_ctx(context):
82
+ if archetype is not None:
83
+ result = compile_and_call(callback, archetype._for_compilation())
84
+ else:
85
+ result = compile_and_call(callback)
86
+ if is_num(result):
87
+ ctx().add_statements(IRInstr(Op.Break, [IRConst(1), result.ir()]))
88
+ 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_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,17 +64,28 @@ def package_engine(engine: EngineData):
61
64
  rom=rom,
62
65
  update_spawn=engine.watch.update_spawn,
63
66
  )
64
- preview_mode = build_preview_mode(
67
+ preview_data = build_preview_mode(
65
68
  archetypes=engine.preview.archetypes,
66
69
  skin=engine.preview.skin,
67
70
  rom=rom,
68
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
+ )
69
83
  return PackagedEngine(
70
84
  configuration=package_output(configuration),
71
85
  play_data=package_output(play_data),
72
86
  watch_data=package_output(watch_data),
73
- preview_data=package_output(preview_mode),
74
- tutorial_data=package_output(EMPTY_ENGINE_TUTORIAL_DATA),
87
+ preview_data=package_output(preview_data),
88
+ tutorial_data=package_output(tutorial_data),
75
89
  rom=package_rom(rom),
76
90
  )
77
91
 
@@ -134,6 +148,35 @@ def build_preview_mode(
134
148
  }
135
149
 
136
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
+
137
180
  def build_skin(skin: Skin) -> JsonValue:
138
181
  return {"sprites": [{"name": name, "id": i} for i, name in enumerate(skin._sprites_)]}
139
182
 
@@ -150,6 +193,13 @@ def build_buckets(buckets: Buckets) -> JsonValue:
150
193
  return [bucket.to_dict() for bucket in buckets._buckets_]
151
194
 
152
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
+
153
203
  def package_rom(rom: ReadOnlyMemory) -> bytes:
154
204
  values = rom.values or [0]
155
205
  output = bytearray()
sonolus/build/level.py CHANGED
@@ -1,23 +1,24 @@
1
- from sonolus.build.engine import package_output
2
- from sonolus.script.level import LevelData
3
-
4
-
5
- def package_level_data(
6
- level_data: LevelData,
7
- ):
8
- return package_output(build_level_data(level_data))
9
-
10
-
11
- def build_level_data(
12
- level_data: LevelData,
13
- ):
14
- return {
15
- "bgmOffset": level_data.bgm_offset,
16
- "entities": [
17
- {
18
- "archetype": entity.name,
19
- "data": entity._level_data_entries(),
20
- }
21
- for entity in level_data.entities
22
- ],
23
- }
1
+ from sonolus.build.engine import package_output
2
+ from sonolus.script.level import LevelData
3
+
4
+
5
+ def package_level_data(
6
+ level_data: LevelData,
7
+ ):
8
+ return package_output(build_level_data(level_data))
9
+
10
+
11
+ def build_level_data(
12
+ level_data: LevelData,
13
+ ):
14
+ level_refs = {entity: i for i, entity in enumerate(level_data.entities)}
15
+ return {
16
+ "bgmOffset": level_data.bgm_offset,
17
+ "entities": [
18
+ {
19
+ "archetype": entity.name,
20
+ "data": entity._level_data_entries(level_refs),
21
+ }
22
+ for entity in level_data.entities
23
+ ],
24
+ }
sonolus/build/node.py CHANGED
@@ -1,43 +1,43 @@
1
- from typing import TypedDict
2
-
3
- from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
-
5
-
6
- class ValueOutputNode(TypedDict):
7
- value: float
8
-
9
-
10
- class FunctionOutputNode(TypedDict):
11
- func: str
12
- args: list[int]
13
-
14
-
15
- class OutputNodeGenerator:
16
- nodes: list[ValueOutputNode | FunctionOutputNode]
17
- indexes: dict[EngineNode, int]
18
-
19
- def __init__(self):
20
- self.nodes = []
21
- self.indexes = {}
22
-
23
- def add(self, node: EngineNode):
24
- if node in self.indexes:
25
- return self.indexes[node]
26
-
27
- match node:
28
- case ConstantNode(value):
29
- index = len(self.nodes)
30
- self.nodes.append({"value": value})
31
- self.indexes[node] = index
32
- return index
33
- case FunctionNode(func, args):
34
- arg_indexes = [self.add(arg) for arg in args]
35
- index = len(self.nodes)
36
- self.nodes.append({"func": func.value, "args": arg_indexes})
37
- self.indexes[node] = index
38
- return index
39
- case _:
40
- raise ValueError("Invalid node")
41
-
42
- def get(self):
43
- return self.nodes
1
+ from typing import TypedDict
2
+
3
+ from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+
5
+
6
+ class ValueOutputNode(TypedDict):
7
+ value: float
8
+
9
+
10
+ class FunctionOutputNode(TypedDict):
11
+ func: str
12
+ args: list[int]
13
+
14
+
15
+ class OutputNodeGenerator:
16
+ nodes: list[ValueOutputNode | FunctionOutputNode]
17
+ indexes: dict[EngineNode, int]
18
+
19
+ def __init__(self):
20
+ self.nodes = []
21
+ self.indexes = {}
22
+
23
+ def add(self, node: EngineNode):
24
+ if node in self.indexes:
25
+ return self.indexes[node]
26
+
27
+ match node:
28
+ case ConstantNode(value):
29
+ index = len(self.nodes)
30
+ self.nodes.append({"value": value})
31
+ self.indexes[node] = index
32
+ return index
33
+ case FunctionNode(func, args):
34
+ arg_indexes = [self.add(arg) for arg in args]
35
+ index = len(self.nodes)
36
+ self.nodes.append({"func": func.value, "args": arg_indexes})
37
+ self.indexes[node] = index
38
+ return index
39
+ case _:
40
+ raise ValueError("Invalid node")
41
+
42
+ def get(self):
43
+ return self.nodes
@@ -10,6 +10,7 @@ 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
15
  from sonolus.script.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
15
16
  from sonolus.script.internal.context import ctx
@@ -80,7 +81,7 @@ class ArchetypeField(SonolusDescriptor):
80
81
  case ArchetypeReferenceData(index=index):
81
82
  result = deref(
82
83
  ctx().blocks.EntitySharedMemoryArray,
83
- self.offset + index * ENTITY_SHARED_MEMORY_SIZE,
84
+ Num._accept_(self.offset) + index * ENTITY_SHARED_MEMORY_SIZE,
84
85
  self.type,
85
86
  )
86
87
  case ArchetypeLevelData():
@@ -135,7 +136,7 @@ class ArchetypeField(SonolusDescriptor):
135
136
  case ArchetypeReferenceData(index=index):
136
137
  target = deref(
137
138
  ctx().blocks.EntitySharedMemoryArray,
138
- self.offset + index * ENTITY_SHARED_MEMORY_SIZE,
139
+ Num._accept_(self.offset) + index * ENTITY_SHARED_MEMORY_SIZE,
139
140
  self.type,
140
141
  )
141
142
  case ArchetypeLevelData():
@@ -257,7 +258,7 @@ class BaseArchetype:
257
258
  @meta_fn
258
259
  def at(cls, index: Num) -> Self:
259
260
  result = cls._new()
260
- result._data_ = ArchetypeReferenceData(index=index)
261
+ result._data_ = ArchetypeReferenceData(index=Num._accept_(index))
261
262
  return result
262
263
 
263
264
  @classmethod
@@ -283,13 +284,13 @@ class BaseArchetype:
283
284
  data.extend(field.type._accept_(bound.arguments[field.name] or zeros(field.type))._to_list_())
284
285
  native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
285
286
 
286
- def _level_data_entries(self):
287
+ def _level_data_entries(self, level_refs: dict[Any, int] | None = None):
287
288
  if not isinstance(self._data_, ArchetypeLevelData):
288
289
  raise RuntimeError("Entity is not level data")
289
290
  entries = []
290
291
  for name, value in self._data_.values.items():
291
292
  field_info = self._imported_fields_.get(name)
292
- for k, v in value._to_flat_dict_(field_info.data_name).items():
293
+ for k, v in value._to_flat_dict_(field_info.data_name, level_refs).items():
293
294
  entries.append({"name": k, "value": v})
294
295
  return entries
295
296
 
@@ -484,6 +485,13 @@ class PlayArchetype(BaseArchetype):
484
485
  case _:
485
486
  raise RuntimeError("Result is only accessible from the entity itself")
486
487
 
488
+ def ref(self):
489
+ if not isinstance(self._data_, ArchetypeLevelData):
490
+ raise RuntimeError("Entity is not level data")
491
+ result = EntityRef[type(self)](index=-1)
492
+ result._ref_ = self
493
+ return result
494
+
487
495
 
488
496
  class WatchArchetype(BaseArchetype):
489
497
  _supported_callbacks_ = WATCH_ARCHETYPE_CALLBACKS
@@ -674,7 +682,16 @@ class EntityRef[A: BaseArchetype](Record):
674
682
  return cls._get_type_arg_(A)
675
683
 
676
684
  def get(self) -> A:
677
- return self.archetype().at(Num(self.index))
685
+ return self.archetype().at(self.index)
686
+
687
+ def _to_list_(self, level_refs: dict[Any, int] | None = None) -> list[float | BlockPlace]:
688
+ ref = getattr(self, "_ref_", None)
689
+ if ref is None:
690
+ return [self.index]
691
+ else:
692
+ if ref not in level_refs:
693
+ raise KeyError("Reference to entity not in level data")
694
+ return [level_refs[ref]]
678
695
 
679
696
 
680
697
  class StandardArchetypeName(StrEnum):
sonolus/script/array.py CHANGED
@@ -97,10 +97,10 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
97
97
  iterator = iter(values)
98
98
  return cls(*(cls.element_type()._from_list_(iterator) for _ in range(cls.size())))
99
99
 
100
- def _to_list_(self) -> list[float | BlockPlace]:
100
+ def _to_list_(self, level_refs: dict[Any, int] | None = None) -> list[float | BlockPlace]:
101
101
  match self._value:
102
102
  case list():
103
- return [entry for value in self._value for entry in value._to_list_()]
103
+ return [entry for value in self._value for entry in value._to_list_(level_refs)]
104
104
  case BlockPlace():
105
105
  return [
106
106
  entry