fix: translate PascalCase procedure names to .NET getter/setter convention
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.
This commit is contained in:
@@ -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<string, ProcedureInfo>();
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<typeof ServiceCache>[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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user