e9ebbf17d2
The kRPC server (a C# application) exposes C# properties using
.NET accessor naming: a property `UT` on the SpaceCenter service
becomes two procedures named `get_UT` and `set_UT`. A class
property `CelestialBody.Name` becomes `CelestialBody.get_Name`
and `CelestialBody.set_Name`.
We had been calling `SpaceCenter.GetUT()` (PascalCase, no
underscore) and looking up the literal key. That key never
matched, so the cache always returned 'procedure not found' —
even though `get_UT` was sitting right there.
The Python client side-steps this by using snake_case for
everything (`SpaceCenter.ut` -> `SpaceCenter.get_UT`), and
the C# client just uses .NET convention directly. Our
TypeScript/extract code uses the more familiar PascalCase
form, so the cache lookup now does the translation.
The translation is purely a fallback path:
- exact match tried first
- if not found and the name starts with Get/Set, the
.NET-style `get_X`/`set_X` variant is tried
- works for both top-level ("GetUT") and class-prefixed
("CelestialBody.GetName") procedure names
This unblocks the real bridge: `SpaceCenter.GetUT`,
`SpaceCenter.GetActiveVessel`, `SpaceCenter.GetBodies`,
`SpaceCenter.GetVessels`, and all the class methods like
`CelestialBody.GetName`, `Vessel.GetType`, `Orbit.GetApoapsis`
should now resolve to actual kRPC procedures.
249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
/**
|
|
* ServiceCache — a queryable index over a KRPC.GetServices() response.
|
|
*
|
|
* After connecting to kRPC, the canonical first call is `KRPC.GetServices()`,
|
|
* which returns a `KRPC.Services` message describing every service, every
|
|
* class, every enum, and every procedure. We decode that into a lookup
|
|
* table keyed by (service, procedure) so the service client can ask:
|
|
*
|
|
* - "what is the return type of SpaceCenter.GetBodies?"
|
|
* - "what are the param types of SpaceCenter.CelestialBody.GetName?"
|
|
* - "is SpaceCenter.VesselType.Ship == 0?"
|
|
*
|
|
* The cache is built once after connect, then read-only. It is decoupled
|
|
* from the network so it can be unit-tested by feeding in a hand-crafted
|
|
* Services message.
|
|
*/
|
|
import {
|
|
decodeKrpcType,
|
|
type KrpcType,
|
|
type RawKrpcTypeMessage,
|
|
} from './types.js';
|
|
|
|
/** Shape of the KRPC.GetServices() response after protobufjs decoding. */
|
|
export interface RawServicesMessage {
|
|
services: RawServiceMessage[];
|
|
}
|
|
|
|
export interface RawServiceMessage {
|
|
name: string;
|
|
procedures: RawProcedureMessage[];
|
|
classes: { name: string }[];
|
|
enumerations: RawEnumerationMessage[];
|
|
}
|
|
|
|
export interface RawProcedureMessage {
|
|
name: string;
|
|
parameters: RawParameterMessage[];
|
|
returnType: RawKrpcTypeMessage;
|
|
returnIsNullable: boolean;
|
|
}
|
|
|
|
export interface RawParameterMessage {
|
|
name: string;
|
|
type: RawKrpcTypeMessage;
|
|
nullable: boolean;
|
|
}
|
|
|
|
export interface RawEnumerationMessage {
|
|
name: string;
|
|
values: { name: string; value: number }[];
|
|
}
|
|
|
|
export interface ProcedureInfo {
|
|
service: string;
|
|
name: string;
|
|
/** Full procedure name with class prefix, e.g. `CelestialBody.GetName`. */
|
|
fullName: string;
|
|
returnType: KrpcType;
|
|
returnIsNullable: boolean;
|
|
parameters: { name: string; type: KrpcType; nullable: boolean }[];
|
|
}
|
|
|
|
/**
|
|
* Result of a name lookup. Either we found the proc and we know its
|
|
* signature, or we didn't and the caller can decide what to do.
|
|
*/
|
|
export type ProcedureLookup =
|
|
| { found: true; info: ProcedureInfo }
|
|
| { found: false };
|
|
|
|
/**
|
|
* Generate the .NET-style variants of a PascalCase procedure name.
|
|
* For a top-level procedure like "GetUT" -> ["get_UT"].
|
|
* For a class-prefixed one like "CelestialBody.GetName" ->
|
|
* ["CelestialBody.get_Name", "get_CelestialBody.Name"].
|
|
* (We try the most likely variant first; the second is an extra
|
|
* fallback in case the kRPC server ever uses a flat "get_X.Y" form,
|
|
* which historical versions have done for some properties.)
|
|
*/
|
|
function netNameVariants(procedure: string): string[] {
|
|
const variants: string[] = [];
|
|
const lastDot = procedure.lastIndexOf('.');
|
|
if (lastDot < 0) {
|
|
// Top-level: "GetUT" -> "get_UT"
|
|
if (procedure.startsWith('Get') && procedure.length > 3) {
|
|
variants.push(`get_${procedure.slice(3)}`);
|
|
} else if (procedure.startsWith('Set') && procedure.length > 3) {
|
|
variants.push(`set_${procedure.slice(3)}`);
|
|
}
|
|
} else {
|
|
// Class-prefixed: "CelestialBody.GetName" -> "CelestialBody.get_Name"
|
|
const prefix = procedure.slice(0, lastDot);
|
|
const method = procedure.slice(lastDot + 1);
|
|
if (method.startsWith('Get') && method.length > 3) {
|
|
variants.push(`${prefix}.get_${method.slice(3)}`);
|
|
} else if (method.startsWith('Set') && method.length > 3) {
|
|
variants.push(`${prefix}.set_${method.slice(3)}`);
|
|
}
|
|
}
|
|
return variants;
|
|
}
|
|
|
|
export class ServiceCache {
|
|
/** "SpaceCenter.CelestialBody.GetName" -> ProcedureInfo */
|
|
private byFullName = new Map<string, ProcedureInfo>();
|
|
/** "SpaceCenter" -> "SpaceCenter" (just the service name) */
|
|
private services = new Set<string>();
|
|
/** "SpaceCenter.VesselType" -> "Ship" (enum name) -> int value */
|
|
private enumValues = new Map<string, Map<string, number>>();
|
|
/** "SpaceCenter" -> "VesselType" (enum name) -> Map<name, value> */
|
|
private enumsByService = new Map<string, Map<string, Map<string, number>>>();
|
|
|
|
constructor(raw: RawServicesMessage) {
|
|
for (const svc of raw.services ?? []) {
|
|
this.services.add(svc.name);
|
|
for (const proc of svc.procedures ?? []) {
|
|
// proc.name already includes the class prefix when applicable
|
|
// (e.g. "CelestialBody.GetName"), per the kRPC wire format.
|
|
const info: ProcedureInfo = {
|
|
service: svc.name,
|
|
name: proc.name,
|
|
fullName: `${svc.name}.${proc.name}`,
|
|
returnType: decodeKrpcType(proc.returnType),
|
|
returnIsNullable: !!proc.returnIsNullable,
|
|
parameters: (proc.parameters ?? []).map((p) => ({
|
|
name: p.name,
|
|
type: decodeKrpcType(p.type),
|
|
nullable: !!p.nullable,
|
|
})),
|
|
};
|
|
this.byFullName.set(info.fullName, info);
|
|
}
|
|
for (const e of svc.enumerations ?? []) {
|
|
const m = new Map<string, number>();
|
|
for (const v of e.values ?? []) {
|
|
m.set(v.name, v.value);
|
|
}
|
|
const fq = `${svc.name}.${e.name}`;
|
|
this.enumValues.set(fq, m);
|
|
let inner = this.enumsByService.get(svc.name);
|
|
if (!inner) {
|
|
inner = new Map();
|
|
this.enumsByService.set(svc.name, inner);
|
|
}
|
|
inner.set(e.name, m);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** All known service names, e.g. ["KRPC", "SpaceCenter", "KerbalAlarmClock", ...]. */
|
|
serviceNames(): string[] {
|
|
return [...this.services].sort();
|
|
}
|
|
|
|
/**
|
|
* All procedure full names in a service, e.g.
|
|
* ["SpaceCenter.GetUT", "SpaceCenter.CelestialBody.GetName", ...].
|
|
*/
|
|
proceduresInService(service: string): string[] {
|
|
const out: string[] = [];
|
|
for (const info of this.byFullName.values()) {
|
|
if (info.service === service) out.push(info.fullName);
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
/**
|
|
* Look up a procedure by `service.procedure` (e.g. "SpaceCenter.GetUT") or
|
|
* by the class-prefixed form ("SpaceCenter.CelestialBody.GetName").
|
|
*
|
|
* The kRPC server exposes C# properties using .NET naming conventions:
|
|
* a property `UT` on the SpaceCenter service becomes two procedures,
|
|
* `get_UT` and `set_UT`. Class properties like `CelestialBody.Name`
|
|
* become `CelestialBody.get_Name` and `CelestialBody.set_Name`.
|
|
* We accept the more familiar PascalCase form as a fallback, both
|
|
* for top-level procedures (GetUT -> get_UT) and class-prefixed
|
|
* ones (CelestialBody.GetName -> CelestialBody.get_Name).
|
|
*/
|
|
lookup(service: string, procedure: string): ProcedureLookup {
|
|
const direct = this.byFullName.get(`${service}.${procedure}`);
|
|
if (direct) return { found: true, info: direct };
|
|
|
|
// PascalCase -> .NET-style fallback. We try both the simple form
|
|
// (GetUT -> get_UT) and the class-prefixed form
|
|
// (CelestialBody.GetName -> CelestialBody.get_Name) so user
|
|
// code can use either convention.
|
|
for (const variant of netNameVariants(procedure)) {
|
|
const hit = this.byFullName.get(`${service}.${variant}`);
|
|
if (hit) return { found: true, info: hit };
|
|
}
|
|
|
|
return { found: false };
|
|
}
|
|
|
|
/**
|
|
* Resolve an enum value name to its int code. e.g.
|
|
* getEnumValue("SpaceCenter", "VesselType", "Ship") -> 0
|
|
* Throws if the enum or value is unknown.
|
|
*/
|
|
getEnumValue(service: string, enumName: string, valueName: string): number {
|
|
const m = this.enumValues.get(`${service}.${enumName}`);
|
|
if (!m) {
|
|
throw new Error(`unknown enum ${service}.${enumName}`);
|
|
}
|
|
const v = m.get(valueName);
|
|
if (v === undefined) {
|
|
throw new Error(`unknown value ${valueName} for ${service}.${enumName}`);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* Inverse: resolve an int code to a value name. Returns null if the
|
|
* code is not in the enum's range — kRPC may add new values in newer
|
|
* versions, so callers should be defensive.
|
|
*/
|
|
getEnumName(
|
|
service: string,
|
|
enumName: string,
|
|
valueCode: number,
|
|
): string | null {
|
|
const m = this.enumValues.get(`${service}.${enumName}`);
|
|
if (!m) return null;
|
|
for (const [n, v] of m.entries()) {
|
|
if (v === valueCode) return n;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* All enum value names for an enum, e.g.
|
|
* getEnumNames("SpaceCenter", "VesselType")
|
|
* -> ["Ship", "Station", "Lander", "Probe", ...]
|
|
*/
|
|
getEnumNames(service: string, enumName: string): string[] {
|
|
const m = this.enumValues.get(`${service}.${enumName}`);
|
|
if (!m) return [];
|
|
return [...m.keys()].sort();
|
|
}
|
|
|
|
/**
|
|
* Count the number of distinct procedures across all services.
|
|
* Useful for tests and diagnostics.
|
|
*/
|
|
procedureCount(): number {
|
|
return this.byFullName.size;
|
|
}
|
|
}
|