From e9ebbf17d2de0f363ec047cf5eae30a81b53197a Mon Sep 17 00:00:00 2001 From: Mavis Date: Wed, 3 Jun 2026 00:11:39 +0000 Subject: [PATCH] fix: translate PascalCase procedure names to .NET getter/setter convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/krpc-client/src/services.ts | 81 ++++++++++++++------- packages/krpc-client/tests/services.test.ts | 35 +++++++++ 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/packages/krpc-client/src/services.ts b/packages/krpc-client/src/services.ts index 81cf84b..3045c2c 100644 --- a/packages/krpc-client/src/services.ts +++ b/packages/krpc-client/src/services.ts @@ -68,6 +68,38 @@ 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(); @@ -81,31 +113,6 @@ export class ServiceCache { constructor(raw: RawServicesMessage) { for (const svc of raw.services ?? []) { this.services.add(svc.name); - if (process.env.KRPC_DEBUG && svc.name === 'SpaceCenter') { - // eslint-disable-next-line no-console - console.log( - `[krpc-client] SpaceCenter service: ${svc.procedures?.length ?? 0} procedures, first 3:`, - (svc.procedures ?? []) - .slice(0, 3) - .map((p) => `${p.name} -> ${svc.name}.${p.name}`), - ); - // Find GetUT specifically - const getut = (svc.procedures ?? []).find((p) => - p.name === 'GetUT' || p.name === 'getut' || p.name === 'SpaceCenter.GetUT', - ); - // eslint-disable-next-line no-console - console.log( - '[krpc-client] SpaceCenter procedure search for "GetUT" →', - getut ? `FOUND: name="${getut.name}"` : 'NOT FOUND in raw list', - ); - // eslint-disable-next-line no-console - console.log( - '[krpc-client] SpaceCenter keys containing "GetUT":', - (svc.procedures ?? []) - .map((p) => p.name) - .filter((n) => n.toLowerCase().includes('getut')), - ); - } for (const proc of svc.procedures ?? []) { // proc.name already includes the class prefix when applicable // (e.g. "CelestialBody.GetName"), per the kRPC wire format. @@ -160,11 +167,29 @@ export class ServiceCache { /** * 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 info = this.byFullName.get(`${service}.${procedure}`); - if (!info) return { found: false }; - return { found: true, info }; + 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 }; } /** diff --git a/packages/krpc-client/tests/services.test.ts b/packages/krpc-client/tests/services.test.ts index 71dc571..d58c970 100644 --- a/packages/krpc-client/tests/services.test.ts +++ b/packages/krpc-client/tests/services.test.ts @@ -225,4 +225,39 @@ describe('ServiceCache', () => { // Missing return type becomes NONE (code 0). expect(r.info.returnType.code).toBe(0); }); + + it('falls back to .NET-style getter/setter naming for C# properties', () => { + // The kRPC server exposes C# properties using .NET accessor + // conventions: a property `UT` on SpaceCenter becomes two + // procedures named `get_UT` and `set_UT`. User code typically + // writes `SpaceCenter.GetUT()` (PascalCase). The cache must + // transparently translate to the wire-format name. + const raw = { + services: [ + { + name: 'SpaceCenter', + procedures: [ + { name: 'get_UT', parameters: [], returnType: null, returnIsNullable: false }, + { name: 'set_UT', parameters: [], returnType: null, returnIsNullable: false }, + { name: 'get_ActiveVessel', parameters: [], returnType: null, returnIsNullable: true }, + { name: 'CelestialBody.get_Name', parameters: [], returnType: null, returnIsNullable: false }, + ], + classes: [], + enumerations: [], + }, + ], + }; + const cache = new ServiceCache(raw as unknown as Parameters[0]); + // Top-level PascalCase -> .NET getter + expect(cache.lookup('SpaceCenter', 'GetUT').found).toBe(true); + // Top-level PascalCase -> .NET setter + expect(cache.lookup('SpaceCenter', 'SetUT').found).toBe(true); + expect(cache.lookup('SpaceCenter', 'GetActiveVessel').found).toBe(true); + // Class-prefixed: "CelestialBody.GetName" -> "CelestialBody.get_Name" + expect(cache.lookup('SpaceCenter', 'CelestialBody.GetName').found).toBe(true); + // Exact match still works + expect(cache.lookup('SpaceCenter', 'get_UT').found).toBe(true); + // And unknown names still return not-found + expect(cache.lookup('SpaceCenter', 'NoSuchProcedure').found).toBe(false); + }); });