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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Official MSFS docs home:
## 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`).
- Member names: `SCREAMING_SNAKE_CASE` stripping the repetitive C++ prefix (e.g. `SIMCONNECT_COMM_BUS_BROADCAST_TO_JS` → `JS`, `SIMCONNECT_CAMERA_AVAILABILITY_NOT_ACQUIRED` → `NOT_ACQUIRED`).
- Values must exactly match the SDK C++ definitions — use bit-shift literals (`1 << 0`) for flags and composite expressions for combined values.

## Build & test
Expand All @@ -32,6 +32,6 @@ npm run lint # ESLint + Prettier

- Pre-commit hook runs `lint-staged` (ESLint fix → Prettier → `tsc --noEmit`).

## Skills / prompt files
## Skills

See [`.github/prompts/`](.github/prompts/) for reusable Copilot prompt files (skills).
See [`.github/skills/`](.github/skills/) for reusable Copilot skills.
152 changes: 152 additions & 0 deletions samples/typescript/cameraControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
CameraAvailability,
CameraData,
CameraDataMask,
CameraFlag,
open,
PositionReferential,
Protocol,
SimConnectConnection,
XYZ,
} from '../../dist';

const APP_NAME = 'SimConnect Camera Sample';

type Mode = 'cycleCameraDefinitions' | 'moveCamera' | 'updateFov' | null;
let activeMode: Mode = null;

let cameraAcquired = false;
let gameControlled = false;
const cameraDefinitions: string[] = [];
let currentCameraDef = 0;
let frame = 0;

// moveCamera state
let moveDeg = -180;
let moveAxis = 0;

// updateFov state
let currentFov = 45.0;
let fovIncrement = true;

function printStatus() {
console.log(
`\nActive mode: ${
activeMode ?? 'none'
} [1] cycleCameraDefinitions [2] moveCamera [3] updateFov [q] quit`
);
}

open(APP_NAME, Protocol.SunRise)
.then(({ recvOpen, handle }) => {
console.log('Connected:', recvOpen.applicationName);
console.log('Press 1/2/3 to toggle modes, q to quit.');
printStatus();

handle.on('exception', recvException => {
console.log('SimConnect exception:', recvException);
});

handle.on('cameraStatus', recvCameraStatus => {
gameControlled = recvCameraStatus.gameControlled;
console.log('Game Controlled =', gameControlled ? 1 : 0);
console.log(
'Camera Availability =',
CameraAvailability[recvCameraStatus.acquiredState]
);
cameraAcquired = recvCameraStatus.acquiredState === CameraAvailability.ACQUIRED;
});

handle.on('cameraDefinitionList', recvCameraDefinitionList => {
for (const cd of recvCameraDefinitionList.cameraDefinitions) {
cameraDefinitions.push(cd.name);
}
});

handle.subscribeToCameraStatusUpdate();
handle.enumerateCameraDefinitions();
handle.cameraAcquire(APP_NAME);
handle.cameraDisableFlag(CameraFlag.INTERACTION | CameraFlag.ABOVE_GROUND);

const loop = setInterval(() => {
if (cameraAcquired && !gameControlled) {
if (activeMode === 'cycleCameraDefinitions') switchToNextCameraDefinition(handle);
if (activeMode === 'updateFov') doUpdateFov(handle);
if (activeMode === 'moveCamera') doMoveCamera(handle);
}
}, 1);

// Keyboard input
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', (key: string) => {
if (key === '1') {
activeMode =
activeMode === 'cycleCameraDefinitions' ? null : 'cycleCameraDefinitions';
printStatus();
} else if (key === '2') {
activeMode = activeMode === 'moveCamera' ? null : 'moveCamera';
printStatus();
} else if (key === '3') {
activeMode = activeMode === 'updateFov' ? null : 'updateFov';
printStatus();
} else if (key === 'q' || key === '\u0003') {
clearInterval(loop);
handle.unsubscribeToCameraStatusUpdate();
handle.close();
process.exit(0);
}
});
})
.catch(error => {
console.log('Failed to connect:', error);
});

function doMoveCamera(handle: SimConnectConnection) {
if (moveDeg === 180) {
moveAxis = (moveAxis + 1) % 3;
moveDeg = -180;
}

const data = new CameraData();
data.position = new XYZ();
data.pbh.pitch = moveAxis === 0 ? moveDeg : 0;
data.pbh.bank = moveAxis === 1 ? moveDeg : 0;
data.pbh.heading = moveAxis === 2 ? moveDeg : 0;
data.rotationReferential = PositionReferential.EYEPOINT;

handle.cameraSet(
data,
CameraDataMask.POSITION | CameraDataMask.REFERENTIAL | CameraDataMask.ROTATION
);

moveDeg++;
}

