sonolus.py 0.11.0__py3-none-any.whl → 0.12.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.

@@ -1,7 +1,7 @@
1
1
  from math import isfinite, isinf, isnan
2
2
 
3
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
4
- from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+ from sonolus.backend.node import EngineNode, FunctionNode
5
5
  from sonolus.backend.ops import Op
6
6
  from sonolus.backend.optimize.flow import BasicBlock, traverse_cfg_reverse_postorder
7
7
  from sonolus.backend.place import BlockPlace
@@ -18,18 +18,18 @@ def cfg_to_engine_node(entry: BasicBlock):
18
18
  }
19
19
  match outgoing:
20
20
  case {**other} if not other:
21
- statements.append(ConstantNode(value=len(block_indexes)))
21
+ statements.append(len(block_indexes))
22
22
  case {None: target, **other} if not other:
23
- statements.append(ConstantNode(value=block_indexes[target]))
23
+ statements.append(block_indexes[target])
24
24
  case {0: f_branch, None: t_branch, **other} if not other:
25
25
  statements.append(
26
26
  FunctionNode(
27
27
  func=Op.If,
28
- args=[
28
+ args=(
29
29
  ir_to_engine_node(block.test),
30
- ConstantNode(value=block_indexes[t_branch]),
31
- ConstantNode(value=block_indexes[f_branch]),
32
- ],
30
+ block_indexes[t_branch],
31
+ block_indexes[f_branch],
32
+ ),
33
33
  )
34
34
  )
35
35
  case {None: default_branch, **other} if len(other) == 1:
@@ -37,11 +37,11 @@ def cfg_to_engine_node(entry: BasicBlock):
37
37
  statements.append(
38
38
  FunctionNode(
39
39
  func=Op.If,
40
- args=[
40
+ args=(
41
41
  ir_to_engine_node(IRPureInstr(Op.Equal, args=[block.test, IRConst(cond)])),
42
- ConstantNode(value=block_indexes[cond_branch]),
43
- ConstantNode(value=block_indexes[default_branch]),
44
- ],
42
+ block_indexes[cond_branch],
43
+ block_indexes[default_branch],
44
+ ),
45
45
  )
46
46
  )
47
47
  case dict() as targets:
@@ -49,23 +49,23 @@ def cfg_to_engine_node(entry: BasicBlock):
49
49
  default = len(block_indexes)
50
50
  conds = [cond for cond in targets if cond is not None]
51
51
  if min(conds) == 0 and max(conds) == len(conds) - 1 and all(int(cond) == cond for cond in conds):
52
- args.extend(ConstantNode(value=block_indexes[targets[cond]]) for cond in range(len(conds)))
52
+ args.extend(block_indexes[targets[cond]] for cond in range(len(conds)))
53
53
  if None in targets:
54
54
  default = block_indexes[targets[None]]
55
- args.append(ConstantNode(value=default))
56
- statements.append(FunctionNode(Op.SwitchIntegerWithDefault, args))
55
+ args.append(default)
56
+ statements.append(FunctionNode(Op.SwitchIntegerWithDefault, tuple(args)))
57
57
  else:
58
58
  for cond, target in targets.items():
59
59
  if cond is None:
60
60
  default = block_indexes[target]
61
61
  continue
62
- args.append(ConstantNode(value=cond))
63
- args.append(ConstantNode(value=block_indexes[target]))
64
- args.append(ConstantNode(value=default))
65
- statements.append(FunctionNode(Op.SwitchWithDefault, args))
66
- block_statements.append(FunctionNode(Op.Execute, statements))
67
- block_statements.append(ConstantNode(value=0))
68
- result = FunctionNode(Op.Block, [FunctionNode(Op.JumpLoop, block_statements)])
62
+ args.append(cond)
63
+ args.append(block_indexes[target])
64
+ args.append(default)
65
+ statements.append(FunctionNode(Op.SwitchWithDefault, tuple(args)))
66
+ block_statements.append(FunctionNode(Op.Execute, tuple(statements)))
67
+ block_statements.append(0)
68
+ result = FunctionNode(Op.Block, (FunctionNode(Op.JumpLoop, tuple(block_statements)),))
69
69
  for block in block_indexes:
70
70
  # Clean up without relying on gc
71
71
  del block.incoming
@@ -81,32 +81,30 @@ def ir_to_engine_node(stmt) -> EngineNode:
81
81
  case int(value) | float(value) | IRConst(value=int(value) | float(value)):
82
82
  value = float(value)
83
83
  if value.is_integer():
84
- return ConstantNode(value=int(value))
84
+ return int(value)
85
85
  elif isfinite(value):
86
- return ConstantNode(value=value)
86
+ return value
87
87
  elif isinf(value):
88
88
  # Read values from ROM
89
- return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
89
+ return FunctionNode(Op.Get, args=(3000, 1 if value > 0 else 2))
90
90
  elif isnan(value):
91
91
  # Read value from ROM
92
- return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
92
+ return FunctionNode(Op.Get, args=(3000, 0))
93
93
  else:
94
94
  raise ValueError(f"Invalid constant value: {value}")
95
95
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
96
- return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
96
+ return FunctionNode(func=op, args=tuple(ir_to_engine_node(arg) for arg in args))
97
97
  case IRGet(place=place):
98
98
  return ir_to_engine_node(place)
99
99
  case BlockPlace() as place:
100
100
  if place.offset == 0:
101
101
  index = ir_to_engine_node(place.index)
102
102
  elif place.index == 0:
103
- index = ConstantNode(value=place.offset)
103
+ index = place.offset
104
104
  else:
105
- index = FunctionNode(
106
- func=Op.Add, args=[ir_to_engine_node(place.index), ConstantNode(value=place.offset)]
107
- )
108
- return FunctionNode(func=Op.Get, args=[ir_to_engine_node(place.block), index])
105
+ index = FunctionNode(func=Op.Add, args=(ir_to_engine_node(place.index), place.offset))
106
+ return FunctionNode(func=Op.Get, args=(ir_to_engine_node(place.block), index))
109
107
  case IRSet(place=place, value=value):
