Initial StartOS package for Station Master (v0.5.1)
Built from source via a git submodule pinned to a tag, not a published image — the Dockerfile, main.ts, and manifest should never need to change for an ordinary version bump, only the submodule pin (see UPDATING.md). One volume, one interface serving the browser client + lobby/intent API + SSE stream from a single origin, no dependencies. The server-wide join secret (D14) is seeded on install, exposed via the Get Join Secret action, and blocks start behind a critical task until retrieved — the same first-set/rotation pattern as actual-budget-startos's admin password. Verified on phoenix.local: installs, the critical task correctly blocks an ordinary start, force-starting confirms the daemon binds its port and serves the client, and store.json correctly holds the install-seeded join secret. Not verified: the Get Join Secret action's execution end-to-end — start-cli's `package action run` fails with a client-side deserialization error that reproduces identically against actual-budget's already-shipped equivalent action, so this looks like a start-cli issue rather than a defect here. icon.svg is still the scaffold's hello-world placeholder — no real Station Master icon exists yet to ship in its place.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
.gitmodules
|
||||||
|
node_modules
|
||||||
|
*.s9pk
|
||||||
|
docker-images
|
||||||
|
javascript
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
paths-ignore: ['*.md']
|
||||||
|
branches: ['master']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
uses: Start9Labs/start-technologies/.github/workflows/build.yml@master
|
||||||
|
# No DEV_KEY — a PR build doesn't publish, so it doesn't need the signing key.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: Start9Labs/start-technologies/.github/workflows/release.yml@master
|
||||||
|
with:
|
||||||
|
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
|
||||||
|
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
|
||||||
|
secrets:
|
||||||
|
DEV_KEY: ${{ secrets.DEV_KEY }}
|
||||||
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Sync next
|
||||||
|
|
||||||
|
# Carries every change that lands on the base branch onto the paired `next`
|
||||||
|
# iteration branch, so `next` never drifts behind what has already shipped.
|
||||||
|
# `next` is created on the first run if the repo does not have one yet.
|
||||||
|
#
|
||||||
|
# List every base branch this package maintains. Most packages have one; the
|
||||||
|
# multi-branch packages list each major line or flavor (e.g. 28.x, 29.x). A
|
||||||
|
# package whose default branch is `main` must say `main` here — a workflow
|
||||||
|
# pointed at a branch the repo does not use never runs.
|
||||||
|
#
|
||||||
|
# No paths-ignore: a docs-only commit on the base still has to reach `next`,
|
||||||
|
# or the next merge back re-introduces the stale copy.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
uses: Start9Labs/start-technologies/.github/workflows/syncNext.yml@master
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Tag and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
paths-ignore: ['*.md']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag:
|
||||||
|
uses: Start9Labs/start-technologies/.github/workflows/tagAndRelease.yml@master
|
||||||
|
with:
|
||||||
|
REFERENCE_REGISTRY: ${{ vars.REFERENCE_REGISTRY }}
|
||||||
|
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
|
||||||
|
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
|
||||||
|
secrets:
|
||||||
|
DEV_KEY: ${{ secrets.DEV_KEY }}
|
||||||
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
*.s9pk
|
||||||
|
startos/*.js
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
docker-images
|
||||||
|
javascript
|
||||||
|
ncc-cache
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "station-master"]
|
||||||
|
path = station-master
|
||||||
|
url = https://draco.local:53871/Jesse.Markowitz/station-master.git
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This is a StartOS service-package repository — it builds a `.s9pk` for StartOS.
|
||||||
|
|
||||||
|
Develop it inside a StartOS packaging workspace created by `start-cli s9pk init-workspace`,
|
||||||
|
which provides the packaging guide and agent context one level up. If you're reading this in a
|
||||||
|
bare clone with no workspace, the full guide is at <https://docs.start9.com/packaging>.
|
||||||
|
|
||||||
|
**Start every task at the recipe index** — `../start-technologies/projects/start-sdk/docs/src/recipes.md`
|
||||||
|
(or <https://docs.start9.com/packaging/recipes.html>). It maps an intent ("prompt the user to create
|
||||||
|
admin credentials", "expose a web UI") to the constructs, the reference pages, and a named production
|
||||||
|
package to copy. Find the recipe before you read this package's neighbours: a package you reach by
|
||||||
|
grepping may be non-conformant, and the recipe outranks it.
|
||||||
|
|
||||||
|
Freshly scaffolded? Work the
|
||||||
|
[New Package Checklist](../start-technologies/projects/start-sdk/docs/src/new-package-checklist.md)
|
||||||
|
(or <https://docs.start9.com/packaging/new-package-checklist.html>) from top to bottom. It is a
|
||||||
|
guide page, not a file in this repo — read it, don't copy it in.
|
||||||
|
|
||||||
|
Keep `README.md` (technical reference for an AI support or administering agent) and
|
||||||
|
`instructions.md` (end-user docs) in sync with your changes.
|
||||||
|
|
||||||
|
**Bugs and feature requests are issues on this repo** (self-hosted Gitea, not GitHub) — file
|
||||||
|
them as you find them. Don't record work in the repo instead: no `TODO.md`, no `NOTES.md`, no
|
||||||
|
`PLAN.md`. What you verified, tried, and decided belongs in the commit message and the PR body.
|
||||||
|
|
||||||
|
## This repo
|
||||||
|
|
||||||
|
- **Station Master is a git submodule (`station-master/`), not vendored source.** It tracks
|
||||||
|
[Jesse's own game repo](https://draco.local:53871/Jesse.Markowitz/station-master), self-hosted,
|
||||||
|
pinned to a tag. See `UPDATING.md` for how the pin is bumped — the `Dockerfile`, `main.ts`, and
|
||||||
|
the manifest should never need to change for an ordinary version bump.
|
||||||
|
- **Built from source, not a published image.** The `Dockerfile` is entirely this repo's own:
|
||||||
|
a `node:*-slim` builder stage runs the submodule's `npm run build:web` to produce the static
|
||||||
|
client, then a runtime stage copies only `package.json`, `src/`, and the built `dist/` — no
|
||||||
|
`node_modules`, since `src/server/index.ts` has zero runtime dependencies (see the submodule's
|
||||||
|
own `TODO.md` for why) and Node's native TypeScript type-stripping means it needs no compile
|
||||||
|
step either.
|
||||||
|
- **The join secret (D14 in the game's `docs/architecture/multiplayer.md`) is this package's one
|
||||||
|
piece of state beyond the game data itself.** `startos/fileModels/store.json.ts` holds it,
|
||||||
|
seeded on install (`startos/init/generateJoinSecret.ts`) since the server refuses to start
|
||||||
|
without one, and rotated by the **Get Join Secret** action
|
||||||
|
(`startos/actions/getJoinSecret.ts`) — the same action handles first retrieval and rotation, per
|
||||||
|
`recipe-admin-credentials.md`.
|
||||||
|
- **One volume, one interface, no dependencies.** Everything the server persists —
|
||||||
|
`store.json` and every game's own save data — lives on the `data` volume; the single `ui`
|
||||||
|
interface carries the browser client, the lobby/intent HTTP API, and the SSE game stream from
|
||||||
|
one port, which is what keeps players reachable from different addresses in the same game
|
||||||
|
(`docs/architecture/deployment.md` §3 in the game repo).
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Station Master's multiplayer server, built from source — there is no published image to wrap.
|
||||||
|
# Source is the `station-master` git submodule, pinned to a tag; see UPDATING.md for how that pin
|
||||||
|
# is bumped. This Dockerfile never needs to change for a version bump, only the submodule ref.
|
||||||
|
|
||||||
|
# --- builder: produces the static browser client (dist/) ---
|
||||||
|
# The server itself has zero runtime dependencies (station-master/TODO.md), but `npm run
|
||||||
|
# build:web` shells out to `tsc`, which lives in devDependencies — hence the separate stage.
|
||||||
|
FROM node:22.23.1-slim AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
COPY station-master/package.json station-master/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY station-master/ ./
|
||||||
|
RUN npm run build:web
|
||||||
|
|
||||||
|
# --- runtime: the server, no node_modules needed at all ---
|
||||||
|
FROM node:22.23.1-slim
|
||||||
|
WORKDIR /app
|
||||||
|
# Node >=22.18 strips TypeScript types natively (station-master/package.json's engines field),
|
||||||
|
# so the server runs straight from source with no build/compile step.
|
||||||
|
COPY station-master/package.json ./
|
||||||
|
COPY station-master/src/ ./src/
|
||||||
|
COPY --from=builder /build/dist/ ./dist/
|
||||||
|
CMD ["node", "src/server/index.ts"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright (c) 2026 Jesse Markowitz
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary. No license is granted to copy, modify,
|
||||||
|
distribute, or use this software, in whole or in part, without the express
|
||||||
|
prior written permission of the copyright holder.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ARCHES := x86 arm
|
||||||
|
# overrides to s9pk.mk must precede the include statement
|
||||||
|
include node_modules/@start9labs/start-sdk/s9pk.mk
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="icon.svg" alt="Station Master Logo" width="21%">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Station Master on StartOS
|
||||||
|
|
||||||
|
> Everything not listed in this document should behave the same as upstream
|
||||||
|
> Station Master. If a feature, setting, or behavior is not mentioned here,
|
||||||
|
> the upstream documentation is accurate and fully applicable — see the
|
||||||
|
> Documentation section of `instructions.md` for links.
|
||||||
|
|
||||||
|
Station Master is a railroad operations board game with an authoritative multiplayer server;
|
||||||
|
solitaire needs no server and is not what this package is for. This package runs that server —
|
||||||
|
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Image and Container Runtime](#image-and-container-runtime)
|
||||||
|
- [Volume and Data Layout](#volume-and-data-layout)
|
||||||
|
- [File Models](#file-models)
|
||||||
|
- [Dependencies](#dependencies)
|
||||||
|
- [Network Access and Interfaces](#network-access-and-interfaces)
|
||||||
|
- [Installation and First-Run Flow](#installation-and-first-run-flow)
|
||||||
|
- [Actions](#actions)
|
||||||
|
- [Tasks](#tasks)
|
||||||
|
- [Health Checks](#health-checks)
|
||||||
|
- [Backups and Restore](#backups-and-restore)
|
||||||
|
- [Limitations and Differences](#limitations-and-differences)
|
||||||
|
- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image and Container Runtime
|
||||||
|
|
||||||
|
Built from source with a custom `Dockerfile` — there is no published Station Master image.
|
||||||
|
|
||||||
|
| What to Document | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies |
|
||||||
|
| Architectures | x86_64, aarch64 |
|
||||||
|
| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations |
|
||||||
|
|
||||||
|
One subcontainer: `station-master-sub`, running the single daemon `server`.
|
||||||
|
|
||||||
|
## Volume and Data Layout
|
||||||
|
|
||||||
|
One volume, `data`, mounted at `/data` (`DATA_DIR`).
|
||||||
|
|
||||||
|
| What to Document | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Volume names | `data` |
|
||||||
|
| Mount points | `/data` |
|
||||||
|
| StartOS files | `store.json` — holds the join secret (see File Models) |
|
||||||
|
| Database | None — flat files. Each game is `games/<gameId>/game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt }`) plus `turn-timings.json`, an `index.json` naming every game, and lobby state for games not yet started |
|
||||||
|
|
||||||
|
## File Models
|
||||||
|
|
||||||
|
One StartOS-managed file, `store.json`, on the `data` volume.
|
||||||
|
|
||||||
|
- **`store.json`** — JSON, holds `joinSecret` (a string). Seeded on install with a random
|
||||||
|
24-character value (`init/generateJoinSecret.ts`) — the daemon will not start without one, so
|
||||||
|
it is never left unset. Rewritten only by the **Get Join Secret** action, which generates a new
|
||||||
|
value on every run; the value is otherwise never touched, so a hand edit to it survives until
|
||||||
|
the action is next run. `main.ts` reads it reactively, so a rewrite restarts the daemon with the
|
||||||
|
new value automatically.
|
||||||
|
|
||||||
|
Everything else under `/data` — `games/`, `index.json`, lobby and session state — is the
|
||||||
|
application's own persistence, written directly by the server process, not by a StartOS file
|
||||||
|
model.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Network Access and Interfaces
|
||||||
|
|
||||||
|
One interface, `ui`, on the single port the daemon listens on. It serves the browser client, the
|
||||||
|
lobby and intent HTTP API, and the per-seat SSE game stream — all from the same origin, which is
|
||||||
|
what lets a player reach the server from a LAN address and another player reach it from a
|
||||||
|
different one in the same game.
|
||||||
|
|
||||||
|
## Installation and First-Run Flow
|
||||||
|
|
||||||
|
No setup wizard. On install, a join secret is generated and stored immediately (see File Models),
|
||||||
|
and a critical task is raised pointing at **Get Join Secret** — the service starts and is usable
|
||||||
|
the moment the daemon is healthy, but a player cannot create or join a game until the join secret
|
||||||
|
has been retrieved and shared with them.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
- **Get Join Secret** (`get-join-secret`) — run this any time you want to read the current join
|
||||||
|
secret, or to invalidate it and issue a new one. Every run **generates a fresh secret,
|
||||||
|
overwrites the stored one, and restarts the daemon** with it — there is no read-only mode.
|
||||||
|
Rotating does not disconnect players already seated in a running game (D14's join secret gates
|
||||||
|
the lobby door, not an in-progress session); it only invalidates the old value for anyone who
|
||||||
|
has not yet joined or created a game. Completes in a few seconds, safe to repeat.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- **Get the join secret to share with players** — raised on install, severity `critical`. Points
|
||||||
|
at the **Get Join Secret** action. Clears the moment that action is run for the first time; it
|
||||||
|
does not return afterward (running it again to rotate does not re-raise it).
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
- **Multiplayer Server** — `checkPortListening` against the daemon's port. "Not ready" means the
|
||||||
|
process has not yet bound its port; on a normal start this clears within a second or two; on a
|
||||||
|
cold container start it briefly reflects the daemon starting up.
|
||||||
|
|
||||||
|
## Backups and Restore
|
||||||
|
|
||||||
|
Strategy: the entire `data` volume is backed up wholesale (`ofVolumes`) — every file copied
|
||||||
|
exactly as it sits on disk, nothing dumped and replayed.
|
||||||
|
|
||||||
|
That includes every game's full intent history, so a restore can resume any in-progress game
|
||||||
|
exactly where it left off, and the join secret in `store.json`, so previously shared join links
|
||||||
|
keep working after a restore. Nothing needs to be rebuilt or re-entered after a restore; the
|
||||||
|
server resumes every saved game on boot the same way it does after an ordinary restart.
|
||||||
|
|
||||||
|
## Limitations and Differences
|
||||||
|
|
||||||
|
1. **No compile step.** The image runs the server directly from TypeScript source using Node's
|
||||||
|
native type stripping, rather than building a `dist/` for the server the way the browser
|
||||||
|
client is built. This is upstream's own deployment shape (`docs/architecture/deployment.md` in
|
||||||
|
the game repo), not something this package changed.
|
||||||
|
2. **The join secret has no per-player identity.** It is a single server-wide value (D14) that
|
||||||
|
gates who may create or join a game — it is not a username or password, and StartOS has no
|
||||||
|
visibility into who a player is once they've joined.
|
||||||
|
3. **No accounts, and one game at a time per person is expected but not enforced** — a deliberate
|
||||||
|
upstream design decision (`docs/architecture/multiplayer.md` §11 D13), not a StartOS-specific
|
||||||
|
limitation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference for AI Consumers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
package_id: station-master
|
||||||
|
image: built from source (Dockerfile)
|
||||||
|
architectures: [x86_64, aarch64]
|
||||||
|
subcontainers: [station-master-sub]
|
||||||
|
volumes:
|
||||||
|
data: /data
|
||||||
|
file_models:
|
||||||
|
- store.json
|
||||||
|
startos_managed_env_vars:
|
||||||
|
- DATA_DIR
|
||||||
|
- JOIN_SECRET
|
||||||
|
dependencies: none
|
||||||
|
interfaces:
|
||||||
|
ui: { type: ui, port: 8081 }
|
||||||
|
actions:
|
||||||
|
- get-join-secret
|
||||||
|
tasks:
|
||||||
|
- { action: get-join-secret, severity: critical }
|
||||||
|
health_checks:
|
||||||
|
- Multiplayer Server
|
||||||
|
```
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Updating the upstream version
|
||||||
|
|
||||||
|
Station Master is built from a git submodule at `station-master/` (tracking
|
||||||
|
[`Jesse.Markowitz/station-master`](https://draco.local:53871/Jesse.Markowitz/station-master), a
|
||||||
|
self-hosted Gitea repo — this is Jesse's own game, not a third-party upstream, but it is packaged
|
||||||
|
the same way: `Dockerfile` builds whatever commit the submodule points at, with no pinned
|
||||||
|
`dockerTag` in the manifest. The submodule pin **is** the version.
|
||||||
|
|
||||||
|
Station Master tags its releases (`v0.5.1`, …) at the commit that bumped `package.json`. Nothing
|
||||||
|
else about this package should need to change for a bump — not the `Dockerfile`, not `main.ts`,
|
||||||
|
not the manifest — only the submodule pin and this package's own version (below).
|
||||||
|
|
||||||
|
## Determining the upstream version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd station-master && git fetch --tags && git tag -l --sort=-v:refname | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
The current pin is the submodule's checked-out commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule status station-master
|
||||||
|
```
|
||||||
|
|
||||||
|
## Applying the bump
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd station-master && git fetch --tags && git checkout v<new version>
|
||||||
|
cd .. && git add station-master
|
||||||
|
```
|
||||||
|
|
||||||
|
Then bump this package's own version in `startos/versions/current.ts` (see
|
||||||
|
[Versions](../start-technologies/projects/start-sdk/docs/src/versions.md)) with release notes
|
||||||
|
naming the Station Master version now bundled — e.g. "Bundles Station Master v0.5.2." — and
|
||||||
|
rebuild (`make`).
|
||||||
|
|
||||||
|
If the jump carries a multiplayer protocol or persistence-format change (check the game repo's
|
||||||
|
`CHANGELOG.md` and `docs/architecture/multiplayer.md`), read it before bumping: a running
|
||||||
|
server's saved games are stamped with `engineVersion` and refuse to resume under a mismatched
|
||||||
|
one (`src/server/index.ts` in the game repo) — nothing in this package works around that, so an
|
||||||
|
in-progress game must finish (or be accepted as lost) before an incompatible bump.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5"/>
|
||||||
|
<stop offset="100%" stop-color="#7C3AED"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
|
||||||
|
<path d="M256 120c-88.4 0-160 58.1-160 129.8 0 41.4 23.6 78.4 60.5 103.2l-12.9 51.5a8 8 0 0 0 11.6 8.8l62.6-36.1c12.3 2.5 25.2 3.8 38.2 3.8 88.4 0 160-58.1 160-129.8S344.4 120 256 120z" fill="white" opacity="0.95"/>
|
||||||
|
<text x="256" y="270" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-weight="bold" font-size="64" fill="#4F46E5">Hello!</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
@@ -0,0 +1,38 @@
|
|||||||
|
# Station Master
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Rules and design docs](https://draco.local:53871/Jesse.Markowitz/station-master/src/branch/main/docs)
|
||||||
|
— the rulebook implications, the multiplayer design, and the game's architecture.
|
||||||
|
|
||||||
|
## What you get on StartOS
|
||||||
|
|
||||||
|
This service runs the multiplayer server: a lobby, and a table for 2 to 4 players to play a
|
||||||
|
competitive or cooperative game together from their own browsers. Everything a player needs — the
|
||||||
|
game itself, the lobby, and the connection back to this server — is served from the single web
|
||||||
|
interface below; there is nothing else to install or configure on a player's side.
|
||||||
|
|
||||||
|
## Getting set up
|
||||||
|
|
||||||
|
1. Open this service's **Multiplayer Table** interface.
|
||||||
|
2. Run the **Get Join Secret** action from this service's Actions menu, and copy the value it
|
||||||
|
returns.
|
||||||
|
3. Share that secret, and the address you opened the interface at, with everyone you want to
|
||||||
|
play with. Anyone holding the secret can create a new game or join one that hasn't started yet.
|
||||||
|
4. In the browser, click **Multiplayer**, then **Create Game** (or **Join Game** with a game code
|
||||||
|
someone else created), and enter the join secret when asked.
|
||||||
|
|
||||||
|
## Using Station Master
|
||||||
|
|
||||||
|
### Multiplayer Table
|
||||||
|
|
||||||
|
Create or join a game, seat players (including filling empty seats with a bot), and start. Once a
|
||||||
|
game is underway, this same interface is where every player plays their turns and watches the
|
||||||
|
table — there is nothing else to open.
|
||||||
|
|
||||||
|
### Get Join Secret
|
||||||
|
|
||||||
|
Run this whenever you need the current join secret again, or want to shut out anyone still
|
||||||
|
holding an old one. Every run replaces the secret and immediately puts the new one into effect —
|
||||||
|
players already seated in a game are unaffected, but the old secret stops working for creating or
|
||||||
|
joining games from that point on.
|
||||||
Generated
+1670
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "station-master-startos",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
|
||||||
|
"prettier": "prettier --write startos",
|
||||||
|
"check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@start9labs/start-sdk": "2.0.9"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@start9labs/start-sdk": "$@start9labs/start-sdk"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.0",
|
||||||
|
"@vercel/ncc": "^0.38.4",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { utils } from '@start9labs/start-sdk'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { storeJson } from '../fileModels/store.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mints a fresh join secret on every run and returns it — first-set and rotation are the same
|
||||||
|
* action (recipe-admin-credentials.md). Writing a new value restarts the daemon with it: main.ts
|
||||||
|
* reads `joinSecret` reactively via `.const(effects)`, so a rotation here takes effect
|
||||||
|
* immediately rather than needing a manual restart.
|
||||||
|
*
|
||||||
|
* Rotating invalidates the old secret for anyone who hasn't joined yet, but never removes a
|
||||||
|
* player already seated in a game — D14's secret gates the lobby door only (join-secret ≠
|
||||||
|
* session token, per `lobby-and-sessions.md` §1).
|
||||||
|
*/
|
||||||
|
export const getJoinSecret = sdk.Action.withoutInput(
|
||||||
|
'get-join-secret',
|
||||||
|
|
||||||
|
async () => ({
|
||||||
|
name: i18n('Get Join Secret'),
|
||||||
|
description: i18n('Retrieve or rotate the secret players need to create or join a game'),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
async ({ effects }) => {
|
||||||
|
const joinSecret = utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 24 })
|
||||||
|
await storeJson.merge(effects, { joinSecret })
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: 'Join Secret',
|
||||||
|
message:
|
||||||
|
'Share this with anyone you want to be able to create or join a game on this server — ' +
|
||||||
|
"it doesn't identify a person or a seat, just who's allowed at the lobby door. Running " +
|
||||||
|
'this action again generates a new one and restarts the server with it; anyone already ' +
|
||||||
|
'seated in a game keeps playing, but the old secret stops working for new games and joins.',
|
||||||
|
result: {
|
||||||
|
type: 'single',
|
||||||
|
name: 'Join Secret',
|
||||||
|
description: null,
|
||||||
|
value: joinSecret,
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { getJoinSecret } from './getJoinSecret'
|
||||||
|
|
||||||
|
export const actions = sdk.Actions.of().addAction(getJoinSecret)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { sdk } from './sdk'
|
||||||
|
|
||||||
|
// Everything worth keeping is on this one volume: every game's { engineVersion, seed, config,
|
||||||
|
// history } and turn timings (src/server/persistence.ts in the game repo), plus this package's
|
||||||
|
// own store.json (the join secret). No database, no second volume — see deployment.md §1.
|
||||||
|
export const { createBackup, restoreInit } = sdk.setupBackups(async ({ effects }) =>
|
||||||
|
sdk.Backups.ofVolumes('data'),
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { sdk } from './sdk'
|
||||||
|
|
||||||
|
export const setDependencies = sdk.setupDependencies(
|
||||||
|
async ({ effects }) => ({}),
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FileHelper, z } from '@start9labs/start-sdk'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `joinSecret` is D14's server-wide secret (`docs/architecture/multiplayer.md` in the game repo):
|
||||||
|
* whoever holds it may create a game, and may join any created game that has not started. The
|
||||||
|
* server refuses to start without one (`src/server/index.ts`), so `joinSecret` is seeded on
|
||||||
|
* install (`init/generateJoinSecret.ts`) and never left unset.
|
||||||
|
*/
|
||||||
|
const shape = z.object({
|
||||||
|
joinSecret: z.string().optional().catch(undefined),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const storeJson = FileHelper.json({ base: sdk.volumes.data, subpath: 'store.json' }, shape)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export const DEFAULT_LANG = 'en_US'
|
||||||
|
|
||||||
|
const dict = {
|
||||||
|
// main.ts
|
||||||
|
'Starting Station Master!': 0,
|
||||||
|
'Multiplayer Server': 1,
|
||||||
|
'The multiplayer server is ready': 2,
|
||||||
|
'The multiplayer server is not ready': 3,
|
||||||
|
// interfaces.ts
|
||||||
|
'Multiplayer Table': 4,
|
||||||
|
'Create or join a game, and play in the browser': 5,
|
||||||
|
// actions/getJoinSecret.ts
|
||||||
|
'Get Join Secret': 6,
|
||||||
|
'Retrieve or rotate the secret players need to create or join a game': 7,
|
||||||
|
// init/generateJoinSecret.ts
|
||||||
|
'Get the join secret to share with players': 8,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plumbing. DO NOT EDIT.
|
||||||
|
*/
|
||||||
|
export type I18nKey = keyof typeof dict
|
||||||
|
export type LangDict = Record<(typeof dict)[I18nKey], string>
|
||||||
|
export default dict
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { LangDict } from './default'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
es_ES: {
|
||||||
|
0: '¡Iniciando Station Master!',
|
||||||
|
1: 'Servidor multijugador',
|
||||||
|
2: 'El servidor multijugador está listo',
|
||||||
|
3: 'El servidor multijugador no está listo',
|
||||||
|
4: 'Mesa multijugador',
|
||||||
|
5: 'Crea o únete a una partida, y juega en el navegador',
|
||||||
|
6: 'Obtener el secreto de acceso',
|
||||||
|
7: 'Recuperar o rotar el secreto que los jugadores necesitan para crear o unirse a una partida',
|
||||||
|
8: 'Obtén el secreto de acceso para compartirlo con los jugadores',
|
||||||
|
},
|
||||||
|
de_DE: {
|
||||||
|
0: 'Starte Station Master!',
|
||||||
|
1: 'Mehrspieler-Server',
|
||||||
|
2: 'Der Mehrspieler-Server ist bereit',
|
||||||
|
3: 'Der Mehrspieler-Server ist nicht bereit',
|
||||||
|
4: 'Mehrspielertisch',
|
||||||
|
5: 'Spiel erstellen oder beitreten und im Browser spielen',
|
||||||
|
6: 'Beitrittsgeheimnis abrufen',
|
||||||
|
7: 'Das Geheimnis abrufen oder erneuern, das Spieler zum Erstellen oder Beitreten einer Partie benötigen',
|
||||||
|
8: 'Hole das Beitrittsgeheimnis, um es mit Spielern zu teilen',
|
||||||
|
},
|
||||||
|
pl_PL: {
|
||||||
|
0: 'Uruchamianie Station Master!',
|
||||||
|
1: 'Serwer wieloosobowy',
|
||||||
|
2: 'Serwer wieloosobowy jest gotowy',
|
||||||
|
3: 'Serwer wieloosobowy nie jest gotowy',
|
||||||
|
4: 'Stół wieloosobowy',
|
||||||
|
5: 'Utwórz lub dołącz do gry i graj w przeglądarce',
|
||||||
|
6: 'Pobierz sekret dołączania',
|
||||||
|
7: 'Pobierz lub wymień sekret potrzebny graczom do tworzenia gier i dołączania do nich',
|
||||||
|
8: 'Pobierz sekret dołączania, aby udostępnić go graczom',
|
||||||
|
},
|
||||||
|
fr_FR: {
|
||||||
|
0: 'Démarrage de Station Master !',
|
||||||
|
1: 'Serveur multijoueur',
|
||||||
|
2: 'Le serveur multijoueur est prêt',
|
||||||
|
3: "Le serveur multijoueur n'est pas prêt",
|
||||||
|
4: 'Table multijoueur',
|
||||||
|
5: 'Créez ou rejoignez une partie, et jouez dans le navigateur',
|
||||||
|
6: 'Obtenir le secret de connexion',
|
||||||
|
7: 'Récupérer ou renouveler le secret dont les joueurs ont besoin pour créer une partie ou la rejoindre',
|
||||||
|
8: 'Obtenez le secret de connexion à partager avec les joueurs',
|
||||||
|
},
|
||||||
|
} satisfies Record<string, LangDict>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Plumbing. DO NOT EDIT this file.
|
||||||
|
*/
|
||||||
|
import { setupI18n } from '@start9labs/start-sdk'
|
||||||
|
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
|
||||||
|
import translations from './dictionaries/translations'
|
||||||
|
|
||||||
|
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Plumbing. DO NOT EDIT.
|
||||||
|
*/
|
||||||
|
export { createBackup } from './backups'
|
||||||
|
export { main } from './main'
|
||||||
|
export { init, uninit } from './init'
|
||||||
|
export { actions } from './actions'
|
||||||
|
import { buildManifest } from '@start9labs/start-sdk'
|
||||||
|
import { manifest as sdkManifest } from './manifest'
|
||||||
|
import { versionGraph } from './versions'
|
||||||
|
export const manifest = buildManifest(versionGraph, sdkManifest)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { utils } from '@start9labs/start-sdk'
|
||||||
|
import { getJoinSecret } from '../actions/getJoinSecret'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { storeJson } from '../fileModels/store.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server exits immediately if `JOIN_SECRET` is unset (`src/server/index.ts` in the game
|
||||||
|
* repo), so a value has to exist before main.ts's daemon ever starts — seeded here rather than
|
||||||
|
* left for the user to generate via the action first.
|
||||||
|
*/
|
||||||
|
export const seedJoinSecret = sdk.setupOnInit(async (effects, kind) => {
|
||||||
|
if (kind === 'install') {
|
||||||
|
await storeJson.merge(effects, {
|
||||||
|
joinSecret: utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 24 }),
|
||||||
|
})
|
||||||
|
await sdk.action.createOwnTask(effects, getJoinSecret, 'critical', {
|
||||||
|
reason: i18n('Get the join secret to share with players'),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 'update' and 'restore' — repairs a corrupted store.json without touching an existing
|
||||||
|
// joinSecret. A restored volume already carries one (it lives inside the backed-up volume,
|
||||||
|
// see backups.ts), so there is nothing to seed.
|
||||||
|
await storeJson.merge(effects, {})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { setDependencies } from '../dependencies'
|
||||||
|
import { setInterfaces } from '../interfaces'
|
||||||
|
import { versionGraph } from '../versions'
|
||||||
|
import { actions } from '../actions'
|
||||||
|
import { restoreInit } from '../backups'
|
||||||
|
import { seedJoinSecret } from './generateJoinSecret'
|
||||||
|
|
||||||
|
export const init = sdk.setupInit(
|
||||||
|
restoreInit,
|
||||||
|
versionGraph,
|
||||||
|
seedJoinSecret,
|
||||||
|
setInterfaces,
|
||||||
|
setDependencies,
|
||||||
|
actions,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const uninit = sdk.setupUninit(versionGraph)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { i18n } from './i18n'
|
||||||
|
import { sdk } from './sdk'
|
||||||
|
import { uiPort } from './utils'
|
||||||
|
|
||||||
|
// One interface: the server serves the browser client, the lobby/intent HTTP API, and the SSE
|
||||||
|
// game stream all from this single port (deployment.md's same-origin rule in the game repo —
|
||||||
|
// baking in a second origin would break relative URLs). type: 'ui' is a label the client is
|
||||||
|
// meant for a browser; it does not stop the same port from also carrying the API calls that
|
||||||
|
// client makes back to itself.
|
||||||
|
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
|
||||||
|
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
|
||||||
|
const uiMultiOrigin = await uiMulti.bindPort(uiPort, { protocol: 'http' })
|
||||||
|
const ui = sdk.createInterface(effects, {
|
||||||
|
name: i18n('Multiplayer Table'),
|
||||||
|
id: 'ui',
|
||||||
|
description: i18n('Create or join a game, and play in the browser'),
|
||||||
|
type: 'ui',
|
||||||
|
masked: false,
|
||||||
|
schemeOverride: null,
|
||||||
|
username: null,
|
||||||
|
path: '',
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const uiReceipt = await uiMultiOrigin.export([ui])
|
||||||
|
|
||||||
|
return [uiReceipt]
|
||||||
|
})
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { i18n } from './i18n'
|
||||||
|
import { sdk } from './sdk'
|
||||||
|
import { uiPort, dataDir } from './utils'
|
||||||
|
import { storeJson } from './fileModels/store.json'
|
||||||
|
|
||||||
|
export const main = sdk.setupMain(async ({ effects }) => {
|
||||||
|
console.info(i18n('Starting Station Master!'))
|
||||||
|
|
||||||
|
// Reactive, field-scoped: rotating the join secret (getJoinSecret.ts) rewrites store.json,
|
||||||
|
// which re-runs setupMain and restarts the daemon with the new value.
|
||||||
|
const joinSecret = await storeJson.read((s) => s.joinSecret).const(effects)
|
||||||
|
|
||||||
|
return sdk.Daemons.of(effects).addDaemon('server', {
|
||||||
|
subcontainer: sdk.SubContainer.of(
|
||||||
|
effects,
|
||||||
|
{ imageId: 'main' },
|
||||||
|
sdk.Mounts.of().mountVolume({
|
||||||
|
volumeId: 'data',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: dataDir,
|
||||||
|
readonly: false,
|
||||||
|
}),
|
||||||
|
'station-master-sub',
|
||||||
|
),
|
||||||
|
exec: {
|
||||||
|
command: ['node', 'src/server/index.ts'],
|
||||||
|
env: {
|
||||||
|
// PORT/BIND_ADDRESS/DIST_DIR are left at src/server/index.ts's own defaults (8081,
|
||||||
|
// 0.0.0.0, ./dist relative to the Dockerfile's WORKDIR) — nothing here needs to differ
|
||||||
|
// from them, so only what actually must be supplied is set explicitly.
|
||||||
|
DATA_DIR: dataDir,
|
||||||
|
// generateJoinSecret.ts seeds this before main.ts ever runs, so it is never actually
|
||||||
|
// empty — the fallback only avoids threading `string | undefined` through `env`, which
|
||||||
|
// wants `Record<string, string>`.
|
||||||
|
JOIN_SECRET: joinSecret ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Health check, run on each polling interval. `checkPortListening` reports ready once the
|
||||||
|
// daemon binds `uiPort`; the 'ui' interface (interfaces.ts) exposes the same port.
|
||||||
|
ready: {
|
||||||
|
display: i18n('Multiplayer Server'),
|
||||||
|
fn: () =>
|
||||||
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||||
|
successMessage: i18n('The multiplayer server is ready'),
|
||||||
|
errorMessage: i18n('The multiplayer server is not ready'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
requires: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export const short = {
|
||||||
|
en_US: 'Railroad operations board game — solitaire or multiplayer',
|
||||||
|
es_ES: 'Juego de mesa de operaciones ferroviarias: solitario o multijugador',
|
||||||
|
de_DE: 'Eisenbahn-Betriebs-Brettspiel — Solitär oder Mehrspieler',
|
||||||
|
pl_PL: 'Gra planszowa o zarządzaniu koleją — pasjans lub tryb wieloosobowy',
|
||||||
|
fr_FR: 'Jeu de plateau de gestion ferroviaire — solo ou multijoueur',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const long = {
|
||||||
|
en_US:
|
||||||
|
'Station Master is a railroad operations game: switch cars, load and unload freight, and ' +
|
||||||
|
'dispatch trains through a shared Division. This package runs the authoritative multiplayer ' +
|
||||||
|
'server — 2 to 4 players connect from their browsers and play a competitive or cooperative ' +
|
||||||
|
'game together. Solitaire needs no server at all; it runs entirely in the browser from the ' +
|
||||||
|
'static client this server also hosts.',
|
||||||
|
es_ES:
|
||||||
|
'Station Master es un juego de operaciones ferroviarias: maniobra vagones, carga y descarga ' +
|
||||||
|
'mercancías, y despacha trenes por una División compartida. Este paquete ejecuta el servidor ' +
|
||||||
|
'multijugador autoritativo — de 2 a 4 jugadores se conectan desde sus navegadores y juegan una ' +
|
||||||
|
'partida competitiva o cooperativa. El modo solitario no necesita servidor: se ejecuta ' +
|
||||||
|
'enteramente en el navegador desde el mismo cliente estático que este servidor aloja.',
|
||||||
|
de_DE:
|
||||||
|
'Station Master ist ein Eisenbahn-Betriebsspiel: Waggons rangieren, Fracht laden und löschen ' +
|
||||||
|
'und Züge durch eine gemeinsame Division disponieren. Dieses Paket betreibt den ' +
|
||||||
|
'autoritativen Mehrspieler-Server — 2 bis 4 Spieler verbinden sich über den Browser und ' +
|
||||||
|
'spielen gemeinsam kompetitiv oder kooperativ. Solitär benötigt keinen Server; es läuft ' +
|
||||||
|
'vollständig im Browser über denselben statischen Client, den dieser Server ebenfalls hostet.',
|
||||||
|
pl_PL:
|
||||||
|
'Station Master to gra o zarządzaniu koleją: manewruj wagonami, załaduj i rozładuj towar oraz ' +
|
||||||
|
'dysponuj pociągami we wspólnej Dywizji. Ten pakiet uruchamia autorytatywny serwer trybu ' +
|
||||||
|
'wieloosobowego — od 2 do 4 graczy łączy się z przeglądarek i rozgrywa partię rywalizacyjną ' +
|
||||||
|
'lub kooperacyjną. Pasjans nie wymaga serwera — działa w całości w przeglądarce, z tego ' +
|
||||||
|
'samego statycznego klienta, który hostuje ten serwer.',
|
||||||
|
fr_FR:
|
||||||
|
"Station Master est un jeu de gestion ferroviaire : manœuvrez des wagons, chargez et " +
|
||||||
|
"déchargez du fret, et dispatchez des trains à travers une Division partagée. Ce paquet " +
|
||||||
|
"exécute le serveur multijoueur faisant autorité — 2 à 4 joueurs se connectent depuis leur " +
|
||||||
|
"navigateur pour une partie compétitive ou coopérative. Le mode solitaire ne nécessite aucun " +
|
||||||
|
"serveur : il tourne entièrement dans le navigateur, à partir du même client statique que ce " +
|
||||||
|
"serveur héberge.",
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { setupManifest } from '@start9labs/start-sdk'
|
||||||
|
import { long, short } from './i18n'
|
||||||
|
|
||||||
|
export const manifest = setupManifest({
|
||||||
|
id: 'station-master',
|
||||||
|
title: 'Station Master',
|
||||||
|
// Not open source — Jesse's own project, packaged for his own StartOS box.
|
||||||
|
license: 'UNLICENSED',
|
||||||
|
packageRepo: 'https://draco.local:53871/Jesse.Markowitz/station-master-startos',
|
||||||
|
upstreamRepo: 'https://draco.local:53871/Jesse.Markowitz/station-master',
|
||||||
|
// No separate marketing site — the repo IS where there is more to learn.
|
||||||
|
marketingUrl: 'https://draco.local:53871/Jesse.Markowitz/station-master',
|
||||||
|
donationUrl: null,
|
||||||
|
description: { short, long },
|
||||||
|
// Everything the server persists — game.json/index.json per game, turn timings,
|
||||||
|
// and this package's own store.json (the join secret) — lives on one volume.
|
||||||
|
volumes: ['data'],
|
||||||
|
images: {
|
||||||
|
// Built from source, not a published image: `main` here is the arbitrary
|
||||||
|
// image id, matched in main.ts's SubContainer.of({ imageId: 'main' }).
|
||||||
|
// The Dockerfile COPYs the pinned `station-master` git submodule — see
|
||||||
|
// UPDATING.md for how that pin is bumped.
|
||||||
|
main: {
|
||||||
|
source: { dockerBuild: { workdir: '.', dockerfile: './Dockerfile' } },
|
||||||
|
arch: ['x86_64', 'aarch64'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies: {},
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { StartSdk } from '@start9labs/start-sdk'
|
||||||
|
import { manifest } from './manifest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plumbing. DO NOT EDIT.
|
||||||
|
*
|
||||||
|
* The exported "sdk" const is used throughout this package codebase.
|
||||||
|
*/
|
||||||
|
export const sdk = StartSdk.of().withManifest(manifest).build(true)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Station Master's server default (src/server/index.ts's `PORT` fallback) — kept identical here
|
||||||
|
// so the env var below is documentation, not a real override.
|
||||||
|
export const uiPort = 8081
|
||||||
|
|
||||||
|
// Where the volume is mounted inside the subcontainer, and so also `DATA_DIR`'s value. The
|
||||||
|
// server's own per-game persistence (`games/<gameId>/`, `index.json`) and this package's
|
||||||
|
// store.json (the join secret) share this one directory — see README's Volume and Data Layout.
|
||||||
|
export const dataDir = '/data'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
export const current = VersionInfo.of({
|
||||||
|
version: '1.0.0:0',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: 'Initial StartOS package, bundling Station Master v0.5.1.',
|
||||||
|
es_ES: 'Paquete inicial para StartOS, con Station Master v0.5.1.',
|
||||||
|
de_DE: 'Erstes StartOS-Paket, mit Station Master v0.5.1.',
|
||||||
|
pl_PL: 'Pierwszy pakiet dla StartOS, zawiera Station Master v0.5.1.',
|
||||||
|
fr_FR: 'Premier paquet StartOS, avec Station Master v0.5.1.',
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
up: async ({ effects }) => {},
|
||||||
|
down: IMPOSSIBLE,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { VersionGraph } from '@start9labs/start-sdk'
|
||||||
|
import { current } from './current'
|
||||||
|
|
||||||
|
export const versionGraph = VersionGraph.of({
|
||||||
|
current,
|
||||||
|
other: [],
|
||||||
|
})
|
||||||
Submodule
+1
Submodule station-master added at e76bd77099
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "@start9labs/start-sdk/tsconfig.base.json",
|
||||||
|
"include": ["startos/**/*.ts", "node_modules/**/startos"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user