Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -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).
155 changes: 155 additions & 0 deletions .github/prompts/add-api-method.prompt.md
Original file line number Diff line number Diff line change
@@ -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:
<https://docs.flightsimulator.com/msfs2024/html/6_Programming_APIs/SimConnect/API_Reference/>

## 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
```
57 changes: 57 additions & 0 deletions samples/typescript/commbus.ts
Original file line number Diff line number Diff line change
@@ -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);
});
23 changes: 23 additions & 0 deletions samples/typescript/enumerateSimobjets.ts
Original file line number Diff line number Diff line change
@@ -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);
});
73 changes: 73 additions & 0 deletions src/SimConnectConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -159,6 +161,7 @@ interface SimConnectRecvEvents {
recvEnumerateSimobjectAndLiveryList: RecvEnumerateSimobjectAndLiveryList
) => void;
flowEvent: (recvFlowEvent: RecvFlowEvent) => void;
commBusEvent: (recvCommBus: RecvCommBus) => void;
}

type ConnectionOptions =
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/SimConnectSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions src/enums/CommBusBroadcastTo.ts
Original file line number Diff line number Diff line change
@@ -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,
}
Loading
Loading