starsec-client 0.910.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.
Files changed (113) hide show
  1. starsec_client/__init__.py +16 -0
  2. starsec_client/_bootstrap.py +41 -0
  3. starsec_client/_vendor/starsec_core/__init__.py +28 -0
  4. starsec_client/_vendor/starsec_core/auth.py +130 -0
  5. starsec_client/_vendor/starsec_core/combat_hitbox.py +102 -0
  6. starsec_client/_vendor/starsec_core/config.py +777 -0
  7. starsec_client/_vendor/starsec_core/docker_transport.py +169 -0
  8. starsec_client/_vendor/starsec_core/editor/__init__.py +52 -0
  9. starsec_client/_vendor/starsec_core/editor/commands.py +725 -0
  10. starsec_client/_vendor/starsec_core/editor/document.py +205 -0
  11. starsec_client/_vendor/starsec_core/fighter_classes.py +67 -0
  12. starsec_client/_vendor/starsec_core/fighter_hud.py +103 -0
  13. starsec_client/_vendor/starsec_core/fighter_input.py +206 -0
  14. starsec_client/_vendor/starsec_core/gltf_meta.py +334 -0
  15. starsec_client/_vendor/starsec_core/home.py +128 -0
  16. starsec_client/_vendor/starsec_core/input.py +327 -0
  17. starsec_client/_vendor/starsec_core/installer/__init__.py +41 -0
  18. starsec_client/_vendor/starsec_core/installer/__main__.py +4 -0
  19. starsec_client/_vendor/starsec_core/installer/desktop.py +70 -0
  20. starsec_client/_vendor/starsec_core/installer/runner.py +47 -0
  21. starsec_client/_vendor/starsec_core/installer/state.py +113 -0
  22. starsec_client/_vendor/starsec_core/installer/wizard.py +281 -0
  23. starsec_client/_vendor/starsec_core/net/__init__.py +41 -0
  24. starsec_client/_vendor/starsec_core/net/bus.py +168 -0
  25. starsec_client/_vendor/starsec_core/net/codec.py +60 -0
  26. starsec_client/_vendor/starsec_core/net/messages.py +161 -0
  27. starsec_client/_vendor/starsec_core/net/stream/__init__.py +38 -0
  28. starsec_client/_vendor/starsec_core/net/stream/header.py +282 -0
  29. starsec_client/_vendor/starsec_core/net/stream/packet.py +72 -0
  30. starsec_client/_vendor/starsec_core/net/stream/reassemble.py +123 -0
  31. starsec_client/_vendor/starsec_core/net/stream/receiver.py +97 -0
  32. starsec_client/_vendor/starsec_core/net/stream/relay.py +284 -0
  33. starsec_client/_vendor/starsec_core/net/stream/sender.py +125 -0
  34. starsec_client/_vendor/starsec_core/net/terminal_protocol.py +74 -0
  35. starsec_client/_vendor/starsec_core/net/updates.py +149 -0
  36. starsec_client/_vendor/starsec_core/render/__init__.py +34 -0
  37. starsec_client/_vendor/starsec_core/render/assets.py +112 -0
  38. starsec_client/_vendor/starsec_core/render/audio.py +562 -0
  39. starsec_client/_vendor/starsec_core/render/avatars.py +74 -0
  40. starsec_client/_vendor/starsec_core/render/camera.py +75 -0
  41. starsec_client/_vendor/starsec_core/render/camera_modes.py +88 -0
  42. starsec_client/_vendor/starsec_core/render/color.py +11 -0
  43. starsec_client/_vendor/starsec_core/render/crt.py +148 -0
  44. starsec_client/_vendor/starsec_core/render/damage_fx.py +74 -0
  45. starsec_client/_vendor/starsec_core/render/datamosh.py +110 -0
  46. starsec_client/_vendor/starsec_core/render/explosions.py +146 -0
  47. starsec_client/_vendor/starsec_core/render/frustum.py +95 -0
  48. starsec_client/_vendor/starsec_core/render/lighting.py +333 -0
  49. starsec_client/_vendor/starsec_core/render/lod_director.py +74 -0
  50. starsec_client/_vendor/starsec_core/render/lod_policy.py +98 -0
  51. starsec_client/_vendor/starsec_core/render/markers.py +91 -0
  52. starsec_client/_vendor/starsec_core/render/material.py +154 -0
  53. starsec_client/_vendor/starsec_core/render/model_render.py +174 -0
  54. starsec_client/_vendor/starsec_core/render/model_shading.py +209 -0
  55. starsec_client/_vendor/starsec_core/render/model_view.py +542 -0
  56. starsec_client/_vendor/starsec_core/render/pick.py +144 -0
  57. starsec_client/_vendor/starsec_core/render/postfx.py +70 -0
  58. starsec_client/_vendor/starsec_core/render/scene.py +312 -0
  59. starsec_client/_vendor/starsec_core/render/shading.py +101 -0
  60. starsec_client/_vendor/starsec_core/render/shield_bubble.py +187 -0
  61. starsec_client/_vendor/starsec_core/render/ship_lod.py +161 -0
  62. starsec_client/_vendor/starsec_core/render/sky.py +209 -0
  63. starsec_client/_vendor/starsec_core/render/stats.py +52 -0
  64. starsec_client/_vendor/starsec_core/render/terminal_screen.py +196 -0
  65. starsec_client/_vendor/starsec_core/render/tri_pick.py +111 -0
  66. starsec_client/_vendor/starsec_core/render/vfx.py +717 -0
  67. starsec_client/_vendor/starsec_core/rig.py +225 -0
  68. starsec_client/_vendor/starsec_core/scene.py +266 -0
  69. starsec_client/_vendor/starsec_core/schema.py +209 -0
  70. starsec_client/_vendor/starsec_core/scoreboard.py +75 -0
  71. starsec_client/_vendor/starsec_core/spectator_camera.py +91 -0
  72. starsec_client/_vendor/starsec_core/terminal.py +905 -0
  73. starsec_client/_vendor/starsec_core/touch_input.py +184 -0
  74. starsec_client/_vendor/starsec_core/tracing.py +45 -0
  75. starsec_client/_vendor/starsec_core/ui/__init__.py +459 -0
  76. starsec_client/_vendor/starsec_core/ui/ascii_stars.py +85 -0
  77. starsec_client/_vendor/starsec_core/ui/audio.py +202 -0
  78. starsec_client/_vendor/starsec_core/ui/components.py +833 -0
  79. starsec_client/_vendor/starsec_core/ui/containers.py +343 -0
  80. starsec_client/_vendor/starsec_core/ui/core.py +250 -0
  81. starsec_client/_vendor/starsec_core/ui/feedback.py +213 -0
  82. starsec_client/_vendor/starsec_core/ui/font.py +110 -0
  83. starsec_client/_vendor/starsec_core/ui/layout.py +188 -0
  84. starsec_client/_vendor/starsec_core/ui/nav.py +232 -0
  85. starsec_client/_vendor/starsec_core/ui/theme.py +190 -0
  86. starsec_client/_vendor/starsec_core/ui/titlebar.py +239 -0
  87. starsec_client/_vendor/starsec_core/ui/tokens.py +77 -0
  88. starsec_client/_vendor/starsec_core/ui/widgets.py +654 -0
  89. starsec_client/_vendor/starsec_core/version.py +32 -0
  90. starsec_client/_vendor/starsec_core/world.py +813 -0
  91. starsec_client/_vendor/starsec_core/world_package.py +219 -0
  92. starsec_client/announce.py +87 -0
  93. starsec_client/bindings_ui.py +177 -0
  94. starsec_client/client.py +996 -0
  95. starsec_client/client_checks.py +89 -0
  96. starsec_client/installer.py +7 -0
  97. starsec_client/lobby.py +92 -0
  98. starsec_client/modes.py +17 -0
  99. starsec_client/net.py +229 -0
  100. starsec_client/pause_mode.py +172 -0
  101. starsec_client/pause_ui.py +165 -0
  102. starsec_client/play_view.py +249 -0
  103. starsec_client/settings_ui.py +248 -0
  104. starsec_client/spectate_mode.py +385 -0
  105. starsec_client/spectator.py +497 -0
  106. starsec_client/spectator_ui.py +334 -0
  107. starsec_client/terminal_remote.py +116 -0
  108. starsec_client/terminal_ws.py +154 -0
  109. starsec_client/transport.py +57 -0
  110. starsec_client-0.910.0.dist-info/METADATA +40 -0
  111. starsec_client-0.910.0.dist-info/RECORD +113 -0
  112. starsec_client-0.910.0.dist-info/WHEEL +4 -0
  113. starsec_client-0.910.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,16 @@
