diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 32f6d31..53af8e4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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 @@ -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. diff --git a/.github/prompts/add-api-method.prompt.md b/.github/skills/add-api-method.skill.md similarity index 100% rename from .github/prompts/add-api-method.prompt.md rename to .github/skills/add-api-method.skill.md diff --git a/samples/typescript/cameraControl.ts b/samples/typescript/cameraControl.ts new file mode 100644 index 0000000..e58e3d0 --- /dev/null +++ b/samples/typescript/cameraControl.ts @@ -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); +} diff --git a/src/SimConnectConnection.ts b/src/SimConnectConnection.ts index ccc3b74..db5f43a 100644 --- a/src/SimConnectConnection.ts +++ b/src/SimConnectConnection.ts @@ -22,6 +22,9 @@ import { RecvAirportList, RecvAssignedObjectID, RecvCloudState, + RecvCameraData, + RecvCameraDefinitionList, + RecvCameraStatus, RecvControllersList, RecvCustomAction, RecvEnumerateInputEventParams, @@ -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; @@ -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; } @@ -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); + } /** * @@ -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)); diff --git a/src/datastructures/CameraDefinitionItem.ts b/src/datastructures/CameraDefinitionItem.ts new file mode 100644 index 0000000..c24acd0 --- /dev/null +++ b/src/datastructures/CameraDefinitionItem.ts @@ -0,0 +1,9 @@ +import { RawBuffer } from '../RawBuffer'; + +export class CameraDefinitionItem { + name: string; + + constructor(data: RawBuffer) { + this.name = data.readString256(); + } +} diff --git a/src/datastructures/index.ts b/src/datastructures/index.ts index 640de6e..cd5ee01 100644 --- a/src/datastructures/index.ts +++ b/src/datastructures/index.ts @@ -8,3 +8,4 @@ export * from './InputEventDescriptor'; export * from './JetwayData'; export * from './EnumerateSimobjectLivery'; export * from './VersionBaseType'; +export * from './CameraDefinitionItem'; diff --git a/src/dto/CameraData.ts b/src/dto/CameraData.ts new file mode 100644 index 0000000..894ff3d --- /dev/null +++ b/src/dto/CameraData.ts @@ -0,0 +1,47 @@ +import { RawBuffer } from '../RawBuffer'; +import { SimConnectPacketBuilder } from '../SimConnectPacketBuilder'; +import { PositionReferential } from '../enums/PositionReferential'; +import { PBH } from './PBH'; +import { XYZ } from './XYZ'; + +export class CameraData { + position: XYZ = new XYZ(); + + positionReferential: PositionReferential = PositionReferential.NONE; + + positionReferentialObjectId = 0; + + targetedPos: XYZ = new XYZ(); + + pbh: PBH = new PBH(); + + rotationReferential: PositionReferential = PositionReferential.NONE; + + rotationReferentialObjectId = 0; + + fov = 0; + + readFrom(buffer: RawBuffer) { + this.position.readFrom(buffer); + this.positionReferential = buffer.readUint32() as PositionReferential; + this.positionReferentialObjectId = buffer.readUint32(); + this.targetedPos.readFrom(buffer); + this.pbh.readFrom(buffer); + this.rotationReferential = buffer.readUint32() as PositionReferential; + this.rotationReferentialObjectId = buffer.readUint32(); + this.fov = buffer.readFloat64(); + } + + writeTo(packetBuilder: SimConnectPacketBuilder) { + this.position.writeTo(packetBuilder); + packetBuilder + .putUint32(this.positionReferential) + .putUint32(this.positionReferentialObjectId); + this.targetedPos.writeTo(packetBuilder); + this.pbh.writeTo(packetBuilder); + packetBuilder + .putUint32(this.rotationReferential) + .putUint32(this.rotationReferentialObjectId) + .putFloat64(this.fov); + } +} diff --git a/src/dto/index.ts b/src/dto/index.ts index 158c4d9..1a250b6 100644 --- a/src/dto/index.ts +++ b/src/dto/index.ts @@ -7,3 +7,4 @@ export * from './PBH'; export * from './SimConnectData'; export * from './Waypoint'; export * from './XYZ'; +export * from './CameraData'; diff --git a/src/enums/CameraAvailability.ts b/src/enums/CameraAvailability.ts new file mode 100644 index 0000000..ada3999 --- /dev/null +++ b/src/enums/CameraAvailability.ts @@ -0,0 +1,6 @@ +export enum CameraAvailability { + NOT_ACQUIRED = 0, + ACQUIRED = 1, + ACQUIRED_BY_OTHER = 2, + USER_DISABLED = 3, +} diff --git a/src/enums/CameraDataMask.ts b/src/enums/CameraDataMask.ts new file mode 100644 index 0000000..c1ffcc4 --- /dev/null +++ b/src/enums/CameraDataMask.ts @@ -0,0 +1,10 @@ +export enum CameraDataMask { + NONE = 0, + POSITION = 1 << 0, + ROTATION = 1 << 1, + TARGETED = 1 << 2, + FOV = 1 << 3, + REFERENTIAL = 1 << 4, + ALL_ROTATION = POSITION | ROTATION | FOV, + ALL_TARGETED = POSITION | TARGETED | FOV, +} diff --git a/src/enums/CameraFlag.ts b/src/enums/CameraFlag.ts new file mode 100644 index 0000000..97bf9d8 --- /dev/null +++ b/src/enums/CameraFlag.ts @@ -0,0 +1,5 @@ +export enum CameraFlag { + NONE = 0x00, + INTERACTION = 0x01, + ABOVE_GROUND = 0x02, +} diff --git a/src/enums/PositionReferential.ts b/src/enums/PositionReferential.ts new file mode 100644 index 0000000..9b0b2fe --- /dev/null +++ b/src/enums/PositionReferential.ts @@ -0,0 +1,7 @@ +export enum PositionReferential { + NONE = 0, + SIMOBJECT = 1, + WORLD = 2, + EYEPOINT = 3, + SIMOBJECT_DATUM = 4, +} diff --git a/src/enums/index.ts b/src/enums/index.ts index 2364467..ba2240a 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -14,3 +14,7 @@ export * from './FacilityDataType'; export * from './FlowEvent'; export * from './JetwayStatus'; export * from './CommBusBroadcastTo'; +export * from './CameraAvailability'; +export * from './PositionReferential'; +export * from './CameraDataMask'; +export * from './CameraFlag'; diff --git a/src/recv/RecvCameraData.ts b/src/recv/RecvCameraData.ts new file mode 100644 index 0000000..69de2fb --- /dev/null +++ b/src/recv/RecvCameraData.ts @@ -0,0 +1,11 @@ +import { CameraData } from '../dto/CameraData'; +import { RawBuffer } from '../RawBuffer'; + +export class RecvCameraData { + cameraData: CameraData; + + constructor(data: RawBuffer) { + this.cameraData = new CameraData(); + this.cameraData.readFrom(data); + } +} diff --git a/src/recv/RecvCameraDefinitionList.ts b/src/recv/RecvCameraDefinitionList.ts new file mode 100644 index 0000000..4269a2b --- /dev/null +++ b/src/recv/RecvCameraDefinitionList.ts @@ -0,0 +1,16 @@ +import { CameraDefinitionItem } from '../datastructures/CameraDefinitionItem'; +import { RawBuffer } from '../RawBuffer'; +import { RecvListTemplate } from './RecvListTemplate'; + +export class RecvCameraDefinitionList extends RecvListTemplate { + cameraDefinitions: CameraDefinitionItem[] = []; + + constructor(data: RawBuffer) { + super(data); + + this.cameraDefinitions = []; + for (let i = 0; i < this.arraySize; i++) { + this.cameraDefinitions.push(new CameraDefinitionItem(data)); + } + } +} diff --git a/src/recv/RecvCameraStatus.ts b/src/recv/RecvCameraStatus.ts new file mode 100644 index 0000000..3186b0e --- /dev/null +++ b/src/recv/RecvCameraStatus.ts @@ -0,0 +1,13 @@ +import { CameraAvailability } from '../enums/CameraAvailability'; +import { RawBuffer } from '../RawBuffer'; + +export class RecvCameraStatus { + acquiredState: CameraAvailability; + + gameControlled: boolean; + + constructor(data: RawBuffer) { + this.acquiredState = data.readUint32() as CameraAvailability; + this.gameControlled = data.readInt32() !== 0; + } +} diff --git a/src/recv/index.ts b/src/recv/index.ts index 195cb45..d7ead0a 100644 --- a/src/recv/index.ts +++ b/src/recv/index.ts @@ -34,3 +34,6 @@ export * from './RecvListTemplate'; export * from './RecvFlowEvent'; export * from './RecvEnumerateSimobjectAndLiveryList'; export * from './RecvCommBus'; +export * from './RecvCameraDefinitionList'; +export * from './RecvCameraStatus'; +export * from './RecvCameraData'; diff --git a/tsconfig.json b/tsconfig.json index 84534b8..91884f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,6 @@ "isolatedModules": true, "types": ["node", "jest"] }, - "include": ["src/**/*", "tests/**/*", "utils/**/*"], + "include": ["src/**/*", "tests/**/*", "utils/**/*", "samples/**/*"], "exclude": ["node_modules", "dist"] }