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.
- sonolus/backend/finalize.py +2 -1
- sonolus/backend/optimize/constant_evaluation.py +2 -2
- sonolus/backend/optimize/copy_coalesce.py +16 -7
- sonolus/backend/optimize/optimize.py +12 -4
- sonolus/backend/optimize/passes.py +2 -1
- sonolus/backend/place.py +95 -14
- sonolus/backend/visitor.py +83 -12
- sonolus/build/cli.py +60 -9
- sonolus/build/collection.py +68 -26
- sonolus/build/compile.py +87 -30
- sonolus/build/engine.py +166 -40
- sonolus/build/level.py +2 -1
- sonolus/build/node.py +8 -1
- sonolus/build/project.py +30 -11
- sonolus/script/archetype.py +169 -51
- sonolus/script/array.py +12 -1
- sonolus/script/bucket.py +26 -8
- sonolus/script/debug.py +2 -2
- sonolus/script/effect.py +2 -2
- sonolus/script/engine.py +123 -15
- sonolus/script/internal/builtin_impls.py +21 -2
- sonolus/script/internal/constant.py +6 -2
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/introspection.py +8 -1
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +5 -1
- sonolus/script/internal/value.py +10 -2
- sonolus/script/interval.py +16 -0
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +130 -8
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +11 -2
- sonolus/script/options.py +5 -3
- sonolus/script/pointer.py +2 -0
- sonolus/script/project.py +41 -5
- sonolus/script/record.py +8 -3
- sonolus/script/runtime.py +61 -10
- sonolus/script/sprite.py +18 -1
- sonolus/script/ui.py +7 -3
- sonolus/script/values.py +8 -5
- sonolus/script/vec.py +28 -0
- {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
- sonolus_py-0.2.0.dist-info/RECORD +90 -0
- {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
- sonolus_py-0.1.8.dist-info/RECORD +0 -89
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.8.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
sonolus/script/engine.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from collections.abc import Callable
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
from typing import Any
|
|
5
8
|
|
|
6
|
-
from sonolus.build.collection import Asset
|
|
9
|
+
from sonolus.build.collection import Asset, load_asset
|
|
7
10
|
from sonolus.script.archetype import PlayArchetype, PreviewArchetype, WatchArchetype, _BaseArchetype
|
|
8
11
|
from sonolus.script.bucket import Buckets, EmptyBuckets
|
|
9
12
|
from sonolus.script.effect import Effects, EmptyEffects
|
|
@@ -13,12 +16,52 @@ from sonolus.script.instruction import (
|
|
|
13
16
|
TutorialInstructionIcons,
|
|
14
17
|
TutorialInstructions,
|
|
15
18
|
)
|
|
19
|
+
from sonolus.script.metadata import AnyText, Tag, as_localization_text
|
|
16
20
|
from sonolus.script.options import EmptyOptions, Options
|
|
17
21
|
from sonolus.script.particle import EmptyParticles, Particles
|
|
18
22
|
from sonolus.script.sprite import EmptySkin, Skin
|
|
19
23
|
from sonolus.script.ui import UiConfig
|
|
20
24
|
|
|
21
25
|
|
|
26
|
+
class ExportedEngine:
|
|
27
|
+
"""An exported Sonolus.py engine."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
item: dict,
|
|
33
|
+
thumbnail: bytes,
|
|
34
|
+
play_data: bytes,
|
|
35
|
+
watch_data: bytes,
|
|
36
|
+
preview_data: bytes,
|
|
37
|
+
tutorial_data: bytes,
|
|
38
|
+
rom: bytes | None = None,
|
|
39
|
+
configuration: bytes,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.item = item
|
|
42
|
+
self.thumbnail = thumbnail
|
|
43
|
+
self.play_data = play_data
|
|
44
|
+
self.watch_data = watch_data
|
|
45
|
+
self.preview_data = preview_data
|
|
46
|
+
self.tutorial_data = tutorial_data
|
|
47
|
+
self.rom = rom
|
|
48
|
+
self.configuration = configuration
|
|
49
|
+
|
|
50
|
+
def write_to_dir(self, path: PathLike):
|
|
51
|
+
"""Write the exported engine to a directory."""
|
|
52
|
+
path = Path(path)
|
|
53
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
(path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
|
|
55
|
+
(path / "thumbnail").write_bytes(self.thumbnail)
|
|
56
|
+
(path / "playData").write_bytes(self.play_data)
|
|
57
|
+
(path / "watchData").write_bytes(self.watch_data)
|
|
58
|
+
(path / "previewData").write_bytes(self.preview_data)
|
|
59
|
+
(path / "tutorialData").write_bytes(self.tutorial_data)
|
|
60
|
+
if self.rom is not None:
|
|
61
|
+
(path / "rom").write_bytes(self.rom)
|
|
62
|
+
(path / "configuration").write_bytes(self.configuration)
|
|
63
|
+
|
|
64
|
+
|
|
22
65
|
class Engine:
|
|
23
66
|
"""A Sonolus.py engine.
|
|
24
67
|
|
|
@@ -33,6 +76,9 @@ class Engine:
|
|
|
33
76
|
particle: The default particle for the engine.
|
|
34
77
|
thumbnail: The thumbnail for the engine.
|
|
35
78
|
data: The engine's modes and configurations.
|
|
79
|
+
tags: The tags of the engine.
|
|
80
|
+
description: The description of the engine.
|
|
81
|
+
meta: Additional metadata of the engine.
|
|
36
82
|
"""
|
|
37
83
|
|
|
38
84
|
version = 12
|
|
@@ -41,26 +87,68 @@ class Engine:
|
|
|
41
87
|
self,
|
|
42
88
|
*,
|
|
43
89
|
name: str,
|
|
44
|
-
title:
|
|
45
|
-
subtitle:
|
|
46
|
-
author:
|
|
90
|
+
title: AnyText | None = None,
|
|
91
|
+
subtitle: AnyText = "Sonolus.py Engine",
|
|
92
|
+
author: AnyText = "Unknown",
|
|
47
93
|
skin: str | None = None,
|
|
48
94
|
background: str | None = None,
|
|
49
95
|
effect: str | None = None,
|
|
50
96
|
particle: str | None = None,
|
|
51
97
|
thumbnail: Asset | None = None,
|
|
52
98
|
data: EngineData,
|
|
99
|
+
tags: list[Tag] | None = None,
|
|
100
|
+
description: AnyText | None = None,
|
|
101
|
+
meta: Any = None,
|
|
53
102
|
) -> None:
|
|
54
103
|
self.name = name
|
|
55
|
-
self.title = title or name
|
|
56
|
-
self.subtitle = subtitle
|
|
57
|
-
self.author = author
|
|
104
|
+
self.title = as_localization_text(title or name)
|
|
105
|
+
self.subtitle = as_localization_text(subtitle)
|
|
106
|
+
self.author = as_localization_text(author)
|
|
58
107
|
self.skin = skin
|
|
59
108
|
self.background = background
|
|
60
109
|
self.effect = effect
|
|
61
110
|
self.particle = particle
|
|
62
111
|
self.thumbnail = thumbnail
|
|
63
112
|
self.data = data
|
|
113
|
+
self.tags = tags or []
|
|
114
|
+
self.description = as_localization_text(description) if description is not None else None
|
|
115
|
+
self.meta = meta
|
|
116
|
+
|
|
117
|
+
def export(self) -> ExportedEngine:
|
|
118
|
+
"""Export the engine in a sonolus-pack compatible format.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
An exported engine.
|
|
122
|
+
"""
|
|
123
|
+
from sonolus.build.engine import package_engine
|
|
124
|
+
from sonolus.build.project import BLANK_PNG
|
|
125
|
+
|
|
126
|
+
item = {
|
|
127
|
+
"version": self.version,
|
|
128
|
+
"title": self.title,
|
|
129
|
+
"subtitle": self.subtitle,
|
|
130
|
+
"author": self.author,
|
|
131
|
+
"tags": [tag.as_dict() for tag in self.tags],
|
|
132
|
+
"skin": self.skin,
|
|
133
|
+
"background": self.background,
|
|
134
|
+
"effect": self.effect,
|
|
135
|
+
"particle": self.particle,
|
|
136
|
+
}
|
|
137
|
+
packaged = package_engine(self.data)
|
|
138
|
+
if self.description is not None:
|
|
139
|
+
item["description"] = self.description
|
|
140
|
+
if self.meta is not None:
|
|
141
|
+
item["meta"] = self.meta
|
|
142
|
+
return ExportedEngine(
|
|
143
|
+
item=item,
|
|
144
|
+
thumbnail=load_asset(self.thumbnail) if self.thumbnail is not None else BLANK_PNG,
|
|
145
|
+
play_data=packaged.play_data,
|
|
146
|
+
watch_data=packaged.watch_data,
|
|
147
|
+
preview_data=packaged.preview_data,
|
|
148
|
+
tutorial_data=packaged.tutorial_data,
|
|
149
|
+
rom=packaged.rom,
|
|
150
|
+
configuration=packaged.configuration,
|
|
151
|
+
)
|
|
64
152
|
|
|
65
153
|
|
|
66
154
|
def default_callback() -> Any:
|
|
@@ -190,6 +278,30 @@ class TutorialMode:
|
|
|
190
278
|
self.update = update
|
|
191
279
|
|
|
192
280
|
|
|
281
|
+
def empty_play_mode() -> PlayMode:
|
|
282
|
+
"""Create an empty play mode."""
|
|
283
|
+
return PlayMode()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def empty_watch_mode() -> WatchMode:
|
|
287
|
+
"""Create an empty watch mode."""
|
|
288
|
+
return WatchMode(update_spawn=default_callback)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def empty_preview_mode() -> PreviewMode:
|
|
292
|
+
"""Create an empty preview mode."""
|
|
293
|
+
return PreviewMode()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def empty_tutorial_mode() -> TutorialMode:
|
|
297
|
+
"""Create an empty tutorial mode."""
|
|
298
|
+
return TutorialMode(
|
|
299
|
+
preprocess=default_callback,
|
|
300
|
+
navigate=default_callback,
|
|
301
|
+
update=default_callback,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
193
305
|
class EngineData:
|
|
194
306
|
"""A Sonolus.py engine's modes and configurations.
|
|
195
307
|
|
|
@@ -214,11 +326,7 @@ class EngineData:
|
|
|
214
326
|
) -> None:
|
|
215
327
|
self.ui = ui or UiConfig()
|
|
216
328
|
self.options = options
|
|
217
|
-
self.play = play or
|
|
218
|
-
self.watch = watch or
|
|
219
|
-
self.preview = preview or
|
|
220
|
-
self.tutorial = tutorial or
|
|
221
|
-
preprocess=default_callback,
|
|
222
|
-
navigate=default_callback,
|
|
223
|
-
update=default_callback,
|
|
224
|
-
)
|
|
329
|
+
self.play = play or empty_play_mode()
|
|
330
|
+
self.watch = watch or empty_watch_mode()
|
|
331
|
+
self.preview = preview or empty_preview_mode()
|
|
332
|
+
self.tutorial = tutorial or empty_tutorial_mode()
|
|
@@ -52,9 +52,11 @@ def _enumerate(iterable, start=0):
|
|
|
52
52
|
from sonolus.backend.visitor import compile_and_call
|
|
53
53
|
|
|
54
54
|
iterable = validate_value(iterable)
|
|
55
|
-
if
|
|
55
|
+
if isinstance(iterable, TupleImpl):
|
|
56
|
+
return TupleImpl._accept_(tuple((start + i, value) for i, value in enumerate(iterable._as_py_(), start=start)))
|
|
57
|
+
elif not hasattr(iterable, "__iter__"):
|
|
56
58
|
raise TypeError(f"'{type(iterable).__name__}' object is not iterable")
|
|
57
|
-
|
|
59
|
+
elif isinstance(iterable, ArrayLike):
|
|
58
60
|
return compile_and_call(iterable._enumerate_, start)
|
|
59
61
|
else:
|
|
60
62
|
iterator = compile_and_call(iterable.__iter__)
|
|
@@ -232,10 +234,27 @@ _int._type_mapping_ = Num
|
|
|
232
234
|
_float._type_mapping_ = Num
|
|
233
235
|
_bool._type_mapping_ = Num
|
|
234
236
|
|
|
237
|
+
|
|
238
|
+
def _any(iterable):
|
|
239
|
+
for value in iterable: # noqa: SIM110
|
|
240
|
+
if value:
|
|
241
|
+
return True
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _all(iterable):
|
|
246
|
+
for value in iterable: # noqa: SIM110
|
|
247
|
+
if not value:
|
|
248
|
+
return False
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
|
|
235
252
|
# classmethod, property, staticmethod are supported as decorators, but not within functions
|
|
236
253
|
|
|
237
254
|
BUILTIN_IMPLS = {
|
|
238
255
|
id(abs): _abs,
|
|
256
|
+
id(all): _all,
|
|
257
|
+
id(any): _any,
|
|
239
258
|
id(bool): _bool,
|
|
240
259
|
id(callable): _callable,
|
|
241
260
|
id(enumerate): _enumerate,
|
|
@@ -97,7 +97,7 @@ class ConstantValue(Value):
|
|
|
97
97
|
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
98
98
|
return cls()
|
|
99
99
|
|
|
100
|
-
def _to_list_(self, level_refs: dict[Any,
|
|
100
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
|
|
101
101
|
return []
|
|
102
102
|
|
|
103
103
|
@classmethod
|
|
@@ -107,7 +107,7 @@ class ConstantValue(Value):
|
|
|
107
107
|
def _get_(self) -> Self:
|
|
108
108
|
return self
|
|
109
109
|
|
|
110
|
-
def _set_(self, value: Any)
|
|
110
|
+
def _set_(self, value: Any):
|
|
111
111
|
if value is not self:
|
|
112
112
|
raise ValueError(f"{type(self).__name__} is immutable")
|
|
113
113
|
|
|
@@ -122,6 +122,10 @@ class ConstantValue(Value):
|
|
|
122
122
|
def _alloc_(cls) -> Self:
|
|
123
123
|
return cls()
|
|
124
124
|
|
|
125
|
+
@classmethod
|
|
126
|
+
def _zero_(cls) -> Self:
|
|
127
|
+
return cls()
|
|
128
|
+
|
|
125
129
|
@meta_fn
|
|
126
130
|
def __eq__(self, other):
|
|
127
131
|
return self is other
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
from contextvars import ContextVar
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from threading import Lock
|
|
6
7
|
from typing import Any, Self
|
|
7
8
|
|
|
8
9
|
from sonolus.backend.blocks import BlockData, PlayBlock
|
|
@@ -25,6 +26,7 @@ class GlobalContextState:
|
|
|
25
26
|
environment_mappings: dict[_GlobalInfo, int]
|
|
26
27
|
environment_offsets: dict[Block, int]
|
|
27
28
|
mode: Mode
|
|
29
|
+
lock: Lock
|
|
28
30
|
|
|
29
31
|
def __init__(self, mode: Mode, archetypes: dict[type, int] | None = None, rom: ReadOnlyMemory | None = None):
|
|
30
32
|
self.archetypes = archetypes or {}
|
|
@@ -33,6 +35,7 @@ class GlobalContextState:
|
|
|
33
35
|
self.environment_mappings = {}
|
|
34
36
|
self.environment_offsets = {}
|
|
35
37
|
self.mode = mode
|
|
38
|
+
self.lock = Lock()
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
class CallbackContextState:
|
|
@@ -88,10 +91,6 @@ class Context:
|
|
|
88
91
|
def used_names(self) -> dict[str, int]:
|
|
89
92
|
return self.callback_state.used_names
|
|
90
93
|
|
|
91
|
-
@property
|
|
92
|
-
def const_mappings(self) -> dict[Any, int]:
|
|
93
|
-
return self.global_state.const_mappings
|
|
94
|
-
|
|
95
94
|
def check_readable(self, place: BlockPlace):
|
|
96
95
|
if not self.callback:
|
|
97
96
|
return
|
|
@@ -204,22 +203,25 @@ class Context:
|
|
|
204
203
|
)
|
|
205
204
|
|
|
206
205
|
def map_constant(self, value: Any) -> int:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
with self.global_state.lock:
|
|
207
|
+
const_mappings = self.global_state.const_mappings
|
|
208
|
+
if value not in const_mappings:
|
|
209
|
+
const_mappings[value] = len(const_mappings)
|
|
210
|
+
return const_mappings[value]
|
|
210
211
|
|
|
211
212
|
def get_global_base(self, value: _GlobalInfo | _GlobalPlaceholder) -> BlockPlace:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if value
|
|
217
|
-
offset
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
213
|
+
with self.global_state.lock:
|
|
214
|
+
block = value.blocks.get(self.global_state.mode)
|
|
215
|
+
if block is None:
|
|
216
|
+
raise RuntimeError(f"Global {value.name} is not available in '{self.global_state.mode.name}' mode")
|
|
217
|
+
if value not in self.global_state.environment_mappings:
|
|
218
|
+
if value.offset is None:
|
|
219
|
+
offset = self.global_state.environment_offsets.get(block, 0)
|
|
220
|
+
self.global_state.environment_mappings[value] = offset
|
|
221
|
+
self.global_state.environment_offsets[block] = offset + value.size
|
|
222
|
+
else:
|
|
223
|
+
self.global_state.environment_mappings[value] = value.offset
|
|
224
|
+
return BlockPlace(block, self.global_state.environment_mappings[value])
|
|
223
225
|
|
|
224
226
|
@classmethod
|
|
225
227
|
def meet(cls, contexts: list[Context]) -> Context:
|
|
@@ -257,19 +259,22 @@ def using_ctx(value: Context | None):
|
|
|
257
259
|
class ReadOnlyMemory:
|
|
258
260
|
values: list[float]
|
|
259
261
|
indexes: dict[tuple[float, ...], int]
|
|
262
|
+
_lock: Lock
|
|
260
263
|
|
|
261
264
|
def __init__(self):
|
|
262
265
|
self.values = []
|
|
263
266
|
self.indexes = {}
|
|
267
|
+
self._lock = Lock()
|
|
264
268
|
|
|
265
269
|
def __getitem__(self, item: tuple[float, ...]) -> BlockPlace:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
with self._lock:
|
|
271
|
+
if item not in self.indexes:
|
|
272
|
+
index = len(self.values)
|
|
273
|
+
self.indexes[item] = index
|
|
274
|
+
self.values.extend(item)
|
|
275
|
+
else:
|
|
276
|
+
index = self.indexes[item]
|
|
277
|
+
return BlockPlace(self.block, index)
|
|
273
278
|
|
|
274
279
|
@property
|
|
275
280
|
def block(self) -> Block:
|
|
@@ -12,6 +12,13 @@ def get_field_specifiers(cls, *, skip: set[str] = frozenset(), globals=None, loc
|
|
|
12
12
|
if class_value is not _missing and key not in skip:
|
|
13
13
|
results[key] = Annotated[value, class_value]
|
|
14
14
|
for key, value in cls.__dict__.items():
|
|
15
|
-
if
|
|
15
|
+
if (
|
|
16
|
+
key not in results
|
|
17
|
+
and key not in skip
|
|
18
|
+
and not key.startswith("__")
|
|
19
|
+
and not callable(value)
|
|
20
|
+
and not hasattr(value, "__func__")
|
|
21
|
+
and not isinstance(value, property)
|
|
22
|
+
):
|
|
16
23
|
raise ValueError(f"Missing annotation for {cls.__name__}.{key}")
|
|
17
24
|
return results
|
|
@@ -26,7 +26,7 @@ class TransientValue(Value):
|
|
|
26
26
|
def _from_list_(cls, values: Iterable[float | BlockPlace]) -> Self:
|
|
27
27
|
raise TypeError(f"{cls.__name__} cannot be constructed from list")
|
|
28
28
|
|
|
29
|
-
def _to_list_(self, level_refs: dict[Any,
|
|
29
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
|
|
30
30
|
raise TypeError(f"{type(self).__name__} cannot be deconstructed to list")
|
|
31
31
|
|
|
32
32
|
@classmethod
|
|
@@ -49,3 +49,7 @@ class TransientValue(Value):
|
|
|
49
49
|
@classmethod
|
|
50
50
|
def _alloc_(cls) -> Self:
|
|
51
51
|
raise TypeError(f"{cls.__name__} is not allocatable")
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _zero_(cls) -> Self:
|
|
55
|
+
raise TypeError(f"{cls.__name__} does not have a zero value")
|
sonolus/script/internal/value.py
CHANGED
|
@@ -66,7 +66,7 @@ class Value:
|
|
|
66
66
|
raise NotImplementedError
|
|
67
67
|
|
|
68
68
|
@abstractmethod
|
|
69
|
-
def _to_list_(self, level_refs: dict[Any,
|
|
69
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[float | str | BlockPlace]:
|
|
70
70
|
"""Converts this value to a list of floats."""
|
|
71
71
|
raise NotImplementedError
|
|
72
72
|
|
|
@@ -76,7 +76,9 @@ class Value:
|
|
|
76
76
|
"""Returns the keys to a flat representation of this value."""
|
|
77
77
|
raise NotImplementedError
|
|
78
78
|
|
|
79
|
-
def _to_flat_dict_(
|
|
79
|
+
def _to_flat_dict_(
|
|
80
|
+
self, prefix: str, level_refs: dict[Any, str] | None = None
|
|
81
|
+
) -> dict[str, float | str | BlockPlace]:
|
|
80
82
|
"""Converts this value to a flat dictionary."""
|
|
81
83
|
return dict(zip(self._flat_keys_(prefix), self._to_list_(level_refs), strict=False))
|
|
82
84
|
|
|
@@ -136,6 +138,12 @@ class Value:
|
|
|
136
138
|
"""Allocates a new value which may be uninitialized."""
|
|
137
139
|
raise NotImplementedError
|
|
138
140
|
|
|
141
|
+
@classmethod
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def _zero_(cls) -> Self:
|
|
144
|
+
"""Returns a zero-initialized value of this type."""
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
139
147
|
def __imatmul__(self, other):
|
|
140
148
|
self._copy_from_(other)
|
|
141
149
|
return self
|
sonolus/script/interval.py
CHANGED
|
@@ -19,6 +19,22 @@ class Interval(Record):
|
|
|
19
19
|
start: float
|
|
20
20
|
end: float
|
|
21
21
|
|
|
22
|
+
@classmethod
|
|
23
|
+
def zero(cls) -> Self:
|
|
24
|
+
"""Get an empty interval."""
|
|
25
|
+
return cls(0, 0)
|
|
26
|
+
|
|
27
|
+
def then(self, length: float) -> Self:
|
|
28
|
+
"""Get the interval after this one with a given length.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
length: The length of the interval.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
An interval that has the end of this interval as the start and has the given length.
|
|
35
|
+
"""
|
|
36
|
+
return Interval(self.end, self.end + length)
|
|
37
|
+
|
|
22
38
|
@property
|
|
23
39
|
def length(self) -> float:
|
|
24
40
|
"""The length of the interval.
|
sonolus/script/iterator.py
CHANGED
|
@@ -165,3 +165,20 @@ class _FilteringIterator[T, Fn](Record, SonolusIterator):
|
|
|
165
165
|
|
|
166
166
|
def advance(self):
|
|
167
167
|
self.iterator.advance()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _ChainingIterator[T](Record, SonolusIterator):
|
|
171
|
+
iterator: T
|
|
172
|
+
|
|
173
|
+
def has_next(self) -> bool:
|
|
174
|
+
return self.iterator.has_next()
|
|
175
|
+
|
|
176
|
+
def get(self) -> Any:
|
|
177
|
+
return self.iterator.get().get()
|
|
178
|
+
|
|
179
|
+
def advance(self):
|
|
180
|
+
self.iterator.get().advance()
|
|
181
|
+
while not self.iterator.get().has_next():
|
|
182
|
+
self.iterator.advance()
|
|
183
|
+
if not self.iterator.has_next():
|
|
184
|
+
break
|
sonolus/script/level.py
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sonolus.build.collection import Asset, load_asset
|
|
4
10
|
from sonolus.script.archetype import PlayArchetype, StandardArchetypeName, StandardImport
|
|
11
|
+
from sonolus.script.metadata import AnyText, Tag, as_localization_text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExportedLevel:
|
|
15
|
+
"""An exported Sonolus level."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
item: dict,
|
|
21
|
+
cover: bytes,
|
|
22
|
+
bgm: bytes,
|
|
23
|
+
preview: bytes | None,
|
|
24
|
+
data: bytes,
|
|
25
|
+
):
|
|
26
|
+
self.item = item
|
|
27
|
+
self.cover = cover
|
|
28
|
+
self.bgm = bgm
|
|
29
|
+
self.preview = preview
|
|
30
|
+
self.data = data
|
|
31
|
+
|
|
32
|
+
def write_to_dir(self, path: PathLike):
|
|
33
|
+
"""Write the exported level to a directory."""
|
|
34
|
+
path = Path(path)
|
|
35
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
|
|
37
|
+
(path / "cover").write_bytes(self.cover)
|
|
38
|
+
(path / "bgm").write_bytes(self.bgm)
|
|
39
|
+
if self.preview is not None:
|
|
40
|
+
(path / "preview").write_bytes(self.preview)
|
|
41
|
+
(path / "data").write_bytes(self.data)
|
|
5
42
|
|
|
6
43
|
|
|
7
44
|
class Level:
|
|
@@ -16,6 +53,13 @@ class Level:
|
|
|
16
53
|
cover: The cover of the level.
|
|
17
54
|
bgm: The background music of the level.
|
|
18
55
|
data: The data of the level.
|
|
56
|
+
use_skin: The skin to use, overriding the engine skin.
|
|
57
|
+
use_background: The background to use, overriding the engine background.
|
|
58
|
+
use_effect: The effect to use, overriding the engine effect.
|
|
59
|
+
use_particle: The particle to use, overriding the engine particle.
|
|
60
|
+
tags: The tags of the level.
|
|
61
|
+
description: The description of the level.
|
|
62
|
+
meta: Additional metadata of the level.
|
|
19
63
|
"""
|
|
20
64
|
|
|
21
65
|
version = 1
|
|
@@ -24,22 +68,100 @@ class Level:
|
|
|
24
68
|
self,
|
|
25
69
|
*,
|
|
26
70
|
name: str,
|
|
27
|
-
title:
|
|
71
|
+
title: AnyText | None = None,
|
|
28
72
|
rating: int = 0,
|
|
29
|
-
artists:
|
|
30
|
-
author:
|
|
73
|
+
artists: AnyText = "Unknown",
|
|
74
|
+
author: AnyText = "Unknown",
|
|
31
75
|
cover: Asset | None = None,
|
|
32
76
|
bgm: Asset | None = None,
|
|
77
|
+
preview: Asset | None = None,
|
|
33
78
|
data: LevelData,
|
|
79
|
+
use_skin: str | None = None,
|
|
80
|
+
use_background: str | None = None,
|
|
81
|
+
use_effect: str | None = None,
|
|
82
|
+
use_particle: str | None = None,
|
|
83
|
+
tags: list[Tag] | None = None,
|
|
84
|
+
description: AnyText | None = None,
|
|
85
|
+
meta: Any = None,
|
|
34
86
|
) -> None:
|
|
35
87
|
self.name = name
|
|
36
|
-
self.title = title or name
|
|
88
|
+
self.title = as_localization_text(title or name)
|
|
37
89
|
self.rating = rating
|
|
38
|
-
self.artists = artists
|
|
39
|
-
self.author = author
|
|
90
|
+
self.artists = as_localization_text(artists)
|
|
91
|
+
self.author = as_localization_text(author)
|
|
40
92
|
self.cover = cover
|
|
41
93
|
self.bgm = bgm
|
|
94
|
+
self.preview = preview
|
|
42
95
|
self.data = data
|
|
96
|
+
self.use_skin = use_skin
|
|
97
|
+
self.use_background = use_background
|
|
98
|
+
self.use_effect = use_effect
|
|
99
|
+
self.use_particle = use_particle
|
|
100
|
+
self.tags = tags or []
|
|
101
|
+
self.description = as_localization_text(description) if description is not None else None
|
|
102
|
+
self.meta = meta
|
|
103
|
+
|
|
104
|
+
def export(self, engine_name: str) -> ExportedLevel:
|
|
105
|
+
"""Export the level in a sonolus-pack compatible format.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
engine_name: The name of the engine this level is for.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The exported level.
|
|
112
|
+
"""
|
|
113
|
+
from sonolus.build.level import package_level_data
|
|
114
|
+
from sonolus.build.project import BLANK_AUDIO, BLANK_PNG
|
|
115
|
+
|
|
116
|
+
item = {
|
|
117
|
+
"version": self.version,
|
|
118
|
+
"rating": self.rating,
|
|
119
|
+
"engine": engine_name,
|
|
120
|
+
"useSkin": {"useDefault": True} if self.use_skin is None else {"useDefault": False, "item": self.use_skin},
|
|
121
|
+
"useBackground": {"useDefault": True}
|
|
122
|
+
if self.use_background is None
|
|
123
|
+
else {"useDefault": False, "item": self.use_background},
|
|
124
|
+
"useEffect": {"useDefault": True}
|
|
125
|
+
if self.use_effect is None
|
|
126
|
+
else {"useDefault": False, "item": self.use_effect},
|
|
127
|
+
"useParticle": {"useDefault": True}
|
|
128
|
+
if self.use_particle is None
|
|
129
|
+
else {"useDefault": False, "item": self.use_particle},
|
|
130
|
+
"title": self.title,
|
|
131
|
+
"artists": self.artists,
|
|
132
|
+
"author": self.author,
|
|
133
|
+
"tags": [tag.as_dict() for tag in self.tags],
|
|
134
|
+
}
|
|
135
|
+
if self.description is not None:
|
|
136
|
+
item["description"] = self.description
|
|
137
|
+
if self.meta is not None:
|
|
138
|
+
item["meta"] = self.meta
|
|
139
|
+
return ExportedLevel(
|
|
140
|
+
item=item,
|
|
141
|
+
cover=load_asset(self.cover) if self.cover is not None else BLANK_PNG,
|
|
142
|
+
bgm=load_asset(self.bgm) if self.bgm is not None else BLANK_AUDIO,
|
|
143
|
+
preview=load_asset(self.preview) if self.preview is not None else None,
|
|
144
|
+
data=package_level_data(self.data),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
type EntityListArg = list[PlayArchetype | EntityListArg]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def flatten_entities(entities: EntityListArg) -> Iterator[PlayArchetype]:
|
|
152
|
+
"""Flatten a list of entities.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
entities: The list of entities.
|
|
156
|
+
|
|
157
|
+
Yields:
|
|
158
|
+
The flattened entities.
|
|
159
|
+
"""
|
|
160
|
+
if isinstance(entities, list):
|
|
161
|
+
for entity in entities:
|
|
162
|
+
yield from flatten_entities(entity)
|
|
163
|
+
else:
|
|
164
|
+
yield entities
|
|
43
165
|
|
|
44
166
|
|
|
45
167
|
class LevelData:
|
|
@@ -55,7 +177,7 @@ class LevelData:
|
|
|
55
177
|
|
|
56
178
|
def __init__(self, bgm_offset: float, entities: list[PlayArchetype]) -> None:
|
|
57
179
|
self.bgm_offset = bgm_offset
|
|
58
|
-
self.entities = entities
|
|
180
|
+
self.entities = [*flatten_entities(entities)]
|
|
59
181
|
|
|
60
182
|
|
|
61
183
|
class BpmChange(PlayArchetype):
|