function doUpdateFov(handle: SimConnectConnection) {
currentFov += fovIncrement ? 0.5 : -0.5;
if (currentFov <= 1.0 || currentFov >= 160.0) {
fovIncrement = !fovIncrement;
}

const data = new CameraData();
data.rotationReferential = PositionReferential.EYEPOINT;
data.fov = currentFov * (Math.PI / 180);

handle.cameraSet(data, CameraDataMask.ALL_ROTATION);
}

function switchToNextCameraDefinition(handle: SimConnectConnection) {
if (cameraDefinitions.length === 0) return;

frame++;
if (frame < 100) return;

frame = 0;
currentCameraDef = (currentCameraDef + 1) % cameraDefinitions.length;

const name = cameraDefinitions[currentCameraDef]!;
console.log('Switch to CameraDefinition', name);
handle.cameraSetUsingCameraDefinition(name);
}
147 changes: 132 additions & 15 deletions src/SimConnectConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import {
RecvAirportList,
RecvAssignedObjectID,
RecvCloudState,
RecvCameraData,
RecvCameraDefinitionList,
RecvCameraStatus,
RecvControllersList,
RecvCustomAction,
RecvEnumerateInputEventParams,
Expand Down Expand Up @@ -68,6 +71,10 @@ import { RecvEnumerateSimobjectAndLiveryList } from './recv/RecvEnumerateSimobje
import { RecvFlowEvent } from './recv/RecvFlowEvent';
import { RecvCommBus } from './recv/RecvCommBus';
import { CommBusBroadcastTo } from './enums/CommBusBroadcastTo';
import { CameraData } from './dto/CameraData';
import { CameraDataMask } from './enums/CameraDataMask';
import { CameraFlag } from './enums/CameraFlag';
import { PositionReferential } from './enums';

type OpenPacketData = {
major: number;
Expand Down Expand Up @@ -161,6 +168,9 @@ interface SimConnectRecvEvents {
recvEnumerateSimobjectAndLiveryList: RecvEnumerateSimobjectAndLiveryList
) => void;
flowEvent: (recvFlowEvent: RecvFlowEvent) => void;
cameraData: (recvCameraData: RecvCameraData) => void;
cameraDefinitionList: (recvCameraDefinitionList: RecvCameraDefinitionList) => void;
cameraStatus: (recvCameraStatus: RecvCameraStatus) => void;
commBusEvent: (recvCommBus: RecvCommBus) => void;
}

Expand Down Expand Up @@ -1850,20 +1860,127 @@ class SimConnectConnection extends EventEmitter {
}

/**
* 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)
*/
cameraAcquire(clientId: string): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x5f).putString(clientId, 2048);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraRelease(cameraDefName: string): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x60).putString(cameraDefName, 2048);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraGetStatus(): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x61);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraEnableFlag(flag: CameraFlag): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x64).putUint32(flag);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraDisableFlag(flag: CameraFlag): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x65).putUint32(flag);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraSet(cameraData: CameraData, dataMask: CameraDataMask): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x62);
cameraData.writeTo(packet);
packet.putUint32(dataMask);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraGet(positionReferential: PositionReferential): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x63).putUint32(positionReferential);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
subscribeToCameraStatusUpdate(): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x66);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
unsubscribeToCameraStatusUpdate(): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x67);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
enumerateCameraDefinitions(): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x68);
return this._buildAndSend(packet);
}

/**
*
* @returns sendId of packet (can be used to identify packet when exception event occurs)
*/
cameraSetUsingCameraDefinition(cameraDefinition: string): number {
if (this._ourProtocol < Protocol.SunRise) throw Error(SimConnectError.BadVersion);

const packet = this._beginPacket(0x69).putString(cameraDefinition, 2048);
return this._buildAndSend(packet);
}

/**
*
Expand Down Expand Up @@ -2063,13 +2180,13 @@ class SimConnectConnection extends EventEmitter {
this.emit('flowEvent', new RecvFlowEvent(data));
break;
case RecvID.ID_CAMERA_DATA:
// TODO
this.emit('cameraData', new RecvCameraData(data));
break;
case RecvID.ID_CAMERA_STATUS:
// TODO
this.emit('cameraStatus', new RecvCameraStatus(data));
break;
case RecvID.ID_CAMERA_DEFINITION_LIST:
// TODO
this.emit('cameraDefinitionList', new RecvCameraDefinitionList(data));
break;
case RecvID.ID_COMM_BUS:
this.emit('commBusEvent', new RecvCommBus(data));
Expand Down
9 changes: 9 additions & 0 deletions src/datastructures/CameraDefinitionItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { RawBuffer } from '../RawBuffer';

export class CameraDefinitionItem {
name: string;

constructor(data: RawBuffer) {
this.name = data.readString256();
}
}
1 change: 1 addition & 0 deletions src/datastructures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './InputEventDescriptor';
export * from './JetwayData';
export * from './EnumerateSimobjectLivery';
export * from './VersionBaseType';
export * from './CameraDefinitionItem';
Loading
Loading