starsec-client 0.910.0__tar.gz

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-0.910.0/PKG-INFO +40 -0
  2. starsec_client-0.910.0/README.md +16 -0
  3. starsec_client-0.910.0/pyproject.toml +42 -0
  4. starsec_client-0.910.0/pyproject.toml.orig +44 -0
  5. starsec_client-0.910.0/src/starsec_client/__init__.py +16 -0
  6. starsec_client-0.910.0/src/starsec_client/_bootstrap.py +41 -0
  7. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/__init__.py +28 -0
  8. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/auth.py +130 -0
  9. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/combat_hitbox.py +102 -0
  10. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/config.py +777 -0
  11. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/docker_transport.py +169 -0
  12. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/editor/__init__.py +52 -0
  13. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/editor/commands.py +725 -0
  14. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/editor/document.py +205 -0
  15. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/fighter_classes.py +67 -0
  16. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/fighter_hud.py +103 -0
  17. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/fighter_input.py +206 -0
  18. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/gltf_meta.py +334 -0
  19. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/home.py +128 -0
  20. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/input.py +327 -0
  21. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/__init__.py +41 -0
  22. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/__main__.py +4 -0
  23. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/desktop.py +70 -0
  24. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/runner.py +47 -0
  25. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/state.py +113 -0
  26. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/installer/wizard.py +281 -0
  27. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/__init__.py +41 -0
  28. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/bus.py +168 -0
  29. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/codec.py +60 -0
  30. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/messages.py +161 -0
  31. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/__init__.py +38 -0
  32. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/header.py +282 -0
  33. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/packet.py +72 -0
  34. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/reassemble.py +123 -0
  35. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/receiver.py +97 -0
  36. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/relay.py +284 -0
  37. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/stream/sender.py +125 -0
  38. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/terminal_protocol.py +74 -0
  39. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/net/updates.py +149 -0
  40. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/__init__.py +34 -0
  41. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/assets.py +112 -0
  42. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/audio.py +562 -0
  43. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/avatars.py +74 -0
  44. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/camera.py +75 -0
  45. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/camera_modes.py +88 -0
  46. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/color.py +11 -0
  47. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/crt.py +148 -0
  48. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/damage_fx.py +74 -0
  49. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/datamosh.py +110 -0
  50. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/explosions.py +146 -0
  51. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/frustum.py +95 -0
  52. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/lighting.py +333 -0
  53. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/lod_director.py +74 -0
  54. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/lod_policy.py +98 -0
  55. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/markers.py +91 -0
  56. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/material.py +154 -0
  57. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/model_render.py +174 -0
  58. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/model_shading.py +209 -0
  59. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/model_view.py +542 -0
  60. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/pick.py +144 -0
  61. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/postfx.py +70 -0
  62. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/scene.py +312 -0
  63. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/shading.py +101 -0
  64. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/shield_bubble.py +187 -0
  65. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/ship_lod.py +161 -0
  66. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/sky.py +209 -0
  67. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/stats.py +52 -0
  68. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/terminal_screen.py +196 -0
  69. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/tri_pick.py +111 -0
  70. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/render/vfx.py +717 -0
  71. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/rig.py +225 -0
  72. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/scene.py +266 -0
  73. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/schema.py +209 -0
  74. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/scoreboard.py +75 -0
  75. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/spectator_camera.py +91 -0
  76. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/terminal.py +905 -0
  77. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/touch_input.py +184 -0
  78. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/tracing.py +45 -0
  79. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/__init__.py +459 -0
  80. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/ascii_stars.py +85 -0
  81. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/audio.py +202 -0
  82. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/components.py +833 -0
  83. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/containers.py +343 -0
  84. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/core.py +250 -0
  85. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/feedback.py +213 -0
  86. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/font.py +110 -0
  87. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/layout.py +188 -0
  88. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/nav.py +232 -0
  89. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/theme.py +190 -0
  90. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/titlebar.py +239 -0
  91. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/tokens.py +77 -0
  92. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/ui/widgets.py +654 -0
  93. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/version.py +32 -0
  94. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/world.py +813 -0
  95. starsec_client-0.910.0/src/starsec_client/_vendor/starsec_core/world_package.py +219 -0
  96. starsec_client-0.910.0/src/starsec_client/announce.py +87 -0
  97. starsec_client-0.910.0/src/starsec_client/bindings_ui.py +177 -0
  98. starsec_client-0.910.0/src/starsec_client/client.py +996 -0
  99. starsec_client-0.910.0/src/starsec_client/client_checks.py +89 -0
  100. starsec_client-0.910.0/src/starsec_client/installer.py +7 -0
  101. starsec_client-0.910.0/src/starsec_client/lobby.py +92 -0
  102. starsec_client-0.910.0/src/starsec_client/modes.py +17 -0
  103. starsec_client-0.910.0/src/starsec_client/net.py +229 -0
  104. starsec_client-0.910.0/src/starsec_client/pause_mode.py +172 -0
  105. starsec_client-0.910.0/src/starsec_client/pause_ui.py +165 -0
  106. starsec_client-0.910.0/src/starsec_client/play_view.py +249 -0
  107. starsec_client-0.910.0/src/starsec_client/settings_ui.py +248 -0
  108. starsec_client-0.910.0/src/starsec_client/spectate_mode.py +385 -0
  109. starsec_client-0.910.0/src/starsec_client/spectator.py +497 -0
  110. starsec_client-0.910.0/src/starsec_client/spectator_ui.py +334 -0
  111. starsec_client-0.910.0/src/starsec_client/terminal_remote.py +116 -0
  112. starsec_client-0.910.0/src/starsec_client/terminal_ws.py +154 -0
  113. starsec_client-0.910.0/src/starsec_client/transport.py +57 -0
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: starsec-client
3
+ Version: 0.910.0
4
+ Summary: STARSEC standalone player client: fly, focus terminals, spectate
5
+ Keywords: starship,competition,cybersecurity,raylib,multiplayer
6
+ License: Proprietary
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Environment :: X11 Applications
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Topic :: Games/Entertainment :: Simulation
16
+ Requires-Dist: msgspec>=0.22.0
17
+ Requires-Dist: pyte==0.8.2
18
+ Requires-Dist: loguru>=0.7.3
19
+ Requires-Dist: tomli-w>=1.2.0
20
+ Requires-Dist: wcwidth>=0.2.13
21
+ Requires-Dist: raylib>=6.0.1.0
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # starsec-client
26
+
27
+ The STARSEC player client: a standalone raylib application that loads a world,
28
+ joins a STARSEC match server, flies, focuses terminals and spectates.
29
+
30
+ ```console
31
+ pip install starsec-client
32
+ starsec-client --help
33
+ starsec-client --world world.toml --host <server-ip> --port 27015
34
+ starsec-client --spectate --host <server-ip> # camera for OBS
35
+ ```
36
+
37
+ Requires Python 3.11+ and a desktop with OpenGL 3.3. raylib ships with the
38
+ package; no system libraries are needed beyond the usual GL/X11 stack.
39
+
40
+ Built by D5-Interactive for the ROWDY-CYBERCON showcase.
@@ -0,0 +1,16 @@
1
+ # starsec-client
2
+
3
+ The STARSEC player client: a standalone raylib application that loads a world,
4
+ joins a STARSEC match server, flies, focuses terminals and spectates.
5
+
6
+ ```console
7
+ pip install starsec-client
8
+ starsec-client --help
9
+ starsec-client --world world.toml --host <server-ip> --port 27015
10
+ starsec-client --spectate --host <server-ip> # camera for OBS
11
+ ```
12
+
13
+ Requires Python 3.11+ and a desktop with OpenGL 3.3. raylib ships with the
14
+ package; no system libraries are needed beyond the usual GL/X11 stack.
15
+
16
+ Built by D5-Interactive for the ROWDY-CYBERCON showcase.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "starsec-client"
3
+ version = "0.910.0"
4
+ description = "STARSEC standalone player client: fly, focus terminals, spectate"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ keywords = [
8
+ "starship",
9
+ "competition",
10
+ "cybersecurity",
11
+ "raylib",
12
+ "multiplayer",
13
+ ]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Environment :: X11 Applications",
20
+ "Intended Audience :: End Users/Desktop",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Operating System :: Microsoft :: Windows",
23
+ "Topic :: Games/Entertainment :: Simulation",
24
+ ]
25
+ dependencies = [
26
+ "msgspec>=0.22.0",
27
+ "pyte==0.8.2",
28
+ "loguru>=0.7.3",
29
+ "tomli-w>=1.2.0",
30
+ "wcwidth>=0.2.13",
31
+ "raylib>=6.0.1.0",
32
+ ]
33
+
34
+ [project.license]
35
+ text = "Proprietary"
36
+
37
+ [project.scripts]
38
+ starsec-client = "starsec_client:main"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.12.17,<0.13.0"]
42
+ build-backend = "uv_build"
@@ -0,0 +1,44 @@
1
+ # starsec-client — standalone player app (players install this, join lobbies).
2
+ # Public: distributed as native installers via Briefcase. In the workspace it
3
+ # depends on starsec-core directly; Briefcase vendors a minimal core copy at
4
+ # build time so the public bundle carries zero private deps (docs/plan.md §2).
5
+
6
+ [project]
7
+ name = "starsec-client"
8
+ version = "0.910.0"
9
+ description = "STARSEC standalone player client: fly, focus terminals, spectate"
10
+ readme = "README.md"
11
+ license = { text = "Proprietary" }
12
+ requires-python = ">=3.11"
13
+ keywords = ["starship", "competition", "cybersecurity", "raylib", "multiplayer"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Environment :: X11 Applications",
20
+ "Intended Audience :: End Users/Desktop",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Operating System :: Microsoft :: Windows",
23
+ "Topic :: Games/Entertainment :: Simulation",
24
+ ]
25
+ # Self-contained: the shared runtime is vendored under starsec_client/_vendor
26
+ # (scripts/vendor_core.py), so the private starsec-core is never published.
27
+ dependencies = [
28
+ "msgspec>=0.22.0",
29
+ "pyte==0.8.2",
30
+ "loguru>=0.7.3",
31
+ "tomli-w>=1.2.0",
32
+ "wcwidth>=0.2.13",
33
+ "raylib>=6.0.1.0",
34
+ ]
35
+
36
+
37
+ [project.scripts]
38
+ starsec-client = "starsec_client:main"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.12.17,<0.13.0"]
42
+ build-backend = "uv_build"
43
+
44
+
@@ -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()