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.
- sonolus/backend/optimize/constant_evaluation.py +2 -2
- sonolus/backend/optimize/optimize.py +12 -4
- sonolus/backend/optimize/passes.py +2 -1
- sonolus/backend/place.py +95 -14
- sonolus/backend/visitor.py +51 -3
- sonolus/build/cli.py +60 -9
- sonolus/build/collection.py +68 -26
- sonolus/build/compile.py +85 -27
- sonolus/build/engine.py +166 -40
- sonolus/build/node.py +8 -1
- sonolus/build/project.py +30 -11
- sonolus/script/archetype.py +110 -26
- sonolus/script/array.py +11 -0
- 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 +5 -1
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +4 -0
- sonolus/script/internal/value.py +6 -0
- sonolus/script/interval.py +16 -0
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +113 -10
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +9 -0
- sonolus/script/options.py +5 -3
- sonolus/script/pointer.py +2 -0
- sonolus/script/project.py +41 -5
- sonolus/script/record.py +7 -2
- 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.9.dist-info → sonolus_py-0.2.0.dist-info}/METADATA +3 -2
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/RECORD +42 -41
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/WHEEL +1 -1
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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:
|
sonolus/script/internal/value.py
CHANGED
|
@@ -138,6 +138,12 @@ class Value:
|
|
|
138
138
|
"""Allocates a new value which may be uninitialized."""
|
|
139
139
|
raise NotImplementedError
|
|
140
140
|
|
|
141
|
+
@classmethod
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def _zero_(cls) -> Self:
|
|
144
|
+
"""Returns a zero-initialized value of this type."""
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
141
147
|
def __imatmul__(self, other):
|
|
142
148
|
self._copy_from_(other)
|
|
143
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,35 +68,94 @@ 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
|
+
)
|
|
43
146
|
|
|
44
147
|
|
|
45
148
|
type EntityListArg = list[PlayArchetype | EntityListArg]
|
|
46
149
|
|
|
47
150
|
|
|
48
|
-
def flatten_entities(entities: EntityListArg):
|
|
151
|
+
def flatten_entities(entities: EntityListArg) -> Iterator[PlayArchetype]:
|
|
49
152
|
"""Flatten a list of entities.
|
|
50
153
|
|
|
51
154
|
Args:
|
|
52
155
|
entities: The list of entities.
|
|
53
156
|
|
|
54
|
-
|
|
55
|
-
The flattened
|
|
157
|
+
Yields:
|
|
158
|
+
The flattened entities.
|
|
56
159
|
"""
|
|
57
160
|
if isinstance(entities, list):
|
|
58
161
|
for entity in entities:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
type LocalizationText = dict[str, str]
|
|
4
|
+
type AnyText = str | LocalizationText
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def as_localization_text(text: AnyText) -> LocalizationText:
|
|
8
|
+
if isinstance(text, str):
|
|
9
|
+
return {"en": text}
|
|
10
|
+
return text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Tag:
|
|
14
|
+
"""A tag for an engine or level.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
title: The title of the tag.
|
|
18
|
+
icon: The icon of the tag.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
title: LocalizationText
|
|
22
|
+
icon: str | None
|
|
23
|
+
|
|
24
|
+
def __init__(self, title: AnyText, icon: str | None = None) -> None:
|
|
25
|
+
self.title = as_localization_text(title)
|
|
26
|
+
self.icon = icon
|
|
27
|
+
|
|
28
|
+
def as_dict(self) -> dict[str, Any]:
|
|
29
|
+
result = {"title": self.title}
|
|
30
|
+
if self.icon is not None:
|
|
31
|
+
result["icon"] = self.icon
|
|
32
|
+
return result
|
sonolus/script/num.py
CHANGED
|
@@ -134,6 +134,15 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
134
134
|
else:
|
|
135
135
|
return Num(-1)
|
|
136
136
|
|
|
137
|
+
@classmethod
|
|
138
|
+
def _zero_(cls) -> Self:
|
|
139
|
+
if ctx():
|
|
140
|
+
result_place = ctx().alloc(size=1)
|
|
141
|
+
ctx().add_statements(IRSet(result_place, IRConst(0)))
|
|
142
|
+
return cls(result_place)
|
|
143
|
+
else:
|
|
144
|
+
return cls(0)
|
|
145
|
+
|
|
137
146
|
def ir(self):
|
|
138
147
|
if isinstance(self.data, BlockPlace):
|
|
139
148
|
return IRGet(self.data)
|
sonolus/script/options.py
CHANGED
|
@@ -69,7 +69,7 @@ class _SelectOption:
|
|
|
69
69
|
standard: bool
|
|
70
70
|
advanced: bool
|
|
71
71
|
scope: str | None
|
|
72
|
-
default:
|
|
72
|
+
default: int
|
|
73
73
|
values: list[str]
|
|
74
74
|
|
|
75
75
|
def to_dict(self):
|
|
@@ -78,7 +78,7 @@ class _SelectOption:
|
|
|
78
78
|
"name": self.name,
|
|
79
79
|
"standard": self.standard,
|
|
80
80
|
"advanced": self.advanced,
|
|
81
|
-
"def": self.
|
|
81
|
+
"def": self.default,
|
|
82
82
|
"values": self.values,
|
|
83
83
|
}
|
|
84
84
|
if self.scope is not None:
|
|
@@ -139,7 +139,7 @@ def select_option(
|
|
|
139
139
|
name: str | None = None,
|
|
140
140
|
standard: bool = False,
|
|
141
141
|
advanced: bool = False,
|
|
142
|
-
default: str,
|
|
142
|
+
default: str | int,
|
|
143
143
|
values: list[str],
|
|
144
144
|
scope: str | None = None,
|
|
145
145
|
) -> Any:
|
|
@@ -153,6 +153,8 @@ def select_option(
|
|
|
153
153
|
values: The values of the option.
|
|
154
154
|
scope: The scope of the option.
|
|
155
155
|
"""
|
|
156
|
+
if isinstance(default, str):
|
|
157
|
+
default = values.index(default)
|
|
156
158
|
return _SelectOption(name, standard, advanced, scope, default, values)
|
|
157
159
|
|
|
158
160
|
|
sonolus/script/pointer.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from sonolus.backend.place import BlockPlace
|
|
2
|
+
from sonolus.script.internal.context import ctx
|
|
2
3
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
3
4
|
from sonolus.script.internal.value import Value
|
|
4
5
|
from sonolus.script.num import Num, _is_num
|
|
@@ -13,6 +14,7 @@ def _deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
|
|
|
13
14
|
block = block._as_py_()
|
|
14
15
|
if not isinstance(block, int):
|
|
15
16
|
raise TypeError("block must be an integer")
|
|
17
|
+
block = ctx().blocks(block)
|
|
16
18
|
else:
|
|
17
19
|
if not _is_num(block):
|
|
18
20
|
raise TypeError("block must be a Num")
|
sonolus/script/project.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
3
5
|
from os import PathLike
|
|
4
6
|
from pathlib import Path
|
|
5
|
-
from typing import Self, TypedDict
|
|
7
|
+
from typing import ClassVar, Self, TypedDict
|
|
6
8
|
|
|
9
|
+
from sonolus.backend.optimize import optimize
|
|
10
|
+
from sonolus.backend.optimize.passes import CompilerPass
|
|
7
11
|
from sonolus.script.archetype import ArchetypeSchema
|
|
8
12
|
from sonolus.script.engine import Engine
|
|
9
13
|
from sonolus.script.level import Level
|
|
@@ -39,27 +43,30 @@ class Project:
|
|
|
39
43
|
"""
|
|
40
44
|
return Project(self.engine, levels, self.resources)
|
|
41
45
|
|
|
42
|
-
def dev(self, build_dir: PathLike, port: int = 8080):
|
|
46
|
+
def dev(self, build_dir: PathLike, port: int = 8080, config: BuildConfig | None = None):
|
|
43
47
|
"""Start a development server for the project.
|
|
44
48
|
|
|
45
49
|
Args:
|
|
46
50
|
build_dir: The path to the build directory.
|
|
47
51
|
port: The port of the development server.
|
|
52
|
+
config: The build configuration.
|
|
48
53
|
"""
|
|
49
54
|
from sonolus.build.cli import build_collection, run_server
|
|
50
55
|
|
|
51
|
-
build_collection(self, Path(build_dir))
|
|
56
|
+
build_collection(self, Path(build_dir), config)
|
|
52
57
|
run_server(Path(build_dir) / "site", port=port)
|
|
53
58
|
|
|
54
|
-
def build(self, build_dir: PathLike):
|
|
59
|
+
def build(self, build_dir: PathLike, config: BuildConfig | None = None):
|
|
55
60
|
"""Build the project.
|
|
56
61
|
|
|
57
62
|
Args:
|
|
58
63
|
build_dir: The path to the build directory.
|
|
64
|
+
config: The build configuration.
|
|
59
65
|
"""
|
|
60
66
|
from sonolus.build.cli import build_project
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
config = config or BuildConfig()
|
|
69
|
+
build_project(self, Path(build_dir), config)
|
|
63
70
|
|
|
64
71
|
def schema(self) -> ProjectSchema:
|
|
65
72
|
"""Generate the schema of the project.
|
|
@@ -74,3 +81,32 @@ class Project:
|
|
|
74
81
|
|
|
75
82
|
class ProjectSchema(TypedDict):
|
|
76
83
|
archetypes: list[ArchetypeSchema]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class BuildConfig:
|
|
88
|
+
"""A configuration for building an engine package."""
|
|
89
|
+
|
|
90
|
+
MINIMAL_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.MINIMAL_PASSES
|
|
91
|
+
"""The minimal list of compiler passes."""
|
|
92
|
+
|
|
93
|
+
FAST_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.FAST_PASSES
|
|
94
|
+
"""The list of compiler passes for faster builds."""
|
|
95
|
+
|
|
96
|
+
STANDARD_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.STANDARD_PASSES
|
|
97
|
+
"""The standard list of compiler passes."""
|
|
98
|
+
|
|
99
|
+
passes: Sequence[CompilerPass] = optimize.STANDARD_PASSES
|
|
100
|
+
"""The list of compiler passes to use."""
|
|
101
|
+
|
|
102
|
+
build_play: bool = True
|
|
103
|
+
"""Whether to build the play package."""
|
|
104
|
+
|
|
105
|
+
build_watch: bool = True
|
|
106
|
+
"""Whether to build the watch package."""
|
|
107
|
+
|
|
108
|
+
build_preview: bool = True
|
|
109
|
+
"""Whether to build the preview package."""
|
|
110
|
+
|
|
111
|
+
build_tutorial: bool = True
|
|
112
|
+
"""Whether to build the tutorial package."""
|
sonolus/script/record.py
CHANGED
|
@@ -206,8 +206,7 @@ class Record(GenericValue):
|
|
|
206
206
|
raise TypeError("Record does not support set_")
|
|
207
207
|
|
|
208
208
|
def _copy_from_(self, value: Self):
|
|
209
|
-
|
|
210
|
-
raise TypeError("Cannot copy from different type")
|
|
209
|
+
value = self._accept_(value)
|
|
211
210
|
for field in self._fields:
|
|
212
211
|
field.__set__(self, field.__get__(value))
|
|
213
212
|
|
|
@@ -221,6 +220,12 @@ class Record(GenericValue):
|
|
|
221
220
|
result._value = {field.name: field.type._alloc_() for field in cls._fields}
|
|
222
221
|
return result
|
|
223
222
|
|
|
223
|
+
@classmethod
|
|
224
|
+
def _zero_(cls) -> Self:
|
|
225
|
+
result = object.__new__(cls)
|
|
226
|
+
result._value = {field.name: field.type._zero_() for field in cls._fields}
|
|
227
|
+
return result
|
|
228
|
+
|
|
224
229
|
def __str__(self):
|
|
225
230
|
return (
|
|
226
231
|
f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)}" for field in self._fields)})"
|
sonolus/script/runtime.py
CHANGED
|
@@ -103,6 +103,12 @@ class _PreviewRuntimeCanvas:
|
|
|
103
103
|
scroll_direction: ScrollDirection
|
|
104
104
|
size: float
|
|
105
105
|
|
|
106
|
+
def update(self, scroll_direction: ScrollDirection | None = None, size: float | None = None):
|
|
107
|
+
if scroll_direction is not None:
|
|
108
|
+
self.scroll_direction = scroll_direction
|
|
109
|
+
if size is not None:
|
|
110
|
+
self.size = size
|
|
111
|
+
|
|
106
112
|
|
|
107
113
|
class RuntimeUiConfig(Record):
|
|
108
114
|
scale: float
|
|
@@ -253,36 +259,57 @@ class _TutorialRuntimeUi:
|
|
|
253
259
|
|
|
254
260
|
|
|
255
261
|
class Touch(Record):
|
|
262
|
+
"""Data of a touch event."""
|
|
263
|
+
|
|
256
264
|
id: int
|
|
265
|
+
"""The unique identifier of the touch."""
|
|
266
|
+
|
|
257
267
|
started: bool
|
|
268
|
+
"""Whether the touch has started this frame."""
|
|
269
|
+
|
|
258
270
|
ended: bool
|
|
271
|
+
"""Whether the touch has ended this frame."""
|
|
272
|
+
|
|
259
273
|
time: float
|
|
274
|
+
"""The time of the touch event.
|
|
275
|
+
|
|
276
|
+
May remain constant while there is no movement.
|
|
277
|
+
"""
|
|
278
|
+
|
|
260
279
|
start_time: float
|
|
280
|
+
"""The time the touch started."""
|
|
281
|
+
|
|
261
282
|
position: Vec2
|
|
283
|
+
"""The current position of the touch."""
|
|
284
|
+
|
|
262
285
|
start_position: Vec2
|
|
286
|
+
"""The position the touch started."""
|
|
287
|
+
|
|
263
288
|
delta: Vec2
|
|
289
|
+
"""The change in position of the touch."""
|
|
290
|
+
|
|
264
291
|
velocity: Vec2
|
|
292
|
+
"""The velocity of the touch."""
|
|
293
|
+
|
|
265
294
|
speed: float
|
|
295
|
+
"""The speed of the touch's movement."""
|
|
296
|
+
|
|
266
297
|
angle: float
|
|
298
|
+
"""The angle of the touch's movement."""
|
|
267
299
|
|
|
268
300
|
@property
|
|
269
|
-
def
|
|
270
|
-
|
|
301
|
+
def prev_position(self) -> Vec2:
|
|
302
|
+
"""The previous position of the touch."""
|
|
303
|
+
return self.position - self.delta
|
|
271
304
|
|
|
272
305
|
@property
|
|
273
306
|
def total_delta(self) -> Vec2:
|
|
307
|
+
"""The total change in position of the touch."""
|
|
274
308
|
return self.position - self.start_position
|
|
275
309
|
|
|
276
|
-
@property
|
|
277
|
-
def total_velocity(self) -> Vec2:
|
|
278
|
-
return self.total_delta / self.total_time if self.total_time > 0 else Vec2(0, 0)
|
|
279
|
-
|
|
280
|
-
@property
|
|
281
|
-
def total_speed(self) -> float:
|
|
282
|
-
return self.total_velocity.magnitude
|
|
283
|
-
|
|
284
310
|
@property
|
|
285
311
|
def total_angle(self) -> float:
|
|
312
|
+
"""The total angle of the touch's movement."""
|
|
286
313
|
return self.total_delta.angle
|
|
287
314
|
|
|
288
315
|
|
|
@@ -449,6 +476,30 @@ def is_debug() -> bool:
|
|
|
449
476
|
return False
|
|
450
477
|
|
|
451
478
|
|
|
479
|
+
@meta_fn
|
|
480
|
+
def is_play() -> bool:
|
|
481
|
+
"""Check if the game is running in play mode."""
|
|
482
|
+
return ctx() and ctx().global_state.mode == Mode.PLAY
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@meta_fn
|
|
486
|
+
def is_preview() -> bool:
|
|
487
|
+
"""Check if the game is running in preview mode."""
|
|
488
|
+
return ctx() and ctx().global_state.mode == Mode.PREVIEW
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@meta_fn
|
|
492
|
+
def is_watch() -> bool:
|
|
493
|
+
"""Check if the game is running in watch mode."""
|
|
494
|
+
return ctx() and ctx().global_state.mode == Mode.WATCH
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@meta_fn
|
|
498
|
+
def is_tutorial() -> bool:
|
|
499
|
+
"""Check if the game is running in tutorial mode."""
|
|
500
|
+
return ctx() and ctx().global_state.mode == Mode.TUTORIAL
|
|
501
|
+
|
|
502
|
+
|
|
452
503
|
@meta_fn
|
|
453
504
|
def aspect_ratio() -> float:
|
|
454
505
|
"""Get the aspect ratio of the game."""
|