sonolus.py 0.10.5__py3-none-any.whl → 0.10.7__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.

@@ -205,8 +205,16 @@ def parse_dev_command(command_line: str) -> Command | None:
205
205
  return HelpCommand()
206
206
  elif args.cmd in {"quit", "q"}:
207
207
  return ExitCommand()
208
+ else:
209
+ # Really, we should not reach here, since argparse would have errored out earlier
210
+ print("Unknown command.\n")
211
+ return None
212
+ except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
213
+ print(f"Error parsing command: {e}\n")
208
214
  return None
209
- except argparse.ArgumentError:
215
+ except SystemExit:
216
+ # argparse throws this on some errors, and will print out help automatically
217
+ print()
210
218
  return None
211
219
 
212
220
 
@@ -227,7 +235,7 @@ def command_input_thread(command_queue: queue.Queue, prompt_event: threading.Eve
227
235
  if isinstance(cmd, ExitCommand):
228
236
  break
229
237
  else:
230
- print(f"Unknown command. Available commands:\n{HELP_TEXT}")
238
+ print(f"Available commands:\n{HELP_TEXT}")
231
239
  # Show prompt again
232
240
  prompt_event.set()
233
241
  else:
@@ -236,6 +244,7 @@ def command_input_thread(command_queue: queue.Queue, prompt_event: threading.Eve
236
244
  break
237
245
  except Exception as e:
238
246
  print(f"Error reading command: {e}\n")
247
+ prompt_event.set()
239
248
 
240
249
 
241
250
  def get_local_ips():
@@ -283,7 +292,7 @@ def run_server(
283
292
 
284
293
  def log_message(self, fmt, *args):
285
294
  sys.stdout.write("\r\033[K") # Clear line
286
- sys.stdout.write(f"{self.address_string()} - - [{self.log_date_time_string()}] {fmt % args}\n")
295
+ sys.stdout.write(f"{self.address_string()} [{self.log_date_time_string()}] {fmt % args}\n")
287
296
  if interactive:
288
297
  sys.stdout.write("> ")
289
298
  sys.stdout.flush()
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Self
3
+ from collections.abc import Callable
4
+ from typing import Any, Protocol, Self
4
5
 
6
+ from sonolus.script.archetype import AnyArchetype, EntityRef
5
7
  from sonolus.script.array import Array
6
8
  from sonolus.script.array_like import ArrayLike, get_positive_index
7
9
  from sonolus.script.debug import error
@@ -643,3 +645,255 @@ class _ArrayMapEntryIterator[K, V, Capacity](Record, SonolusIterator):
643
645
  self._index += 1
644
646
  return Some(result)
645
647
  return Nothing
648
+
649
+
650
+ class _LinkedListNodeRef[TKey, TValue](Protocol):
651
+ def get_value(self) -> TValue: ...
652
+
653
+ def get_next(self) -> Self: ...
654
+
655
+ def set_next(self, next_node: Self): ...
656
+
657
+ def set_prev(self, prev_node: Self):
658
+ # No-op for singly linked lists
659
+ return
660
+
661
+ def is_present(self) -> bool: ...
662
+
663
+ def set(self, other: Self): ...
664
+
665
+ def copy(self) -> Self: ...
666
+
667
+ def empty(self) -> Self: ...
668
+
669
+
670
+ def _merge_linked_list_nodes[TNode: _LinkedListNodeRef](
671
+ a: TNode,
672
+ b: TNode,
673
+ ) -> TNode:
674
+ head = a.empty()
675
+ tail = a.empty()
676
+ left = a.copy()
677
+ right = b.copy()
678
+
679
+ while left.is_present() and right.is_present():
680
+ if left.get_value() <= right.get_value():
681
+ if not head.is_present():
682
+ head.set(left)
683
+ tail.set(left)
684
+ else:
685
+ tail.set_next(left)
686
+ tail.set(left)
687
+ left.set(left.get_next())
688
+ else:
689
+ if not head.is_present():
690
+ head.set(right)
691
+ tail.set(right)
692
+ else:
693
+ tail.set_next(right)
694
+ tail.set(right)
695
+ right.set(right.get_next())
696
+
697
+ while left.is_present():
698
+ if not head.is_present():
699
+ head.set(left)
700
+ tail.set(left)
701
+ else:
702
+ tail.set_next(left)
703
+ tail.set(left)
704
+ left.set(left.get_next())
705
+
706
+ while right.is_present():
707
+ if not head.is_present():
708
+ head.set(right)
709
+ tail.set(right)
710
+ else:
711
+ tail.set_next(right)
712
+ tail.set(right)
713
+ right.set(right.get_next())
714
+
715
+ if tail.is_present():
716
+ tail.set_next(a.empty())
717
+
718
+ return head
719
+
720
+
721
+ def _merge_sort_linked_list_nodes[TNode: _LinkedListNodeRef](
722
+ head: TNode,
723
+ ) -> TNode:
724
+ # Calculate length
725
+ length = 0
726
+ node = head.copy()
727
+ while node.is_present():
728
+ length += 1
729
+ node.set(node.get_next())
730
+
731
+ # Trivial case
732
+ if length <= 1:
733
+ return head
734
+
735
+ # Bottom-up merge sort: start with sublists of size 1, then 2, 4, 8, etc.
736
+ size = 1
737
+ while size < length:
738
+ current = head.copy()
739
+ new_head = head.empty()
740
+ new_tail = head.empty()
741
+
742
+ # Process all pairs of sublists of the current size
743
+ while current.is_present():
744
+ # Extract the first sublist
745
+ left = current.copy()
746
+ prev = current.empty()
747
+ i = 0
748
+ while i < size and current.is_present():
749
+ prev.set(current)
750
+ current.set(current.get_next())
751
+ i += 1
752
+ if prev.is_present():
753
+ prev.set_next(prev.empty())
754
+
755
+ # We've made it to the end without a second sublist to merge, so just attach it to the end
756
+ if not current.is_present():
757
+ # Since size < length, we know a full iteration must have happened already, so new_tail is valid
758
+ new_tail.set_next(left)
759
+ break
760
+
761
+ # Extract the second sublist
762
+ right = current.copy()
763
+ prev = current.empty()
764
+ i = 0
765
+ while i < size and current.is_present():
766
+ prev.set(current)
767
+ current.set(current.get_next())
768
+ i += 1
769
+ if prev.is_present():
770
+ prev.set_next(prev.empty())
771
+
772
+ merged = _merge_linked_list_nodes(left, right)
773
+
774
+ # Append the merged result
775
+ if not new_head.is_present():
776
+ new_head.set(merged)
777
+ new_tail.set(merged)
778
+ else:
779
+ new_tail.set_next(merged)
780
+
781
+ # Move tail to the end of the merged section
782
+ while new_tail.get_next().is_present():
783
+ new_tail.set(new_tail.get_next())
784
+
785
+ # Update head for the next iteration
786
+ head.set(new_head)
787
+ size *= 2
788
+
789
+ return head
790
+
791
+
792
+ class _EntityNodeRef[Archetype, GetValue, GetNextRef, GetPrevRef](Record):
793
+ index: int
794
+
795
+ def get_value(self) -> Any:
796
+ return self.get_value_fn(self.archetype.at(self.index))
797
+
798
+ def get_next(self) -> _EntityNodeRef:
799
+ next_ref = self.get_next_ref_fn(self.archetype.at(self.index))
800
+ return self.with_index(next_ref.index)
801
+
802
+ def set_next(self, next_node: _EntityNodeRef):
803
+ entity = self.archetype.at(self.index)
804
+ next_ref = self.get_next_ref_fn(entity)
805
+ next_ref.index = next_node.index
806
+
807
+ def set_prev(self, prev_node: _EntityNodeRef):
808
+ if self.get_prev_ref_fn is not None:
809
+ entity = self.archetype.at(self.index)
810
+ prev_ref = self.get_prev_ref_fn(entity)
811
+ prev_ref.index = prev_node.index
812
+
813
+ def is_present(self) -> bool:
814
+ return self.index > 0
815
+
816
+ def set(self, other: _EntityNodeRef):
817
+ self.index = other.index
818
+
819
+ def copy(self) -> _EntityNodeRef:
820
+ return self.with_index(self.index)
821
+
822
+ def empty(self) -> _EntityNodeRef:
823
+ return self.with_index(0)
824
+
825
+ def with_index(self, index: int) -> _EntityNodeRef:
826
+ return _EntityNodeRef[
827
+ self.archetype,
828
+ self.get_value_fn,
829
+ self.get_next_ref_fn,
830
+ self.get_prev_ref_fn,
831
+ ](index)
832
+
833
+ @property
834
+ def archetype(self):
835
+ return self.type_var_value(Archetype)
836
+
837
+ @property
838
+ def get_value_fn(self):
839
+ return self.type_var_value(GetValue)
840
+
841
+ @property
842
+ def get_next_ref_fn(self):
843
+ return self.type_var_value(GetNextRef)
844
+
845
+ @property
846
+ def get_prev_ref_fn(self):
847
+ return self.type_var_value(GetPrevRef)
848
+
849
+
850
+ def sort_linked_entities[T: AnyArchetype](
851
+ head_ref: EntityRef[T],
852
+ /,
853
+ *,
854
+ get_value: Callable[[T], Any],
855
+ get_next_ref: Callable[[T], EntityRef[T]],
856
+ get_prev_ref: Callable[[T], EntityRef[T]] | None = None,
857
+ ) -> EntityRef[T]:
858
+ """Sort a linked list of entities using merge sort.
859
+
860
+ If get_prev_ref is provided, the backward links will be updated as well.
861
+
862
+ Usage:
863
+ ```python
864
+ class MyArchetype(PlayArchetype):
865
+ sort_key: int
866
+ next: EntityRef[MyArchetype]
867
+
868
+ def sort_my_archetype(head: EntityRef[MyArchetype]) -> EntityRef[MyArchetype]:
869
+ return sort_linked_entities(
870
+ head,
871
+ get_value=lambda e: e.sort_key,
872
+ get_next_ref=lambda e: e.next,
873
+ )
874
+ ```
875
+
876
+ Args:
877
+ head_ref: A reference to the head of the linked list.
878
+ get_value: A function that takes an entity and returns the value to sort by.
879
+ get_next_ref: A function that takes an entity and returns a reference to the next entity.
880
+ get_prev_ref: An optional function that takes an entity and returns a reference to the previous entity.
881
+
882
+ Returns:
883
+ A reference to the head of the sorted linked list.
884
+ """
885
+ archetype = head_ref.archetype()
886
+
887
+ sorted_head_index = _merge_sort_linked_list_nodes(
888
+ _EntityNodeRef[archetype, get_value, get_next_ref, get_prev_ref](head_ref.index)
889
+ ).index
890
+
891
+ if get_prev_ref is not None:
892
+ current_ref = _EntityNodeRef[archetype, get_value, get_next_ref, get_prev_ref](sorted_head_index)
893
+ prev_ref = current_ref.empty()
894
+ while current_ref.is_present():
895
+ current_ref.set_prev(prev_ref)
896
+ prev_ref.set(current_ref)
897
+ current_ref.set(current_ref.get_next())
898
+
899
+ return EntityRef[archetype](sorted_head_index)
@@ -388,6 +388,11 @@ def _super(*args):
388
388
  return super(*(arg._as_py_() if arg._is_py_() else arg for arg in args))
389
389
 
390
390
 
391
+ @meta_fn
392
+ def _type(value):
393
+ return type(value)
394
+
395
+
391
396
  @meta_fn
392
397
  def _assert_never(arg: Never, /):
393
398
  error("Expected code to be unreachable")
@@ -416,6 +421,7 @@ BUILTIN_IMPLS = {
416
421
  id(range): Range,
417
422
  id(reversed): _reversed,
418
423
  id(super): _super,
424
+ id(type): _type,
419
425
  id(zip): _zip,
420
426
  id(assert_never): _assert_never,
421
427
  **MATH_BUILTIN_IMPLS, # Includes round
sonolus/script/vec.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from math import pi
4
+
3
5
  from sonolus.script.array import Array
4
6
  from sonolus.script.array_like import ArrayLike
5
7
  from sonolus.script.debug import assert_true
@@ -285,3 +287,22 @@ def pnpoly(vertices: ArrayLike[Vec2] | tuple[Vec2, ...], test: Vec2) -> bool:
285
287
  j = i
286
288
  i += 1
287
289
  return c
290
+
291
+
292
+ def angle_diff(a: float, b: float, /) -> float:
293
+ """Return the smallest absolute difference between two angles in radians.
294
+
295
+ The result is in the range [0, π].
296
+ """
297
+ return abs((a - b + pi) % (2 * pi) - pi)
298
+
299
+
300
+ def signed_angle_diff(a: float, b: float, /) -> float:
301
+ """Return the signed smallest difference between two angles in radians.
302
+
303
+ The result is in the range [-π, π). A positive result means a is counter-clockwise from b.
304
+ A negative result means a is clockwise from b.
305
+
306
+ If the two angles are exactly opposite, the result will be -π, but this should not be relied upon.
307
+ """
308
+ return (a - b + pi) % (2 * pi) - pi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.10.5
3
+ Version: 0.10.7
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
@@ -29,7 +29,7 @@ sonolus/build/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  sonolus/build/cli.py,sha256=t6oK0SGU6fkxXVbGu6OuRIHDKD6u4SqXAShdPtatZ-8,10060
30
30
  sonolus/build/collection.py,sha256=6hniAzriPWBKUeGDkXabNXpbdHiHnqiK9shs6U1OExM,12748
31
31
  sonolus/build/compile.py,sha256=KOmncDKmGfgzC_FWB_LTxAl0s9w4wnaDe-luACMlCVs,8397
32
- sonolus/build/dev_server.py,sha256=N2MaBaChiVnAPZJzlXRFGEIAHhELj4vDBAtuvgAF1qo,10172
32
+ sonolus/build/dev_server.py,sha256=xe6C2_dpODk51inL02F3xFMjDueCSay7-XzkR56lapY,10560
33
33
  sonolus/build/engine.py,sha256=jMymxbBXu-ekv71uU8TF2KbFaHs3yGjyJAztd1SoRDs,14808
34
34
  sonolus/build/level.py,sha256=KLqUAtxIuIqrzeFURJA97rdqjA5pcvYSmwNZQhElaMQ,702
35
35
  sonolus/build/node.py,sha256=gnX71RYDUOK_gYMpinQi-bLWO4csqcfiG5gFmhxzSec,1330
@@ -39,7 +39,7 @@ sonolus/script/archetype.py,sha256=ck_LR8z0ipVq3T9b735VwvQI2mxVUyjHylr4BFagXT8,4
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=yIod3DgX7Hv7RLe-4Cn81FcydvbkbdMt26FzpRj7oUI,7794
42
- sonolus/script/containers.py,sha256=nPjyyLmGnTxndMipqdfJ9CxmIT09wNLRQM7efagtZHI,19417
42
+ sonolus/script/containers.py,sha256=SnLXflwlX47EMQmTWnSzm9NYCi-as8lWnKzgCIK0PmM,26940
43
43
  sonolus/script/debug.py,sha256=yYg6EZt3NUOUeph1pu_5cA_2lxs8SZ91v76eOC1Sw-8,7747
44
44
  sonolus/script/easing.py,sha256=2FUJI_nfp990P_armCcRqHm2329O985glJAhSC6tnxs,11379
45
45
  sonolus/script/effect.py,sha256=SfJxSNF3RlPCRXnkt62ZlWhCXw3mmmRCsoMsvTErUP0,7960
@@ -67,9 +67,9 @@ sonolus/script/timing.py,sha256=DklMvuxcFg3MzXsecUo6Yhdk7pScOJ7STwXvAiTvLKM,3067
67
67
  sonolus/script/transform.py,sha256=4aS7-NNzX0v9KMXZ4gIGOaU1Cd-ok7DO_OvIBca0mGU,21418
68
68
  sonolus/script/ui.py,sha256=DYPGWIjHj1IFPxW1zaEuIUQx0b32FJPXtiwCvrtJ6oo,7528
69
69
  sonolus/script/values.py,sha256=6iJG6h4IDlbcK8FH4GENSHOQc7C_7fCGa34wM80qToA,1629
70
- sonolus/script/vec.py,sha256=s-_lplwu6UIvNpxiJHi2y1sM58-3ot1v1knAGbTJzCA,8162
70
+ sonolus/script/vec.py,sha256=LueAHrx5pKouHvLaO1nMbl5Plh3UgPn8J7UdJPHWFCM,8840
71
71
  sonolus/script/internal/__init__.py,sha256=T6rzLoiOUaiSQtaHMZ88SNO-ijSjSSv33TKtUwu-Ms8,136
72
- sonolus/script/internal/builtin_impls.py,sha256=tpNbaH6fLICd8TYj9Hf_wrPSWk3RkhmSPVN9nqOuqj4,13372
72
+ sonolus/script/internal/builtin_impls.py,sha256=b4gt54JyFoM9mhtxrhfCR8w2u38h4sJ3bzdOB0usNEs,13445
73
73
  sonolus/script/internal/callbacks.py,sha256=vWzJG8uiJoEtsNnbeZPqOHogCwoLpz2D1MnHY2wVV8s,2801
74
74
  sonolus/script/internal/constant.py,sha256=3ycbGkDJVUwcrCZ96vLjAoAARgsvaqDM8rJ_YCrLrvo,4289
75
75
  sonolus/script/internal/context.py,sha256=56pPjiPy8ZaxY3t5iEufsOMEj6BSy31G-5SoYqS6tPo,19694
@@ -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=DPNdmmRmupU8Ah4_XKq6-PdT336l4nt15_uCJKQGkkk,3587
89
89
  sonolus/script/internal/value.py,sha256=OngrCdmY_h6mV2Zgwqhuo4eYFad0kTk6263UAxctZcY,6963
90
- sonolus_py-0.10.5.dist-info/METADATA,sha256=OQvnIBSfASxlv-XRLl2RWz4OyYvvO_4wRda1yDmh8h0,554
91
- sonolus_py-0.10.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- sonolus_py-0.10.5.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
- sonolus_py-0.10.5.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
- sonolus_py-0.10.5.dist-info/RECORD,,
90
+ sonolus_py-0.10.7.dist-info/METADATA,sha256=sXdmgTuEdNIRF2McqWR7H1c_l-uolUuow8Nt3-ZhYpY,554
91
+ sonolus_py-0.10.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ sonolus_py-0.10.7.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
+ sonolus_py-0.10.7.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
+ sonolus_py-0.10.7.dist-info/RECORD,,