110
- return FunctionNode(func=Op.Set, args=[*ir_to_engine_node(place).args, ir_to_engine_node(value)])
108
+ return FunctionNode(func=Op.Set, args=(*ir_to_engine_node(place).args, ir_to_engine_node(value)))
111
109
  case _:
112
110
  raise TypeError(f"Unsupported IR statement: {stmt}")
@@ -2,7 +2,7 @@ import math
2
2
  import operator
3
3
  import random
4
4
 
5
- from sonolus.backend.node import ConstantNode, EngineNode
5
+ from sonolus.backend.node import EngineNode, FunctionNode
6
6
  from sonolus.backend.ops import Op
7
7
 
8
8
 
@@ -24,8 +24,8 @@ class Interpreter:
24
24
  self.log = []
25
25
 
26
26
  def run(self, node: EngineNode) -> float:
27
- if isinstance(node, ConstantNode):
28
- return node.value
27
+ if not isinstance(node, FunctionNode):
28
+ return node
29
29
  func = node.func
30
30
  args = node.args
31
31
  match func:
sonolus/backend/node.py CHANGED
@@ -1,40 +1,18 @@
1
1
  import textwrap
2
- from dataclasses import dataclass, field
2
+ from typing import NamedTuple
3
3
 
4
4
  from sonolus.backend.ops import Op
5
5
 
6
- type EngineNode = ConstantNode | FunctionNode
6
+ type EngineNode = int | float | FunctionNode
7
7
 
8
8
 
9
- @dataclass(slots=True)
10
- class ConstantNode:
11
- value: float
12
- _hash: int = field(init=False, repr=False)
13
-
14
- def __post_init__(self):
15
- self._hash = hash(self.value)
16
-
17
- def __hash__(self):
18
- return hash(self.value)
19
-
20
-
21
- @dataclass(slots=True)
22
- class FunctionNode:
9
+ class FunctionNode(NamedTuple):
23
10
  func: Op
24
- args: list[EngineNode]
25
- _hash: int = field(init=False, repr=False)
26
-
27
- def __post_init__(self):
28
- self._hash = hash((self.func, tuple(self.args)))
29
-
30
- def __hash__(self):
31
- return self._hash
11
+ args: tuple[EngineNode, ...]
32
12
 
33
13
 
34
14
  def format_engine_node(node: EngineNode) -> str:
35
- if isinstance(node, ConstantNode):
36
- return str(node.value)
37
- elif isinstance(node, FunctionNode):
15
+ if isinstance(node, FunctionNode):
38
16
  match len(node.args):
39
17
  case 0:
40
18
  return f"{node.func.name}()"
@@ -45,4 +23,4 @@ def format_engine_node(node: EngineNode) -> str:
45
23
  textwrap.indent('\n'.join(format_engine_node(arg) for arg in node.args), ' ')
46
24
  }\n)"
47
25
  else:
48
- raise ValueError(f"Invalid engine node: {node}")
26
+ return str(node)
sonolus/build/compile.py CHANGED
@@ -32,9 +32,11 @@ class CompileCache:
32
32
  self._cache = {}
33
33
  self._lock = Lock()
34
34
  self._event = Event()
35
+ self._accessed_hashes = set()
35
36
 
36
37
  def get(self, entry_hash: int) -> EngineNode | None:
37
38
  with self._lock:
39
+ self._accessed_hashes.add(entry_hash)
38
40
  if entry_hash not in self._cache:
39
41
  self._cache[entry_hash] = None # Mark as being compiled
40
42
  return None
@@ -47,10 +49,21 @@ class CompileCache:
47
49
 
48
50
  def set(self, entry_hash: int, node: EngineNode) -> None:
49
51
  with self._lock:
52
+ self._accessed_hashes.add(entry_hash)
50
53
  self._cache[entry_hash] = node
51
54
  self._event.set()
52
55
  self._event.clear()
53
56
 
57
+ def reset_accessed(self) -> None:
58
+ with self._lock:
59
+ self._accessed_hashes.clear()
60
+
61
+ def prune_unaccessed(self) -> None:
62
+ with self._lock:
63
+ unaccessed_hashes = set(self._cache.keys()) - self._accessed_hashes
64
+ for h in unaccessed_hashes:
65
+ del self._cache[h]
66
+
54
67
 
