sonolus.py 0.1.9__py3-none-any.whl → 0.2.1__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 +154 -32
- sonolus/script/array.py +14 -3
- sonolus/script/array_like.py +6 -2
- sonolus/script/containers.py +166 -0
- sonolus/script/debug.py +22 -4
- sonolus/script/effect.py +2 -2
- sonolus/script/engine.py +125 -17
- sonolus/script/internal/builtin_impls.py +21 -2
- sonolus/script/internal/constant.py +8 -4
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +7 -3
- sonolus/script/internal/value.py +33 -11
- sonolus/script/interval.py +8 -3
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +113 -10
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +35 -15
- sonolus/script/options.py +23 -6
- sonolus/script/pointer.py +11 -1
- sonolus/script/project.py +41 -5
- sonolus/script/quad.py +55 -1
- sonolus/script/record.py +10 -5
- sonolus/script/runtime.py +78 -16
- sonolus/script/sprite.py +18 -1
- sonolus/script/text.py +9 -0
- sonolus/script/ui.py +20 -7
- sonolus/script/values.py +8 -5
- sonolus/script/vec.py +28 -0
- sonolus_py-0.2.1.dist-info/METADATA +10 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/RECORD +46 -45
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/WHEEL +1 -1
- sonolus_py-0.1.9.dist-info/METADATA +0 -9
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -315,8 +315,8 @@ class SparseConditionalConstantPropagation(CompilerPass):
|
|
|
315
315
|
return 1
|
|
316
316
|
return functools.reduce(operator.pow, args)
|
|
317
317
|
case Op.Log:
|
|
318
|
-
assert len(args) ==
|
|
319
|
-
return math.log(args[0]
|
|
318
|
+
assert len(args) == 1
|
|
319
|
+
return math.log(args[0])
|
|
320
320
|
case Op.Ceil:
|
|
321
321
|
assert len(args) == 1
|
|
322
322
|
return math.ceil(args[0])
|
|
@@ -12,13 +12,21 @@ from sonolus.backend.optimize.passes import run_passes
|
|
|
12
12
|
from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
|
|
13
13
|
from sonolus.backend.optimize.ssa import FromSSA, ToSSA
|
|
14
14
|
|
|
15
|
-
MINIMAL_PASSES =
|
|
15
|
+
MINIMAL_PASSES = (
|
|
16
16
|
CoalesceFlow(),
|
|
17
17
|
UnreachableCodeElimination(),
|
|
18
18
|
AllocateBasic(),
|
|
19
|
-
|
|
19
|
+
)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
FAST_PASSES = (
|
|
22
|
+
CoalesceFlow(),
|
|
23
|
+
UnreachableCodeElimination(),
|
|
24
|
+
AdvancedDeadCodeElimination(),
|
|
25
|
+
CoalesceFlow(),
|
|
26
|
+
Allocate(),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
STANDARD_PASSES = (
|
|
22
30
|
CoalesceFlow(),
|
|
23
31
|
UnreachableCodeElimination(),
|
|
24
32
|
DeadCodeElimination(),
|
|
@@ -37,7 +45,7 @@ STANDARD_PASSES = [
|
|
|
37
45
|
CoalesceFlow(),
|
|
38
46
|
NormalizeSwitch(),
|
|
39
47
|
Allocate(),
|
|
40
|
-
|
|
48
|
+
)
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
def optimize_and_allocate(cfg: BasicBlock):
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections import deque
|
|
5
|
+
from collections.abc import Sequence
|
|
5
6
|
|
|
6
7
|
from sonolus.backend.optimize.flow import BasicBlock
|
|
7
8
|
|
|
@@ -35,7 +36,7 @@ class CompilerPass(ABC):
|
|
|
35
36
|
pass
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def run_passes(entry: BasicBlock, passes:
|
|
39
|
+
def run_passes(entry: BasicBlock, passes: Sequence[CompilerPass]) -> BasicBlock:
|
|
39
40
|
active_passes = set()
|
|
40
41
|
queue = deque(passes)
|
|
41
42
|
while queue:
|
sonolus/backend/place.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Iterator
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Self
|
|
3
3
|
|
|
4
4
|
from sonolus.backend.blocks import Block
|
|
5
5
|
|
|
@@ -8,9 +8,13 @@ type BlockValue = Block | int | TempBlock | Place
|
|
|
8
8
|
type IndexValue = int | Place
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class TempBlock
|
|
12
|
-
name
|
|
13
|
-
|
|
11
|
+
class TempBlock:
|
|
12
|
+
__slots__ = ("__hash", "name", "size")
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str, size: int = 1):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.size = size
|
|
17
|
+
self.__hash = hash(name) # Precompute hash based on name alone
|
|
14
18
|
|
|
15
19
|
def __repr__(self):
|
|
16
20
|
return f"TempBlock(name={self.name!r}, size={self.size!r})"
|
|
@@ -28,14 +32,38 @@ class TempBlock(NamedTuple):
|
|
|
28
32
|
def __eq__(self, other):
|
|
29
33
|
return isinstance(other, TempBlock) and self.name == other.name and self.size == other.size
|
|
30
34
|
|
|
35
|
+
def __lt__(self, other):
|
|
36
|
+
if not isinstance(other, TempBlock):
|
|
37
|
+
return NotImplemented
|
|
38
|
+
return str(self) < str(other)
|
|
39
|
+
|
|
40
|
+
def __le__(self, other):
|
|
41
|
+
if not isinstance(other, TempBlock):
|
|
42
|
+
return NotImplemented
|
|
43
|
+
return str(self) <= str(other)
|
|
44
|
+
|
|
45
|
+
def __gt__(self, other):
|
|
46
|
+
if not isinstance(other, TempBlock):
|
|
47
|
+
return NotImplemented
|
|
48
|
+
return str(self) > str(other)
|
|
49
|
+
|
|
50
|
+
def __ge__(self, other):
|
|
51
|
+
if not isinstance(other, TempBlock):
|
|
52
|
+
return NotImplemented
|
|
53
|
+
return str(self) >= str(other)
|
|
54
|
+
|
|
31
55
|
def __hash__(self):
|
|
32
|
-
return
|
|
56
|
+
return self.__hash
|
|
33
57
|
|
|
34
58
|
|
|
35
|
-
class BlockPlace
|
|
36
|
-
block
|
|
37
|
-
|
|
38
|
-
offset: int = 0
|
|
59
|
+
class BlockPlace:
|
|
60
|
+
__slots__ = ("__hash", "block", "index", "offset")
|
|
61
|
+
|
|
62
|
+
def __init__(self, block: BlockValue, index: IndexValue = 0, offset: int = 0):
|
|
63
|
+
self.block = block
|
|
64
|
+
self.index = index
|
|
65
|
+
self.offset = offset
|
|
66
|
+
self.__hash = hash((block, index, offset))
|
|
39
67
|
|
|
40
68
|
def __repr__(self):
|
|
41
69
|
return f"BlockPlace(block={self.block!r}, index={self.index!r}, offset={self.offset!r})"
|
|
@@ -61,13 +89,42 @@ class BlockPlace(NamedTuple):
|
|
|
61
89
|
and self.offset == other.offset
|
|
62
90
|
)
|
|
63
91
|
|
|
92
|
+
def __lt__(self, other):
|
|
93
|
+
if not isinstance(other, BlockPlace):
|
|
94
|
+
return NotImplemented
|
|
95
|
+
return str(self) < str(other)
|
|
96
|
+
|
|
97
|
+
def __le__(self, other):
|
|
98
|
+
if not isinstance(other, BlockPlace):
|
|
99
|
+
return NotImplemented
|
|
100
|
+
return str(self) <= str(other)
|
|
101
|
+
|
|
102
|
+
def __gt__(self, other):
|
|
103
|
+
if not isinstance(other, BlockPlace):
|
|
104
|
+
return NotImplemented
|
|
105
|
+
return str(self) > str(other)
|
|
106
|
+
|
|
107
|
+
def __ge__(self, other):
|
|
108
|
+
if not isinstance(other, BlockPlace):
|
|
109
|
+
return NotImplemented
|
|
110
|
+
return str(self) >= str(other)
|
|
111
|
+
|
|
64
112
|
def __hash__(self):
|
|
65
|
-
return
|
|
113
|
+
return self.__hash
|
|
114
|
+
|
|
115
|
+
def __iter__(self):
|
|
116
|
+
yield self.block
|
|
117
|
+
yield self.index
|
|
118
|
+
yield self.offset
|
|
66
119
|
|
|
67
120
|
|
|
68
|
-
class SSAPlace
|
|
69
|
-
name
|
|
70
|
-
|
|
121
|
+
class SSAPlace:
|
|
122
|
+
__slots__ = ("__hash", "name", "num")
|
|
123
|
+
|
|
124
|
+
def __init__(self, name: str, num: int):
|
|
125
|
+
self.name = name
|
|
126
|
+
self.num = num
|
|
127
|
+
self.__hash = hash((name, num))
|
|
71
128
|
|
|
72
129
|
def __repr__(self):
|
|
73
130
|
return f"SSAPlace(name={self.name!r}, num={self.num!r})"
|
|
@@ -78,5 +135,29 @@ class SSAPlace(NamedTuple):
|
|
|
78
135
|
def __eq__(self, other):
|
|
79
136
|
return isinstance(other, SSAPlace) and self.name == other.name and self.num == other.num
|
|
80
137
|
|
|
138
|
+
def __lt__(self, other):
|
|
139
|
+
if not isinstance(other, SSAPlace):
|
|
140
|
+
return NotImplemented
|
|
141
|
+
return str(self) < str(other)
|
|
142
|
+
|
|
143
|
+
def __le__(self, other):
|
|
144
|
+
if not isinstance(other, SSAPlace):
|
|
145
|
+
return NotImplemented
|
|
146
|
+
return str(self) <= str(other)
|
|
147
|
+
|
|
148
|
+
def __gt__(self, other):
|
|
149
|
+
if not isinstance(other, SSAPlace):
|
|
150
|
+
return NotImplemented
|
|
151
|
+
return str(self) > str(other)
|
|
152
|
+
|
|
153
|
+
def __ge__(self, other):
|
|
154
|
+
if not isinstance(other, SSAPlace):
|
|
155
|
+
return NotImplemented
|
|
156
|
+
return str(self) >= str(other)
|
|
157
|
+
|
|
81
158
|
def __hash__(self):
|
|
82
|
-
return
|
|
159
|
+
return self.__hash
|
|
160
|
+
|
|
161
|
+
def __iter__(self):
|
|
162
|
+
yield self.name
|
|
163
|
+
yield self.num
|
sonolus/backend/visitor.py
CHANGED
|
@@ -172,7 +172,9 @@ class Visitor(ast.NodeVisitor):
|
|
|
172
172
|
bound_args: inspect.BoundArguments
|
|
173
173
|
used_names: dict[str, int]
|
|
174
174
|
return_ctxs: list[Context] # Contexts at return statements, which will branch to the exit
|
|
175
|
-
loop_head_ctxs: list[
|
|
175
|
+
loop_head_ctxs: list[
|
|
176
|
+
Context | list[Context]
|
|
177
|
+
] # Contexts at loop heads, from outer to inner. Contains a list for unrolled (tuple) loops
|
|
176
178
|
break_ctxs: list[list[Context]] # Contexts at break statements, from outer to inner
|
|
177
179
|
active_ctx: Context | None # The active context for use in nested functions=
|
|
178
180
|
parent: Self | None # The parent visitor for use in nested functions
|
|
@@ -203,6 +205,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
203
205
|
case ast.FunctionDef(body=body):
|
|
204
206
|
ctx().scope.set_value("$return", validate_value(None))
|
|
205
207
|
for stmt in body:
|
|
208
|
+
if not ctx().live:
|
|
209
|
+
break
|
|
206
210
|
self.visit(stmt)
|
|
207
211
|
case ast.Lambda(body=body):
|
|
208
212
|
result = self.visit(body)
|
|
@@ -318,11 +322,21 @@ class Visitor(ast.NodeVisitor):
|
|
|
318
322
|
iterable = self.visit(node.iter)
|
|
319
323
|
if isinstance(iterable, TupleImpl):
|
|
320
324
|
# Unroll the loop
|
|
325
|
+
break_ctxs = []
|
|
321
326
|
for value in iterable.value:
|
|
322
327
|
set_ctx(ctx().branch(None))
|
|
328
|
+
self.loop_head_ctxs.append([])
|
|
329
|
+
self.break_ctxs.append([])
|
|
323
330
|
self.handle_assign(node.target, validate_value(value))
|
|
324
331
|
for stmt in node.body:
|
|
332
|
+
if not ctx().live:
|
|
333
|
+
break
|
|
325
334
|
self.visit(stmt)
|
|
335
|
+
continue_ctxs = [*self.loop_head_ctxs.pop(), ctx()]
|
|
336
|
+
break_ctxs.extend(self.break_ctxs.pop())
|
|
337
|
+
set_ctx(Context.meet(continue_ctxs))
|
|
338
|
+
if break_ctxs:
|
|
339
|
+
set_ctx(Context.meet([*break_ctxs, ctx()]))
|
|
326
340
|
return
|
|
327
341
|
iterator = self.handle_call(node, iterable.__iter__)
|
|
328
342
|
if not isinstance(iterator, SonolusIterator):
|
|
@@ -338,6 +352,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
338
352
|
self.loop_head_ctxs.pop()
|
|
339
353
|
self.break_ctxs.pop()
|
|
340
354
|
for stmt in node.orelse:
|
|
355
|
+
if not ctx().live:
|
|
356
|
+
break
|
|
341
357
|
self.visit(stmt)
|
|
342
358
|
return
|
|
343
359
|
ctx().test = has_next.ir()
|
|
@@ -347,6 +363,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
347
363
|
set_ctx(body_ctx)
|
|
348
364
|
self.handle_assign(node.target, self.handle_call(node, iterator.next))
|
|
349
365
|
for stmt in node.body:
|
|
366
|
+
if not ctx().live:
|
|
367
|
+
break
|
|
350
368
|
self.visit(stmt)
|
|
351
369
|
ctx().branch_to_loop_header(header_ctx)
|
|
352
370
|
|
|
@@ -355,6 +373,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
355
373
|
|
|
356
374
|
set_ctx(else_ctx)
|
|
357
375
|
for stmt in node.orelse:
|
|
376
|
+
if not ctx().live:
|
|
377
|
+
break
|
|
358
378
|
self.visit(stmt)
|
|
359
379
|
else_end_ctx = ctx()
|
|
360
380
|
|
|
@@ -374,6 +394,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
374
394
|
body_ctx = ctx().branch(None)
|
|
375
395
|
set_ctx(body_ctx)
|
|
376
396
|
for stmt in node.body:
|
|
397
|
+
if not ctx().live:
|
|
398
|
+
break
|
|
377
399
|
self.visit(stmt)
|
|
378
400
|
ctx().branch_to_loop_header(header_ctx)
|
|
379
401
|
|
|
@@ -390,6 +412,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
390
412
|
self.loop_head_ctxs.pop()
|
|
391
413
|
self.break_ctxs.pop()
|
|
392
414
|
for stmt in node.orelse:
|
|
415
|
+
if not ctx().live:
|
|
416
|
+
break
|
|
393
417
|
self.visit(stmt)
|
|
394
418
|
return
|
|
395
419
|
ctx().test = test.ir()
|
|
@@ -398,6 +422,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
398
422
|
|
|
399
423
|
set_ctx(body_ctx)
|
|
400
424
|
for stmt in node.body:
|
|
425
|
+
if not ctx().live:
|
|
426
|
+
break
|
|
401
427
|
self.visit(stmt)
|
|
402
428
|
ctx().branch_to_loop_header(header_ctx)
|
|
403
429
|
|
|
@@ -406,6 +432,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
406
432
|
|
|
407
433
|
set_ctx(else_ctx)
|
|
408
434
|
for stmt in node.orelse:
|
|
435
|
+
if not ctx().live:
|
|
436
|
+
break
|
|
409
437
|
self.visit(stmt)
|
|
410
438
|
else_end_ctx = ctx()
|
|
411
439
|
|
|
@@ -418,9 +446,13 @@ class Visitor(ast.NodeVisitor):
|
|
|
418
446
|
if test._is_py_():
|
|
419
447
|
if test._as_py_():
|
|
420
448
|
for stmt in node.body:
|
|
449
|
+
if not ctx().live:
|
|
450
|
+
break
|
|
421
451
|
self.visit(stmt)
|
|
422
452
|
else:
|
|
423
453
|
for stmt in node.orelse:
|
|
454
|
+
if not ctx().live:
|
|
455
|
+
break
|
|
424
456
|
self.visit(stmt)
|
|
425
457
|
return
|
|
426
458
|
|
|
@@ -431,11 +463,15 @@ class Visitor(ast.NodeVisitor):
|
|
|
431
463
|
|
|
432
464
|
set_ctx(true_ctx)
|
|
433
465
|
for stmt in node.body:
|
|
466
|
+
if not ctx().live:
|
|
467
|
+
break
|
|
434
468
|
self.visit(stmt)
|
|
435
469
|
true_end_ctx = ctx()
|
|
436
470
|
|
|
437
471
|
set_ctx(false_ctx)
|
|
438
472
|
for stmt in node.orelse:
|
|
473
|
+
if not ctx().live:
|
|
474
|
+
break
|
|
439
475
|
self.visit(stmt)
|
|
440
476
|
false_end_ctx = ctx()
|
|
441
477
|
|
|
@@ -462,6 +498,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
462
498
|
if guard._is_py_():
|
|
463
499
|
if guard._as_py_():
|
|
464
500
|
for stmt in case.body:
|
|
501
|
+
if not ctx().live:
|
|
502
|
+
break
|
|
465
503
|
self.visit(stmt)
|
|
466
504
|
end_ctxs.append(ctx())
|
|
467
505
|
else:
|
|
@@ -473,6 +511,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
473
511
|
guard_false_ctx = ctx().branch(0)
|
|
474
512
|
set_ctx(guard_true_ctx)
|
|
475
513
|
for stmt in case.body:
|
|
514
|
+
if not ctx().live:
|
|
515
|
+
break
|
|
476
516
|
self.visit(stmt)
|
|
477
517
|
end_ctxs.append(ctx())
|
|
478
518
|
false_ctx = Context.meet([false_ctx, guard_false_ctx])
|
|
@@ -633,7 +673,11 @@ class Visitor(ast.NodeVisitor):
|
|
|
633
673
|
set_ctx(ctx().into_dead())
|
|
634
674
|
|
|
635
675
|
def visit_Continue(self, node):
|
|
636
|
-
|
|
676
|
+
loop_head = self.loop_head_ctxs[-1]
|
|
677
|
+
if isinstance(loop_head, list):
|
|
678
|
+
loop_head.append(ctx())
|
|
679
|
+
else:
|
|
680
|
+
ctx().branch_to_loop_header(loop_head)
|
|
637
681
|
set_ctx(ctx().into_dead())
|
|
638
682
|
|
|
639
683
|
def visit_BoolOp(self, node) -> Value:
|
|
@@ -982,7 +1026,11 @@ class Visitor(ast.NodeVisitor):
|
|
|
982
1026
|
if isinstance(target, ConstantValue):
|
|
983
1027
|
# Unwrap so we can access fields
|
|
984
1028
|
target = target._as_py_()
|
|
985
|
-
descriptor =
|
|
1029
|
+
descriptor = None
|
|
1030
|
+
for cls in type.mro(type(target)):
|
|
1031
|
+
descriptor = cls.__dict__.get(key, None)
|
|
1032
|
+
if descriptor is not None:
|
|
1033
|
+
break
|
|
986
1034
|
match descriptor:
|
|
987
1035
|
case property(fget=getter):
|
|
988
1036
|
return self.handle_call(node, getter, target)
|
sonolus/build/cli.py
CHANGED
|
@@ -10,10 +10,11 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from time import perf_counter
|
|
12
12
|
|
|
13
|
+
from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
|
|
13
14
|
from sonolus.build.engine import package_engine
|
|
14
15
|
from sonolus.build.level import package_level_data
|
|
15
16
|
from sonolus.build.project import build_project_to_collection, get_project_schema
|
|
16
|
-
from sonolus.script.project import Project
|
|
17
|
+
from sonolus.script.project import BuildConfig, Project
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def find_default_module() -> str | None:
|
|
@@ -63,29 +64,29 @@ def import_project(module_path: str) -> Project | None:
|
|
|
63
64
|
return project
|
|
64
65
|
except Exception as e:
|
|
65
66
|
print(f"Error: Failed to import project: {e}")
|
|
66
|
-
|
|
67
|
+
raise e from None
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
def build_project(project: Project, build_dir: Path):
|
|
70
|
+
def build_project(project: Project, build_dir: Path, config: BuildConfig):
|
|
70
71
|
dist_dir = build_dir / "dist"
|
|
71
72
|
levels_dir = dist_dir / "levels"
|
|
72
73
|
shutil.rmtree(dist_dir, ignore_errors=True)
|
|
73
74
|
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
74
75
|
levels_dir.mkdir(parents=True, exist_ok=True)
|
|
75
76
|
|
|
76
|
-
package_engine(project.engine.data).write(dist_dir / "engine")
|
|
77
|
+
package_engine(project.engine.data, config).write(dist_dir / "engine")
|
|
77
78
|
|
|
78
79
|
for level in project.levels:
|
|
79
80
|
level_path = levels_dir / level.name
|
|
80
81
|
level_path.write_bytes(package_level_data(level.data))
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
def build_collection(project: Project, build_dir: Path):
|
|
84
|
+
def build_collection(project: Project, build_dir: Path, config: BuildConfig):
|
|
84
85
|
site_dir = build_dir / "site"
|
|
85
86
|
shutil.rmtree(site_dir, ignore_errors=True)
|
|
86
87
|
site_dir.mkdir(parents=True, exist_ok=True)
|
|
87
88
|
|
|
88
|
-
collection = build_project_to_collection(project)
|
|
89
|
+
collection = build_project_to_collection(project, config)
|
|
89
90
|
collection.write(site_dir)
|
|
90
91
|
|
|
91
92
|
|
|
@@ -125,10 +126,55 @@ def run_server(base_dir: Path, port: int = 8000):
|
|
|
125
126
|
httpd.shutdown()
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
def get_config(args: argparse.Namespace) -> BuildConfig:
|
|
130
|
+
if hasattr(args, "optimize_minimal") and args.optimize_minimal:
|
|
131
|
+
optimization_passes = MINIMAL_PASSES
|
|
132
|
+
elif hasattr(args, "optimize_fast") and args.optimize_fast:
|
|
133
|
+
optimization_passes = FAST_PASSES
|
|
134
|
+
elif hasattr(args, "optimize_standard") and args.optimize_standard:
|
|
135
|
+
optimization_passes = STANDARD_PASSES
|
|
136
|
+
else:
|
|
137
|
+
optimization_passes = FAST_PASSES if args.command == "dev" else STANDARD_PASSES
|
|
138
|
+
|
|
139
|
+
if any(hasattr(args, attr) and getattr(args, attr) for attr in ["play", "watch", "preview", "tutorial"]):
|
|
140
|
+
build_play = hasattr(args, "play") and args.play
|
|
141
|
+
build_watch = hasattr(args, "watch") and args.watch
|
|
142
|
+
build_preview = hasattr(args, "preview") and args.preview
|
|
143
|
+
build_tutorial = hasattr(args, "tutorial") and args.tutorial
|
|
144
|
+
else:
|
|
145
|
+
build_play = build_watch = build_preview = build_tutorial = True
|
|
146
|
+
|
|
147
|
+
return BuildConfig(
|
|
148
|
+
passes=optimization_passes,
|
|
149
|
+
build_play=build_play,
|
|
150
|
+
build_watch=build_watch,
|
|
151
|
+
build_preview=build_preview,
|
|
152
|
+
build_tutorial=build_tutorial,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
128
156
|
def main():
|
|
129
157
|
parser = argparse.ArgumentParser(description="Sonolus project build and development tools")
|
|
130
158
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
131
159
|
|
|
160
|
+
def add_common_arguments(parser):
|
|
161
|
+
optimization_group = parser.add_mutually_exclusive_group()
|
|
162
|
+
optimization_group.add_argument(
|
|
163
|
+
"-o0", "--optimize-minimal", action="store_true", help="Use minimal optimization passes"
|
|
164
|
+
)
|
|
165
|
+
optimization_group.add_argument(
|
|
166
|
+
"-o1", "--optimize-fast", action="store_true", help="Use fast optimization passes"
|
|
167
|
+
)
|
|
168
|
+
optimization_group.add_argument(
|
|
169
|
+
"-o2", "--optimize-standard", action="store_true", help="Use standard optimization passes"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
build_components = parser.add_argument_group("build components")
|
|
173
|
+
build_components.add_argument("--play", action="store_true", help="Build play component")
|
|
174
|
+
build_components.add_argument("--watch", action="store_true", help="Build watch component")
|
|
175
|
+
build_components.add_argument("--preview", action="store_true", help="Build preview component")
|
|
176
|
+
build_components.add_argument("--tutorial", action="store_true", help="Build tutorial component")
|
|
177
|
+
|
|
132
178
|
build_parser = subparsers.add_parser("build")
|
|
133
179
|
build_parser.add_argument(
|
|
134
180
|
"module",
|
|
@@ -137,6 +183,7 @@ def main():
|
|
|
137
183
|
help="Module path (e.g., 'module.name'). If omitted, will auto-detect if only one module exists.",
|
|
138
184
|
)
|
|
139
185
|
build_parser.add_argument("--build-dir", type=str, default="./build")
|
|
186
|
+
add_common_arguments(build_parser)
|
|
140
187
|
|
|
141
188
|
dev_parser = subparsers.add_parser("dev")
|
|
142
189
|
dev_parser.add_argument(
|
|
@@ -147,6 +194,7 @@ def main():
|
|
|
147
194
|
)
|
|
148
195
|
dev_parser.add_argument("--build-dir", type=str, default="./build")
|
|
149
196
|
dev_parser.add_argument("--port", type=int, default=8000)
|
|
197
|
+
add_common_arguments(dev_parser)
|
|
150
198
|
|
|
151
199
|
schema_parser = subparsers.add_parser("schema")
|
|
152
200
|
schema_parser.add_argument(
|
|
@@ -161,7 +209,8 @@ def main():
|
|
|
161
209
|
if not args.module:
|
|
162
210
|
default_module = find_default_module()
|
|
163
211
|
if default_module:
|
|
164
|
-
|
|
212
|
+
if args.command != "schema":
|
|
213
|
+
print(f"Using auto-detected module: {default_module}")
|
|
165
214
|
args.module = default_module
|
|
166
215
|
else:
|
|
167
216
|
parser.error("Module argument is required when multiple or no modules are found")
|
|
@@ -173,13 +222,15 @@ def main():
|
|
|
173
222
|
if args.command == "build":
|
|
174
223
|
build_dir = Path(args.build_dir)
|
|
175
224
|
start_time = perf_counter()
|
|
176
|
-
|
|
225
|
+
config = get_config(args)
|
|
226
|
+
build_project(project, build_dir, config)
|
|
177
227
|
end_time = perf_counter()
|
|
178
228
|
print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
|
|
179
229
|
elif args.command == "dev":
|
|
180
230
|
build_dir = Path(args.build_dir)
|
|
181
231
|
start_time = perf_counter()
|
|
182
|
-
|
|
232
|
+
config = get_config(args)
|
|
233
|
+
build_collection(project, build_dir, config)
|
|
183
234
|
end_time = perf_counter()
|
|
184
235
|
print(f"Build finished in {end_time - start_time:.2f}s")
|
|
185
236
|
run_server(build_dir / "site", port=args.port)
|
sonolus/build/collection.py
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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")
|