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: true; info: ProcedureInfo }
|
||||||
| { found: false };
|
| { 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 {
|
export class ServiceCache {
|
||||||
/** "SpaceCenter.CelestialBody.GetName" -> ProcedureInfo */
|
/** "SpaceCenter.CelestialBody.GetName" -> ProcedureInfo */
|
||||||
private byFullName = new Map<string, ProcedureInfo>();
|
private byFullName = new Map<string, ProcedureInfo>();
|
||||||
@@ -81,31 +113,6 @@ export class ServiceCache {
|
|||||||
constructor(raw: RawServicesMessage) {
|
constructor(raw: RawServicesMessage) {
|
||||||
for (const svc of raw.services ?? []) {
|
for (const svc of raw.services ?? []) {
|
||||||
this.services.add(svc.name);
|
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 ?? []) {
|
for (const proc of svc.procedures ?? []) {
|
||||||
// proc.name already includes the class prefix when applicable
|
// proc.name already includes the class prefix when applicable
|
||||||
// (e.g. "CelestialBody.GetName"), per the kRPC wire format.
|
// (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
|
* Look up a procedure by `service.procedure` (e.g. "SpaceCenter.GetUT") or
|
||||||
* by the class-prefixed form ("SpaceCenter.CelestialBody.GetName").
|
* 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 {
|
lookup(service: string, procedure: string): ProcedureLookup {
|
||||||
const info = this.byFullName.get(`${service}.${procedure}`);
|
const direct = this.byFullName.get(`${service}.${procedure}`);
|
||||||
if (!info) return { found: false };
|
if (direct) return { found: true, info: direct };
|
||||||
return { found: true, info };
|
|
||||||
|
// 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).
|
// Missing return type becomes NONE (code 0).
|
||||||
expect(r.info.returnType.code).toBe(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