aebee77843
Built the missing piece that connects the ksp-bridge to a real KSP instance via kRPC. This adds a typed service client on top of the existing KRPCClient, plus the SpaceCenter-specific extraction logic that pulls the universe state from a running KSP save. @kerbal-rt/krpc-client - types.ts — runtime representation of kRPC Type descriptors (TypeCode enum, KrpcType interface, decodeKrpcType, typeName) - decoder.ts — kRPC value codec: primitive decode/encode, class refs, enums, collections (LIST/SET/TUPLE/DICTIONARY), system messages (STATUS/SERVICES/STREAM/EVENT/PROCEDURE_CALL). 34 unit tests. - services.ts — ServiceCache built from KRPC.GetServices() response. Lookup by (service, procedure), enum value/name resolution. 12 tests. - service-client.ts — KrpcServices: high-level invoke-by-name client. loadServices() helper to connect + load catalog. 9 integration tests with a mock kRPC server. - schema.ts — added Set/Dictionary/Event/Expression types so the decoder can handle system messages without external .proto files. Also fixed a bug where 'STREAM' was being encoded as 0 due to protobufjs's nested-enum string lookup. ksp-bridge - extract.ts — the actual SpaceCenter calls. ~280 procedure calls per poll for a typical KSP save (UT, bodies, vessels, then per-body and per-vessel class methods in parallel). Build the UniverseSnapshot. - krpc-adapter.ts — rewrote to use KrpcServices (typed) instead of the stub extract function. Supports an optional injected services for testing. - bridge.ts — uses the new ExtractedState type and buildSnapshot. - index.ts — connects to kRPC; falls back to mock mode if no server. - convert.ts — backward-compat shim, re-exports from extract.ts. The ksp-bridge can now talk to a real KSP install. We do NOT need the kRPC mod's .proto files on disk — the server's GetServices() response is the source of truth for type info. Documented the full list of procedures we call, the kRPC value encoding, and the new architecture in ksp/README.md. Tests: 119 total, all green. Typecheck and build clean across all 11 projects. Bonus: fixed an integer-overflow bug in the krpc-client connect() handshake (was passing 'RPC'/'STREAM' strings to protobufjs; its nested-enum string lookup silently encodes as 0, which made the stream handshake send the wrong type. Switched to numeric codes.)
181 lines
7.4 KiB
Markdown
181 lines
7.4 KiB
Markdown
# KSP ↔ kRPC integration
|
|
|
|
The `ksp-bridge` app connects to a running KSP instance via the
|
|
[kRPC mod](https://github.com/krpc/krpc) and pushes state to the
|
|
kerbal-rt API. This README documents what's wired up today and what's
|
|
still TODO.
|
|
|
|
## Quick start
|
|
|
|
### A. Mock mode (no KSP needed)
|
|
|
|
```bash
|
|
cd apps/tools/ksp-bridge
|
|
KERBAL_RT_API_URL=http://localhost:4000 \
|
|
BRIDGE_POLL_MS=500 \
|
|
pnpm start
|
|
```
|
|
|
|
The bridge starts, tries to connect to `127.0.0.1:50000`, fails (no
|
|
kRPC server running), and falls back to MOCK mode: it emits synthetic
|
|
state every poll. This is great for verifying the HTTP pipeline and
|
|
the live-map / hub end-to-end without KSP.
|
|
|
|
### B. With real KSP
|
|
|
|
1. Install KSP 1.12.5 (this is the version kRPC 0.5.x targets).
|
|
2. Install [CKAN](https://github.com/KSP-CKAN/CKAN).
|
|
3. From CKAN, install:
|
|
- `kRPC` (the mod itself, by [djungelorm](https://github.com/djungelorm))
|
|
- Any other mods you want
|
|
4. Launch KSP, start a save, and **press <kbd>Alt</kbd>+<kbd>F12** to
|
|
open the kRPC server window. Make sure the RPC server is on
|
|
`127.0.0.1:50000` and the Stream server is on `127.0.0.1:50001`.
|
|
5. Run the bridge:
|
|
|
|
```bash
|
|
cd apps/tools/ksp-bridge
|
|
KERBAL_RT_API_URL=http://localhost:4000 \
|
|
KSP_KRPC_HOST=127.0.0.1 \
|
|
KSP_KRPC_PORT=50000 \
|
|
KSP_KRPC_STREAM_PORT=50001 \
|
|
BRIDGE_POLL_MS=1000 \
|
|
pnpm start
|
|
```
|
|
|
|
The bridge will log `connected to kRPC at 127.0.0.1:50000 — running
|
|
with real KSP state` and start polling.
|
|
|
|
## Architecture
|
|
|
|
### Two layers
|
|
|
|
1. **`@kerbal-rt/krpc-client`** — the low-level kRPC client.
|
|
- TCP connection (RPC port + stream port)
|
|
- Length-prefixed protobuf framing
|
|
- Connection handshake (`ConnectionRequest`/`ConnectionResponse`)
|
|
- Procedure invocation (`Request`/`Response`)
|
|
- Stream subscription (`AddStream`/`StreamUpdate`)
|
|
- Plus a **typed service client** built on top:
|
|
- Loads the service catalog via `KRPC.GetServices()` on connect
|
|
- Encodes procedure arguments based on the cached type info
|
|
- Decodes return values based on the cached type info
|
|
- Knows about the kRPC value encoding (primitives, classes,
|
|
enums, collections, system messages)
|
|
|
|
2. **`apps/tools/ksp-bridge`** — the actual KSP bridge.
|
|
- `krpc-adapter.ts` — owns the KRPCClient + KrpcServices, exposes
|
|
`connect()` / `readState()` / `disconnect()`
|
|
- `extract.ts` — calls SpaceCenter methods to read the full universe
|
|
state and produces a `UniverseSnapshot`
|
|
- `bridge.ts` — the polling loop, HTTP POST to API, retry / reconnect
|
|
- `index.ts` — entrypoint; falls back to MOCK mode if no kRPC server
|
|
|
|
### No .proto files needed
|
|
|
|
We do **not** need the kRPC mod's `.proto` files on disk. The kRPC
|
|
server provides the full service catalog (procedures, classes, enums,
|
|
exceptions) via `KRPC.GetServices()` on connect, and we cache that into
|
|
a `ServiceCache` for lookups. The value encoding is implemented in
|
|
`packages/krpc-client/src/decoder.ts`.
|
|
|
|
The original plan (Phase 1c) called for loading the `.proto` files
|
|
with `protobufjs.loadSync()`. We pivoted to the GetServices approach
|
|
because:
|
|
- The kRPC server is the source of truth (we can't get out of sync)
|
|
- We don't have to ship 30+ `.proto` files with the bridge
|
|
- The .proto files are mostly for static code generation in other
|
|
languages; for a dynamic client, GetServices is sufficient
|
|
|
|
## What we read from KSP
|
|
|
|
Per poll, the bridge makes ~280 procedure calls (for a stock KSP save
|
|
with 15 bodies and 5 vessels). At `BRIDGE_POLL_MS=1000` that's
|
|
comfortably within what kRPC can handle on loopback. If you need more
|
|
throughput, the obvious optimization is to batch the calls into a
|
|
single `KRPC.Request` with multiple `ProcedureCall` entries (the
|
|
server already supports this; we just don't use it yet).
|
|
|
|
### Top-level
|
|
|
|
| Procedure | Returns | Used for |
|
|
|---|---|---|
|
|
| `SpaceCenter.GetUT()` | `double` | Universal Time (game seconds since epoch) |
|
|
| `SpaceCenter.GetBodies()` | `list<CelestialBody>` | Object ids of all bodies |
|
|
| `SpaceCenter.GetVessels()` | `list<Vessel>` | Object ids of all vessels |
|
|
|
|
### Per CelestialBody (8 calls per body)
|
|
|
|
| Procedure | Returns | Field |
|
|
|---|---|---|
|
|
| `CelestialBody.GetName(self)` | `string` | `name` |
|
|
| `CelestialBody.GetParent(self)` | `CelestialBody` (nullable) | `parentId` |
|
|
| `CelestialBody.GetRadius(self)` | `double` | `radius` (m) |
|
|
| `CelestialBody.GetSphereOfInfluence(self)` | `double` | `sphereOfInfluence` (m) |
|
|
| `CelestialBody.GetGravitationalParameter(self)` | `double` | `μ` (m³/s²) |
|
|
| `CelestialBody.GetRotationPeriod(self)` | `double` | `rotationPeriod` (s) |
|
|
| `CelestialBody.GetAxialTilt(self)` | `double` | `axialTilt` (rad) |
|
|
| `CelestialBody.GetOrbit(self)` | `Orbit` | (then 8 orbit calls) |
|
|
|
|
### Per Orbit (8 calls per orbit)
|
|
|
|
| Procedure | Returns | Field |
|
|
|---|---|---|
|
|
| `Orbit.GetSemiMajorAxis(self)` | `double` | `semiMajorAxis` (m) |
|
|
| `Orbit.GetEccentricity(self)` | `double` | `eccentricity` |
|
|
| `Orbit.GetInclination(self)` | `double` | `inclination` (rad) |
|
|
| `Orbit.GetLongitudeOfAscendingNode(self)` | `double` | `longitudeOfAscendingNode` (rad) |
|
|
| `Orbit.GetArgumentOfPeriapsis(self)` | `double` | `argumentOfPeriapsis` (rad) |
|
|
| `Orbit.GetMeanAnomalyAtEpoch(self)` | `double` | `meanAnomalyAtEpoch` (rad) |
|
|
| `Orbit.GetEpoch(self)` | `double` | `epoch` (s) |
|
|
| `Orbit.GetReferenceBody(self)` | `CelestialBody` (nullable) | (for verification only) |
|
|
|
|
### Per Vessel (5 calls per vessel)
|
|
|
|
| Procedure | Returns | Field |
|
|
|---|---|---|
|
|
| `Vessel.GetName(self)` | `string` | `name` |
|
|
| `Vessel.GetType(self)` | `VesselType` (enum) | `type` (resolved to name) |
|
|
| `Vessel.GetSituation(self)` | `VesselSituation` (enum) | `situation` (raw int code) |
|
|
| `Vessel.GetOrbit(self)` | `Orbit` | (then 8 orbit calls) |
|
|
| `Vessel.GetReferenceBody(self)` | `CelestialBody` | `referenceBodyId` (resolved to name) |
|
|
|
|
## What's NOT in scope yet (deferred work)
|
|
|
|
- **Streams** — we don't subscribe to kRPC streams yet. We're
|
|
polling. For a real-time UI, switching to streams (or hybrid
|
|
poll+stream) would reduce latency and load. kRPC has `AddStream`
|
|
and the stream port is already wired in.
|
|
- **Batched calls** — every kRPC call is its own request. We could
|
|
batch multiple `ProcedureCall` entries in a single `Request` for
|
|
~10x throughput.
|
|
- **Ground stations** — kRPC doesn't expose ground stations natively.
|
|
The ksp-bridge accepts them as static config or via mod integration.
|
|
- **Comm nets / signal strength** — needs the `CommNet` API. kRPC
|
|
has it, but we don't use it yet.
|
|
- **Crew / science** — not in the ksp-bridge scope right now.
|
|
- **Maneuver nodes** — easy to add (`Vessel.GetManeuverNode()` etc.)
|
|
but not needed for the live map / mission clock.
|
|
|
|
## Troubleshooting
|
|
|
|
### `no kRPC server at 127.0.0.1:50000`
|
|
|
|
Either KSP isn't running, or the kRPC server isn't enabled. Open the
|
|
kRPC window in-game (<kbd>Alt</kbd>+<kbd>F12</kbd>) and make sure
|
|
"Start server" is checked.
|
|
|
|
### `procedure not found in service cache`
|
|
|
|
The kRPC server returned a procedure that we don't know about. This
|
|
usually means the kRPC version is older or newer than we expect
|
|
(we target 0.5.x). The ServiceCache will log the procedures it knows
|
|
about; cross-check with the in-game kRPC window.
|
|
|
|
### `wrong number of arguments`
|
|
|
|
The procedure signature in the cache doesn't match what we're sending.
|
|
This can happen if the kRPC version has a different parameter order
|
|
or count for a procedure we use. The fix is in
|
|
`apps/tools/ksp-bridge/src/extract.ts` — adjust the call site.
|