55
68
  def compile_mode(
56
69
  mode: Mode,
@@ -67,7 +80,7 @@ def compile_mode(
67
80
 
68
81
  mode_state = ModeContextState(
69
82
  mode,
70
- {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
83
+ archetypes,
71
84
  )
72
85
  nodes = OutputNodeGenerator()
73
86
  results = {}
@@ -134,9 +147,10 @@ def compile_mode(
134
147
  archetype_data["exports"] = [*archetype._exported_keys_]
135
148
 
136
149
  callback_items = [
137
- (cb_name, cb_info, getattr(archetype, cb_name))
150
+ (cb_name, cb_info, archetype._callbacks_[cb_name])
138
151
  for cb_name, cb_info in archetype._supported_callbacks_.items()
139
- if getattr(archetype, cb_name) not in archetype._default_callbacks_
152
+ if cb_name in archetype._callbacks_
153
+ and archetype._callbacks_[cb_name] not in archetype._default_callbacks_
140
154
  ]
141
155
 
142
156
  if thread_pool is not None:
@@ -119,6 +119,7 @@ class RebuildCommand:
119
119
  print("Rebuilding...")
120
120
  try:
121
121
  start_time = perf_counter()
122
+ server_state.cache.reset_accessed()
122
123
  server_state.project_state = ProjectContextState.from_build_config(server_state.config)
123
124
  server_state.project = project_module.project
124
125
  build_collection(
@@ -128,6 +129,7 @@ class RebuildCommand:
128
129
  cache=server_state.cache,
129
130
  project_state=server_state.project_state,
130
131
  )
132
+ server_state.cache.prune_unaccessed()
131
133
  end_time = perf_counter()
132
134
  print(f"Rebuild completed in {end_time - start_time:.2f} seconds")
133
135
  except CompilationError:
sonolus/build/node.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from threading import Lock
2
2
  from typing import TypedDict
3
3
 
4
- from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
4
+ from sonolus.backend.node import EngineNode, FunctionNode
5
5
 
6
6
 
7
7
  class ValueOutputNode(TypedDict):
@@ -32,19 +32,17 @@ class OutputNodeGenerator:
32
32
  return self.indexes[node]
33
33
 
34
34
  match node:
35
- case ConstantNode(value):
36
- index = len(self.nodes)
37
- self.nodes.append({"value": value})
38
- self.indexes[node] = index
39
- return index
40
35
  case FunctionNode(func, args):
41
36
  arg_indexes = [self._add(arg) for arg in args]
42
37
  index = len(self.nodes)
43
38
  self.nodes.append({"func": func.value, "args": arg_indexes})
44
39
  self.indexes[node] = index
45
40
  return index
46
- case _:
47
- raise ValueError("Invalid node")
41
+ case constant:
42
+ index = len(self.nodes)
43
+ self.nodes.append({"value": constant})
44
+ self.indexes[node] = index
45
+ return index
48
46
 
49
47
  def get(self):
50
48
  return self.nodes
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  from abc import abstractmethod
5
- from collections.abc import Callable
5
+ from collections.abc import Callable, Sequence
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum, StrEnum
8
8
  from types import FunctionType
@@ -11,7 +11,9 @@ from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
11
11
  from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr, IRStmt
12
12
  from sonolus.backend.mode import Mode
13
13
  from sonolus.backend.ops import Op
14
+ from sonolus.backend.visitor import compile_and_call
14
15
  from sonolus.script.bucket import Bucket, Judgment
16
+ from sonolus.script.debug import runtime_checks_enabled, static_error
15
17
  from sonolus.script.internal.callbacks import PLAY_CALLBACKS, PREVIEW_CALLBACKS, WATCH_ARCHETYPE_CALLBACKS, CallbackInfo
16
18
  from sonolus.script.internal.context import ctx
17
19
  from sonolus.script.internal.descriptor import SonolusDescriptor
@@ -207,7 +209,7 @@ class _IsScoredDescriptor(SonolusDescriptor):
207
209
  if instance is None:
208
210
  return self.value
209
211
  elif ctx():
210
- return ctx().mode_state.is_scored_by_archetype_id[instance.id]
212
+ return ctx().mode_state.is_scored_by_archetype_id.get_unchecked(instance.id)
211
213
  else:
212
214
  return self.value
213
215
 
@@ -221,7 +223,7 @@ class _IdDescriptor(SonolusDescriptor):
221
223
  raise RuntimeError("Archetype id is only available during compilation")
222
224
  if instance is None:
223
225
  result = ctx().mode_state.archetypes.get(owner)
224
- if result is None:
226
+ if result is None or owner in ctx().mode_state.compile_time_only_archetypes:
225
227
  raise RuntimeError("Archetype is not registered")
226
228
  return result
227
229
  else:
@@ -237,7 +239,7 @@ class _KeyDescriptor(SonolusDescriptor):
237
239
 
238
240
  def __get__(self, instance, owner):
239
241
  if instance is not None and ctx():
240
- return ctx().mode_state.keys_by_archetype_id[instance.id]
242
+ return ctx().mode_state.keys_by_archetype_id.get_unchecked(instance.id)
241
243
  else:
242
244
  return self.value
243
245
 
@@ -427,7 +429,7 @@ class _BaseArchetype:
427
429
 
428
430
  _imported_keys_: ClassVar[dict[str, int]]
429
431
  _exported_keys_: ClassVar[dict[str, int]]
430
- _callbacks_: ClassVar[list[Callable]]
432
+ _callbacks_: ClassVar[dict[str, Callable]]
431
433
  _data_constructor_signature_: ClassVar[inspect.Signature]
432
434
  _spawn_signature_: ClassVar[inspect.Signature]
433
435
 
@@ -491,17 +493,67 @@ class _BaseArchetype:
491
493
 
492
494
  @classmethod
493
495
  @meta_fn
494
- def at(cls, index: int) -> Self:
496
+ def _compile_time_id(cls):
497
+ if not ctx():
498
+ raise RuntimeError("Archetype id is only available during compilation")
499
+ result = ctx().mode_state.archetypes.get(cls)
500
+ if result is None:
501
+ raise RuntimeError("Archetype is not registered")
502
+ return result
503
+
504
+ @classmethod
505
+ @meta_fn
506
+ def at(cls, index: int, check: bool = True) -> Self:
507
+ """Access the entity of this archetype at the given index.
508
+
509
+ Args:
510
+ index: The index of the entity to reference.
511
+ check: If true, raises an error if the entity at the index is not of this archetype or of a subclass of
512
+ this archetype. If false, no validation is performed.
513
+
514
+ Returns:
515
+ The entity at the given index.
516
+ """
517
+ if not ctx():
518
+ raise RuntimeError("Archetype.at is only available during compilation")
519
+ compile_and_call(cls._check_is_at, index, check=check)
495
520
  result = cls._new()
496
521
  result._data_ = _ArchetypeReferenceData(index=Num._accept_(index))
497
522
  return result
498
523
 
499
524
  @classmethod
525
+ def is_at(cls, index: int, strict: bool = False) -> bool:
526
+ """Return whether the entity at the given index is of this archetype.
527
+
528
+ Args:
529
+ index: The index of the entity to check.
530
+ strict: If true, only returns true if the entity is exactly of this archetype. If false, also returns true
531
+ if the entity is of a subclass of this archetype.
532
+
533
+ Returns:
534
+ Whether the entity at the given index is of this archetype.
535
+ """
536
+ if strict:
537
+ return index >= 0 and cls._compile_time_id() == entity_info_at(index).archetype_id
538
+ else:
539
+ mro_ids = cls._get_mro_id_array(entity_info_at(index).archetype_id)
540
+ return index >= 0 and cls._compile_time_id() in mro_ids
541
+
542
+ @classmethod
543
+ def _check_is_at(cls, index: int, check: bool):
544
+ if not check or not runtime_checks_enabled():
545
+ return
546
+ assert index >= 0, "Entity index must be non-negative"
547
+ assert cls._compile_time_id() in cls._get_mro_id_array(entity_info_at(index).archetype_id), (
548
+ "Entity at index is not of the expected archetype"
549
+ )
550
+
551
+ @staticmethod
500
552
  @meta_fn
501
- def is_at(cls, index: int) -> bool:
553
+ def _get_mro_id_array(archetype_id: int) -> Sequence[int]:
502
554
  if not ctx():
503
- raise RuntimeError("is_at is only available during compilation")
504
- return entity_info_at(index).archetype_id == cls.id
555
+ raise RuntimeError("Archetype._get_mro_id_array is only available during compilation")
556
+ return ctx().get_archetype_mro_id_array(archetype_id)
505
557
 
506
558
  @classmethod
507
559
  @meta_fn
@@ -562,13 +614,16 @@ class _BaseArchetype:
562
614
  return
563
615
  if cls.name is None or cls.name in {getattr(mro_entry, "name", None) for mro_entry in cls.mro()[1:]}:
564
616
  cls.name = cls.__name__.removeprefix(cls._removable_prefix)
565
- cls._callbacks_ = []
617
+ cls._callbacks_ = {}
566
618
  for name in cls._supported_callbacks_:
567
- cb = getattr(cls, name)
568
- if cb in cls._default_callbacks_:
569
- continue
570
- cls._callbacks_.append(cb)
619
+ for mro_entry in cls.mro():
620
+ if name in mro_entry.__dict__:
621
+ cb = mro_entry.__dict__[name]
622
+ if cb not in cls._default_callbacks_:
623
+ cls._callbacks_[name] = cb
624
+ break
571
625
  cls._field_init_done = False
626
+ cls._is_concrete_archetype_ = True
572
627
  cls.id = _IdDescriptor()
573
628
  cls._key_ = cls.key
574
629
  cls.key = _KeyDescriptor(cls.key)
@@ -584,6 +639,14 @@ class _BaseArchetype:
584
639
  for mro_entry in cls.mro()[1:]:
585
640
  if hasattr(mro_entry, "_field_init_done"):
586
641
  mro_entry._init_fields()
642
+ if sum(issubclass(base, _BaseArchetype) for base in cls.__bases__) > 1:
643
+ raise TypeError("Multiple inheritance of Archetypes is not supported")
644
+ mro_from_archetype_parents = set(
645
+ type("Dummy", tuple(base for base in cls.__bases__ if issubclass(base, _BaseArchetype)), {}).mro()
646
+ )
647
+ # Archetype parents would have already initialized relevant fields, so only consider the current class
648
+ # and mixins that were not already included via an archetype parent
649
+ mro_excluding_archetype_parents = [entry for entry in cls.mro() if entry not in mro_from_archetype_parents]
587
650
  field_specifiers = get_field_specifiers(
588
651
  cls,
589
652
  skip={
@@ -599,7 +662,9 @@ class _BaseArchetype:
599
662
  "_default_callbacks_",
600
663
  "_callbacks_",
601
664
  "_field_init_done",
665
+ "_is_concrete_archetype_",
602
666
  },
667
+ included_classes=mro_excluding_archetype_parents,
603
668
  ).items()
604
669
  if not hasattr(cls, "_imported_fields_"):
605
670
  cls._imported_fields_ = {}
@@ -652,6 +717,13 @@ class _BaseArchetype:
652
717
  f"Missing field annotation for '{name}', "
653
718
  f"expected exactly one of imported, exported, entity_memory, or shared_memory"
654
719
  )
720
+ if (
721
+ name in cls._imported_fields_
722
+ or name in cls._exported_fields_
723
+ or name in cls._memory_fields_
724
+ or name in cls._shared_memory_fields_
725
+ ):
726
+ raise ValueError(f"Field '{name}' is already defined in a superclass")
655
727
  field_type = validate_concrete_type(value.__args__[0])
656
728
  match field_info.storage:
657
729
  case _StorageType.IMPORTED:
@@ -770,6 +842,18 @@ class _BaseArchetype:
770
842
  new_cls = type(name, (cls,), cls_dict)
771
843
  return new_cls
772
844
 
845
+ @meta_fn
846
+ def _delegate(self, name: str, default: int | float | None = None):
847
+ name = validate_value(name)._as_py_()
848
+ if hasattr(super(), name):
849
+ fn = getattr(super(), name)
850
+ if ctx():
851
+ return compile_and_call(fn)
852
+ else:
853
+ return fn()
854
+ else:
855
+ return default
856
+
773
857
 
774
858
  class PlayArchetype(_BaseArchetype):
775
859
  """Base class for play mode archetypes.
@@ -807,26 +891,28 @@ class PlayArchetype(_BaseArchetype):
807
891
 
808
892
  Runs first when the level is loaded.
809
893
  """
894
+ self._delegate("preprocess")
810
895
 
811
896
  def spawn_order(self) -> float:
812
897
  """Return the spawn order of the entity.
813
898
 
814
899
  Runs when the level is loaded after [`preprocess`][sonolus.script.archetype.PlayArchetype.preprocess].
815
900
  """
816
- return 0.0
901
+ return self._delegate("spawn_order", default=0.0)
817
902
 
818
903
  def should_spawn(self) -> bool:
819
904
  """Return whether the entity should be spawned.
820
905
 
821
906
  Runs each frame while the entity is the first entity in the spawn queue.
822
907
  """
823
- return True
908
+ return self._delegate("should_spawn", default=True)
824
909
 
825
910
  def initialize(self):
826
911
  """Initialize this entity.
827
912
 
828
913
  Runs when this entity is spawned.
829
914
  """
915
+ self._delegate("initialize")
830
916
 
831
917
  def update_sequential(self):
832
918
  """Perform non-parallel actions for this frame.
@@ -838,6 +924,7 @@ class PlayArchetype(_BaseArchetype):
838
924
  [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel]
839
925
  for better performance.
840
926
  """
927
+ self._delegate("update_sequential")
841
928
 
842
929
  def update_parallel(self):
843
930
  """Perform parallel actions for this frame.
@@ -846,18 +933,21 @@ class PlayArchetype(_BaseArchetype):
846
933
 
847
934
  This is where most gameplay logic should be placed.
848
935
  """
936
+ self._delegate("update_parallel")
849
937
 
850
938
  def touch(self):
851
939
  """Handle user input.
852
940
 
853
941
  Runs after [`update_sequential`][sonolus.script.archetype.PlayArchetype.update_sequential] each frame.
854
942
  """
943
+ self._delegate("touch")
855
944
 
856
945
  def terminate(self):
857
946
  """Finalize before despawning.
858
947
 
859
948
  Runs when the entity is despawned.
860
949
  """
950
+ self._delegate("terminate")
861
951
 
862
952
  @property
863
953
  @meta_fn
@@ -965,20 +1055,22 @@ class WatchArchetype(_BaseArchetype):
965
1055
 
966
1056
  Runs first when the level is loaded.
967
1057
  """
1058
+ self._delegate("preprocess")
968
1059
 
969
1060
  def spawn_time(self) -> float:
970
1061
  """Return the spawn time of the entity."""
971
- return 0.0
1062
+ return self._delegate("spawn_time", default=0.0)
972
1063
 
973
1064
  def despawn_time(self) -> float:
974
1065
  """Return the despawn time of the entity."""
975
- return 0.0
1066
+ return self._delegate("despawn_time", default=0.0)
976
1067
 
977
1068
  def initialize(self):
978
1069
  """Initialize this entity.
979
1070
 
980
1071
  Runs when this entity is spawned.
981
1072
  """
1073
+ self._delegate("initialize")
982
1074
 
983
1075
  def update_sequential(self):
984
1076
  """Perform non-parallel actions for this frame.
@@ -989,6 +1081,7 @@ class WatchArchetype(_BaseArchetype):
989
1081
  Other logic should typically be placed in
990
1082
  [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel] for better performance.
991
1083
  """
1084
+ self._delegate("update_sequential")
992
1085
 
993
1086
  def update_parallel(self):
994
1087
  """Parallel update callback.
@@ -997,12 +1090,14 @@ class WatchArchetype(_BaseArchetype):
997
1090
 
998
1091
  This is where most gameplay logic should be placed.
999
1092
  """
1093
+ self._delegate("update_parallel")
1000
1094
 
1001
1095
  def terminate(self):
1002
1096
  """Finalize before despawning.
1003
1097
 
1004
1098
  Runs when the entity is despawned.
1005
1099
  """
1100
+ self._delegate("terminate")
1006
1101
 
1007
1102
  @property
1008
1103
  @meta_fn
@@ -1073,12 +1168,14 @@ class PreviewArchetype(_BaseArchetype):
1073
1168
 
1074
1169
  Runs first when the level is loaded.
1075
1170
  """
1171
+ self._delegate("preprocess")
1076
1172
 
1077
1173
  def render(self):
1078
1174
  """Render the entity.
1079
1175
 
1080
1176
  Runs after `preprocess`.
1081
1177
  """
1178
+ self._delegate("render")
1082
1179
 
1083
1180
  @property
1084
1181
  @meta_fn
@@ -1210,6 +1307,8 @@ class EntityRef[A: _BaseArchetype](Record):
1210
1307
 
1211
1308
  Usage:
1212
1309
  ```python
1310
+ ref = EntityRef[MyArchetype](index=123)
1311
+
1213
1312
  class MyArchetype(PlayArchetype):
1214
1313
  ref_1: EntityRef[OtherArchetype] = imported()
1215
1314
  ref_2: EntityRef[Any] = imported()
@@ -1239,11 +1338,25 @@ class EntityRef[A: _BaseArchetype](Record):
1239
1338
  return super().__hash__()
1240
1339
 
1241
1340
  @meta_fn
1242
- def get(self) -> A:
1243
- """Get the entity."""
1341
+ def __bool__(self):
1342
+ if ctx():
1343
+ static_error("EntityRef cannot be used in a boolean context. Check index directly instead.")
1344
+ return True
1345
+
1346
+ @meta_fn
1347
+ def get(self, *, check: bool = True) -> A:
1348
+ """Get the entity this reference points to.
1349
+
1350
+ Args:
1351
+ check: If true, raises an error if the referenced entity is not of this archetype or of a subclass of
1352
+ this archetype. If false, no validation is performed.
1353
+
1354
+ Returns:
1355
+ The entity this reference points to.
1356
+ """
1244
1357
  if ref := getattr(self, "_ref_", None):
1245
1358
  return ref
1246
- return self.archetype().at(self.index)
1359
+ return self.archetype().at(self.index, check=check)
1247
1360
 
1248
1361
  @meta_fn
1249
1362
  def get_as(self, archetype: type[_BaseArchetype]) -> _BaseArchetype:
@@ -1252,9 +1365,17 @@ class EntityRef[A: _BaseArchetype](Record):
1252
1365
  raise TypeError("Using get_as in level data is not supported.")
1253
1366
  return self.with_archetype(archetype).get()
1254
1367
 
1255
- def archetype_matches(self) -> bool:
1256
- """Check if entity at the index is precisely of the archetype."""
1257
- return self.index >= 0 and self.archetype().is_at(self.index)
1368
+ def archetype_matches(self, strict: bool = False) -> bool:
1369
+ """Check if entity at the index is of this archetype.
1370
+
1371
+ Args:
1372
+ strict: If true, only returns true if the entity is exactly of this archetype. If false, also returns true
1373
+ if the entity is of a subclass of this archetype.
1374
+
1375
+ Returns:
1376
+ Whether the entity at the given index is of this archetype.
1377
+ """
1378
+ return self.archetype().is_at(self.index, strict=strict)
1258
1379
 
1259
1380
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1260
1381
  ref = getattr(self, "_ref_", None)
@@ -468,11 +468,8 @@ class FrozenNumSet[Size](Record):
468
468
  return len(self._values)
469
469
 
470
470
  def __contains__(self, value: Num) -> bool:
471
- if len(self) < 15:
472
- for i in range(len(self)): # noqa: SIM110
473
- if self._values.get_unchecked(i) == value:
474
- return True
475
- return False
471
+ if len(self) < 8:
472
+ return value in self._as_tuple()
476
473
  else:
477
474
  left = 0
478
475
  right = len(self) - 1
@@ -490,6 +487,10 @@ class FrozenNumSet[Size](Record):
490
487
  def __iter__(self) -> SonolusIterator[Num]:
491
488
  return self._values.__iter__()
492
489
 
490
+ @meta_fn
491
+ def _as_tuple(self) -> tuple[Num, ...]:
492
+ return tuple(self._values.get_unchecked(i) for i in range(Num._accept_(len(self))._as_py_()))
493
+
493
494
 
494
495
  class _ArrayMapEntry[K, V](Record):
495
496
  key: K
sonolus/script/debug.py CHANGED
@@ -102,6 +102,14 @@ def notify(message: str):
102
102
  print(f"[NOTIFY] {message}")
103
103
 
104
104
 
105
+ @meta_fn
106
+ def runtime_checks_enabled() -> bool:
107
+ if ctx():
108
+ return ctx().project_state.runtime_checks != RuntimeChecks.NONE
109
+ else:
110
+ return True
111
+
112
+
105
113
  @meta_fn
106
114
  def require(value: int | float | bool, message: str | None = None):
107
115
  """Require a condition to be true, or raise an error.
@@ -212,7 +220,7 @@ def visualize_cfg(
212
220
  project_state = ProjectContextState()
213
221
  mode_state = ModeContextState(
214
222
  mode,
215
- {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
223
+ archetypes,
216
224
  )
217
225
 
218
226
  cfg = callback_to_cfg(project_state, mode_state, fn, callback, archetype=archetype) # type: ignore
@@ -94,25 +94,36 @@ class ProjectContextState:
94
94
 
95
95
  class ModeContextState:
96
96
  archetypes: dict[type, int]
97
+ compile_time_only_archetypes: set[type]
97
98
  archetypes_by_name: dict[str, type]
98
99
  keys_by_archetype_id: Sequence[int]
99
100
  is_scored_by_archetype_id: Sequence[bool]
101
+ archetype_mro_id_array_rom_indexes: Sequence[int] | None = None
100
102
  environment_mappings: dict[_GlobalInfo, int]
101
103
  environment_offsets: dict[Block, int]
102
104
  mode: Mode
103
105
  lock: Lock
104
106
 
105
- def __init__(self, mode: Mode, archetypes: dict[type, int] | None = None):
107
+ def __init__(self, mode: Mode, archetypes: list[type] | None = None):
106
108
  from sonolus.script.array import Array
107
109
 
108
- self.archetypes = archetypes or {}
110
+ archetypes = [*archetypes] if archetypes is not None else []
111
+ seen_archetypes = {*archetypes}
112
+ compile_time_only_archetypes = set()
113
+ for type_ in [*archetypes]:
114
+ for entry in type_.mro():
115
+ if getattr(entry, "_is_concrete_archetype_", False) and entry not in seen_archetypes:
116
+ archetypes.append(entry)
117
+ seen_archetypes.add(entry)
118
+ compile_time_only_archetypes.add(entry)
119
+ self.archetypes = {type_: idx for idx, type_ in enumerate(archetypes)}
120
+ self.compile_time_only_archetypes = compile_time_only_archetypes
109
121
  self.archetypes_by_name = {type_.name: type_ for type_, _ in self.archetypes.items()} # type: ignore
110
- ordered_archetypes = sorted(self.archetypes, key=lambda a: self.archetypes[a])
111
122
  self.keys_by_archetype_id = (
112
- Array(*((getattr(a, "_key_", -1)) for a in ordered_archetypes)) if archetypes else Array[int, Literal[0]]()
123
+ Array(*((getattr(a, "_key_", -1)) for a in archetypes)) if archetypes else Array[int, Literal[0]]()
113
124
  )
114
125
  self.is_scored_by_archetype_id = (
115
- Array(*((getattr(a, "_is_scored_", False)) for a in ordered_archetypes))
126
+ Array(*((getattr(a, "_is_scored_", False)) for a in archetypes))
116
127
  if archetypes
117
128
  else Array[bool, Literal[0]]()
118
129
  )
@@ -121,6 +132,27 @@ class ModeContextState:
121
132
  self.mode = mode
122
133
  self.lock = Lock()
123
134
 
135
+ def _init_archetype_mro_info(self, rom: ReadOnlyMemory):
136
+ from sonolus.script.array import Array
137
+ from sonolus.script.num import Num
138
+
139
+ with self.lock:
140
+ if self.archetype_mro_id_array_rom_indexes is not None:
141
+ return
142
+ archetype_mro_id_values = []
143
+ archetype_mro_id_offsets = []
144
+ for type_ in self.archetypes:
145
+ mro_ids = [self.archetypes[entry] for entry in type_.mro() if entry in self.archetypes]
146
+ archetype_mro_id_offsets.append(len(archetype_mro_id_values))
147
+ archetype_mro_id_values.append(len(mro_ids))
148
+ archetype_mro_id_values.extend(mro_ids)
149
+ archetype_mro_id_array_place = rom[tuple(archetype_mro_id_values)]
150
+
151
+ archetype_mro_id_rom_indexes = Array[int, len(archetype_mro_id_offsets)]._with_value(
152
+ [Num._accept_(offset + archetype_mro_id_array_place.index) for offset in archetype_mro_id_offsets]
153
+ )
154
+ self.archetype_mro_id_array_rom_indexes = archetype_mro_id_rom_indexes
155
+
124
156
 
125
157
  class CallbackContextState:
126
158
  callback: str
@@ -371,6 +403,15 @@ class Context:
371
403
  self.mode_state.archetypes[type_] = len(self.mode_state.archetypes)
372
404
  return self.mode_state.archetypes[type_]
373
405
 
406
+ def get_archetype_mro_id_array(self, archetype_id: int) -> Sequence[int]:
407
+ from sonolus.script.containers import ArrayPointer
408
+ from sonolus.script.num import Num
409
+ from sonolus.script.pointer import _deref
410
+
411
+ self.mode_state._init_archetype_mro_info(self.rom)
412
+ rom_index = self.mode_state.archetype_mro_id_array_rom_indexes[archetype_id]
413
+ return ArrayPointer[int](_deref(self.blocks.EngineRom, rom_index, Num), self.blocks.EngineRom, rom_index + 1)
414
+
374
415
 
375
416
  def ctx() -> Context | Any: # Using Any to silence type checker warnings if it's None
376
417
  return context_var.get()
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ from collections.abc import Sequence
2
3
  from typing import Annotated
3
4
 
4
5
  _missing = object()
@@ -11,9 +12,15 @@ def get_field_specifiers(
11
12
  globals=None, # noqa: A002
12
13
  locals=None, # noqa: A002
13
14
  eval_str=True,
15
+ included_classes: Sequence[type] | None = None,
14
16
  ):
15
17
  """Like inspect.get_annotations, but also turns class attributes into Annotated."""
16
- results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
18
+ if included_classes is not None:
19
+ results = {}
20
+ for entry in reversed(included_classes):
21
+ results.update(inspect.get_annotations(entry, eval_str=eval_str))
22
+ else:
23
+ results = inspect.get_annotations(cls, globals=globals, locals=locals, eval_str=eval_str)
17
24
  for key, value in results.items():
18
25
  class_value = getattr(cls, key, _missing)
19
26
  if class_value is not _missing and key not in skip:
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Sonolus engine development in Python
5
5
  Project-URL: Documentation, https://sonolus.py.qwewqa.xyz/
6
6
  Project-URL: Repository, https://github.com/qwewqa/sonolus.py
7
7
  Project-URL: Issues, https://github.com/qwewqa/sonolus.py/issues
8
8
  Project-URL: Changelog, https://sonolus.py.qwewqa.xyz/changelog/
9
9
  License-File: LICENSE
10
- Requires-Python: >=3.12
10
+ Requires-Python: >=3.12.5
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  # Sonolus.py
@@ -3,11 +3,11 @@ sonolus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  sonolus/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  sonolus/backend/blocks.py,sha256=3peyb9eYBy0s53xNVJ1KmK4IgoyVkkwG-lqDQ_VZTHc,18531
5
5
  sonolus/backend/excepthook.py,sha256=ezsTi8hPXSUhqZ7-H0rmkWcndBQcZFAShF543zzaEPM,1912
6
- sonolus/backend/finalize.py,sha256=kytaqAVyoryWJTyJEbvIGysfxmt_DeCic2oxmrqAPYI,5508
7
- sonolus/backend/interpret.py,sha256=B0jqlLmEGoyO2mxpcvwRwV17Tq_gOE9wLNt26Q5QOfs,14306
6
+ sonolus/backend/finalize.py,sha256=MRp_ATPreSmBGn8N6iwesS50rFWH9FIiQI6_Ea8RVTM,5090
7
+ sonolus/backend/interpret.py,sha256=dnm0SJFfI4KIE6Ca2tKrLq10XwBC9EPSqN7OOYtA8Ws,14304
8
8
  sonolus/backend/ir.py,sha256=eyNXorOQY4zgKOvN4kO1MdJF3sU8H0Qw5RTPqbEjJHY,3854
9
9
  sonolus/backend/mode.py,sha256=NkcPZJm8dn83LX35uP24MtQOCnfRDFZ280dHeEEfauE,613
10
- sonolus/backend/node.py,sha256=eEzPP14jzWJp2xrZCAaPlNtokxdoqg0bSM7xQiwx1j8,1254
10
+ sonolus/backend/node.py,sha256=w5y2GwSc2E9rQvGJNaMzvPW_FYjjfHw4QDUmKs1dAnc,714
11
11
  sonolus/backend/ops.py,sha256=5weB_vIxbkwCSJuzYZyKUk7vVXsSIEDJYRlvE-2ke8A,10572
12
12
  sonolus/backend/place.py,sha256=7qwV732hZ4WP-9GNN8FQSEKssPJZELip1wLXTWfop7Y,4717
13
13
  sonolus/backend/utils.py,sha256=OwD1EPh8j-hsfkLzeKNzPQojT_3kklpJou0WTJNoCbc,2337
@@ -28,19 +28,19 @@ sonolus/backend/optimize/ssa.py,sha256=raQO0furQQRPYb8iIBKfNrJlj-_5wqtI4EWNfLZ8Q
28
28
  sonolus/build/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  sonolus/build/cli.py,sha256=uLY7JG3PTAb2a1bbjulFlgUGDzsHXyRAfkGV0lbuMgQ,10538
30
30
  sonolus/build/collection.py,sha256=6hniAzriPWBKUeGDkXabNXpbdHiHnqiK9shs6U1OExM,12748
31
- sonolus/build/compile.py,sha256=KOmncDKmGfgzC_FWB_LTxAl0s9w4wnaDe-luACMlCVs,8397
32
- sonolus/build/dev_server.py,sha256=u-p3wVdIPNTHjFLNZzPdW4JlFmeVbqqEle_O3otUhyw,10607
31
+ sonolus/build/compile.py,sha256=m4v4wYWnI6ebot4-fJi9a7eoF0WIo3uPOPy0_NpLgK0,8855
32
+ sonolus/build/dev_server.py,sha256=yHD3KGqzebdqcEBOC5JDlNptzlH8Sq4i-DNnh_zXWCA,10705
33
33
  sonolus/build/engine.py,sha256=jMymxbBXu-ekv71uU8TF2KbFaHs3yGjyJAztd1SoRDs,14808
34
34
  sonolus/build/level.py,sha256=KLqUAtxIuIqrzeFURJA97rdqjA5pcvYSmwNZQhElaMQ,702
35
- sonolus/build/node.py,sha256=gnX71RYDUOK_gYMpinQi-bLWO4csqcfiG5gFmhxzSec,1330
35
+ sonolus/build/node.py,sha256=Dhuz_-UlRd-EJC7-AP1NuyvrjHWNo7jGssniRh4dZhI,1239
36
36
  sonolus/build/project.py,sha256=Uuz82QtTNFdklrVJ_i7EPp8hSjyOxLU1xAeOloa6G00,8579
37
37
  sonolus/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- sonolus/script/archetype.py,sha256=J_0E6a183Spez8NTd_jyH0kjIO81Q1a3i6GrNzjyWuw,49631
38
+ sonolus/script/archetype.py,sha256=qK1tcREt56qJPh1zw_cjZGz0UUQngLGXUDA1HmwNpY8,55083
39
39
  sonolus/script/array.py,sha256=EbrNwl_WuJ0JjjkX0s_VJNXWqvYdm_ljTbyrDEMLGUY,13348
40
40
  sonolus/script/array_like.py,sha256=E6S4TW2muXgcyVkhUASQVt7JSYUkpvdJPgHz6YiSHNo,14708
41
41
  sonolus/script/bucket.py,sha256=LNePLmCwgXfKLmH4Z7ZcTFKWR32eq4AnnagI7jacrsU,7782
42
- sonolus/script/containers.py,sha256=iZWPVvVr24iHBb8umuYV0LrCkr9Xgj1piWaat_he7-w,28643
43
- sonolus/script/debug.py,sha256=21_bvhP2cZ4kwS3Spxp8tP6ojIfmthQxlq4gtqrt0lo,7757
42
+ sonolus/script/containers.py,sha256=KKySXTh_ON9hut1ScMS31ewRNnVd4t7OwpIZg2bPhno,28676
43
+ sonolus/script/debug.py,sha256=sWMZ-c68GggD8eUZJUPRXxxDjtCt9j8X4-_FUgTvUNE,7856
44
44
  sonolus/script/easing.py,sha256=2FUJI_nfp990P_armCcRqHm2329O985glJAhSC6tnxs,11379
45
45
  sonolus/script/effect.py,sha256=SfJxSNF3RlPCRXnkt62ZlWhCXw3mmmRCsoMsvTErUP0,7960
46
46
  sonolus/script/engine.py,sha256=etI9dJsQ7V9YZICVNZg54WqpLijPxG8eTPHiV-_EiG8,10687
@@ -72,13 +72,13 @@ sonolus/script/internal/__init__.py,sha256=T6rzLoiOUaiSQtaHMZ88SNO-ijSjSSv33TKtU
72
72
  sonolus/script/internal/builtin_impls.py,sha256=1fo6UuWlaLoqpVwFSrFS5BabNeRCdS2T2mjsS4BPYcY,13603
73
73
  sonolus/script/internal/callbacks.py,sha256=vWzJG8uiJoEtsNnbeZPqOHogCwoLpz2D1MnHY2wVV8s,2801
74
74
  sonolus/script/internal/constant.py,sha256=3ycbGkDJVUwcrCZ96vLjAoAARgsvaqDM8rJ_YCrLrvo,4289
75
- sonolus/script/internal/context.py,sha256=fRvdxkx7EILU54-ogR1HSjLOZnH2PVw2gQri9m86K5Y,19852
75
+ sonolus/script/internal/context.py,sha256=dhWQS7fFPMhb0FJyw3y--0HlBtwuYANdIYiv7u_96jk,22023
76
76
  sonolus/script/internal/descriptor.py,sha256=XRFey-EjiAm_--KsNl-8N0Mi_iyQwlPh68gDp0pKf3E,392
77
77
  sonolus/script/internal/dict_impl.py,sha256=alu_wKGSk1kZajNf64qbe7t71shEzD4N5xNIATH8Swo,1885
78
78
  sonolus/script/internal/error.py,sha256=ZNnsvQVQAnFKzcvsm6-sste2lo-tP5pPI8sD7XlAZWc,490
79
79
  sonolus/script/internal/generic.py,sha256=_3d5Rn_tn214-77fPE67vdbdqt1PQF8-2WB_XDu5YRg,7551
80
80
  sonolus/script/internal/impl.py,sha256=R88cl4nLcfF0UhA9qdYRBOsl4nMx8ucgz8l7_oRY-l8,3503
81
- sonolus/script/internal/introspection.py,sha256=guL9_NR2D3OJAnNpeFdyYkO_vVXk-3KQr2-y4YielM0,1133
81
+ sonolus/script/internal/introspection.py,sha256=q6c83G_jmuLSoYSJj8ln0XtuwNEdndE0PkLbNWOhGmk,1421
82
82
  sonolus/script/internal/math_impls.py,sha256=ox2pBJ6ELRO0LdLn_RZxgHHs_PCgQOHIhmDkwmLxJaU,2975
83
83
  sonolus/script/internal/native.py,sha256=zOuRtgI3XJ_ExyR_ZkvbDABVc_JIWaKl62lFEL_bMaw,2007
84
84
  sonolus/script/internal/random.py,sha256=6Ku5edRcDUh7rtqEEYCJz0BQavw69RALsVHS25z50pI,1695
@@ -87,8 +87,8 @@ sonolus/script/internal/simulation_context.py,sha256=LGxLTvxbqBIhoe1R-SfwGajNIDw
87
87
  sonolus/script/internal/transient.py,sha256=y2AWABqF1aoaP6H4_2u4MMpNioC4OsZQCtPyNI0txqo,1634
88
88
  sonolus/script/internal/tuple_impl.py,sha256=WaI5HSF5h03ddXiSHEwzY9ttfsPUItaf86Y5VbZypek,3754
89
89
  sonolus/script/internal/value.py,sha256=OngrCdmY_h6mV2Zgwqhuo4eYFad0kTk6263UAxctZcY,6963
90
- sonolus_py-0.11.0.dist-info/METADATA,sha256=hhf11U7J2Zk1rkB-vixzUjS94Ygoh2R1DZ2jmdhnbC4,554
91
- sonolus_py-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- sonolus_py-0.11.0.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
- sonolus_py-0.11.0.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
- sonolus_py-0.11.0.dist-info/RECORD,,
90
+ sonolus_py-0.12.0.dist-info/METADATA,sha256=ncmBBjgZ1BHrxEb9xrtEIa_CiF8pK-lknGve69U3_LY,556
91
+ sonolus_py-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ sonolus_py-0.12.0.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
+ sonolus_py-0.12.0.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
+ sonolus_py-0.12.0.dist-info/RECORD,,