diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fe0a2ad --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,34 @@ +# Copilot Instructions + +## Project overview + +`node-simconnect` is a TypeScript/Node.js library that implements the SimConnect binary protocol used by Microsoft Flight Simulator (FSX through MSFS 2024). It communicates with the simulator over a TCP socket using little-endian binary packets. + +## Encoding conventions + +- **All strings** (both reading and writing) use **`latin1`** encoding — the same encoding used by `RawBuffer.readString()` / `writeString()`. Never use `utf8`. +- Fixed-length string field lengths vary per field — always check the SDK documentation for the exact size. +- Numbers are little-endian — handled automatically by `RawBuffer` helpers. +- Variable-length text payloads sent as bytes: `Buffer.from(str, 'latin1')`. +- Variable-length text payloads received as bytes: `data.readBytes(n).toString('latin1')`. + +## Enum conventions + +- File names: `PascalCase.ts` (e.g. `CommBusBroadcastTo.ts`), exported from `src/enums/index.ts`. +- Member names: short `PascalCase` stripping the repetitive C++ prefix (e.g. `SIMCONNECT_COMM_BUS_BROADCAST_TO_JS` → `JS`). +- Values must exactly match the SDK C++ definitions — use bit-shift literals (`1 << 0`) for flags and composite expressions for combined values. + +## Build & test + +```bash +npm install # install dependencies +npm run build # compile TypeScript (tsc -p tsconfig.build.json → dist/) +npm test # run Jest tests +npm run lint # ESLint + Prettier +``` + +- Pre-commit hook runs `lint-staged` (ESLint fix → Prettier → `tsc --noEmit`). + +## Skills / prompt files + +See [`.github/prompts/`](.github/prompts/) for reusable Copilot prompt files (skills). diff --git a/.github/prompts/add-api-method.prompt.md b/.github/prompts/add-api-method.prompt.md new file mode 100644 index 0000000..63187d4 --- /dev/null +++ b/.github/prompts/add-api-method.prompt.md @@ -0,0 +1,155 @@ +--- +mode: agent +description: Add a new SimConnect API method to node-simconnect +tools: + - read_file + - create_file + - insert_edit_into_file + - run_in_terminal +--- + +Add a new SimConnect API method following the steps below. + +**Before starting, ask the user to provide:** + +1. The method name(s) to implement. +2. The contents of (or path to) their local `SimConnect.h` — this is the single authoritative source for function signatures, packet opcodes, struct field layouts, enum orderings, and string field sizes. Do **not** rely solely on the online docs, which may be outdated or incomplete. + +The online SDK docs are a useful supplement but must never override what `SimConnect.h` says: + + +## Checklist + +### 1. Recv struct (only if the server sends a response packet) + +Create `src/recv/RecvXxx.ts`: + +```ts +import { RawBuffer } from '../RawBuffer'; + +export class RecvXxx { + // one property per field in the C++ struct + myField: number; + + constructor(data: RawBuffer) { + // read fields in the exact order they appear in the C++ struct + this.myField = data.readUint32(); + } +} +``` + +Rules: + +- Constructor takes a single `RawBuffer` argument. +- **Check if the SDK struct inherits from a base struct** (e.g., `SIMCONNECT_RECV_LIST_TEMPLATE`). If it does, extend the corresponding TypeScript base class (e.g., `RecvListTemplate`) and call `super(data)` first. +- Read fields in the exact order they appear in the C++ struct. +- Fixed-length string fields: `data.readString(N)` where **N is the exact byte length from the SDK struct**. +- Variable-length text payloads declared with the `SIMCONNECT_STRINGV(name)` macro (expands to `char name[1]`): use `data.readStringV()` typed as `string`. +- Variable-length raw byte blobs that are not text: `data.readBytes(data.remaining())` typed as `Buffer`. +- **Verify every field name and type against the SDK struct** — do not rename fields or change types based on assumptions. + +Then export the new class from `src/recv/index.ts`: + +```ts +export { RecvXxx } from './RecvXxx'; +``` + +### 2. RecvID (only if step 1 applies) + +Read the `SIMCONNECT_RECV_ID` enum directly from `SimConnect.h` and add the new value at the exact position it occupies there. Do **not** guess or append blindly to the end. + +**Critical rules:** + +- Never assign explicit integer values to enum members (e.g. `ID_FOO = 40`) — rely on TypeScript's sequential auto-increment. Explicit values cause the entire subsequent sequence to shift if any entry is inserted before it. +- Before adding entries, find the full `SIMCONNECT_RECV_ID` enum in `SimConnect.h` and verify the complete ordering. Pay attention to entries that may have been inserted between existing ones. + +```ts +ID_XXX, // position must match SDK enum order, no explicit value +``` + +### 3. New enum (only if the method needs one) + +Create `src/enums/XxxEnum.ts`, mirroring the exact C++ values from `SimConnect.h`: + +```ts +export enum XxxEnum { + VALUE_A = 1 << 0, + VALUE_B = 1 << 1, + COMBINED = XxxEnum.VALUE_A | XxxEnum.VALUE_B, +} +``` + +Rules: + +- File name: `PascalCase.ts`. +- Member names: short `PascalCase` stripping the repetitive C++ prefix. +- Values must exactly match the SDK definitions. + +Then export from `src/enums/index.ts`: + +```ts +export { XxxEnum } from './XxxEnum'; +``` + +### 4. Method on SimConnectConnection + +Add the public method to `src/SimConnectConnection.ts`. + +**Before writing the method, determine:** + +- The exact packet opcode (hex ID) — **do not guess**. Opcodes are sequential based on the order functions appear in `SimConnect.h`. To find the correct opcode: + 1. Find the last implemented method in `src/SimConnectConnection.ts` and note its opcode. + 2. Locate that same function in `SimConnect.h` and count forward to the target function. + 3. Increment the last known opcode by the number of steps between them. +- The exact parameter order as declared in the SDK function signature in `SimConnect.h` +- The exact string field sizes + +```ts +/** + * @returns sendId of packet (can be used to identify packet when exception event occurs) + */ +// Only add @param / summary JSDoc if the description can be copied directly from the +// online SDK docs for the C function. Do NOT guess or paraphrase. +methodName(foo: string): number { + // guard for version-gated methods: + if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion); + + const packet = this._beginPacket(0xNN) // opcode = last known opcode + offset in SimConnect.h + .putString256(foo) + .putUint32(someFlag); + return this._buildAndSend(packet); +} +``` + +Payload encoding rules: + +- All strings use **`latin1`**. Never use `utf8`. +- **String field sizes**: use the correct `putStringN` helper matching the byte length in `SimConnect.h` (e.g. `.putString256(value)` for 256-byte fields). Always check the struct definition. +- If `SimConnect.h` or the function's documentation comment explicitly states the payload carries **JSON**, accept `string | object`; objects are serialized with `JSON.stringify` before encoding. Otherwise the parameter type is plain `string`. +- Variable-length string fields declared with `SIMCONNECT_STRINGV` in the SDK struct: `.putUint32(str.length).putString(str)` (no fixed-size argument to `putString`). +- Variable-length byte blobs: `.putUint32(buf.length).putBytes(buf)`. + +### 5. Event handler (only if step 1 applies) + +In the `_handleMessage` switch in `src/SimConnectConnection.ts`, add a case for the new `RecvID` and emit the event: + +```ts +case RecvID.ID_XXX: { + const recv = new RecvXxx(packet); + this.emit('xxxEvent', recv); + break; +} +``` + +Also add the event to the `SimConnectRecvEvents` interface at the top of the file: + +```ts +xxxEvent: (recv: RecvXxx) => void; +``` + +### 6. Validate + +```bash +npm run build # must produce no TypeScript errors +npm test # all tests must pass +``` diff --git a/samples/typescript/commbus.ts b/samples/typescript/commbus.ts new file mode 100644 index 0000000..e6bec35 --- /dev/null +++ b/samples/typescript/commbus.ts @@ -0,0 +1,57 @@ +import { CommBusBroadcastTo, open, Protocol } from '../../dist'; + +const MY_COMM_BUS_EVENT_ID = 1234; + +open('CommBus sample', Protocol.SunRise) + .then(({ recvOpen, handle }) => { + console.log('Connected: ', recvOpen); + + handle.subscribeToCommBusEvent(MY_COMM_BUS_EVENT_ID, 'MyCommBusEvent'); + + setInterval(() => { + const payload = JSON.stringify({ + message: 'Hello from Node.js!', + timestamp: new Date(), + }); + handle.callCommBusEvent('MyCommBusEvent', CommBusBroadcastTo.ALL_SIMCONNECT, payload); + }, 1000); + + setTimeout(() => { + console.log('Unsubscribing from CommBus event'); + handle.unsubscribeToCommBusEvent(MY_COMM_BUS_EVENT_ID); + }, 5000); + + function handleReceivedData(text: string) { + const json = JSON.parse(text); + console.log('Received commbus event data:', json); + } + + let receptionBuffer = ''; // In case the data is split across multiple events, we buffer it until we have the full message + handle.on('commBusEvent', recvCommBusEvent => { + switch (recvCommBusEvent.eventId) { + case MY_COMM_BUS_EVENT_ID: + if (recvCommBusEvent.outOf === 1) { + handleReceivedData(recvCommBusEvent.data); + } else { + receptionBuffer += recvCommBusEvent.data; + if (recvCommBusEvent.entryNumber + 1 === recvCommBusEvent.outOf) { + handleReceivedData(receptionBuffer); + receptionBuffer = ''; + } + } + + break; + } + }); + + handle.on('error', error => { + console.log('Error:', error); + }); + + handle.on('exception', recvException => { + console.log('SimConnect Exception:', recvException); + }); + }) + .catch(error => { + console.log('Failed to connect', error); + }); diff --git a/samples/typescript/enumerateSimobjets.ts b/samples/typescript/enumerateSimobjets.ts new file mode 100644 index 0000000..8801b7e --- /dev/null +++ b/samples/typescript/enumerateSimobjets.ts @@ -0,0 +1,23 @@ +import { open, Protocol, SimObjectType } from '../../dist'; + +open('My app', Protocol.SunRise) + .then(({ recvOpen, handle }) => { + console.log('Connected: ', recvOpen); + + handle.enumerateSimObjectsAndLiveries(123, SimObjectType.AIRCRAFT); + + handle.on('enumerateSimobjectAndLiveryList', recv => { + console.log(recv.simobjectLiveries); + }); + + handle.on('eventWeatherMode', recvWeatherMode => { + console.log('New weather mode:', recvWeatherMode.mode); + }); + + handle.on('eventFrame', recvEventFrame => { + // console.log('Framerate:', recvEventFrame.frameRate); + }); + }) + .catch(error => { + console.log('Failed to connect', error); + }); diff --git a/src/SimConnectConnection.ts b/src/SimConnectConnection.ts index 3bc43b0..ccc3b74 100644 --- a/src/SimConnectConnection.ts +++ b/src/SimConnectConnection.ts @@ -66,6 +66,8 @@ import type { SimConnectMessage } from './SimConnectSocket'; import Timeout = NodeJS.Timeout; import { RecvEnumerateSimobjectAndLiveryList } from './recv/RecvEnumerateSimobjectAndLiveryList'; import { RecvFlowEvent } from './recv/RecvFlowEvent'; +import { RecvCommBus } from './recv/RecvCommBus'; +import { CommBusBroadcastTo } from './enums/CommBusBroadcastTo'; type OpenPacketData = { major: number; @@ -159,6 +161,7 @@ interface SimConnectRecvEvents { recvEnumerateSimobjectAndLiveryList: RecvEnumerateSimobjectAndLiveryList ) => void; flowEvent: (recvFlowEvent: RecvFlowEvent) => void; + commBusEvent: (recvCommBus: RecvCommBus) => void; } type ConnectionOptions = @@ -1846,6 +1849,61 @@ class SimConnectConnection extends EventEmitter { return this._buildAndSend(packet); } + /** + * TODO: implement new camera APIs here + * + * SimConnect_CameraAcquire: 0x5f + * SimConnect_CameraRelease: 0x60 + * SimConnect_CameraGetStatus: 0x61 + * SimConnect_CameraSet: 0x62 + * SimConnect_CameraGet: 0x63 + * SimConnect_CameraEnableFlag: 0x64 + * SimConnect_CameraDisableFlag: 0x65 + * SimConnect_SubscribeToCameraStatusUpdate: 0x66 + * SimConnect_UnsubscribeToCameraStatusUpdate: 0x67 + * SimConnect_EnumerateCameraDefinitions: 0x68 + * SimConnect_CameraSetUsingCameraDefinition: 0x69 + */ + + /** + * + * @returns sendId of packet (can be used to identify packet when exception event occurs) + */ + subscribeToCommBusEvent(eventId: number, eventName: string): number { + if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion); + + const packet = this._beginPacket(0x6a).putUint32(eventId).putString256(eventName); + + return this._buildAndSend(packet); + } + + /** + * + * @returns sendId of packet (can be used to identify packet when exception event occurs) + */ + unsubscribeToCommBusEvent(eventId: number): number { + if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion); + + const packet = this._beginPacket(0x6b).putUint32(eventId); + + return this._buildAndSend(packet); + } + + /** + * + * @returns sendId of packet (can be used to identify packet when exception event occurs) + */ + callCommBusEvent(eventName: string, broadcastTo: CommBusBroadcastTo, payload: string): number { + if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion); + + const packet = this._beginPacket(0x6c) + .putString256(eventName) + .putUint32(broadcastTo) + .putUint32(payload.length) + .putString(payload); + return this._buildAndSend(packet); + } + close() { if (this._openTimeout !== null) { clearTimeout(this._openTimeout); @@ -2004,6 +2062,21 @@ class SimConnectConnection extends EventEmitter { case RecvID.ID_FLOW_EVENT: this.emit('flowEvent', new RecvFlowEvent(data)); break; + case RecvID.ID_CAMERA_DATA: + // TODO + break; + case RecvID.ID_CAMERA_STATUS: + // TODO + break; + case RecvID.ID_CAMERA_DEFINITION_LIST: + // TODO + break; + case RecvID.ID_COMM_BUS: + this.emit('commBusEvent', new RecvCommBus(data)); + break; + case RecvID.ID_CAMERA_WORLD_LOCKER: + // TODO + break; } } diff --git a/src/SimConnectSocket.ts b/src/SimConnectSocket.ts index d93ebde..b541f3e 100644 --- a/src/SimConnectSocket.ts +++ b/src/SimConnectSocket.ts @@ -47,6 +47,11 @@ enum RecvID { ID_ENUMERATE_INPUT_EVENT_PARAMS, ID_ENUMERATE_SIMOBJECT_AND_LIVERY_LIST, ID_FLOW_EVENT, + ID_CAMERA_DATA, + ID_CAMERA_STATUS, + ID_CAMERA_DEFINITION_LIST, + ID_COMM_BUS, + ID_CAMERA_WORLD_LOCKER, } interface SimConnectMessage { diff --git a/src/enums/CommBusBroadcastTo.ts b/src/enums/CommBusBroadcastTo.ts new file mode 100644 index 0000000..7b65e01 --- /dev/null +++ b/src/enums/CommBusBroadcastTo.ts @@ -0,0 +1,12 @@ +export enum CommBusBroadcastTo { + JS = 1 << 0, + WASM = 1 << 1, + SIMCONNECT = 1 << 3, + SIMCONNECT_SELF_CALL = 1 << 4, + DEFAULT = CommBusBroadcastTo.JS | CommBusBroadcastTo.WASM | CommBusBroadcastTo.SIMCONNECT, + ALL_SIMCONNECT = CommBusBroadcastTo.SIMCONNECT | CommBusBroadcastTo.SIMCONNECT_SELF_CALL, + ALL = CommBusBroadcastTo.JS | + CommBusBroadcastTo.WASM | + CommBusBroadcastTo.SIMCONNECT | + CommBusBroadcastTo.SIMCONNECT_SELF_CALL, +} diff --git a/src/enums/index.ts b/src/enums/index.ts index eb3242f..2364467 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -13,3 +13,4 @@ export * from './InputEventType'; export * from './FacilityDataType'; export * from './FlowEvent'; export * from './JetwayStatus'; +export * from './CommBusBroadcastTo'; diff --git a/src/recv/RecvCommBus.ts b/src/recv/RecvCommBus.ts new file mode 100644 index 0000000..29e1775 --- /dev/null +++ b/src/recv/RecvCommBus.ts @@ -0,0 +1,14 @@ +import { RawBuffer } from '../RawBuffer'; +import { RecvListTemplate } from './RecvListTemplate'; + +export class RecvCommBus extends RecvListTemplate { + eventId: number; + + data: string; + + constructor(data: RawBuffer) { + super(data); + this.eventId = data.readUint32(); + this.data = data.readStringV(); + } +} diff --git a/src/recv/index.ts b/src/recv/index.ts index 0b208fc..195cb45 100644 --- a/src/recv/index.ts +++ b/src/recv/index.ts @@ -33,3 +33,4 @@ export * from './RecvActionCallback'; export * from './RecvListTemplate'; export * from './RecvFlowEvent'; export * from './RecvEnumerateSimobjectAndLiveryList'; +export * from './RecvCommBus';