1
+ # ruff: noqa: I001 (the vendored-core shim must import before starsec_core)
2
+ """starsec-client — the standalone STARSEC player app.
3
+
4
+ Entrypoint is :func:`main` (console script ``starsec-client``). The player
5
+ app shows a main menu, joins a match lobby, and plays: FPS movement plus
6
+ in-world terminal focus. Level authoring lives in starsec-engine, not here.
7
+ """
8
+
9
+ # Must be first: registers the vendored starsec_core when it is not installed.
10
+ import starsec_client._bootstrap # noqa: F401
11
+
12
+ from starsec_client.client import Client, Mode, main
13
+ from starsec_core.version import CLIENT_VERSION
14
+
15
+ __version__ = CLIENT_VERSION
16
+ __all__ = ["Client", "Mode", "main"]
@@ -0,0 +1,41 @@
1
+ """Resolve ``starsec_core`` to the vendored copy when it is not installed.
2
+
3
+ The client ships a copy of the shared runtime (``_vendor/starsec_core``) so the
4
+ PyPI package never depends on the private core distribution. The copy is loaded
5
+ **under the name** ``starsec_core`` so every absolute import inside it (and in
6
+ the client) resolves unchanged.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import importlib.util
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def install() -> bool:
18
+ """Make ``starsec_core`` importable. True when the vendored copy is used."""
19
+ if "starsec_core" in sys.modules:
20
+ return False
21
+ try:
22
+ importlib.import_module("starsec_core")
23
+ return False
24
+ except ImportError:
25
+ pass
26
+ package_dir = Path(__file__).resolve().parent / "_vendor" / "starsec_core"
27
+ init = package_dir / "__init__.py"
28
+ if not init.is_file():
29
+ raise ImportError("starsec_core is not installed and no vendored copy ships")
30
+ spec = importlib.util.spec_from_file_location(
31
+ "starsec_core", init, submodule_search_locations=[str(package_dir)]
32
+ )
33
+ if spec is None or spec.loader is None:
34
+ raise ImportError("vendored starsec_core is malformed")
35
+ module = importlib.util.module_from_spec(spec)
36
+ sys.modules["starsec_core"] = module
37
+ spec.loader.exec_module(module)
38
+ return True
39
+
40
+
41
+ install()
@@ -0,0 +1,28 @@
1
+ """starsec-core — shared STARSEC engine runtime.
2
+
3
+ Public API re-exports:
4
+ - config, tracing
5
+ - world (World, Team, Platform, Terminal, Spawn, Camera, Model)
6
+ - terminal (Terminal, LocalPtyTransport, LocalDockerTransport)
7
+ - input (PlayerController)
8
+ - render (lighting, color, camera, terminal3d)
9
+ - net (codec, messages, bus)
10
+ - ui (Button, Panel, TextInput, Slider, Checkbox, Modal, modal_confirm)
11
+ """
12
+
13
+ from starsec_core.version import CORE_VERSION
14
+
15
+ from . import config, input, net, render, terminal, tracing, ui, world
16
+
17
+ __version__ = CORE_VERSION
18
+
19
+ __all__ = [
20
+ "config",
21
+ "input",
22
+ "net",
23
+ "render",
24
+ "terminal",
25
+ "tracing",
26
+ "ui",
27
+ "world",
28
+ ]
@@ -0,0 +1,130 @@
1
+ """D5i account auth — GitHub device flow + credentials store (Phase 11).
2
+
3
+ D5-Interactive accounts ride GitHub: client-side device-flow OAuth (stdlib
4
+ ``urllib``; ``http`` injectable for tests), token + login stored in the
5
+ home's ``credentials.json`` (chmod 600; Windows: best-effort ACL later).
6
+ The updater (``net.updates``) reads the token for the private engine
7
+ channel; nothing else consumes credentials yet.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import stat
15
+ import time
16
+ import urllib.parse
17
+ import urllib.request
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+
21
+ DEVICE_URL = "https://github.com/login/device/code"
22
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
23
+
24
+ #: Set when the D5i OAuth app is registered; empty = the flow refuses so
25
+ #: callers fail loudly instead of hitting an unconfigured app entry.
26
+ CLIENT_ID = ""
27
+
28
+ CREDENTIALS_FALLBACK = "~/.starsec_credentials.json"
29
+
30
+ Http = Callable[[str, str, dict, dict], "tuple[int, bytes]"]
31
+
32
+
33
+ def _http_default(method: str, url: str, headers: dict, data: dict):
34
+ body = urllib.parse.urlencode(data).encode()
35
+ request = urllib.request.Request(url, data=body, headers=headers, method=method)
36
+ try:
37
+ with urllib.request.urlopen(request, timeout=30) as resp:
38
+ return resp.status, resp.read()
39
+ except urllib.error.HTTPError as exc: # GitHub answers errors in-band
40
+ return exc.code, exc.read()
41
+
42
+
43
+ def request_device_code(client_id: str, *, http: Http | None = None) -> dict:
44
+ """Start the device flow; returns the device-code payload from GitHub."""
45
+ if not client_id:
46
+ raise ValueError("client_id not configured")
47
+ transport = http or _http_default
48
+ status, body = transport(
49
+ "POST", DEVICE_URL, {"Accept": "application/json"}, {"client_id": client_id}
50
+ )
51
+ if status != 200:
52
+ raise RuntimeError(f"device-code request failed: HTTP {status}")
53
+ return json.loads(body)
54
+
55
+
56
+ def poll_token(
57
+ device_code: dict,
58
+ client_id: str,
59
+ *,
60
+ http: Http | None = None,
61
+ tries: int = 8,
62
+ sleep: Callable[[float], None] = time.sleep,
63
+ ) -> dict:
64
+ """Poll for the access token; ``None``-free contract: raise on timeout.
65
+
66
+ Bounded on purpose (Review Focus 3): ``tries`` rounds at the device
67
+ code's ``interval``; ``slow_down`` adds the 5-second penalty and keeps
68
+ polling. Exhaustion raises ``TimeoutError``.
69
+ """
70
+ if not client_id:
71
+ raise ValueError("client_id not configured")
72
+ transport = http or _http_default
73
+ interval = float(device_code.get("interval", 5))
74
+ data = {
75
+ "client_id": client_id,
76
+ "device_code": device_code.get("device_code", ""),
77
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
78
+ }
79
+ for _attempt in range(tries):
80
+ status, body = transport("POST", TOKEN_URL, {"Accept": "application/json"}, dict(data))
81
+ payload = json.loads(body)
82
+ error = payload.get("error")
83
+ if status == 200 and payload.get("access_token"):
84
+ return payload
85
+ if error == "slow_down":
86
+ interval += 5.0
87
+ elif error not in (None, "authorization_pending"):
88
+ raise RuntimeError(f"device flow error: {error}")
89
+ sleep(interval)
90
+ raise TimeoutError(f"device flow timed out after {tries} rounds")
91
+
92
+
93
+ # --------------------------------------------------------------------------- #
94
+ # Credentials store
95
+ # --------------------------------------------------------------------------- #
96
+ def credentials_path(env: dict[str, str] | None = None) -> Path:
97
+ """``<home>/credentials.json`` when home is active, else the user file."""
98
+ from starsec_core.home import home_root
99
+
100
+ home = home_root(env=env)
101
+ if home is not None:
102
+ return home / "credentials.json"
103
+ return Path(CREDENTIALS_FALLBACK).expanduser()
104
+
105
+
106
+ def save_credentials(path: Path, creds: dict) -> None:
107
+ """Write atomically-ish with owner-only permissions."""
108
+ path.parent.mkdir(parents=True, exist_ok=True)
109
+ path.write_text(json.dumps(creds, indent=2))
110
+ try:
111
+ os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
112
+ except OSError: # Windows: no chmod; ACL pass later
113
+ pass
114
+
115
+
116
+ def load_credentials(path: Path) -> dict:
117
+ return json.loads(Path(path).read_text())
118
+
119
+
120
+ def _self_check() -> None:
121
+ assert (
122
+ credentials_path(env={"STARSEC_HOME_MARKER": "/nonexistent"}).name
123
+ == ".starsec_credentials.json"
124
+ )
125
+ assert CLIENT_ID == "" # flow refuses until the OAuth app is registered
126
+ print("auth self-check ok")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ _self_check()
@@ -0,0 +1,102 @@
1
+ """Rectangular OBB hitboxes (Phase 23C): same test for combat and editor.
2
+
3
+ Euler degrees use the same world convention as ``world.euler``: rotate around
4
+ X, then Y, then Z. Segment tests operate in box-local space via inverse
5
+ rotation, so a rotated fighter's hitbox matches its rendered box.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+
13
+ from starsec_core.world import Vec3, v3_add, v3_dot, v3_rotate_axis, v3_sub
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Hitbox:
18
+ half_extents: Vec3 = (1.0, 0.5, 1.0)
19
+ offset: Vec3 = (0.0, 0.0, 0.0)
20
+ rotation: Vec3 = (0.0, 0.0, 0.0)
21
+
22
+ def valid(self) -> bool:
23
+ return all(value > 0.0 for value in self.half_extents)
24
+
25
+
26
+ def _to_local(point: Vec3, center: Vec3, rotation: Vec3) -> Vec3:
27
+ relative = v3_sub(point, center)
28
+ # inverse Euler order: undo Z, then Y, then X
29
+ out = v3_rotate_axis(relative, (0.0, 0.0, 1.0), -math.radians(rotation[2]))
30
+ out = v3_rotate_axis(out, (0.0, 1.0, 0.0), -math.radians(rotation[1]))
31
+ return v3_rotate_axis(out, (1.0, 0.0, 0.0), -math.radians(rotation[0]))
32
+
33
+
34
+ def hitbox_center(hitbox: Hitbox, position: Vec3, rotation: Vec3) -> Vec3:
35
+ # offset rotates with the fighter; reuse forward rotation via positive axes
36
+ return v3_add(position, _rotate_forward(hitbox.offset, rotation))
37
+
38
+
39
+ def _rotate_forward(vector: Vec3, rotation: Vec3) -> Vec3:
40
+ out = v3_rotate_axis(vector, (1.0, 0.0, 0.0), math.radians(rotation[0]))
41
+ out = v3_rotate_axis(out, (0.0, 1.0, 0.0), math.radians(rotation[1]))
42
+ return v3_rotate_axis(out, (0.0, 0.0, 1.0), math.radians(rotation[2]))
43
+
44
+
45
+ def point_in_hitbox(point: Vec3, hitbox: Hitbox, position: Vec3, rotation: Vec3) -> bool:
46
+ center = hitbox_center(hitbox, position, rotation)
47
+ local = _to_local(point, center, rotation)
48
+ return all(abs(local[i]) <= hitbox.half_extents[i] for i in range(3))
49
+
50
+
51
+ def segment_hitbox(
52
+ start: Vec3,
53
+ end: Vec3,
54
+ hitbox: Hitbox,
55
+ position: Vec3,
56
+ rotation: Vec3,
57
+ ) -> float | None:
58
+ """Ray/segment vs OBB; returns the entry fraction 0..1 or None."""
59
+ if not hitbox.valid():
60
+ return None
61
+ center = hitbox_center(hitbox, position, rotation)
62
+ local_start = _to_local(start, center, rotation)
63
+ local_end = _to_local(end, center, rotation)
64
+ direction = v3_sub(local_end, local_start)
65
+ t_min, t_max = 0.0, 1.0
66
+ for axis in range(3):
67
+ if abs(direction[axis]) < 1e-9:
68
+ if abs(local_start[axis]) > hitbox.half_extents[axis]:
69
+ return None
70
+ continue
71
+ first = (-hitbox.half_extents[axis] - local_start[axis]) / direction[axis]
72
+ second = (hitbox.half_extents[axis] - local_start[axis]) / direction[axis]
73
+ low, high = sorted((first, second))
74
+ t_min = max(t_min, low)
75
+ t_max = min(t_max, high)
76
+ if t_min > t_max:
77
+ return None
78
+ return t_min if t_max >= 0.0 else None
79
+
80
+
81
+ def sphere_hitbox(
82
+ center: Vec3, radius: float, hitbox: Hitbox, position: Vec3, rotation: Vec3
83
+ ) -> bool:
84
+ """Sphere vs OBB via the closest local point."""
85
+ box_center = hitbox_center(hitbox, position, rotation)
86
+ local = _to_local(center, box_center, rotation)
87
+ closest = tuple(
88
+ max(-hitbox.half_extents[i], min(hitbox.half_extents[i], local[i])) for i in range(3)
89
+ )
90
+ delta = v3_sub(local, closest)
91
+ return v3_dot(delta, delta) <= radius * radius
92
+
93
+
94
+ def _self_check() -> None:
95
+ box = Hitbox((1.0, 1.0, 1.0))
96
+ assert point_in_hitbox((0.0, 0.0, 0.0), box, (0.0, 0.0, 0.0), (0, 0, 0))
97
+ assert segment_hitbox((-5, 0, 0), (5, 0, 0), box, (0, 0, 0), (0, 0, 0)) is not None
98
+ print("hitbox self-check ok")
99
+
100
+
101
+ if __name__ == "__main__":
102
+ _self_check()