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); + }); });