/** * 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(); /** "SpaceCenter" -> "SpaceCenter" (just the service name) */ private services = new Set(); /** "SpaceCenter.VesselType" -> "Ship" (enum name) -> int value */ private enumValues = new Map>(); /** "SpaceCenter" -> "VesselType" (enum name) -> Map */ private enumsByService = new Map>>(); 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(); 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; } }