sonolus.py 0.1.9__py3-none-any.whl → 0.2.0__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 (42) hide show
  1. sonolus/backend/optimize/constant_evaluation.py +2 -2
  2. sonolus/backend/optimize/optimize.py +12 -4
  3. sonolus/backend/optimize/passes.py +2 -1
  4. sonolus/backend/place.py +95 -14
  5. sonolus/backend/visitor.py +51 -3
  6. sonolus/build/cli.py +60 -9
  7. sonolus/build/collection.py +68 -26
  8. sonolus/build/compile.py +85 -27
  9. sonolus/build/engine.py +166 -40
  10. sonolus/build/node.py +8 -1
  11. sonolus/build/project.py +30 -11
  12. sonolus/script/archetype.py +110 -26
  13. sonolus/script/array.py +11 -0
  14. sonolus/script/debug.py +2 -2
  15. sonolus/script/effect.py +2 -2
  16. sonolus/script/engine.py +123 -15
  17. sonolus/script/internal/builtin_impls.py +21 -2
  18. sonolus/script/internal/constant.py +5 -1
  19. sonolus/script/internal/context.py +30 -25
  20. sonolus/script/internal/math_impls.py +2 -1
  21. sonolus/script/internal/transient.py +4 -0
  22. sonolus/script/internal/value.py +6 -0
  23. sonolus/script/interval.py +16 -0
  24. sonolus/script/iterator.py +17 -0
  25. sonolus/script/level.py +113 -10
  26. sonolus/script/metadata.py +32 -0
  27. sonolus/script/num.py +9 -0
  28. sonolus/script/options.py +5 -3
  29. sonolus/script/pointer.py +2 -0
  30. sonolus/script/project.py +41 -5
  31. sonolus/script/record.py +7 -2
  32. sonolus/script/runtime.py +61 -10
  33. sonolus/script/sprite.py +18 -1
  34. sonolus/script/ui.py +7 -3
  35. sonolus/script/values.py +8 -5
  36. sonolus/script/vec.py +28 -0
  37. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
  38. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/RECORD +42 -41
  39. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
  40. /sonolus/script/{print.py → printing.py} +0 -0
  41. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
  42. {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
sonolus/build/compile.py CHANGED
@@ -1,11 +1,13 @@
1
- from collections.abc import Callable
1
+ from collections.abc import Callable, Sequence
2
+ from concurrent.futures import Executor, Future
2
3
 
3
4
  from sonolus.backend.finalize import cfg_to_engine_node
4
5
  from sonolus.backend.ir import IRConst, IRInstr
5
6
  from sonolus.backend.mode import Mode
6
7
  from sonolus.backend.ops import Op
7
8
  from sonolus.backend.optimize.flow import BasicBlock
8
- from sonolus.backend.optimize.optimize import optimize_and_allocate
9
+ from sonolus.backend.optimize.optimize import STANDARD_PASSES
10
+ from sonolus.backend.optimize.passes import CompilerPass, run_passes
9
11
  from sonolus.backend.visitor import compile_and_call
10
12
  from sonolus.build.node import OutputNodeGenerator
11
13
  from sonolus.script.archetype import _BaseArchetype
@@ -27,16 +29,49 @@ def compile_mode(
27
29
  rom: ReadOnlyMemory,
28
30
  archetypes: list[type[_BaseArchetype]] | None,
29
31
  global_callbacks: list[tuple[CallbackInfo, Callable]] | None,
32
+ passes: Sequence[CompilerPass] | None = None,
33
+ thread_pool: Executor | None = None,
30
34
  ) -> dict:
35
+ if passes is None:
36
+ passes = STANDARD_PASSES
37
+
31
38
  global_state = GlobalContextState(
32
- mode, {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None, rom
39
+ mode,
40
+ {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
41
+ rom,
33
42
  )
34
43
  nodes = OutputNodeGenerator()
35
44
  results = {}
45
+
46
+ def process_callback(
47
+ cb_info: CallbackInfo,
48
+ cb: Callable,
49
+ arch: type[_BaseArchetype] | None = None,
50
+ ) -> tuple[str, int] | tuple[str, dict]:
51
+ """Compile a single callback to a node.
52
+
53
+ Returns either:
54
+ - (cb_info.name, node_index) for global callbacks, or
55
+ - (cb_info.name, {"index": node_index, "order": cb_order}) for archetype callbacks.
56
+ """
57
+ cfg = callback_to_cfg(global_state, cb, cb_info.name, arch)
58
+ cfg = run_passes(cfg, passes)
59
+ node = cfg_to_engine_node(cfg)
60
+ node_index = nodes.add(node)
61
+
62
+ if arch is not None:
63
+ cb_order = getattr(cb, "_callback_order_", 0)
64
+ return cb_info.name, {"index": node_index, "order": cb_order}
65
+ else:
66
+ return cb_info.name, node_index
67
+
68
+ all_futures = {}
69
+ archetype_entries = []
70
+
36
71
  if archetypes is not None:
37
- archetype_entries = []
38
72
  for archetype in archetypes:
39
73
  archetype._init_fields()
74
+
40
75
  archetype_data = {
41
76
  "name": archetype.name,
42
77
  "hasInput": archetype.is_scored,
@@ -44,36 +79,59 @@ def compile_mode(
44
79
  }
45
80
  if mode == Mode.PLAY:
46
81
  archetype_data["exports"] = [*archetype._exported_keys_]
47
- for cb_name, cb_info in archetype._supported_callbacks_.items():
48
- cb = getattr(archetype, cb_name)
49
- if cb in archetype._default_callbacks_:
50
- continue
51
- cb_order = getattr(cb, "_callback_order_", 0)
52
- if not cb_info.supports_order and cb_order != 0:
53
- raise ValueError(f"Callback '{cb_name}' does not support a non-zero order")
54
- cfg = callback_to_cfg(global_state, cb, cb_info.name, archetype)
55
- cfg = optimize_and_allocate(cfg)
56
- node = cfg_to_engine_node(cfg)
57
- node_index = nodes.add(node)
58
- archetype_data[cb_info.name] = {
59
- "index": node_index,
60
- "order": cb_order,
61
- }
82
+
83
+ callback_items = [
84
+ (cb_name, cb_info, getattr(archetype, cb_name))
85
+ for cb_name, cb_info in archetype._supported_callbacks_.items()
86
+ if getattr(archetype, cb_name) not in archetype._default_callbacks_
87
+ ]
88
+
89
+ if thread_pool is not None:
90
+ for cb_name, cb_info, cb in callback_items:
91
+ cb_order = getattr(cb, "_callback_order_", 0)
92
+ if not cb_info.supports_order and cb_order != 0:
93
+ raise ValueError(f"Callback '{cb_name}' does not support a non-zero order")
94
+ f: Future = thread_pool.submit(process_callback, cb_info, cb, archetype)
95
+ all_futures[f] = ("archetype", archetype_data, cb_name)
96
+ else:
97
+ for cb_name, cb_info, cb in callback_items:
98
+ cb_order = getattr(cb, "_callback_order_", 0)
99
+ if not cb_info.supports_order and cb_order != 0:
100
+ raise ValueError(f"Callback '{cb_name}' does not support a non-zero order")
101
+ cb_name, result_data = process_callback(cb_info, cb, archetype)
102
+ archetype_data[cb_name] = result_data
103
+
62
104
  archetype_entries.append(archetype_data)
63
- results["archetypes"] = archetype_entries
64
- if global_callbacks is not None:
105
+
106
+ if global_callbacks is not None and thread_pool is not None:
107
+ for cb_info, cb in global_callbacks:
108
+ f: Future = thread_pool.submit(process_callback, cb_info, cb, None)
109
+ all_futures[f] = ("global", None, cb_info.name)
110
+
111
+ if thread_pool is not None:
112
+ for f, (callback_type, archetype_data, cb_name) in all_futures.items():
113
+ cb_name, result_data = f.result()
114
+ if callback_type == "archetype":
115
+ archetype_data[cb_name] = result_data
116
+ else: # global callback
117
+ results[cb_name] = result_data
118
+ elif global_callbacks is not None:
65
119
  for cb_info, cb in global_callbacks:
66
- cfg = callback_to_cfg(global_state, cb, cb_info.name)
67
- cfg = optimize_and_allocate(cfg)
68
- node = cfg_to_engine_node(cfg)
69
- node_index = nodes.add(node)
70
- results[cb_info.name] = node_index
120
+ cb_name, node_index = process_callback(cb_info, cb, None)
121
+ results[cb_name] = node_index
122
+
123
+ if archetypes is not None:
124
+ results["archetypes"] = archetype_entries
125
+
71
126
  results["nodes"] = nodes.get()
72
127
  return results
73
128
 
74
129
 
75
130
  def callback_to_cfg(
76
- global_state: GlobalContextState, callback: Callable, name: str, archetype: type[_BaseArchetype] | None = None
131
+ global_state: GlobalContextState,
132
+ callback: Callable,
133
+ name: str,
134
+ archetype: type[_BaseArchetype] | None = None,
77
135
  ) -> BasicBlock:
78
136
  callback_state = CallbackContextState(name)
79
137
  context = Context(global_state, callback_state)
sonolus/build/engine.py CHANGED
@@ -1,8 +1,12 @@
1
1
  import gzip
2
2
  import json
3
3
  import struct
4
+ import sys
4
5
  from collections.abc import Callable
6
+ from concurrent.futures import Executor
7
+ from concurrent.futures.thread import ThreadPoolExecutor
5
8
  from dataclasses import dataclass
9
+ from os import process_cpu_count
6
10
  from pathlib import Path
7
11
 
8
12
  from sonolus.backend.mode import Mode
@@ -10,7 +14,7 @@ from sonolus.build.compile import compile_mode
10
14
  from sonolus.script.archetype import _BaseArchetype
11
15
  from sonolus.script.bucket import Buckets
12
16
  from sonolus.script.effect import Effects
13
- from sonolus.script.engine import EngineData
17
+ from sonolus.script.engine import EngineData, empty_play_mode, empty_preview_mode, empty_tutorial_mode, empty_watch_mode
14
18
  from sonolus.script.instruction import (
15
19
  TutorialInstructionIcons,
16
20
  TutorialInstructions,
@@ -24,6 +28,7 @@ from sonolus.script.internal.callbacks import (
24
28
  from sonolus.script.internal.context import ReadOnlyMemory
25
29
  from sonolus.script.options import Options
26
30
  from sonolus.script.particle import Particles
31
+ from sonolus.script.project import BuildConfig
27
32
  from sonolus.script.sprite import Skin
28
33
  from sonolus.script.ui import UiConfig
29
34
 
@@ -48,43 +53,132 @@ class PackagedEngine:
48
53
  (path / "EngineTutorialData").write_bytes(self.tutorial_data)
49
54
  (path / "EngineRom").write_bytes(self.rom)
50
55
 
56
+ def read(self, path: Path):
57
+ self.configuration = (path / "EngineConfiguration").read_bytes()
58
+ self.play_data = (path / "EnginePlayData").read_bytes()
59
+ self.watch_data = (path / "EngineWatchData").read_bytes()
60
+ self.preview_data = (path / "EnginePreviewData").read_bytes()
61
+ self.tutorial_data = (path / "EngineTutorialData").read_bytes()
62
+ self.rom = (path / "EngineRom").read_bytes()
51
63
 
52
- def package_engine(engine: EngineData):
64
+
65
+ def no_gil() -> bool:
66
+ return sys.version_info >= (3, 13) and not sys._is_gil_enabled()
67
+
68
+
69
+ def package_engine(
70
+ engine: EngineData,
71
+ config: BuildConfig | None = None,
72
+ ):
73
+ config = config or BuildConfig()
53
74
  rom = ReadOnlyMemory()
54
75
  configuration = build_engine_configuration(engine.options, engine.ui)
55
- play_data = build_play_mode(
56
- archetypes=engine.play.archetypes,
57
- skin=engine.play.skin,
58
- effects=engine.play.effects,
59
- particles=engine.play.particles,
60
- buckets=engine.play.buckets,
61
- rom=rom,
62
- )
63
- watch_data = build_watch_mode(
64
- archetypes=engine.watch.archetypes,
65
- skin=engine.watch.skin,
66
- effects=engine.watch.effects,
67
- particles=engine.watch.particles,
68
- buckets=engine.watch.buckets,
69
- rom=rom,
70
- update_spawn=engine.watch.update_spawn,
71
- )
72
- preview_data = build_preview_mode(
73
- archetypes=engine.preview.archetypes,
74
- skin=engine.preview.skin,
75
- rom=rom,
76
- )
77
- tutorial_data = build_tutorial_mode(
78
- skin=engine.tutorial.skin,
79
- effects=engine.tutorial.effects,
80
- particles=engine.tutorial.particles,
81
- instructions=engine.tutorial.instructions,
82
- instruction_icons=engine.tutorial.instruction_icons,
83
- preprocess=engine.tutorial.preprocess,
84
- navigate=engine.tutorial.navigate,
85
- update=engine.tutorial.update,
86
- rom=rom,
87
- )
76
+ if no_gil():
77
+ thread_pool = ThreadPoolExecutor(process_cpu_count() or 1)
78
+ else:
79
+ thread_pool = None
80
+
81
+ play_mode = engine.play if config.build_play else empty_play_mode()
82
+ watch_mode = engine.watch if config.build_watch else empty_watch_mode()
83
+ preview_mode = engine.preview if config.build_preview else empty_preview_mode()
84
+ tutorial_mode = engine.tutorial if config.build_tutorial else empty_tutorial_mode()
85
+
86
+ if thread_pool is not None:
87
+ futures = {
88
+ "play": thread_pool.submit(
89
+ build_play_mode,
90
+ archetypes=play_mode.archetypes,
91
+ skin=play_mode.skin,
92
+ effects=play_mode.effects,
93
+ particles=play_mode.particles,
94
+ buckets=play_mode.buckets,
95
+ rom=rom,
96
+ config=config,
97
+ thread_pool=thread_pool,
98
+ ),
99
+ "watch": thread_pool.submit(
100
+ build_watch_mode,
101
+ archetypes=watch_mode.archetypes,
102
+ skin=watch_mode.skin,
103
+ effects=watch_mode.effects,
104
+ particles=watch_mode.particles,
105
+ buckets=watch_mode.buckets,
106
+ rom=rom,
107
+ update_spawn=watch_mode.update_spawn,
108
+ config=config,
109
+ thread_pool=thread_pool,
110
+ ),
111
+ "preview": thread_pool.submit(
112
+ build_preview_mode,
113
+ archetypes=preview_mode.archetypes,
114
+ skin=preview_mode.skin,
115
+ rom=rom,
116
+ config=config,
117
+ thread_pool=thread_pool,
118
+ ),
119
+ "tutorial": thread_pool.submit(
120
+ build_tutorial_mode,
121
+ skin=tutorial_mode.skin,
122
+ effects=tutorial_mode.effects,
123
+ particles=tutorial_mode.particles,
124
+ instructions=tutorial_mode.instructions,
125
+ instruction_icons=tutorial_mode.instruction_icons,
126
+ preprocess=tutorial_mode.preprocess,
127
+ navigate=tutorial_mode.navigate,
128
+ update=tutorial_mode.update,
129
+ rom=rom,
130
+ config=config,
131
+ thread_pool=thread_pool,
132
+ ),
133
+ }
134
+
135
+ play_data = futures["play"].result()
136
+ watch_data = futures["watch"].result()
137
+ preview_data = futures["preview"].result()
138
+ tutorial_data = futures["tutorial"].result()
139
+ else:
140
+ play_data = build_play_mode(
141
+ archetypes=play_mode.archetypes,
142
+ skin=play_mode.skin,
143
+ effects=play_mode.effects,
144
+ particles=play_mode.particles,
145
+ buckets=play_mode.buckets,
146
+ rom=rom,
147
+ config=config,
148
+ thread_pool=None,
149
+ )
150
+ watch_data = build_watch_mode(
151
+ archetypes=watch_mode.archetypes,
152
+ skin=watch_mode.skin,
153
+ effects=watch_mode.effects,
154
+ particles=watch_mode.particles,
155
+ buckets=watch_mode.buckets,
156
+ rom=rom,
157
+ update_spawn=watch_mode.update_spawn,
158
+ config=config,
159
+ thread_pool=None,
160
+ )
161
+ preview_data = build_preview_mode(
162
+ archetypes=preview_mode.archetypes,
163
+ skin=preview_mode.skin,
164
+ rom=rom,
165
+ config=config,
166
+ thread_pool=None,
167
+ )
168
+ tutorial_data = build_tutorial_mode(
169
+ skin=tutorial_mode.skin,
170
+ effects=tutorial_mode.effects,
171
+ particles=tutorial_mode.particles,
172
+ instructions=tutorial_mode.instructions,
173
+ instruction_icons=tutorial_mode.instruction_icons,
174
+ preprocess=tutorial_mode.preprocess,
175
+ navigate=tutorial_mode.navigate,
176
+ update=tutorial_mode.update,
177
+ rom=rom,
178
+ config=config,
179
+ thread_pool=None,
180
+ )
181
+
88
182
  return PackagedEngine(
89
183
  configuration=package_output(configuration),
90
184
  play_data=package_output(play_data),
@@ -112,9 +206,18 @@ def build_play_mode(
112
206
  particles: Particles,
113
207
  buckets: Buckets,
114
208
  rom: ReadOnlyMemory,
209
+ config: BuildConfig,
210
+ thread_pool: Executor | None = None,
115
211
  ):
116
212
  return {
117
- **compile_mode(mode=Mode.PLAY, rom=rom, archetypes=archetypes, global_callbacks=None),
213
+ **compile_mode(
214
+ mode=Mode.PLAY,
215
+ rom=rom,
216
+ archetypes=archetypes,
217
+ global_callbacks=None,
218
+ passes=config.passes,
219
+ thread_pool=thread_pool,
220
+ ),
118
221
  "skin": build_skin(skin),
119
222
  "effect": build_effects(effects),
120
223
  "particle": build_particles(particles),
@@ -130,10 +233,17 @@ def build_watch_mode(
130
233
  buckets: Buckets,
131
234
  rom: ReadOnlyMemory,
132
235
  update_spawn: Callable[[], float],
236
+ config: BuildConfig,
237
+ thread_pool: Executor | None = None,
133
238
  ):
134
239
  return {
135
240
  **compile_mode(
136
- mode=Mode.WATCH, rom=rom, archetypes=archetypes, global_callbacks=[(update_spawn_callback, update_spawn)]
241
+ mode=Mode.WATCH,
242
+ rom=rom,
243
+ archetypes=archetypes,
244
+ global_callbacks=[(update_spawn_callback, update_spawn)],
245
+ passes=config.passes,
246
+ thread_pool=thread_pool,
137
247
  ),
138
248
  "skin": build_skin(skin),
139
249
  "effect": build_effects(effects),
@@ -146,9 +256,18 @@ def build_preview_mode(
146
256
  archetypes: list[type[_BaseArchetype]],
147
257
  skin: Skin,
148
258
  rom: ReadOnlyMemory,
259
+ config: BuildConfig,
260
+ thread_pool: Executor | None = None,
149
261
  ):
150
262
  return {
151
- **compile_mode(mode=Mode.PREVIEW, rom=rom, archetypes=archetypes, global_callbacks=None),
263
+ **compile_mode(
264
+ mode=Mode.PREVIEW,
265
+ rom=rom,
266
+ archetypes=archetypes,
267
+ global_callbacks=None,
268
+ passes=config.passes,
269
+ thread_pool=thread_pool,
270
+ ),
152
271
  "skin": build_skin(skin),
153
272
  }
154
273
 
@@ -160,9 +279,11 @@ def build_tutorial_mode(
160
279
  instructions: TutorialInstructions,
161
280
  instruction_icons: TutorialInstructionIcons,
162
281
  preprocess: Callable[[], None],
163
- navigate: Callable[[int], None],
282
+ navigate: Callable[[], None],
164
283
  update: Callable[[], None],
165
284
  rom: ReadOnlyMemory,
285
+ config: BuildConfig,
286
+ thread_pool: Executor | None = None,
166
287
  ):
167
288
  return {
168
289
  **compile_mode(
@@ -174,6 +295,8 @@ def build_tutorial_mode(
174
295
  (navigate_callback, navigate),
175
296
  (update_callback, update),
176
297
  ],
298
+ passes=config.passes,
299
+ thread_pool=thread_pool,
177
300
  ),
178
301
  "skin": build_skin(skin),
179
302
  "effect": build_effects(effects),
@@ -183,7 +306,10 @@ def build_tutorial_mode(
183
306
 
184
307
 
185
308
  def build_skin(skin: Skin) -> JsonValue:
186
- return {"sprites": [{"name": name, "id": i} for i, name in enumerate(skin._sprites_)]}
309
+ return {
310
+ "renderMode": skin.render_mode,
311
+ "sprites": [{"name": name, "id": i} for i, name in enumerate(skin._sprites_)],
312
+ }
187
313
 
188
314
 
189
315
  def build_effects(effects: Effects) -> JsonValue:
sonolus/build/node.py CHANGED
@@ -1,3 +1,4 @@
1
+ from threading import Lock
1
2
  from typing import TypedDict
2
3
 
3
4
  from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
@@ -15,12 +16,18 @@ class FunctionOutputNode(TypedDict):
15
16
  class OutputNodeGenerator:
16
17
  nodes: list[ValueOutputNode | FunctionOutputNode]
17
18
  indexes: dict[EngineNode, int]
19
+ lock: Lock
18
20
 
19
21
  def __init__(self):
20
22
  self.nodes = []
21
23
  self.indexes = {}
24
+ self.lock = Lock()
22
25
 
23
26
  def add(self, node: EngineNode):
27
+ with self.lock:
28
+ return self._add(node)
29
+
30
+ def _add(self, node: EngineNode):
24
31
  if node in self.indexes:
25
32
  return self.indexes[node]
26
33
 
@@ -31,7 +38,7 @@ class OutputNodeGenerator:
31
38
  self.indexes[node] = index
32
39
  return index
33
40
  case FunctionNode(func, args):
34
- arg_indexes = [self.add(arg) for arg in args]
41
+ arg_indexes = [self._add(arg) for arg in args]
35
42
  index = len(self.nodes)
36
43
  self.nodes.append({"func": func.value, "args": arg_indexes})
37
44
  self.indexes[node] = index
sonolus/build/project.py CHANGED
@@ -5,7 +5,7 @@ from sonolus.build.engine import package_engine
5
5
  from sonolus.build.level import package_level_data
6
6
  from sonolus.script.engine import Engine
7
7
  from sonolus.script.level import Level
8
- from sonolus.script.project import Project, ProjectSchema
8
+ from sonolus.script.project import BuildConfig, Project, ProjectSchema
9
9
 
10
10
  BLANK_PNG = (
11
11
  b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x00\x007n\xf9$"
@@ -17,24 +17,24 @@ BLANK_AUDIO = (
17
17
  )
18
18
 
19
19
 
20
- def build_project_to_collection(project: Project):
20
+ def build_project_to_collection(project: Project, config: BuildConfig):
21
21
  collection = load_resources_files_to_collection(project.resources)
22
- add_engine_to_collection(collection, project, project.engine)
22
+ add_engine_to_collection(collection, project, project.engine, config)
23
23
  for level in project.levels:
24
24
  add_level_to_collection(collection, project, level)
25
25
  collection.name = f"{project.engine.name}"
26
26
  return collection
27
27
 
28
28
 
29
- def add_engine_to_collection(collection: Collection, project: Project, engine: Engine):
30
- packaged_engine = package_engine(engine.data)
29
+ def add_engine_to_collection(collection: Collection, project: Project, engine: Engine, config: BuildConfig):
30
+ packaged_engine = package_engine(engine.data, config)
31
31
  item = {
32
32
  "name": engine.name,
33
33
  "version": engine.version,
34
34
  "title": engine.title,
35
35
  "subtitle": engine.subtitle,
36
36
  "author": engine.author,
37
- "tags": [],
37
+ "tags": [tag.as_dict() for tag in engine.tags],
38
38
  "skin": collection.get_item("skins", engine.skin) if engine.skin else collection.get_default_item("skins"),
39
39
  "background": collection.get_item("backgrounds", engine.background)
40
40
  if engine.background
@@ -53,6 +53,8 @@ def add_engine_to_collection(collection: Collection, project: Project, engine: E
53
53
  "rom": collection.add_asset(packaged_engine.rom),
54
54
  "configuration": collection.add_asset(packaged_engine.configuration),
55
55
  }
56
+ if engine.description is not None:
57
+ item["description"] = engine.description
56
58
  collection.add_item("engines", engine.name, item)
57
59
 
58
60
 
@@ -65,16 +67,28 @@ def add_level_to_collection(collection: Collection, project: Project, level: Lev
65
67
  "title": level.title,
66
68
  "artists": level.artists,
67
69
  "author": level.author,
68
- "tags": [],
70
+ "tags": [tag.as_dict() for tag in level.tags],
69
71
  "engine": collection.get_item("engines", project.engine.name),
70
- "useSkin": {"useDefault": True},
71
- "useBackground": {"useDefault": True},
72
- "useEffect": {"useDefault": True},
73
- "useParticle": {"useDefault": True},
72
+ "useSkin": {"useDefault": True}
73
+ if level.use_skin is None
74
+ else {"useDefault": False, "item": collection.get_item("skins", level.use_skin)},
75
+ "useBackground": {"useDefault": True}
76
+ if level.use_background is None
77
+ else {"useDefault": False, "item": collection.get_item("backgrounds", level.use_background)},
78
+ "useEffect": {"useDefault": True}
79
+ if level.use_effect is None
80
+ else {"useDefault": False, "item": collection.get_item("effects", level.use_effect)},
81
+ "useParticle": {"useDefault": True}
82
+ if level.use_particle is None
83
+ else {"useDefault": False, "item": collection.get_item("particles", level.use_particle)},
74
84
  "cover": load_resource(collection, level.cover, project.resources, BLANK_PNG),
75
85
  "bgm": load_resource(collection, level.bgm, project.resources, BLANK_AUDIO),
76
86
  "data": collection.add_asset(packaged_level_data),
77
87
  }
88
+ if level.description is not None:
89
+ item["description"] = level.description
90
+ if level.preview is not None:
91
+ item["preview"] = load_resource(collection, level.preview, project.resources, BLANK_AUDIO)
78
92
  collection.add_item("levels", level.name, item)
79
93
 
80
94
 
@@ -97,6 +111,7 @@ def load_resources_files_to_collection(base_path: Path) -> Collection:
97
111
  def get_project_schema(project: Project) -> ProjectSchema:
98
112
  by_archetype: dict[str, dict[str, bool]] = {}
99
113
  for archetype in project.engine.data.play.archetypes:
114
+ archetype._init_fields()
100
115
  fields = by_archetype.setdefault(archetype.name, {})
101
116
  # If a field is exported, we should exclude it if it's imported in watch mode
102
117
  for field in archetype._exported_keys_:
@@ -104,11 +119,15 @@ def get_project_schema(project: Project) -> ProjectSchema:
104
119
  for field in archetype._imported_keys_:
105
120
  fields[field] = True
106
121
  for archetype in project.engine.data.watch.archetypes:
122
+ archetype._init_fields()
107
123
  fields = by_archetype.setdefault(archetype.name, {})
108
124
  for field in archetype._imported_keys_:
125
+ if field in {"#ACCURACY", "#JUDGMENT"}:
126
+ continue
109
127
  if field not in fields:
110
128
  fields[field] = True
111
129
  for archetype in project.engine.data.preview.archetypes:
130
+ archetype._init_fields()
112
131
  fields = by_archetype.setdefault(archetype.name, {})
113
132
  for field in archetype._imported_keys_:
114
133
  fields[field] = True