SLOPSHOPPER

current-branch

Shows the session's current git branch in the status line under the prompt

newguardstatusprocesstimer
★ 1v0.1.0no licenseupdated 2026-10-24wivuu/stellarallegiance/.claude/skills/current-branch
A shopper browsing a rack in a slop shop
README

Stellar Allegiance (stellarallegiance)

[!CAUTION] This is 'Slopllegiance'. it is a functional prototype Allegiance-style game written by Claude under supervision. It is not a finished game, expect bugs, and expect the code to be rough and unpolished - no pride in ownership here - this is slop. If this demonstrates anything it's that

  1. Allegiance can be re-platformed as a modern crossplatform game, and
  2. SOTA LLMs are capable of getting quite far in generating functional game code.

A 3D multiplayer space-combat game: a Godot client rendering and predicting flight, and a standalone .NET sim server that runs the authoritative 20 Hz simulation and hosts the lobby. Clients connect directly to a server by ip:port and download everything they need (world, content defs, live state) from it — there is no database to stand up.

client/        Godot 4.7 (C#/.NET 10) — rendering, input, client-side prediction
server/        .NET 10 console — authoritative 20 Hz sim + lobby host (the only gameplay authority)
public-lobby/  .NET 10 web — PUBLIC LOBBY: player accounts + game-server registry + WebRTC signaling + match ladder (Postgres/Orleans)
shared/        deterministic FlightModel + content defs, compiled into BOTH client and server
tools/         simbot (load bot swarm), asteroid-gen (mesh catalog)
tests/         ~20 test suites (FlightModel determinism/golden, Crypto, …) + factions/tests

The client only ever predicts; the server is the single source of truth and reconciles clients against its authoritative snapshots. The shared/ project is referenced (not copied) by both sides so their physics and content stay bit-identical.

Prerequisites

  • .NET 10 SDK (dotnet --version ≥ 10) — newer SDKs work too.
  • Aspire CLI — dotnet tool install -g Aspire.Cli or curl -sSL https://aspire.dev/install.sh | bash. Orchestrates the local stack (aspire run).
  • Docker — runs the local Postgres container the public lobby needs.
  • Godot 4.7.2 — Mono/.NET build, to run the client. Auto-detected from the GODOT env var, the godot.executablePath user secret, PATH, or standard install locations; the Aspire dashboard prompts for the path (and offers to save it to user secrets) if none of those resolve.
  • PowerShell 7+ (pwsh) — only needed now for scripts/export-clients.ps1 and the tools/*.ps1 helpers. Preinstalled on Windows; on macOS/Linux install it (brew install powershell or your package manager's apt/dnf package).

Quick start (local)

From the repo root:

aspire run

This brings up a Postgres container, applies the lobby's migrations, then the public lobby (http://localhost:8091) and the sim server (ws://localhost:8090/game), and opens the Aspire dashboard. In the dashboard, click Start on the client resource to build and launch the Godot client (it opens the local lobby's server browser; parameter client-mode = direct dials localhost:8090 instead).

Pick a side, ready up, and the match starts.

Against the hosted lobby instead. To publish a server to, or browse, the real public lobby (https://stellarlobby.wivuu.com) without the local stack, use the scripts: scripts/run-server.ps1 (publishes under your hostname; -Local = private) and scripts/run-client.ps1 (hosted server browser; -Local = direct localhost:8090). See scripts/README.md.

Configuration. The root .env (see .env.example) is read by both docker compose and the AppHost: every KEY=VALUE becomes an AppHost parameter (kebab-cased, e.g. SIM_AUTOSTART=1 → parameter sim-autostart). Anything left unresolved is prompted for in the dashboard and can be saved to user secrets. Local defaults turn on the lobby's dev login and register the server under your machine's hostname; it stays unlisted (direct-connect only) until you approve its device code — aspire resource lobby approve-device-code --user-code XXXX-XXXX (or the button on the lobby resource) — after which it is Verified and clients join through the lobby browser. The local stack always talks to the local lobby; the lobby-public-url/public-lobby parameters only feed Railway deploys. See Public lobby & NAT traversal. Accounts, Verified servers, the ladder and how to deploy them: docs/LOBBY-ACCOUNTS-AND-RANKING.md.

Accounts. On first launch the client asks you to sign in: it shows a short code and opens the lobby's approval page in your browser (passkeys always work; Google/GitHub/Steam when the lobby has them configured). Signed in, you see the public server list with VERIFIED (authenticated, operator shown, results count on the ladder) and UNVERIFIED badges; joining a Verified server uses a single-use join token under your account name. Continue without account (or --anonymous) keeps direct-by-address joins only. The web pages (/ladder, /players/<name>, /me) live on the lobby.

Solo testing tip: set SIM_AUTOSTART=1 (parameter sim-autostart) to skip the ready-up gate and start a perpetual match immediately.

See QUICKSTART.md for a step-by-step walkthrough and CONTRIBUTING.md for project layout, building, and tests.

Dev setup (VS Code tasks)

.vscode/tasks.json wires up the Aspire workflow as VS Code tasks. Run them with Cmd/Ctrl+Shift+P → "Tasks: Run Task":

TaskCommandWhat it does
Aspire: runaspire runStart the whole local stack (Postgres, lobby, sim server) with the dashboard, in a dedicated background panel.
Aspire: launch client`aspire resource client launch --mode <direct\lobby\autofly>`Build and launch an extra/custom Godot client (prompts for the mode).
Run server (public lobby)scripts/run-server.ps1Build (Release) + run the sim server published to the hosted public lobby.
Run client (public lobby)scripts/run-client.ps1Rebuild + launch a Godot client on the hosted lobby's server browser.
Export clients (all platforms)scripts/export-clients.ps1Export macOS/Windows/Linux builds (macOS .app only when run on macOS).
Package client (this OS)scripts/package-clients.ps1 -Version <x.y.z>Build this OS's release package: Game Launcher + client as a Velopack installer with auto-update (docs/RELEASING.md). -FakeGame skips the Godot export.
Godot: import assets (if needed)tools/godot-import.ps1Import GLB assets. Runs automatically on folder-open; a no-op unless something needs importing.
Godot: reimport assets (force)tools/godot-import.ps1 -ForceForce a full reimport after editing a .glb.
Asteroid-gen: build catalogtools/asteroid-gen/build.ps1Regenerate the asteroid mesh catalog (Docker).

The same commands run from a terminal — the tasks are just a convenient front-end. The dashboard's own Start/Launch client buttons on the client resource cover the common case (a single client) without going through VS Code at all.

Godot path (configurable, not committed). Aspire auto-detects Godot from GODOT, the godot.executablePath user secret, PATH, or standard install locations, so most setups need no configuration; if none resolve, the dashboard prompts for the path and offers to save it to user secrets (dotnet user-secrets set godot.executablePath "/path/to/Godot" --id stellarallegiance, outside the repo — never committed, survives git clean).

PowerShell 7+ is still needed for scripts/export-clients.ps1, scripts/package-clients.ps1 and the tools/*.ps1 helpers (Godot import/export, asteroid-gen). It's preinstalled on Windows; on macOS/Linux install it with brew install powershell or your package manager.

GLB assets are one file each. Only the .glb is committed (it embeds its own textures); Godot's .import/extracted-.png artifacts are gitignored and regenerated by the import task. A fresh clone imports automatically on first open; for headless/CI/export builds run tools/godot-import.ps1 (or -Force) first, or res:// asset loads fall back to placeholders.

Building manually

dotnet build shared/Shared.csproj          # deterministic core + content defs
dotnet build server/SimServer.csproj -c Release
dotnet build client/stellarallegiance.csproj  # or just open client/ in Godot-mono
dotnet run    --project server -c Release -- --port 8090   # run the server directly

Server options

dotnet run --project server -- [flags] (also settable via env in docker compose, see .env.example) — the same flags for a raw/perf-sensitive run outside Aspire:

FlagEnvAppHost parameterEffect
--port NSIM_PORTsim-portListen port (default 8090).
--secret PWSIM_SECRETsim-secretRequire a shared-secret password in every client Hello (open if unset).
--autostartSIM_AUTOSTART=1sim-autostartSkip the lobby ready-up; run a perpetual match (bots/benchmarking).
--seed N——World generation seed.

The client reads SIM_SECRET (to send the password) and PILOT_NAME (lobby name) from the environment; SIM_URI is a dev override that connects to a full ws://…/game URL directly. Under aspire run/aspire start, set the corresponding .env key (or override the parameter in the dashboard) instead of passing flags directly.

Public lobby & NAT traversal

A player can reach a server two ways:

  • Direct — type ip:port on the connect screen (or --host). A plain WebSocket join; works on a LAN or against a public/port-forwarded server.
  • Public lobby — the public-lobby/ service is a small registry + WebRTC signaling relay. A game server that sets SIM_PUBLIC_NAME registers itself there and clients browse the list. Discovery is direct-first: on register, the lobby probes the server's port and, if it's reachable, advertises a direct host:port so clients connect straight to it over WebSocket (no traffic through the lobby). A server that isn't reachable (NAT, no port-forward) falls back to a WebRTC DataChannel (P2P hole-punching via public STUN), with only the SDP handshake relayed through the lobby. The same binary protocol rides both transports.
  • Verified listings — a server only lists as Verified (its Operator's display name shown as "hosted by") once it authenticates with the lobby: first boot prints a one-time device code and stays unlisted until approved at the printed URL (locally: aspire resource lobby approve-device-code --user-code XXXX-XXXX); the resulting credential persists to SIM_AUTH_FILE (apphost/.local/server/ under Aspire) so later restarts re-list silently. See public-lobby/README.md's "Identity: device codes…" and "Listings: Verified vs Unverified".

There is no TURN relay — the lobby never carries game traffic. The trade-off is that a client behind a symmetric NAT can't reach a NAT'd server; it can always join direct servers, so hosting on a public/forwarded port gives the widest reach. All of this is automatic: forward a port and your server is listed as directly joinable; don't, and it's listed as WebRTC/STUN.

Flag/EnvWhereEffect
SIM_PUBLIC_NAMEserver3-50 char name; gates public-lobby registration (unset = private).
PUBLIC_LOBBYserver + clientLobby base — host:port or https://domain (default https://stellarlobby.wivuu.com). Client also takes --lobby.
SIM_PUBLIC_PORTserverPublic-facing port the lobby probes/advertises (default = listen port).
SIM_PUBLIC_ENDPOINTserverOptional address the server asserts as reachable — host:port (container NAT / proxy) or https://domain (a PaaS edge); advertised only if it answers /health. Auto-derives from RAILWAY_PUBLIC_DOMAIN on Railway.
SIM_AUTH_FILEserverPath to the persisted Game Server credential (device-code auth); default beside the sim-cache dir. Delete it to re-pair under a different Operator.
SHARE_PORTpublic-lobbyListen port (default 8091; a PaaS PORT overrides it).
STUN_URLpublic-lobbyPublic STUN URL(s) for the WebRTC fallback, comma-separated for redundancy (default Cloudflare's).

To host the lobby yourself — the single required port, the reachability probe, and production hardening — see public-lobby/README.md.

Running with Docker

cp .env.example .env        # optionally set SIM_SECRET / SIM_AUTOSTART / SIM_PUBLIC_NAME
docker compose up --build   # sim-server (ws://localhost:8090/game) + public-lobby (:8091)

Host a game server from the prebuilt image

Released images are published to GHCR — no checkout or build needed. The image keeps itself updated to the latest release once it has no players connected; set SIM_AUTO_UPDATE=warn (log only) or off to turn that down (see docs/DEPLOY.md → Server auto-update).

docker run -p 8090:8090 \
  -e SIM_PUBLIC_NAME="My Server" \
  -v sa-server-data:/data \
  ghcr.io/wivuu/stellarallegiance-sim:latest

An update relaunches inside this same container, so no --rm — the volume keeps the paired lobby credential in /data across a manual restart.

Deployment

Deploy from the Aspire dashboard — Deploy to Railway on the lobby or server resource — or from the CLI: aspire do deploy-lobby / aspire do deploy-server. See docs/DEPLOY.md for production (TLS termination, single-service deploy, and the one-time manual Railway steps for the lobby + a game server).

Documentation

Third-party assets

The 3D ship and station models are converted from Allegiance, originally developed by Microsoft and open-sourced by the FreeAllegiance project under the MIT license (copyright Microsoft Corporation). See NOTICE for the full license text.

Source 1 files
hooks/register.ts 81 lines
1import type { EngineInterface, ProcessRunResult, Register, Timer } from 'claude-code'
2
3const POLL_MS = 10_000
4const GIT_TIMEOUT_MS = 5_000
5
6// The module's own: a reload starts them over, and session.start sets them again.
7let shown: string | undefined
8let isShown = false
9let checking: Promise<void> | undefined
10let timer: Timer | undefined
11
12/** The status line for a checkout: its branch, or the commit a detached HEAD sits on. */
13export function label(branch: string, sha: string): string | undefined {
14  if (branch !== '') {
15    return `⎇ ${branch}`
16  }
17
18  return sha === '' ? undefined : `⎇ detached at ${sha}`
19}
20
21function git($: EngineInterface, cwd: string, args: string[]): Promise<ProcessRunResult> {
22  return $.process.run(['git', '-C', cwd, ...args], { timeoutMs: GIT_TIMEOUT_MS })
23}
24
25/** What the status line should say for the session's directory; undefined outside a git checkout. */
26async function current($: EngineInterface): Promise<string | undefined> {
27  const cwd = await $.session.cwd()
28  const branch = await git($, cwd, ['branch', '--show-current'])
29  if (branch.exitCode !== 0) {
30    return undefined
31  }
32
33  const name = branch.stdout.trim()
34  const head = name === '' ? await git($, cwd, ['rev-parse', '--short', 'HEAD']) : undefined
35
36  return label(name, head?.exitCode === 0 ? head.stdout.trim() : '')
37}
38
39/** Asks git for the branch and pins it as the status line when it changed; one check at a time. */
40function check($: EngineInterface): Promise<void> {
41  checking ??= checkNow($).finally(() => {
42    checking = undefined
43  })
44
45  return checking
46}
47
48/** Checks again once a tool call has run: it may have switched the branch or moved the session's directory. */
49function checkAfter<T>($: EngineInterface, ran: T): T {
50  void check($)
51
52  return ran
53}
54
55async function checkNow($: EngineInterface) {
56  const next = await current($).catch(() => undefined)
57  if (isShown && next === shown) {
58    return
59  }
60
61  $.ui.status(next)
62  shown = next
63  isShown = true
64}
65
66export const register: Register = on => {
67  on('session.start', async ($, e, next) => {
68    timer?.cancel()
69    isShown = false
70    void check($)
71    timer = $.clock.every(POLL_MS, () => void check($))
72
73    return next(e)
74  })
75
76  // A switch made through these shows at once; the poll catches one made outside the session.
77  on('tool.call', { tool: 'Bash' }, async ($, e, next) => checkAfter($, await next(e)))
78  on('tool.call', { tool: 'EnterWorktree' }, async ($, e, next) => checkAfter($, await next(e)))
79  on('tool.call', { tool: 'ExitWorktree' }, async ($, e, next) => checkAfter($, await next(e)))
80}
81