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.)
217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
bodyToOurs,
|
|
vesselToOurs,
|
|
buildSnapshot,
|
|
type KRPCBody,
|
|
type ExtractedState,
|
|
} from '../src/extract.js';
|
|
|
|
// bodyToOurs is also re-exported from convert.ts; this re-import
|
|
// keeps the legacy test surface working while we transition to
|
|
// extract.ts as the single source of truth.
|
|
import { krpcSituationToOurs } from '../src/convert.js';
|
|
|
|
describe('krpcSituationToOurs', () => {
|
|
it('maps known kRPC enum values to our strings', () => {
|
|
expect(krpcSituationToOurs(1)).toBe('ORBITING');
|
|
expect(krpcSituationToOurs(2)).toBe('ESCAPING');
|
|
expect(krpcSituationToOurs(3)).toBe('LANDED');
|
|
expect(krpcSituationToOurs(4)).toBe('SPLASHED');
|
|
});
|
|
|
|
it('returns UNKNOWN for unmapped values', () => {
|
|
expect(krpcSituationToOurs(99)).toBe('UNKNOWN');
|
|
expect(krpcSituationToOurs(-1)).toBe('UNKNOWN');
|
|
});
|
|
});
|
|
|
|
describe('bodyToOurs', () => {
|
|
it('normalizes the body name to a lowercase id', () => {
|
|
const ksp: KRPCBody = {
|
|
name: 'Kerbin',
|
|
kind: 'planet',
|
|
parentId: 'Kerbol',
|
|
radius: 600_000,
|
|
sphereOfInfluence: 84_159_286,
|
|
gravitationalParameter: 3.5316e12,
|
|
rotationPeriod: 21_600,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 13_599_840_256,
|
|
eccentricity: 0.05,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
const ours = bodyToOurs(ksp);
|
|
expect(ours.id).toBe('kerbin');
|
|
expect(ours.parentId).toBe('kerbol');
|
|
expect(ours.name).toBe('Kerbin');
|
|
expect(ours.kind).toBe('planet');
|
|
expect(ours.radius).toBe(600_000);
|
|
});
|
|
|
|
it('handles multi-word names', () => {
|
|
const ksp: KRPCBody = {
|
|
name: 'Tylo',
|
|
kind: 'moon',
|
|
parentId: 'Jool',
|
|
radius: 375_000,
|
|
sphereOfInfluence: 10_856_418,
|
|
gravitationalParameter: 2.122e11,
|
|
rotationPeriod: 84_600,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 68_500_000,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
const ours = bodyToOurs(ksp);
|
|
expect(ours.id).toBe('tylo');
|
|
});
|
|
|
|
it('preserves null parentId for the star', () => {
|
|
const ksp: KRPCBody = {
|
|
name: 'Kerbol',
|
|
kind: 'star',
|
|
parentId: null,
|
|
radius: 261_600_000,
|
|
sphereOfInfluence: 1e30,
|
|
gravitationalParameter: 1.172332794e18,
|
|
rotationPeriod: 432_000,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 0,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
const ours = bodyToOurs(ksp);
|
|
expect(ours.parentId).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('vesselToOurs', () => {
|
|
it('maps situation enum and assigns ACTIVE status', () => {
|
|
const ours = vesselToOurs({
|
|
id: 'v-1',
|
|
name: 'Probe',
|
|
type: 'Probe',
|
|
owner: 'KASA',
|
|
situation: 'ORBITING',
|
|
orbit: {
|
|
semiMajorAxis: 7e6,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
referenceBodyId: 'kerbin',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
});
|
|
expect(ours.situation).toBe('ORBITING');
|
|
expect(ours.status).toBe('ACTIVE');
|
|
expect(ours.retiredAt).toBeNull();
|
|
expect(ours.owner).toBe('KASA');
|
|
});
|
|
});
|
|
|
|
describe('buildSnapshot', () => {
|
|
it('produces a valid UniverseSnapshot from a KRPCState', () => {
|
|
const state: ExtractedState = {
|
|
ut: 100,
|
|
bodies: [
|
|
{
|
|
name: 'Kerbol',
|
|
kind: 'star',
|
|
parentId: null,
|
|
radius: 1,
|
|
sphereOfInfluence: 1e30,
|
|
gravitationalParameter: 1,
|
|
rotationPeriod: 1,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 0,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
},
|
|
{
|
|
name: 'Kerbin',
|
|
kind: 'planet',
|
|
parentId: 'Kerbol',
|
|
radius: 600_000,
|
|
sphereOfInfluence: 84_159_286,
|
|
gravitationalParameter: 3.5316e12,
|
|
rotationPeriod: 21_600,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 13_599_840_256,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
},
|
|
],
|
|
vessels: [
|
|
{
|
|
id: 'v1',
|
|
name: 'Probe',
|
|
type: 'Probe',
|
|
owner: 'KASA',
|
|
situation: 1, // ORBITING
|
|
orbit: {
|
|
semiMajorAxis: 7e6,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
referenceBodyId: 'Kerbin',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
groundStations: [
|
|
{ id: 'montana', name: 'Montana', bodyId: 'Kerbin', lat: 47, lon: -110, alt: 0 },
|
|
],
|
|
};
|
|
|
|
const snap = buildSnapshot(state, '2026-01-01T00:00:00Z');
|
|
expect(snap.ut).toBe(100);
|
|
expect(snap.capturedAt).toBe('2026-01-01T00:00:00Z');
|
|
expect(snap.bodies).toHaveLength(2);
|
|
expect(snap.bodies[0]!.id).toBe('kerbol');
|
|
expect(snap.bodies[0]!.parentId).toBeNull();
|
|
expect(snap.bodies[1]!.id).toBe('kerbin');
|
|
expect(snap.bodies[1]!.parentId).toBe('kerbol');
|
|
expect(snap.vessels).toHaveLength(1);
|
|
expect(snap.vessels[0]!.situation).toBe('ORBITING');
|
|
expect(snap.vessels[0]!.referenceBodyId).toBe('kerbin');
|
|
expect(snap.groundStations).toHaveLength(1);
|
|
});
|
|
});
|