sonolus.py 0.1.8__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 (48) hide show
  1. sonolus/backend/finalize.py +2 -1
  2. sonolus/backend/optimize/constant_evaluation.py +2 -2
  3. sonolus/backend/optimize/copy_coalesce.py +16 -7
  4. sonolus/backend/optimize/optimize.py +12 -4
  5. sonolus/backend/optimize/passes.py +2 -1
  6. sonolus/backend/place.py +95 -14
  7. sonolus/backend/visitor.py +83 -12
  8. sonolus/build/cli.py +60 -9
  9. sonolus/build/collection.py +68 -26
  10. sonolus/build/compile.py +87 -30
  11. sonolus/build/engine.py +166 -40
  12. sonolus/build/level.py +2 -1
  13. sonolus/build/node.py +8 -1
  14. sonolus/build/project.py +30 -11
  15. sonolus/script/archetype.py +169 -51
  16. sonolus/script/array.py +12 -1
  17. sonolus/script/bucket.py +26 -8
  18. sonolus/script/debug.py +2 -2
  19. sonolus/script/effect.py +2 -2
  20. sonolus/script/engine.py +123 -15
  21. sonolus/script/internal/builtin_impls.py +21 -2
  22. sonolus/script/internal/constant.py +6 -2
  23. sonolus/script/internal/context.py +30 -25
  24. sonolus/script/internal/introspection.py +8 -1
  25. sonolus/script/internal/math_impls.py +2 -1
  26. sonolus/script/internal/transient.py +5 -1
  27. sonolus/script/internal/value.py +10 -2
  28. sonolus/script/interval.py +16 -0
  29. sonolus/script/iterator.py +17 -0
  30. sonolus/script/level.py +130 -8
  31. sonolus/script/metadata.py +32 -0
  32. sonolus/script/num.py +11 -2
  33. sonolus/script/options.py +5 -3
  34. sonolus/script/pointer.py +2 -0
  35. sonolus/script/project.py +41 -5
  36. sonolus/script/record.py +8 -3
  37. sonolus/script/runtime.py +61 -10
  38. sonolus/script/sprite.py +18 -1
  39. sonolus/script/ui.py +7 -3
  40. sonolus/script/values.py +8 -5
  41. sonolus/script/vec.py +28 -0
  42. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
  43. sonolus_py-0.2.0.dist-info/RECORD +90 -0
  44. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
  45. sonolus_py-0.1.8.dist-info/RECORD +0 -89
  46. /sonolus/script/{print.py → printing.py} +0 -0
  47. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
  48. {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -36,7 +36,7 @@ SINGULAR_CATEGORY_NAMES: dict[Category, str] = {
36
36
  }
37
37
  BASE_PATH = "/sonolus/"
38
38
  RESERVED_FILENAMES = {"info", "list"}
39
- LOCALIZED_KEYS = {"title", "subtitle", "author", "description"}
39
+ LOCALIZED_KEYS = {"title", "subtitle", "author", "description", "artists"}
40
40
  CATEGORY_SORT_ORDER = {
41
41
  "levels": 0,
42
42
  "engines": 1,
@@ -69,10 +69,10 @@ class Collection:
69
69
  def add_item(self, category: Category, name: str, item: Any) -> None:
70
70
  self.categories.setdefault(category, {})[name] = self._make_item_details(item)
71
71
 
72
- @staticmethod
73
- def _make_item_details(item: dict[str, Any]) -> dict[str, Any]:
72
+ @classmethod
73
+ def _make_item_details(cls, item: dict[str, Any]) -> dict[str, Any]:
74
74
  return {
75
- "item": item,
75
+ "item": cls._localize_item(item),
76
76
  "actions": [],
77
77
  "hasCommunity": False,
78
78
  "leaderboards": [],
@@ -81,16 +81,7 @@ class Collection:
81
81
 
82
82
  @staticmethod
83
83
  def _load_data(value: Asset) -> bytes:
84
- match value:
85
- case str() if value.startswith(("http://", "https://")):
86
- with urllib.request.urlopen(value) as response:
87
- return response.read()
88
- case PathLike():
89
- return Path(value).read_bytes()
90
- case bytes():
91
- return value
92
- case _:
93
- raise TypeError("value must be a URL, a path, or bytes")
84
+ return load_asset(value)
94
85
 
95
86
  def add_asset(self, value: Asset, /) -> Srl:
96
87
  data = self._load_data(value)
@@ -123,7 +114,7 @@ class Collection:
123
114
  continue
124
115
 
125
116
  try:
126
- item_data = json.loads(item_json_path.read_text())
117
+ item_data = json.loads(item_json_path.read_text(encoding="utf-8"))
127
118
  except json.JSONDecodeError:
128
119
  continue
129
120
 
@@ -149,19 +140,32 @@ class Collection:
149
140
 
150
141
  self.add_item(category_name, item_dir.name, item_data)
151
142
 
152
- @staticmethod
153
- def _localize_item(item: dict[str, Any]) -> dict[str, Any]:
143
+ @classmethod
144
+ def _localize_item(cls, item: dict[str, Any]) -> dict[str, Any]:
154
145
  localized_item = item.copy()
155
146
  for key in LOCALIZED_KEYS:
156
- match localized_item.get(key):
157
- case {"en": localized_value}:
158
- localized_item[key] = localized_value
159
- case {**other_languages} if other_languages:
160
- localized_item[key] = localized_item[key][min(other_languages)]
161
- case _:
162
- localized_item[key] = ""
147
+ if key not in localized_item:
148
+ continue
149
+ localized_item[key] = cls._localize_text(localized_item[key])
150
+ if "tags" in localized_item:
151
+ localized_item["tags"] = [
152
+ {**tag, "title": cls._localize_text(tag["title"])} for tag in localized_item["tags"]
153
+ ]
154
+ localized_item.pop("meta", None)
163
155
  return localized_item
164
156
 
157
+ @staticmethod
158
+ def _localize_text(text: str | dict[str, str]) -> str:
159
+ match text:
160
+ case str():
161
+ return text
162
+ case {"en": localized_text}:
163
+ return localized_text
164
+ case {**other_languages} if other_languages:
165
+ return text[min(other_languages)]
166
+ case _:
167
+ return ""
168
+
165
169
  def _group_zip_entries_by_directory(self, file_list: list[zipfile.ZipInfo]) -> dict[str, list[zipfile.ZipInfo]]:
166
170
  files_by_dir: dict[str, list[zipfile.ZipInfo]] = {}
167
171
 
@@ -207,7 +211,7 @@ class Collection:
207
211
  ) -> None:
208
212
  for zip_entry in zip_entries:
209
213
  try:
210
- item_details = json.loads(zf.read(zip_entry))
214
+ item_details = json.loads(zf.read(zip_entry).decode("utf-8"))
211
215
  except json.JSONDecodeError:
212
216
  continue
213
217
 
@@ -220,11 +224,28 @@ class Collection:
220
224
  self.categories[dir_name][item_name] = item_details
221
225
 
222
226
  def write(self, path: Asset) -> None:
227
+ self.link()
223
228
  base_dir = self._create_base_directory(path)
224
229
  self._write_main_info(base_dir)
225
230
  self._write_category_items(base_dir)
226
231
  self._write_repository_items(base_dir)
227
232
 
233
+ def link(self):
234
+ for level_details in self.categories.get("levels", {}).values():
235
+ level = level_details["item"]
236
+ if isinstance(level["engine"], str):
237
+ level["engine"] = self.get_item("engines", level["engine"])
238
+ for key, category in (
239
+ ("useSkin", "skins"),
240
+ ("useBackground", "backgrounds"),
241
+ ("useEffect", "effects"),
242
+ ("useParticle", "particles"),
243
+ ):
244
+ use_item = level[key]
245
+ if "item" not in use_item:
246
+ continue
247
+ use_item["item"] = self.get_item(category, use_item["item"])
248
+
228
249
  def _create_base_directory(self, path: Asset) -> Path:
229
250
  base_dir = Path(path) / BASE_PATH.strip("/")
230
251
  base_dir.mkdir(parents=True, exist_ok=True)
@@ -280,7 +301,7 @@ class Collection:
280
301
 
281
302
  @staticmethod
282
303
  def _write_json(path: Path, content: Any) -> None:
283
- path.write_text(json.dumps(content))
304
+ path.write_text(json.dumps(content), encoding="utf-8")
284
305
 
285
306
  def update(self, other: Collection) -> None:
286
307
  self.repository.update(other.repository)
@@ -291,3 +312,24 @@ class Collection:
291
312
  class Srl(TypedDict):
292
313
  hash: str
293
314
  url: str
315
+
316
+
317
+ def load_asset(value: Asset) -> bytes:
318
+ match value:
319
+ case str() if value.startswith(("http://", "https://")):
320
+ headers = {
321
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
322
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
323
+ "Accept": "*/*",
324
+ "Accept-Language": "en-US,en;q=0.9",
325
+ "Connection": "keep-alive",
326
+ }
327
+ request = urllib.request.Request(value, headers=headers)
328
+ with urllib.request.urlopen(request) as response:
329
+ return response.read()
330
+ case PathLike():
331
+ return Path(value).read_bytes()
332
+ case bytes():
333
+ return value
334
+ case _:
335
+ raise TypeError("value must be a URL, a path, or bytes")
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,54 +29,109 @@ 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:
73
+ archetype._init_fields()
74
+
39
75
  archetype_data = {
40
76
  "name": archetype.name,
41
77
  "hasInput": archetype.is_scored,
42
78
  "imports": [{"name": name, "index": index} for name, index in archetype._imported_keys_.items()],
43
79
  }
44
80
  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
- }
81
+ archetype_data["exports"] = [*archetype._exported_keys_]
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
+
63
104
  archetype_entries.append(archetype_data)
64
- results["archetypes"] = archetype_entries
65
- 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:
66
119
  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
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
+
72
126
  results["nodes"] = nodes.get()
73
127
  return results
74
128
 
75
129
 
76
130
  def callback_to_cfg(
77
- 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,
78
135
  ) -> BasicBlock:
79
136
  callback_state = CallbackContextState(name)
80
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/level.py CHANGED
@@ -11,11 +11,12 @@ def package_level_data(
11
11
  def build_level_data(
12
12
  level_data: LevelData,
13
13
  ):
14
- level_refs = {entity: i for i, entity in enumerate(level_data.entities)}
14
+ level_refs = {entity: f"{i}_{entity.name}" for i, entity in enumerate(level_data.entities)}
15
15
  return {
16
16
  "bgmOffset": level_data.bgm_offset,
17
17
  "entities": [
18
18
  {
19
+ "name": level_refs[entity],
19
20
  "archetype": entity.name,
20
21
  "data": entity._level_data_entries(level_refs),
21
22
  }
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