From 0f8688161c063028415c4a7909f2765c920a7c33 Mon Sep 17 00:00:00 2001 From: Daveee19 Date: Sun, 18 Oct 2026 15:29:52 +0000 Subject: [PATCH] Automated Extension submission for issue #2321 --- extensions/community/DualSense.json | 5255 +++++++++++++++++++++++++++ 1 file changed, 5255 insertions(+) create mode 100644 extensions/community/DualSense.json diff --git a/extensions/community/DualSense.json b/extensions/community/DualSense.json new file mode 100644 index 000000000..3bff43cfe --- /dev/null +++ b/extensions/community/DualSense.json @@ -0,0 +1,5255 @@ +{ + "author": "Antigravity", + "category": "Input", + "dimension": "", + "extensionNamespace": "", + "fullName": "DualSense", + "gdevelopVersion": "5.0.0", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWdvb2dsZS1jb250cm9sbGVyIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTcuOTcsMTZMNSwxOUM0LjY3LDE5LjMgNC4yMywxOS41IDMuNzUsMTkuNUExLjc1LDEuNzUgMCAwLDEgMiwxNy43NVYxNy41TDMsMTAuMTJDMy4yMSw3LjgxIDUuMTQsNiA3LjUsNkgxNi41QzE4Ljg2LDYgMjAuNzksNy44MSAyMSwxMC4xMkwyMiwxNy41VjE3Ljc1QTEuNzUsMS43NSAwIDAsMSAyMC4yNSwxOS41QzE5Ljc3LDE5LjUgMTkuMzMsMTkuMyAxOSwxOUwxNi4wMywxNkg3Ljk3TTcsOFYxMEg1VjExSDdWMTNIOFYxMUgxMFYxMEg4VjhIN00xNi41LDhBMC43NSwwLjc1IDAgMCwwIDE1Ljc1LDguNzVBMC43NSwwLjc1IDAgMCwwIDE2LjUsOS41QTAuNzUsMC43NSAwIDAsMCAxNy4yNSw4Ljc1QTAuNzUsMC43NSAwIDAsMCAxNi41LDhNMTQuNzUsOS43NUEwLjc1LDAuNzUgMCAwLDAgMTQsMTAuNUEwLjc1LDAuNzUgMCAwLDAgMTQuNzUsMTEuMjVBMC43NSwwLjc1IDAgMCwwIDE1LjUsMTAuNUEwLjc1LDAuNzUgMCAwLDAgMTQuNzUsOS43NU0xOC4yNSw5Ljc1QTAuNzUsMC43NSAwIDAsMCAxNy41LDEwLjVBMC43NSwwLjc1IDAgMCwwIDE4LjI1LDExLjI1QTAuNzUsMC43NSAwIDAsMCAxOSwxMC41QTAuNzUsMC43NSAwIDAsMCAxOC4yNSw5Ljc1TTE2LjUsMTEuNUEwLjc1LDAuNzUgMCAwLDAgMTUuNzUsMTIuMjVBMC43NSwwLjc1IDAgMCwwIDE2LjUsMTNBMC43NSwwLjc1IDAgMCwwIDE3LjI1LDEyLjI1QTAuNzUsMC43NSAwIDAsMCAxNi41LDExLjVaIiAvPjwvc3ZnPg==", + "name": "DualSense", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/1fcd8f4d4fc4b3dff3bcf96977701adcc457ac81ac2bd668b51268170b9d896e_google-controller.svg", + "shortDescription": "Full DualSense & DualSense Edge integration over USB and Bluetooth.", + "version": "1.0.0", + "description": "Adds the DualSense / DualSense Edge features, connection supported over USB and Bluetooth. Works alongside the built-in Gamepad extension (which still handles the normal buttons and sticks).", + "tags": [ + "dualsense", + "ps5", + "playstation", + "controller", + "gamepad", + "adaptive triggers", + "touchpad", + "gyroscope", + "lightbar", + "haptics" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Prepares the DualSense helper code. Does not connect anything by itself.", + "fullName": "On first scene loaded", + "functionType": "Action", + "name": "onFirstSceneLoaded", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) {", + " gdjs._dualSenseEnsureInit = function(runtimeScene) {", + " if (gdjs._dualSenseManager) {", + " return gdjs._dualSenseManager;", + " }", + "", + " // CRC32 lookup table for DualSense Bluetooth output & feature reports", + " const CRC32_TABLE = new Uint32Array(256);", + " for (let i = 0; i < 256; i++) {", + " let c = i;", + " for (let k = 0; k < 8; k++) {", + " c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);", + " }", + " CRC32_TABLE[i] = c >>> 0;", + " }", + "", + " function computeCrc32(prefixBytes, dataView, length) {", + " let crc = 0xFFFFFFFF;", + " for (let i = 0; i < prefixBytes.length; i++) {", + " crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ prefixBytes[i]) & 0xFF];", + " }", + " for (let i = 0; i < length; i++) {", + " const b = dataView.getUint8(i);", + " crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ b) & 0xFF];", + " }", + " return (crc ^ 0xFFFFFFFF) >>> 0;", + " }", + "", + " function fillOutputCrc32(reportId, reportBytes) {", + " const dv = new DataView(reportBytes.buffer, reportBytes.byteOffset, reportBytes.byteLength);", + " const crc = computeCrc32([0xA2, reportId], dv, reportBytes.byteLength - 4);", + " dv.setUint32(reportBytes.byteLength - 4, crc, true);", + " }", + "", + " function fillFeatureCrc32(reportId, reportBytes) {", + " const dv = new DataView(reportBytes.buffer, reportBytes.byteOffset, reportBytes.byteLength);", + " const crc = computeCrc32([0x53, reportId], dv, reportBytes.byteLength - 4);", + " dv.setUint32(reportBytes.byteLength - 4, crc, true);", + " }", + "", + " function hsvToRgb(h, s, v) {", + " const c = v * s;", + " const x = c * (1 - Math.abs(((h / 60) % 2) - 1));", + " const m = v - c;", + " let r = 0, g = 0, b = 0;", + " if (h < 60) { r = c; g = x; b = 0; }", + " else if (h < 120) { r = x; g = c; b = 0; }", + " else if (h < 180) { r = 0; g = c; b = x; }", + " else if (h < 240) { r = 0; g = x; b = c; }", + " else if (h < 300) { r = x; g = 0; b = c; }", + " else { r = c; g = 0; b = x; }", + " return [", + " Math.round((r + m) * 255),", + " Math.round((g + m) * 255),", + " Math.round((b + m) * 255)", + " ];", + " }", + "", + " const VENDOR_SONY = 0x054C;", + " const PID_DUALSENSE = 0x0CE6;", + " const PID_DUALSENSE_EDGE = 0x0DF2;", + "", + " const mgr = {", + " device: null,", + " connectionType: 'Disconnected', // 'USB' | 'Bluetooth' | 'Disconnected'", + " model: 'None', // 'DualSense' | 'DualSense Edge' | 'None'", + " connecting: false,", + " outputSeq: 0,", + " sendPending: false,", + " sendQueued: false,", + " outputDirty: false,", + " _lastOutputSentTime: 0,", + " _flushTimer: null,", + " _microtaskPromise: null,", + " _lastStepNow: 0,", + " wantConnection: false, // only true after the user's \"Connect\" action", + " status: 'Not connected (use the Connect action)',", + " lastError: '',", + " gestureArmed: false,", + " electronChecked: false,", + " electronWebContents: null,", + " electronPermissionsReady: false,", + " _retryTimer: 0,", + " _retryCount: 0,", + "", + " // 47-byte base output report buffer (shared structure for USB 0x02 and BT 0x31+2)", + " outStruct: new Uint8Array(47),", + " pendingFlags0: 0,", + " pendingFlags1: 0,", + " pendingFlags2: 0,", + "", + " // Input state", + " psPressed: false,", + " psPrev: false,", + " psJustPressed: false,", + " rawPs: false,", + "", + " micBtnPressed: false,", + " micBtnPrev: false,", + " micBtnJustPressed: false,", + " rawMicBtn: false,", + "", + " tpPressed: false,", + " tpPrev: false,", + " tpJustPressed: false,", + " rawTpPressed: false,", + "", + " fnLPressed: false,", + " fnRPressed: false,", + " paddleLPressed: false,", + " paddleRPressed: false,", + " rawFnL: false,", + " rawFnR: false,", + " rawPaddleL: false,", + " rawPaddleR: false,", + "", + " triggerStopLeft: 0,", + " triggerStopRight: 0,", + "", + " // Touchpad 2-point capacitive state", + " touch1: { active: false, prevActive: false, justStarted: false, rawActive: false, id: 0, x: 0, y: 0, prevX: 0, prevY: 0, dx: 0, dy: 0 },", + " touch2: { active: false, prevActive: false, justStarted: false, rawActive: false, id: 0, x: 0, y: 0, prevX: 0, prevY: 0, dx: 0, dy: 0 },", + "", + " // 6-axis IMU (Gyro + Accel)", + " gyroPitch: 0,", + " gyroYaw: 0,", + " gyroRoll: 0,", + " gyroBiasPitch: 0,", + " gyroBiasYaw: 0,", + " gyroBiasRoll: 0,", + " accelX: 0,", + " accelY: 0,", + " accelZ: 0,", + " tiltPitchDeg: 0,", + " tiltRollDeg: 0,", + "", + " // Telemetry", + " batteryLevel: 0,", + " batteryStatus: 'Unknown',", + " headphonePlugged: false,", + " micPlugged: false,", + "", + " // Output state tracking", + " lightbarR: 0,", + " lightbarG: 120,", + " lightbarB: 255,", + " rainbowEnabled: false,", + " rainbowSpeed: 120,", + " rainbowHue: 0,", + " rainbowTimer: 0,", + "", + " rumbleLeft: 0,", + " rumbleRight: 0,", + " rumbleRemaining: 0,", + " rumbleEndTime: 0,", + " _rumbleStopTimer: null,", + "", + " speakerVolume: 85,", + " headphoneVolume: 65,", + " toneRemaining: -1,", + " toneTarget: 'speaker',", + "", + " // Microphone volume monitoring", + " micActive: false,", + " micStarting: false,", + " micVolume: 0,", + " micPeak: 0,", + " micAudioCtx: null,", + " micAnalyser: null,", + " micDataArray: null,", + " micStream: null,", + " micSource: null,", + " micSplitter: null,", + " btMicDecoder: null,", + " btMicStateSeq: 0,", + " btMicControlSeq: 0,", + " btMicTimestamp: 0,", + " btMicHandler: null,", + "", + " // Speaker WebAudio player & Bluetooth Opus streamer", + " spkAudioCtx: null,", + " spkSinkId: null,", + " _spkUsbSource: null,", + " _btAudioAbort: null,", + " _btAudioPlaying: false", + " };", + "", + " // Default initial output values", + " mgr.outStruct[4] = 0x50; // headphone vol", + " mgr.outStruct[5] = 0x55; // speaker vol", + " mgr.outStruct[6] = 0x40; // mic vol", + " mgr.outStruct[41] = 2; // lightbarSetup", + " mgr.outStruct[42] = 0; // ledBrightness High", + " mgr.outStruct[43] = 0x04; // Player 1 LED", + " mgr.outStruct[44] = 0;", + " mgr.outStruct[45] = 120;", + " mgr.outStruct[46] = 255;", + "", + " function detectConnectionType(device) {", + " if (device && device.collections) {", + " for (let i = 0; i < device.collections.length; i++) {", + " const c = device.collections[i];", + " if (c.usagePage !== 0x0001 || c.usage !== 0x0005) continue;", + " let maxBits = 0;", + " if (c.inputReports) {", + " for (let r = 0; r < c.inputReports.length; r++) {", + " const rep = c.inputReports[r];", + " let bits = 0;", + " if (rep.items) {", + " for (let it = 0; it < rep.items.length; it++) {", + " bits += (rep.items[it].reportSize || 0) * (rep.items[it].reportCount || 0);", + " }", + " }", + " if (bits > maxBits) maxBits = bits;", + " }", + " }", + " if (maxBits === 504) return 'USB';", + " if (maxBits === 616) return 'Bluetooth';", + " }", + " }", + " return 'Unknown';", + " }", + "", + " function isGamepadHidDevice(device) {", + " if (!device || device.vendorId !== VENDOR_SONY) return false;", + " if (device.productId !== PID_DUALSENSE && device.productId !== PID_DUALSENSE_EDGE) return false;", + " if (device.collections && device.collections.length > 0) {", + " return device.collections.some(function(c) {", + " return c.usagePage === 0x0001 && c.usage === 0x0005;", + " });", + " }", + " return true;", + " }", + "", + " function handleInputReport(event) {", + " const data = event.data;", + " const reportId = event.reportId;", + " if (!data) return;", + "", + " // Check if this is a Bluetooth microphone Opus audio frame (reportId 0x31, low nibble === 0x02)", + " if (reportId === 0x31 && data.byteLength >= 73 && (data.getUint8(0) & 0x0F) === 0x02) {", + " if (mgr.micActive && mgr.btMicDecoder && mgr.btMicDecoder.state === 'configured') {", + " try {", + " const opusPayload = new Uint8Array(data.buffer, data.byteOffset + 2, 71);", + " mgr.btMicDecoder.decode(new EncodedAudioChunk({", + " type: 'key',", + " timestamp: mgr.btMicTimestamp,", + " duration: 10000,", + " data: opusPayload", + " }));", + " mgr.btMicTimestamp += 10000;", + " } catch (e) {}", + " }", + " return;", + " }", + "", + " // Determine offset: USB report 0x01 (63 bytes) has offset 0; BT extended report 0x31 (77 bytes) has offset +1", + " let isUsbReport = true;", + " if (reportId === 0x31) {", + " isUsbReport = false;", + " if (mgr.connectionType !== 'Bluetooth') mgr.connectionType = 'Bluetooth';", + " } else if (reportId === 0x01 && mgr.connectionType === 'Bluetooth' && data.byteLength < 55) {", + " // Legacy 9-byte Bluetooth report before extended 0x31 switch completes:", + " if (data.byteLength >= 7) {", + " const b6 = data.getUint8(6);", + " mgr.rawPs = (b6 & 1) !== 0;", + " mgr.rawTpPressed = (b6 & 2) !== 0;", + " }", + " return;", + " }", + "", + " const num = isUsbReport ? 0 : 1;", + " if (data.byteLength < 54 + num) return;", + "", + " // Digital keys (PS, Touchpad click, Mic Mute, DualSense Edge Fn & Back Paddles)", + " const keys2 = data.getUint8(7 + num + 2);", + " mgr.rawPs = (keys2 & 1) !== 0;", + " mgr.rawTpPressed = (keys2 & 2) !== 0;", + " mgr.rawMicBtn = (keys2 & 4) !== 0;", + " mgr.rawFnL = (keys2 & 16) !== 0;", + " mgr.rawFnR = (keys2 & 32) !== 0;", + " mgr.rawPaddleL = (keys2 & 64) !== 0;", + " mgr.rawPaddleR = (keys2 & 128) !== 0;", + "", + " // Gyroscope (16-bit signed little-endian)", + " const rawGyroPitch = data.getInt16(15 + num, true);", + " const rawGyroYaw = data.getInt16(17 + num, true);", + " const rawGyroRoll = data.getInt16(19 + num, true);", + " mgr.gyroPitch = rawGyroPitch - mgr.gyroBiasPitch;", + " mgr.gyroYaw = rawGyroYaw - mgr.gyroBiasYaw;", + " mgr.gyroRoll = rawGyroRoll - mgr.gyroBiasRoll;", + "", + " // Accelerometer (16-bit signed little-endian)", + " const ax = data.getInt16(21 + num, true);", + " const ay = data.getInt16(23 + num, true);", + " const az = data.getInt16(25 + num, true);", + " mgr.accelX = ax;", + " mgr.accelY = ay;", + " mgr.accelZ = az;", + "", + " // Compute intuitive tilt angles in degrees from gravity vector", + " const norm = Math.sqrt(ax * ax + ay * ay + az * az);", + " if (norm > 100) {", + " mgr.tiltPitchDeg = Math.atan2(az, Math.sqrt(ax * ax + ay * ay)) * (180 / Math.PI);", + " mgr.tiltRollDeg = Math.atan2(-ax, ay) * (180 / Math.PI);", + " }", + "", + " // Touchpad 2-finger capacitive coordinates (1920 x 1080)", + " const td = 32 + num;", + " const idByte1 = data.getUint8(td);", + " const active1 = (idByte1 & 0x80) === 0;", + " const x1 = ((data.getUint8(td + 2) & 0x0F) << 8) | data.getUint8(td + 1);", + " const y1 = (data.getUint8(td + 3) << 4) | (data.getUint8(td + 2) >> 4);", + " mgr.touch1.rawActive = active1;", + " mgr.touch1.id = idByte1 & 0x7F;", + " if (active1) {", + " mgr.touch1.x = x1;", + " mgr.touch1.y = y1;", + " }", + "", + " const idByte2 = data.getUint8(td + 4);", + " const active2 = (idByte2 & 0x80) === 0;", + " const x2 = ((data.getUint8(td + 6) & 0x0F) << 8) | data.getUint8(td + 5);", + " const y2 = (data.getUint8(td + 7) << 4) | (data.getUint8(td + 6) >> 4);", + " mgr.touch2.rawActive = active2;", + " mgr.touch2.id = idByte2 & 0x7F;", + " if (active2) {", + " mgr.touch2.x = x2;", + " mgr.touch2.y = y2;", + " }", + "", + " // DualSense Edge hardware trigger stop sliders", + " const trigLvl = data.getUint8(49 + num);", + " mgr.triggerStopLeft = (trigLvl >> 4) & 3;", + " mgr.triggerStopRight = (trigLvl >> 6) & 3;", + "", + " // Battery & jack status", + " const status0 = data.getUint8(52 + num);", + " const status1 = data.getUint8(53 + num);", + " const chargeCode = (status0 & 0xF0) >> 4;", + " let batStep = status0 & 0x0F;", + " if (chargeCode === 2) {", + " mgr.batteryStatus = 'Full';", + " mgr.batteryLevel = 100;", + " } else if (chargeCode === 1) {", + " mgr.batteryStatus = 'Charging';", + " mgr.batteryLevel = Math.min(100, batStep * 10 + 5);", + " } else if (chargeCode === 0) {", + " mgr.batteryStatus = 'Discharging';", + " mgr.batteryLevel = Math.min(100, batStep * 10 + 5);", + " } else {", + " mgr.batteryStatus = 'Error';", + " }", + " mgr.headphonePlugged = (status1 & 1) !== 0;", + " mgr.micPlugged = (status1 & 2) !== 0;", + " }", + "", + " mgr.flushOutputReport = function(f0, f1, f2) {", + " mgr.pendingFlags0 |= (f0 || 0);", + " mgr.pendingFlags1 |= (f1 || 0);", + " mgr.pendingFlags2 |= (f2 || 0);", + " mgr.outputDirty = true;", + "", + " if (!mgr.device || !mgr.device.opened) return;", + "", + " if (!mgr._microtaskPromise) {", + " mgr._microtaskPromise = Promise.resolve().then(function() {", + " mgr._microtaskPromise = null;", + " mgr._doFlush();", + " });", + " }", + " return mgr._microtaskPromise;", + " };", + "", + " mgr._doFlush = function() {", + " if (!mgr.device || !mgr.device.opened) return;", + " if (!mgr.outputDirty && !mgr.sendQueued) return;", + "", + " if (mgr.sendPending) {", + " mgr.sendQueued = true;", + " return;", + " }", + "", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " const elapsed = now - mgr._lastOutputSentTime;", + "", + " if (elapsed < minInterval) {", + " mgr.sendQueued = true;", + " if (!mgr._flushTimer && typeof setTimeout !== 'undefined') {", + " const delay = Math.max(1, minInterval - elapsed);", + " mgr._flushTimer = setTimeout(function() {", + " mgr._flushTimer = null;", + " mgr._doFlush();", + " }, delay);", + " }", + " return;", + " }", + "", + " if (mgr._flushTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._flushTimer);", + " mgr._flushTimer = null;", + " }", + "", + " mgr.sendPending = true;", + " (async function() {", + " try {", + " mgr.sendQueued = false;", + " let useF0 = mgr.pendingFlags0;", + " let useF1 = mgr.pendingFlags1;", + " let useF2 = mgr.pendingFlags2;", + " mgr.pendingFlags0 = 0;", + " mgr.pendingFlags1 = 0;", + " mgr.pendingFlags2 = 0;", + " mgr.outputDirty = false;", + "", + " // If rumble is active, ALWAYS maintain motor values and validFlag0 bits 0 & 1 active", + " // so no other event (lightbar, trigger, LED, volume) ever starves or resets the vibration!", + " if (mgr.rumbleLeft > 0 || mgr.rumbleRight > 0) {", + " useF0 |= 0x03;", + " mgr.outStruct[2] = mgr.rumbleRight;", + " mgr.outStruct[3] = mgr.rumbleLeft;", + " }", + "", + " mgr.outStruct[0] = useF0;", + " mgr.outStruct[1] = useF1;", + " mgr.outStruct[38] = useF2;", + "", + " if (mgr.connectionType === 'USB') {", + " await mgr.device.sendReport(0x02, new Uint8Array(mgr.outStruct));", + " } else {", + " const btBuf = new Uint8Array(77);", + " btBuf[0] = (mgr.outputSeq & 0x0F) << 4;", + " mgr.outputSeq = (mgr.outputSeq + 1) & 0xFF;", + " btBuf[1] = 0x10;", + " btBuf.set(mgr.outStruct, 2);", + " fillOutputCrc32(0x31, btBuf);", + " await mgr.device.sendReport(0x31, btBuf);", + " }", + "", + " mgr._lastOutputSentTime = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + "", + " mgr.outStruct[0] = 0;", + " mgr.outStruct[1] = 0;", + " mgr.outStruct[38] = 0;", + " } catch (e) {", + " } finally {", + " mgr.sendPending = false;", + " if ((mgr.sendQueued || mgr.outputDirty) && mgr.device && mgr.device.opened) {", + " if (!mgr._flushTimer && typeof setTimeout !== 'undefined') {", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " mgr._flushTimer = setTimeout(function() {", + " mgr._flushTimer = null;", + " mgr._doFlush();", + " }, minInterval);", + " }", + " }", + " }", + " })();", + " };", + "", + " mgr.stopRumble = function() {", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " mgr._rumbleStopTimer = null;", + " }", + " mgr.rumbleLeft = 0;", + " mgr.rumbleRight = 0;", + " mgr.rumbleRemaining = 0;", + " mgr.rumbleEndTime = 0;", + " mgr.outStruct[2] = 0;", + " mgr.outStruct[3] = 0;", + " if (mgr.device && mgr.device.opened) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " };", + "", + " mgr.startRumble = function(left, right, durationSec) {", + " if (left <= 0 && right <= 0) {", + " mgr.stopRumble();", + " return;", + " }", + "", + " mgr.rumbleLeft = left;", + " mgr.rumbleRight = right;", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " const dur = Number(durationSec);", + " if (dur > 0) {", + " mgr.rumbleRemaining = dur;", + " mgr.rumbleEndTime = now + dur * 1000;", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " }", + " if (typeof setTimeout !== 'undefined') {", + " mgr._rumbleStopTimer = setTimeout(function() {", + " mgr._rumbleStopTimer = null;", + " mgr.stopRumble();", + " }, dur * 1000);", + " }", + " } else {", + " mgr.rumbleRemaining = -1;", + " mgr.rumbleEndTime = 0;", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " mgr._rumbleStopTimer = null;", + " }", + " }", + "", + " mgr.outStruct[2] = right;", + " mgr.outStruct[3] = left;", + " if (mgr.device && mgr.device.opened) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " };", + "", + " mgr.sendFeatureCmd = async function(reportId, payloadBytes) {", + " if (!mgr.device || !mgr.device.opened) return false;", + " try {", + " let idealLen = 63;", + " const col = mgr.device.collections && mgr.device.collections.find(function(c) {", + " return c.usagePage === 0x0001 && c.usage === 0x0005;", + " });", + " if (col && col.featureReports) {", + " const meta = col.featureReports.find(function(r) { return r.reportId === reportId; });", + " if (meta && meta.items && meta.items[0] && meta.items[0].reportCount) {", + " idealLen = meta.items[0].reportCount;", + " }", + " }", + " const buf = new Uint8Array(idealLen);", + " buf.set(payloadBytes.subarray(0, Math.min(payloadBytes.length, idealLen)));", + " if (mgr.connectionType === 'Bluetooth') {", + " fillFeatureCrc32(reportId, buf);", + " }", + " await mgr.device.sendFeatureReport(reportId, buf);", + " return true;", + " } catch (e) {", + " return false;", + " }", + " };", + "", + " mgr.controlWaveOut = async function(enable, target) {", + " const ctrl = new Uint8Array(2 + 3);", + " ctrl[0] = 6; // DualSenseTestDeviceId.AUDIO", + " ctrl[1] = 2; // DualSenseTestActionId.WAVEOUT_CTRL", + " ctrl[2] = enable ? 1 : 0;", + " ctrl[3] = 1;", + " ctrl[4] = 0;", + "", + " if (enable) {", + " const calib = new Uint8Array(2 + 20);", + " calib[0] = 6; // AUDIO", + " calib[1] = 4; // BUILTIN_MIC_CALIB_DATA_VERIFY", + " if (target === 'headphone') {", + " calib[2 + 4] = 4;", + " calib[2 + 6] = 6;", + " } else {", + " calib[2 + 2] = 8;", + " }", + " await mgr.sendFeatureCmd(0x80, calib);", + " }", + " await mgr.sendFeatureCmd(0x80, ctrl);", + " };", + "", + " const HID_FILTERS = [", + " { vendorId: VENDOR_SONY, productId: PID_DUALSENSE, usagePage: 0x0001, usage: 0x0005 },", + " { vendorId: VENDOR_SONY, productId: PID_DUALSENSE_EDGE, usagePage: 0x0001, usage: 0x0005 }", + " ];", + "", + " function errMsg(e) {", + " if (!e) return 'Unknown error';", + " return (e.name ? e.name + ': ' : '') + (e.message || String(e));", + " }", + "", + " function pickDualSense(list) {", + " if (!list || !list.length) return null;", + " for (let i = 0; i < list.length; i++) {", + " if (isGamepadHidDevice(list[i])) return list[i];", + " }", + " return null;", + " }", + "", + " function isConnectedNow() {", + " return !!(mgr.device && mgr.device.opened);", + " }", + "", + " // ---------------------------------------------------------------", + " // GDevelop Desktop (Electron) support.", + " // WebHID permission handlers MUST live in the Electron main process:", + " // functions passed through @electron/remote become asynchronous IPC", + " // proxies that return \"undefined\" (= permission denied). So the", + " // handlers are compiled directly inside the main process instead.", + " // ---------------------------------------------------------------", + " const MAIN_PROCESS_SETUP_SRC = [", + " '(function (ses) {',", + " ' function isDs(d) { return !!d && d.vendorId === 1356 && (d.productId === 3302 || d.productId === 3570); }',", + " ' ses.setPermissionCheckHandler(function () { return true; });',", + " ' ses.setPermissionRequestHandler(function (wc, permission, callback) { callback(true); });',", + " ' ses.setDevicePermissionHandler(function (details) {',", + " ' if (details && details.deviceType === \"hid\") return isDs(details.device);',", + " ' return false;',", + " ' });',", + " ' var g = globalThis;',", + " ' if (g.__gdDualSenseSelectHid) { try { ses.removeListener(\"select-hid-device\", g.__gdDualSenseSelectHid); } catch (e) {} }',", + " ' g.__gdDualSenseSelectHid = function (event, details, callback) {',", + " ' event.preventDefault();',", + " ' var list = (details && details.deviceList) || [];',", + " ' for (var i = 0; i < list.length; i++) { if (isDs(list[i])) { callback(list[i].deviceId); return; } }',", + " ' var done = false;',", + " ' var onAdded = function (e, d) {',", + " ' if (done || !d || !isDs(d.device)) return;',", + " ' done = true;',", + " ' ses.removeListener(\"hid-device-added\", onAdded);',", + " ' callback(d.device.deviceId);',", + " ' };',", + " ' ses.on(\"hid-device-added\", onAdded);',", + " ' setTimeout(function () {',", + " ' if (done) return;',", + " ' done = true;',", + " ' ses.removeListener(\"hid-device-added\", onAdded);',", + " ' callback(\"\");',", + " ' }, 3000);',", + " ' };',", + " ' ses.on(\"select-hid-device\", g.__gdDualSenseSelectHid);',", + " ' return true;',", + " '})'", + " ].join(' ');", + "", + " function getElectronRemote(runtimeScene) {", + " let remote = null;", + " try {", + " const game = runtimeScene && runtimeScene.getGame ? runtimeScene.getGame() : null;", + " const renderer = game && game.getRenderer ? game.getRenderer() : null;", + " if (renderer && typeof renderer.getElectronRemote === 'function') {", + " remote = renderer.getElectronRemote();", + " }", + " } catch (e) {}", + " if (!remote && typeof require === 'function') {", + " try { remote = require('@electron/remote'); } catch (e) {}", + " }", + " return remote || null;", + " }", + "", + " function setupElectronPermissions(runtimeScene) {", + " if (mgr.electronChecked) return mgr.electronWebContents;", + " mgr.electronChecked = true;", + " const remote = getElectronRemote(runtimeScene);", + " if (!remote || typeof remote.getCurrentWebContents !== 'function') return null;", + " try {", + " const wc = remote.getCurrentWebContents();", + " mgr.electronWebContents = wc || null;", + " const ses = wc ? wc.session : null;", + " if (!ses) return mgr.electronWebContents;", + " let setupFn = null;", + " try { setupFn = remote.require('vm').runInThisContext(MAIN_PROCESS_SETUP_SRC); } catch (e) {}", + " if (!setupFn) {", + " try { setupFn = remote.getGlobal('eval')(MAIN_PROCESS_SETUP_SRC); } catch (e) {}", + " }", + " if (setupFn) {", + " mgr.electronPermissionsReady = !!setupFn(ses);", + " } else {", + " mgr.lastError = 'Could not install WebHID permissions in GDevelop';", + " }", + " } catch (e) {", + " mgr.lastError = 'Electron setup failed: ' + errMsg(e);", + " }", + " return mgr.electronWebContents;", + " }", + "", + " async function attachDevice(device) {", + " if (!device) return false;", + " if (mgr.device === device && device.opened) return true;", + " try {", + " if (!device.opened) {", + " await device.open();", + " }", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " mgr.status = 'Controller found but could not be opened (close Steam Input / DS4Windows / other apps using it)';", + " return false;", + " }", + " try {", + " const cType = detectConnectionType(device);", + " mgr.device = device;", + " mgr.connectionType = (cType === 'Unknown') ? 'USB' : cType;", + " mgr.model = (device.productId === PID_DUALSENSE_EDGE) ? 'DualSense Edge' : 'DualSense';", + " device.addEventListener('inputreport', handleInputReport);", + "", + " // Bluetooth: reading feature report 0x05/0x20 switches the controller from the", + " // reduced 9-byte 0x01 report to the full 77-byte 0x31 report (touchpad, gyro, ...).", + " try {", + " await device.receiveFeatureReport(0x05);", + " } catch (e) {", + " try { await device.receiveFeatureReport(0x20); } catch (e2) {}", + " }", + "", + " mgr.status = 'Connected';", + " mgr.lastError = '';", + " mgr._retryCount = 0;", + "", + " // Apply the full output state, including anything the events already set", + " // (lightbar, player LEDs, mute LED, triggers, volumes) while we were connecting.", + " await mgr.flushOutputReport(0x7F, 0x15, 0x03);", + " return true;", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " mgr.device = null;", + " mgr.connectionType = 'Disconnected';", + " mgr.model = 'None';", + " mgr.status = 'Connection failed';", + " return false;", + " }", + " }", + "", + " function detachDevice(device) {", + " if (mgr.device && (!device || mgr.device === device)) {", + " if (typeof mgr.stopSoundOnSpeaker === 'function') mgr.stopSoundOnSpeaker();", + " if (typeof mgr.stopRumble === 'function') mgr.stopRumble();", + " if (mgr._flushTimer) { clearTimeout(mgr._flushTimer); mgr._flushTimer = null; }", + " if (mgr._rumbleStopTimer) { clearTimeout(mgr._rumbleStopTimer); mgr._rumbleStopTimer = null; }", + " mgr.outputDirty = false;", + " mgr.sendQueued = false;", + " mgr.sendPending = false;", + " try {", + " mgr.device.removeEventListener('inputreport', handleInputReport);", + " } catch (e) {}", + " mgr.device = null;", + " mgr.connectionType = 'Disconnected';", + " mgr.model = 'None';", + " mgr.rawPs = false; mgr.rawMicBtn = false; mgr.rawTpPressed = false;", + " mgr.touch1.rawActive = false; mgr.touch2.rawActive = false;", + " mgr.status = mgr.wantConnection ? 'Controller lost - waiting for it to come back' : 'Disconnected';", + " }", + " }", + "", + " // Already-permitted devices (instant in GDevelop Desktop thanks to the main-process permission handler)", + " mgr.tryReconnectKnownDevices = async function() {", + " if (typeof navigator === 'undefined' || !navigator.hid) return false;", + " if (isConnectedNow()) return true;", + " try {", + " const devices = await navigator.hid.getDevices();", + " const ds = pickDualSense(devices);", + " if (ds) return await attachDevice(ds);", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " }", + " return false;", + " };", + "", + " // Must be called synchronously inside a user gesture (or Electron's simulated gesture).", + " mgr.requestDeviceNow = function() {", + " if (typeof navigator === 'undefined' || !navigator.hid) return Promise.resolve(false);", + " let p;", + " try {", + " p = navigator.hid.requestDevice({ filters: HID_FILTERS });", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " return Promise.resolve(false);", + " }", + " return p.then(function(chosen) {", + " const ds = pickDualSense(chosen);", + " if (!ds) return false;", + " return attachDevice(ds);", + " }).catch(function(e) {", + " mgr.lastError = errMsg(e);", + " return false;", + " });", + " };", + " if (typeof window !== 'undefined') {", + " window.__gdDualSenseRequestDevice = function() {", + " return mgr.requestDeviceNow().then(function(ok) { return !!ok; });", + " };", + " }", + "", + " // Browsers only show the WebHID device picker right after a click / key press.", + " function armGestureRetry() {", + " if (mgr.gestureArmed || typeof window === 'undefined') return;", + " mgr.gestureArmed = true;", + " const handler = function() {", + " window.removeEventListener('pointerdown', handler, true);", + " window.removeEventListener('keydown', handler, true);", + " mgr.gestureArmed = false;", + " if (!mgr.wantConnection || isConnectedNow()) return;", + " mgr.requestDeviceNow().then(function(ok) {", + " if (!ok && !isConnectedNow()) mgr.status = 'No DualSense selected';", + " });", + " };", + " window.addEventListener('pointerdown', handler, true);", + " window.addEventListener('keydown', handler, true);", + " }", + "", + " mgr.runConnectAttempt = async function(allowPicker) {", + " if (mgr.connecting || !mgr.wantConnection) return;", + " if (isConnectedNow()) return;", + " mgr.connecting = true;", + " if (mgr.status.indexOf('Connected') !== 0) mgr.status = 'Connecting...';", + " try {", + " if (await mgr.tryReconnectKnownDevices()) return;", + " if (!allowPicker) {", + " if (mgr.status.indexOf('could not be opened') === -1) mgr.status = 'Searching for a DualSense (plug it in or pair it via Bluetooth)';", + " return;", + " }", + " if (mgr.electronWebContents) {", + " // GDevelop Desktop: run requestDevice with a simulated user gesture.", + " // The main-process \"select-hid-device\" handler picks the DualSense automatically (no dialog).", + " try {", + " await mgr.electronWebContents.executeJavaScript(", + " '(window.__gdDualSenseRequestDevice ? window.__gdDualSenseRequestDevice() : false)',", + " true", + " );", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " }", + " if (isConnectedNow()) return;", + " // Last resort (only if the simulated gesture route was refused)", + " if (/SecurityError|gesture|activation/i.test(mgr.lastError) && await mgr.requestDeviceNow()) return;", + " if (mgr.status.indexOf('could not be opened') === -1) mgr.status = 'Searching for a DualSense (plug it in or pair it via Bluetooth)';", + " return;", + " }", + " // Web browser: try directly, otherwise wait for the next click / key press.", + " if (await mgr.requestDeviceNow()) return;", + " armGestureRetry();", + " mgr.status = 'Click or press a key in the game to choose your DualSense';", + " } finally {", + " mgr.connecting = false;", + " }", + " };", + "", + " // Called by the \"Connect DualSense controller\" action.", + " mgr.connect = function(runtimeScene) {", + " mgr.wantConnection = true;", + " if (isConnectedNow()) {", + " mgr.status = 'Connected';", + " return;", + " }", + " if (typeof navigator === 'undefined' || !navigator.hid) {", + " mgr.status = 'WebHID is not available here (use GDevelop Desktop, Chrome, Edge or Opera)';", + " return;", + " }", + " setupElectronPermissions(runtimeScene);", + " mgr._retryTimer = 0;", + " mgr.runConnectAttempt(true);", + " };", + "", + " // Called by the \"Disconnect DualSense controller\" action.", + " mgr.disconnect = async function() {", + " mgr.wantConnection = false;", + " if (typeof mgr.stopSoundOnSpeaker === 'function') mgr.stopSoundOnSpeaker();", + " if (typeof mgr.stopRumble === 'function') mgr.stopRumble();", + " const dev = mgr.device;", + " if (dev) {", + " try {", + " // Turn effects off before letting go of the controller", + " mgr.rainbowEnabled = false;", + " mgr.outStruct[2] = 0; mgr.outStruct[3] = 0;", + " mgr.outStruct[10] = 0; mgr.outStruct[21] = 0;", + " await mgr.flushOutputReport(0x0F, 0, 0);", + " } catch (e) {}", + " detachDevice(dev);", + " try { await dev.close(); } catch (e) {}", + " }", + " mgr.status = 'Disconnected';", + " };", + "", + " // Microphone volume monitoring (USB WebAudio + Bluetooth Opus 0x31/0x32)", + " mgr.startMicMonitoring = async function() {", + " if (mgr.micActive || mgr.micStarting) return;", + " mgr.micStarting = true;", + " try {", + " if (mgr.connectionType === 'Bluetooth' && mgr.device && mgr.device.opened && typeof AudioDecoder !== 'undefined') {", + " mgr.btMicTimestamp = 0;", + " mgr.btMicDecoder = new AudioDecoder({", + " output: function(audioData) {", + " try {", + " const n = audioData.numberOfFrames;", + " if (n > 0) {", + " const samples = new Float32Array(n);", + " audioData.copyTo(samples, { planeIndex: 0, format: 'f32-planar' });", + " let sumSq = 0;", + " let maxAbs = 0;", + " for (let i = 0; i < n; i++) {", + " const s = Math.abs(samples[i]);", + " sumSq += s * s;", + " if (s > maxAbs) maxAbs = s;", + " }", + " const rms = Math.sqrt(sumSq / n);", + " const vol = Math.min(100, rms * 320);", + " mgr.micVolume = mgr.micVolume * 0.65 + vol * 0.35;", + " mgr.micPeak = Math.min(100, maxAbs * 140);", + " }", + " } finally {", + " audioData.close();", + " }", + " },", + " error: function() {}", + " });", + " mgr.btMicDecoder.configure({", + " codec: 'opus',", + " sampleRate: 48000,", + " numberOfChannels: 1", + " });", + "", + " // Send BT mic enable reports (0x31 state + 0x32 control)", + " const r31 = new Uint8Array(77);", + " r31[0] = (mgr.btMicStateSeq++ & 0x0F) << 4;", + " r31[1] = 0x10;", + " r31[2] = 0xC0;", + " r31[3] = 0x83;", + " r31[8] = 0x08;", + " r31[9] = mgr.micPlugged ? 0x08 : 0x09;", + " r31[10] = 0x00;", + " r31[11] = 0x0F;", + " r31[39] = 0x01;", + " fillOutputCrc32(0x31, r31);", + " await mgr.device.sendReport(0x31, r31);", + "", + " const r32 = new Uint8Array(141);", + " const seq = mgr.btMicControlSeq++ & 0x0F;", + " r32[0] = seq << 4;", + " r32[1] = 0x91;", + " r32[2] = 0x07;", + " r32[3] = 0xFF;", + " r32.fill(0x40, 4, 9);", + " r32[9] = seq;", + " r32[10] = 0x92;", + " r32[11] = 0x40;", + " fillOutputCrc32(0x32, r32);", + " await mgr.device.sendReport(0x32, r32);", + "", + " mgr.micActive = true;", + " return;", + " }", + "", + " // USB / Standard WebAudio Microphone fallback (prefers DualSense / Wireless Controller input)", + " if (typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {", + " let targetDeviceId = undefined;", + " try {", + " const temp = await navigator.mediaDevices.getUserMedia({ audio: true });", + " temp.getTracks().forEach(function(t) { t.stop(); });", + " const devs = await navigator.mediaDevices.enumerateDevices();", + " const dsInput = devs.find(function(d) {", + " if (d.kind !== 'audioinput' || !d.label) return false;", + " const l = d.label.toLowerCase();", + " return l.indexOf('dualsense') !== -1 || l.indexOf('wireless controller') !== -1;", + " });", + " if (dsInput) targetDeviceId = dsInput.deviceId;", + " } catch (e) {}", + "", + " const constraints = targetDeviceId", + " ? { audio: { deviceId: { exact: targetDeviceId }, channelCount: { ideal: 2 }, echoCancellation: false, noiseSuppression: false, autoGainControl: false } }", + " : { audio: true };", + "", + " const stream = await navigator.mediaDevices.getUserMedia(constraints);", + " const ctx = new (window.AudioContext || window.webkitAudioContext)();", + " await ctx.resume();", + " const source = ctx.createMediaStreamSource(stream);", + " const analyser = ctx.createAnalyser();", + " analyser.fftSize = 512;", + " analyser.smoothingTimeConstant = 0.75;", + "", + " const track = stream.getAudioTracks()[0];", + " const chCount = (track && track.getSettings && track.getSettings().channelCount) || 2;", + " if (targetDeviceId && chCount >= 2) {", + " const splitter = ctx.createChannelSplitter(chCount);", + " source.connect(splitter);", + " splitter.connect(analyser, 1);", + " mgr.micSplitter = splitter;", + " } else {", + " source.connect(analyser);", + " }", + "", + " mgr.micStream = stream;", + " mgr.micAudioCtx = ctx;", + " mgr.micSource = source;", + " mgr.micAnalyser = analyser;", + " mgr.micDataArray = new Uint8Array(analyser.fftSize);", + " mgr.micActive = true;", + " }", + " } catch (e) {", + " mgr.micActive = false;", + " } finally {", + " mgr.micStarting = false;", + " }", + " };", + "", + " mgr.stopMicMonitoring = async function() {", + " mgr.micActive = false;", + " mgr.micVolume = 0;", + " mgr.micPeak = 0;", + " if (mgr.btMicDecoder) {", + " try { mgr.btMicDecoder.close(); } catch (e) {}", + " mgr.btMicDecoder = null;", + " if (mgr.device && mgr.device.opened && mgr.connectionType === 'Bluetooth') {", + " try {", + " const r32 = new Uint8Array(141);", + " const seq = mgr.btMicControlSeq++ & 0x0F;", + " r32[0] = seq << 4;", + " r32[1] = 0x91;", + " r32[2] = 0x07;", + " r32[3] = 0xFE;", + " r32.fill(0x40, 4, 9);", + " r32[9] = seq;", + " r32[10] = 0x92;", + " r32[11] = 0x40;", + " fillOutputCrc32(0x32, r32);", + " await mgr.device.sendReport(0x32, r32);", + " } catch (e) {}", + " }", + " }", + " if (mgr.micSplitter) {", + " try { mgr.micSplitter.disconnect(); } catch (e) {}", + " mgr.micSplitter = null;", + " }", + " if (mgr.micSource) {", + " try { mgr.micSource.disconnect(); } catch (e) {}", + " mgr.micSource = null;", + " }", + " if (mgr.micStream) {", + " try { mgr.micStream.getTracks().forEach(function(t) { t.stop(); }); } catch (e) {}", + " mgr.micStream = null;", + " }", + " if (mgr.micAudioCtx) {", + " try { await mgr.micAudioCtx.close(); } catch (e) {}", + " mgr.micAudioCtx = null;", + " }", + " mgr.micAnalyser = null;", + " mgr.micDataArray = null;", + " };", + "", + " // DualSense Speaker Controller (Bluetooth HID Opus Report 0x36 & USB Multi-Channel)", + " mgr.stopSoundOnSpeaker = function() {", + " if (mgr._btAudioAbort) {", + " try { mgr._btAudioAbort(); } catch (e) {}", + " mgr._btAudioAbort = null;", + " }", + " if (mgr._spkUsbSource) {", + " try { mgr._spkUsbSource.stop(); } catch (e) {}", + " mgr._spkUsbSource = null;", + " }", + " mgr._btAudioPlaying = false;", + " };", + "", + " async function loadAudioArrayBuffer(urlOrName, runtimeScene) {", + " let candidates = [urlOrName];", + " try {", + " if (runtimeScene && typeof runtimeScene.getGame === 'function') {", + " const game = runtimeScene.getGame();", + " const rl = game && typeof game.getResourceLoader === 'function' ? game.getResourceLoader() : null;", + " if (rl) {", + " const res = typeof rl.getResource === 'function' ? rl.getResource(urlOrName) : null;", + " if (res && res.file) {", + " if (typeof rl.getFullUrl === 'function') candidates.unshift(rl.getFullUrl(res.file));", + " candidates.push(res.file);", + " }", + " if (typeof rl.getFullUrl === 'function') {", + " candidates.push(rl.getFullUrl(urlOrName));", + " }", + " }", + " }", + " } catch (e) {}", + "", + " const isAbs = urlOrName.indexOf('://') !== -1 || urlOrName.indexOf('blob:') === 0 || urlOrName.indexOf('data:') === 0;", + " if (!isAbs) {", + " candidates.push('assets/' + urlOrName);", + " candidates.push('./' + urlOrName);", + " candidates.push(urlOrName);", + " }", + "", + " const uniqueUrls = Array.from(new Set(candidates));", + "", + " for (let i = 0; i < uniqueUrls.length; i++) {", + " const u = uniqueUrls[i];", + " if (!u) continue;", + " try {", + " const resp = await fetch(u);", + " if (resp && resp.ok) {", + " const buf = await resp.arrayBuffer();", + " if (buf && buf.byteLength > 0) return buf;", + " }", + " } catch (e) {}", + " try {", + " const fs = (typeof require === 'function') ? require('fs') : null;", + " if (fs && fs.readFileSync) {", + " let filePath = u;", + " if (filePath.indexOf('file:///') === 0) filePath = filePath.slice(8);", + " else if (filePath.indexOf('file://') === 0) filePath = filePath.slice(7);", + " if (fs.existsSync && fs.existsSync(filePath)) {", + " const nodeBuf = fs.readFileSync(filePath);", + " return nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);", + " }", + " }", + " } catch (e) {}", + " }", + " return null;", + " }", + "", + " async function playBtAudioStream(audioBuf, volumePct) {", + " if (!mgr.device || !mgr.device.opened) return false;", + " if (typeof AudioEncoder === 'undefined') {", + " mgr.lastError = 'WebCodecs AudioEncoder is not available for Bluetooth Opus streaming';", + " return false;", + " }", + "", + " // 1. Resample to 45,000 Hz stereo", + " // The DualSense DAC runs Bluetooth audio at ~45kHz.", + " // Nominal 48k Opus encoding of a 45k buffer results in ~10.667ms frame duration and correct pitch.", + " const sampleCount45k = Math.ceil(audioBuf.duration * 45000);", + " const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;", + " const offline45 = new OfflineCtx(2, sampleCount45k, 45000);", + " const src45 = offline45.createBufferSource();", + " src45.buffer = audioBuf;", + " src45.connect(offline45.destination);", + " src45.start(0);", + " const buffer45k = await offline45.startRendering();", + "", + " // 2. Encode to 200-byte Opus frames via WebCodecs AudioEncoder (48k, 2ch, 160k CBR, 10ms lowdelay)", + " const OPUS_FRAME_BYTES = 200;", + " const frames = [];", + " let encodeError = null;", + "", + " const encoder = new AudioEncoder({", + " output: function(chunk) {", + " const raw = new Uint8Array(chunk.byteLength);", + " chunk.copyTo(raw);", + " const frame = new Uint8Array(OPUS_FRAME_BYTES);", + " frame.set(raw.subarray(0, OPUS_FRAME_BYTES));", + " frames.push(frame);", + " },", + " error: function(err) {", + " encodeError = err;", + " }", + " });", + "", + " encoder.configure({", + " codec: 'opus',", + " sampleRate: 48000,", + " numberOfChannels: 2,", + " bitrate: 160000,", + " bitrateMode: 'constant',", + " opus: {", + " format: 'opus',", + " frameDuration: 10000,", + " application: 'lowdelay',", + " signal: 'music',", + " complexity: 1", + " }", + " });", + "", + " const ch0 = buffer45k.getChannelData(0);", + " const ch1 = buffer45k.numberOfChannels > 1 ? buffer45k.getChannelData(1) : ch0;", + " const planar = new Float32Array(ch0.length + ch1.length);", + " planar.set(ch0, 0);", + " planar.set(ch1, ch0.length);", + "", + " const audioData = new AudioData({", + " format: 'f32-planar',", + " sampleRate: 48000,", + " numberOfFrames: ch0.length,", + " numberOfChannels: 2,", + " timestamp: 0,", + " data: planar", + " });", + "", + " encoder.encode(audioData);", + " audioData.close();", + " await encoder.flush();", + " encoder.close();", + "", + " if (encodeError || frames.length === 0) {", + " if (encodeError) mgr.lastError = 'Bluetooth Opus encoding failed: ' + errMsg(encodeError);", + " return false;", + " }", + "", + " // 3. Prepare streaming clock and control", + " let aborted = false;", + " let rafId = 0;", + " let intervalId = 0;", + " let clockCtx = null;", + " let clockSource = null;", + "", + " mgr._btAudioAbort = function() {", + " aborted = true;", + " if (rafId) {", + " if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(rafId);", + " rafId = 0;", + " }", + " if (intervalId) {", + " if (typeof clearInterval !== 'undefined') clearInterval(intervalId);", + " intervalId = 0;", + " }", + " if (clockSource) {", + " try { clockSource.stop(); } catch (e) {}", + " try { clockSource.disconnect(); } catch (e) {}", + " clockSource = null;", + " }", + " if (clockCtx) {", + " try { clockCtx.close(); } catch (e) {}", + " clockCtx = null;", + " }", + " mgr._btAudioPlaying = false;", + " };", + "", + " mgr._btAudioPlaying = true;", + "", + " const FRAME_DURATION_S = 480 / 45000; // ~0.010667s (10.667ms per frame)", + " const MAX_CATCHUP_FRAMES = 8;", + " const devVol = Math.round(Math.max(0, Math.min(100, volumePct)) * 0x64 / 100);", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " clockCtx = new AudioCtx();", + " await clockCtx.resume();", + "", + " clockSource = clockCtx.createBufferSource();", + " const clockBuffer = clockCtx.createBuffer(1, Math.max(1, Math.ceil(audioBuf.duration * clockCtx.sampleRate)), clockCtx.sampleRate);", + " clockSource.buffer = clockBuffer;", + " const silentGain = clockCtx.createGain();", + " silentGain.gain.value = 0;", + " clockSource.connect(silentGain);", + " silentGain.connect(clockCtx.destination);", + "", + " let startedAt = clockCtx.currentTime;", + " let lastSentFrame = -1;", + " let sendSeq = 0;", + " let frameCounter = 0;", + " const totalFrames = frames.length;", + "", + " clockSource.onended = function() {", + " if (!aborted && mgr._btAudioAbort) {", + " mgr._btAudioAbort();", + " }", + " };", + " clockSource.start(0);", + "", + " function sendFrame(idx) {", + " if (aborted || !mgr.device || !mgr.device.opened) return;", + " const opus = frames[idx];", + " if (!opus) return;", + "", + " const payload = new Uint8Array(397);", + " payload[0] = ((sendSeq & 0x0F) << 4);", + "", + " // Control subpacket (speaker volume + audio route)", + " payload[1] = 0x90;", + " payload[2] = 0x3F;", + " payload[3] = 0xA0; // valid_flag0: bit 5 (speaker volume) + bit 7 (audio control)", + " payload[4] = 0x00;", + " payload[8] = devVol & 0xFF; // speaker volume (0x00 .. 0x64)", + " payload[10] = 0x09; // audioControl", + "", + " // Audio subpacket", + " payload[66] = 0x91;", + " payload[67] = 0x07;", + " payload[68] = 0xFE;", + " payload[69] = 0x40;", + " payload[70] = 0x40;", + " payload[71] = 0x40;", + " payload[72] = 0x40;", + " payload[73] = 0x40;", + " payload[74] = frameCounter & 0xFF;", + " payload[75] = 0x93; // 0x93 = route to speaker", + " payload[76] = 0xC8; // 200 bytes opus frame", + " payload.set(opus.subarray(0, 200), 77);", + "", + " // Haptic subpacket (silence)", + " payload[277] = 0x92;", + " payload[278] = 0x40;", + "", + " fillOutputCrc32(0x36, payload);", + "", + " sendSeq = (sendSeq + 1) & 0x0F;", + " frameCounter = (frameCounter + 1) & 0xFF;", + "", + " mgr.device.sendReport(0x36, payload).catch(function(err) {});", + " }", + "", + " function tick() {", + " if (aborted || !clockCtx) return;", + " const t = clockCtx.currentTime - startedAt;", + " const targetFrame = Math.floor(t / FRAME_DURATION_S);", + "", + " if (targetFrame - lastSentFrame > MAX_CATCHUP_FRAMES) {", + " lastSentFrame = targetFrame - 1;", + " }", + "", + " while (lastSentFrame < targetFrame && lastSentFrame < totalFrames - 1) {", + " lastSentFrame++;", + " sendFrame(lastSentFrame);", + " }", + "", + " if (lastSentFrame >= totalFrames - 1 && t >= audioBuf.duration) {", + " if (mgr._btAudioAbort) mgr._btAudioAbort();", + " }", + " }", + "", + " if (typeof requestAnimationFrame !== 'undefined') {", + " const rafLoop = function() {", + " if (aborted) return;", + " tick();", + " if (!aborted) rafId = requestAnimationFrame(rafLoop);", + " };", + " rafId = requestAnimationFrame(rafLoop);", + " }", + " tick();", + " if (typeof setInterval !== 'undefined') intervalId = setInterval(tick, 8);", + "", + " return true;", + " }", + "", + " async function playUsbAudio(audioBuf, vol) {", + " mgr.outStruct[5] = Math.round(vol * 0x64 / 100);", + " mgr.outStruct[7] = (3 << 4); // route to speaker", + " await mgr.flushOutputReport(0xA0, 0, 0);", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " if (!mgr.spkAudioCtx) {", + " mgr.spkAudioCtx = new AudioCtx();", + " }", + " const ctx = mgr.spkAudioCtx;", + " await ctx.resume();", + "", + " if (!mgr.spkSinkId && typeof ctx.setSinkId === 'function' && navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {", + " try {", + " const devs = await navigator.mediaDevices.enumerateDevices();", + " const dsOut = devs.find(function(d) {", + " if (d.kind !== 'audiooutput' || !d.label) return false;", + " const l = d.label.toLowerCase();", + " return l.indexOf('dualsense') !== -1 || l.indexOf('wireless controller') !== -1;", + " });", + " if (dsOut && dsOut.deviceId) {", + " await ctx.setSinkId(dsOut.deviceId);", + " mgr.spkSinkId = dsOut.deviceId;", + " }", + " } catch (e) {}", + " }", + "", + " const src = ctx.createBufferSource();", + " src.buffer = audioBuf;", + " const gainNode = ctx.createGain();", + " gainNode.gain.value = vol / 100;", + "", + " if (ctx.destination.maxChannelCount >= 4) {", + " ctx.destination.channelCount = 4;", + " ctx.destination.channelCountMode = 'explicit';", + " ctx.destination.channelInterpretation = 'discrete';", + " const merger = ctx.createChannelMerger(4);", + " const splitter = ctx.createChannelSplitter(2);", + " src.connect(gainNode);", + " gainNode.connect(splitter);", + " splitter.connect(merger, 0, 0);", + " splitter.connect(merger, 1, 1);", + " merger.connect(ctx.destination);", + " } else {", + " src.connect(gainNode);", + " gainNode.connect(ctx.destination);", + " }", + " mgr._spkUsbSource = src;", + " src.onended = function() {", + " if (mgr._spkUsbSource === src) mgr._spkUsbSource = null;", + " };", + " src.start(0);", + " }", + "", + " // Play audio file through DualSense Speaker (Bluetooth HID Opus Report 0x36 or USB 4-ch endpoint)", + " mgr.playSoundOnSpeaker = async function(audioResourceName, volumePct, runtimeScene) {", + " if (!mgr.device || !mgr.device.opened) {", + " mgr.lastError = 'DualSense controller is not connected';", + " return;", + " }", + " try {", + " const vol = Math.max(0, Math.min(100, volumePct));", + " mgr.speakerVolume = vol;", + "", + " const arrBuf = await loadAudioArrayBuffer(audioResourceName, runtimeScene);", + " if (!arrBuf) {", + " mgr.lastError = 'Could not load audio resource: ' + audioResourceName;", + " return;", + " }", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " const decodeCtx = new AudioCtx();", + " let audioBuf = null;", + " try {", + " audioBuf = await decodeCtx.decodeAudioData(arrBuf);", + " } catch (e) {", + " mgr.lastError = 'Audio decoding failed: ' + errMsg(e);", + " return;", + " } finally {", + " try { await decodeCtx.close(); } catch (e) {}", + " }", + "", + " if (!audioBuf) return;", + "", + " mgr.stopSoundOnSpeaker();", + "", + " const isBt = (mgr.connectionType === 'Bluetooth') || (mgr.device && detectConnectionType(mgr.device) === 'Bluetooth');", + " if (isBt) {", + " await playBtAudioStream(audioBuf, vol);", + " } else {", + " await playUsbAudio(audioBuf, vol);", + " }", + " } catch (e) {", + " mgr.lastError = 'Speaker playback error: ' + errMsg(e);", + " }", + " };", + "", + " // Frame update (called automatically in onScenePreEvents)", + " mgr.stepFrame = function(runtimeScene) {", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " if (now - mgr._lastStepNow < 4) return;", + " mgr._lastStepNow = now;", + "", + " const dt = runtimeScene ? runtimeScene.getElapsedTime() / 1000.0 : 0.016;", + "", + " // Keep looking for the controller while the events want a connection", + " if (mgr.wantConnection && !isConnectedNow() && !mgr.connecting) {", + " mgr._retryTimer += dt;", + " if (mgr._retryTimer >= 1.5) {", + " mgr._retryTimer = 0;", + " mgr._retryCount++;", + " // Electron: occasionally re-run the (automatic, dialog-free) device picker too", + " const allowPicker = !!mgr.electronWebContents && (mgr._retryCount % 4 === 0);", + " mgr.runConnectAttempt(allowPicker);", + " }", + " }", + "", + " // Update edge-triggered button states", + " mgr.psJustPressed = mgr.rawPs && !mgr.psPrev;", + " mgr.psPressed = mgr.rawPs;", + " mgr.psPrev = mgr.rawPs;", + "", + " mgr.micBtnJustPressed = mgr.rawMicBtn && !mgr.micBtnPrev;", + " mgr.micBtnPressed = mgr.rawMicBtn;", + " mgr.micBtnPrev = mgr.rawMicBtn;", + "", + " mgr.tpJustPressed = mgr.rawTpPressed && !mgr.tpPrev;", + " mgr.tpPressed = mgr.rawTpPressed;", + " mgr.tpPrev = mgr.rawTpPressed;", + "", + " mgr.fnLPressed = mgr.rawFnL;", + " mgr.fnRPressed = mgr.rawFnR;", + " mgr.paddleLPressed = mgr.rawPaddleL;", + " mgr.paddleRPressed = mgr.rawPaddleR;", + "", + " // Update Touchpad deltas & edge-triggers", + " const t1 = mgr.touch1;", + " t1.justStarted = t1.rawActive && !t1.prevActive;", + " t1.active = t1.rawActive;", + " if (t1.active) {", + " if (t1.prevActive) {", + " t1.dx = t1.x - t1.prevX;", + " t1.dy = t1.y - t1.prevY;", + " } else {", + " t1.dx = 0;", + " t1.dy = 0;", + " }", + " t1.prevX = t1.x;", + " t1.prevY = t1.y;", + " } else {", + " t1.dx = 0;", + " t1.dy = 0;", + " }", + " t1.prevActive = t1.active;", + "", + " const t2 = mgr.touch2;", + " t2.justStarted = t2.rawActive && !t2.prevActive;", + " t2.active = t2.rawActive;", + " if (t2.active) {", + " if (t2.prevActive) {", + " t2.dx = t2.x - t2.prevX;", + " t2.dy = t2.y - t2.prevY;", + " } else {", + " t2.dx = 0;", + " t2.dy = 0;", + " }", + " t2.prevX = t2.x;", + " t2.prevY = t2.y;", + " } else {", + " t2.dx = 0;", + " t2.dy = 0;", + " }", + " t2.prevActive = t2.active;", + "", + " // Update USB WebAudio Mic analyser if active", + " if (mgr.micActive && mgr.micAnalyser && mgr.micDataArray) {", + " mgr.micAnalyser.getByteTimeDomainData(mgr.micDataArray);", + " let sumSq = 0;", + " let maxAbs = 0;", + " const len = mgr.micDataArray.length;", + " for (let i = 0; i < len; i++) {", + " const norm = (mgr.micDataArray[i] - 128) / 128.0;", + " const absVal = Math.abs(norm);", + " sumSq += norm * norm;", + " if (absVal > maxAbs) maxAbs = absVal;", + " }", + " const rms = Math.sqrt(sumSq / len);", + " const vol = Math.min(100, rms * 300);", + " mgr.micVolume = mgr.micVolume * 0.7 + vol * 0.3;", + " mgr.micPeak = Math.min(100, maxAbs * 130);", + " }", + "", + " // Timed vibration update and watchdog refresh", + " const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " if (mgr.rumbleEndTime > 0) {", + " if (nowMs >= mgr.rumbleEndTime) {", + " mgr.stopRumble();", + " } else {", + " mgr.rumbleRemaining = Math.max(0, (mgr.rumbleEndTime - nowMs) / 1000.0);", + " }", + " }", + " if (mgr.rumbleLeft > 0 || mgr.rumbleRight > 0) {", + " if (nowMs - mgr._lastOutputSentTime >= 35) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " }", + "", + " // If output is dirty or queued, and rate-limit window has elapsed, flush it now", + " if (mgr.outputDirty || mgr.sendQueued) {", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " if (nowMs - mgr._lastOutputSentTime >= minInterval) {", + " mgr._doFlush();", + " }", + " }", + "", + " // Timed hardware test tone auto-stop", + " if (mgr.toneRemaining > 0) {", + " mgr.toneRemaining -= dt;", + " if (mgr.toneRemaining <= 0) {", + " mgr.toneRemaining = -1;", + " mgr.controlWaveOut(false, mgr.toneTarget);", + " }", + " }", + "", + " // Rainbow lightbar cycle", + " if (mgr.rainbowEnabled && mgr.device && mgr.device.opened) {", + " mgr.rainbowHue = (mgr.rainbowHue + mgr.rainbowSpeed * dt) % 360;", + " if (mgr.rainbowHue < 0) mgr.rainbowHue += 360;", + " mgr.rainbowTimer += dt;", + " if (mgr.rainbowTimer >= 0.033) {", + " mgr.rainbowTimer = 0;", + " const rgb = hsvToRgb(mgr.rainbowHue, 1, 1);", + " mgr.lightbarR = rgb[0];", + " mgr.lightbarG = rgb[1];", + " mgr.lightbarB = rgb[2];", + " mgr.outStruct[44] = rgb[0];", + " mgr.outStruct[45] = rgb[1];", + " mgr.outStruct[46] = rgb[2];", + " mgr.flushOutputReport(0, 0x04, 0);", + " }", + " }", + " };", + "", + " // WebHID hotplug: only (re)attach when the events asked for a connection", + " if (typeof navigator !== 'undefined' && navigator.hid) {", + " navigator.hid.addEventListener('connect', function(ev) {", + " if (mgr.wantConnection && !isConnectedNow() && isGamepadHidDevice(ev.device)) {", + " attachDevice(ev.device);", + " }", + " });", + " navigator.hid.addEventListener('disconnect', function(ev) {", + " detachDevice(ev.device);", + " });", + " }", + "", + " gdjs._dualSenseManager = mgr;", + " return mgr;", + " };", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Updates DualSense touchpad deltas, edge-triggered buttons, microphone volume, timed effects and connection retries each frame.", + "fullName": "On scene pre-events", + "functionType": "Action", + "name": "onScenePreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const mgr = gdjs._dualSenseManager;", + "if (!mgr) return;", + "mgr.stepFrame(runtimeScene);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Starts connecting to a PlayStation 5 DualSense or DualSense Edge controller over USB or Bluetooth. It keeps searching in the background until a controller is found, and reconnects automatically if needed.", + "fullName": "Connect DualSense controller", + "functionType": "Action", + "name": "ConnectDualSense", + "sentence": "Connect DualSense controller (USB or Bluetooth)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) {", + " gdjs._dualSenseEnsureInit = function(runtimeScene) {", + " if (gdjs._dualSenseManager) {", + " return gdjs._dualSenseManager;", + " }", + "", + " // CRC32 lookup table for DualSense Bluetooth output & feature reports", + " const CRC32_TABLE = new Uint32Array(256);", + " for (let i = 0; i < 256; i++) {", + " let c = i;", + " for (let k = 0; k < 8; k++) {", + " c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);", + " }", + " CRC32_TABLE[i] = c >>> 0;", + " }", + "", + " function computeCrc32(prefixBytes, dataView, length) {", + " let crc = 0xFFFFFFFF;", + " for (let i = 0; i < prefixBytes.length; i++) {", + " crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ prefixBytes[i]) & 0xFF];", + " }", + " for (let i = 0; i < length; i++) {", + " const b = dataView.getUint8(i);", + " crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ b) & 0xFF];", + " }", + " return (crc ^ 0xFFFFFFFF) >>> 0;", + " }", + "", + " function fillOutputCrc32(reportId, reportBytes) {", + " const dv = new DataView(reportBytes.buffer, reportBytes.byteOffset, reportBytes.byteLength);", + " const crc = computeCrc32([0xA2, reportId], dv, reportBytes.byteLength - 4);", + " dv.setUint32(reportBytes.byteLength - 4, crc, true);", + " }", + "", + " function fillFeatureCrc32(reportId, reportBytes) {", + " const dv = new DataView(reportBytes.buffer, reportBytes.byteOffset, reportBytes.byteLength);", + " const crc = computeCrc32([0x53, reportId], dv, reportBytes.byteLength - 4);", + " dv.setUint32(reportBytes.byteLength - 4, crc, true);", + " }", + "", + " function hsvToRgb(h, s, v) {", + " const c = v * s;", + " const x = c * (1 - Math.abs(((h / 60) % 2) - 1));", + " const m = v - c;", + " let r = 0, g = 0, b = 0;", + " if (h < 60) { r = c; g = x; b = 0; }", + " else if (h < 120) { r = x; g = c; b = 0; }", + " else if (h < 180) { r = 0; g = c; b = x; }", + " else if (h < 240) { r = 0; g = x; b = c; }", + " else if (h < 300) { r = x; g = 0; b = c; }", + " else { r = c; g = 0; b = x; }", + " return [", + " Math.round((r + m) * 255),", + " Math.round((g + m) * 255),", + " Math.round((b + m) * 255)", + " ];", + " }", + "", + " const VENDOR_SONY = 0x054C;", + " const PID_DUALSENSE = 0x0CE6;", + " const PID_DUALSENSE_EDGE = 0x0DF2;", + "", + " const mgr = {", + " device: null,", + " connectionType: 'Disconnected', // 'USB' | 'Bluetooth' | 'Disconnected'", + " model: 'None', // 'DualSense' | 'DualSense Edge' | 'None'", + " connecting: false,", + " outputSeq: 0,", + " sendPending: false,", + " sendQueued: false,", + " outputDirty: false,", + " _lastOutputSentTime: 0,", + " _flushTimer: null,", + " _microtaskPromise: null,", + " _lastStepNow: 0,", + " wantConnection: false, // only true after the user's \"Connect\" action", + " status: 'Not connected (use the Connect action)',", + " lastError: '',", + " gestureArmed: false,", + " electronChecked: false,", + " electronWebContents: null,", + " electronPermissionsReady: false,", + " _retryTimer: 0,", + " _retryCount: 0,", + "", + " // 47-byte base output report buffer (shared structure for USB 0x02 and BT 0x31+2)", + " outStruct: new Uint8Array(47),", + " pendingFlags0: 0,", + " pendingFlags1: 0,", + " pendingFlags2: 0,", + "", + " // Input state", + " psPressed: false,", + " psPrev: false,", + " psJustPressed: false,", + " rawPs: false,", + "", + " micBtnPressed: false,", + " micBtnPrev: false,", + " micBtnJustPressed: false,", + " rawMicBtn: false,", + "", + " tpPressed: false,", + " tpPrev: false,", + " tpJustPressed: false,", + " rawTpPressed: false,", + "", + " fnLPressed: false,", + " fnRPressed: false,", + " paddleLPressed: false,", + " paddleRPressed: false,", + " rawFnL: false,", + " rawFnR: false,", + " rawPaddleL: false,", + " rawPaddleR: false,", + "", + " triggerStopLeft: 0,", + " triggerStopRight: 0,", + "", + " // Touchpad 2-point capacitive state", + " touch1: { active: false, prevActive: false, justStarted: false, rawActive: false, id: 0, x: 0, y: 0, prevX: 0, prevY: 0, dx: 0, dy: 0 },", + " touch2: { active: false, prevActive: false, justStarted: false, rawActive: false, id: 0, x: 0, y: 0, prevX: 0, prevY: 0, dx: 0, dy: 0 },", + "", + " // 6-axis IMU (Gyro + Accel)", + " gyroPitch: 0,", + " gyroYaw: 0,", + " gyroRoll: 0,", + " gyroBiasPitch: 0,", + " gyroBiasYaw: 0,", + " gyroBiasRoll: 0,", + " accelX: 0,", + " accelY: 0,", + " accelZ: 0,", + " tiltPitchDeg: 0,", + " tiltRollDeg: 0,", + "", + " // Telemetry", + " batteryLevel: 0,", + " batteryStatus: 'Unknown',", + " headphonePlugged: false,", + " micPlugged: false,", + "", + " // Output state tracking", + " lightbarR: 0,", + " lightbarG: 120,", + " lightbarB: 255,", + " rainbowEnabled: false,", + " rainbowSpeed: 120,", + " rainbowHue: 0,", + " rainbowTimer: 0,", + "", + " rumbleLeft: 0,", + " rumbleRight: 0,", + " rumbleRemaining: 0,", + " rumbleEndTime: 0,", + " _rumbleStopTimer: null,", + "", + " speakerVolume: 85,", + " headphoneVolume: 65,", + " toneRemaining: -1,", + " toneTarget: 'speaker',", + "", + " // Microphone volume monitoring", + " micActive: false,", + " micStarting: false,", + " micVolume: 0,", + " micPeak: 0,", + " micAudioCtx: null,", + " micAnalyser: null,", + " micDataArray: null,", + " micStream: null,", + " micSource: null,", + " micSplitter: null,", + " btMicDecoder: null,", + " btMicStateSeq: 0,", + " btMicControlSeq: 0,", + " btMicTimestamp: 0,", + " btMicHandler: null,", + "", + " // Speaker WebAudio player & Bluetooth Opus streamer", + " spkAudioCtx: null,", + " spkSinkId: null,", + " _spkUsbSource: null,", + " _btAudioAbort: null,", + " _btAudioPlaying: false", + " };", + "", + " // Default initial output values", + " mgr.outStruct[4] = 0x50; // headphone vol", + " mgr.outStruct[5] = 0x55; // speaker vol", + " mgr.outStruct[6] = 0x40; // mic vol", + " mgr.outStruct[41] = 2; // lightbarSetup", + " mgr.outStruct[42] = 0; // ledBrightness High", + " mgr.outStruct[43] = 0x04; // Player 1 LED", + " mgr.outStruct[44] = 0;", + " mgr.outStruct[45] = 120;", + " mgr.outStruct[46] = 255;", + "", + " function detectConnectionType(device) {", + " if (device && device.collections) {", + " for (let i = 0; i < device.collections.length; i++) {", + " const c = device.collections[i];", + " if (c.usagePage !== 0x0001 || c.usage !== 0x0005) continue;", + " let maxBits = 0;", + " if (c.inputReports) {", + " for (let r = 0; r < c.inputReports.length; r++) {", + " const rep = c.inputReports[r];", + " let bits = 0;", + " if (rep.items) {", + " for (let it = 0; it < rep.items.length; it++) {", + " bits += (rep.items[it].reportSize || 0) * (rep.items[it].reportCount || 0);", + " }", + " }", + " if (bits > maxBits) maxBits = bits;", + " }", + " }", + " if (maxBits === 504) return 'USB';", + " if (maxBits === 616) return 'Bluetooth';", + " }", + " }", + " return 'Unknown';", + " }", + "", + " function isGamepadHidDevice(device) {", + " if (!device || device.vendorId !== VENDOR_SONY) return false;", + " if (device.productId !== PID_DUALSENSE && device.productId !== PID_DUALSENSE_EDGE) return false;", + " if (device.collections && device.collections.length > 0) {", + " return device.collections.some(function(c) {", + " return c.usagePage === 0x0001 && c.usage === 0x0005;", + " });", + " }", + " return true;", + " }", + "", + " function handleInputReport(event) {", + " const data = event.data;", + " const reportId = event.reportId;", + " if (!data) return;", + "", + " // Check if this is a Bluetooth microphone Opus audio frame (reportId 0x31, low nibble === 0x02)", + " if (reportId === 0x31 && data.byteLength >= 73 && (data.getUint8(0) & 0x0F) === 0x02) {", + " if (mgr.micActive && mgr.btMicDecoder && mgr.btMicDecoder.state === 'configured') {", + " try {", + " const opusPayload = new Uint8Array(data.buffer, data.byteOffset + 2, 71);", + " mgr.btMicDecoder.decode(new EncodedAudioChunk({", + " type: 'key',", + " timestamp: mgr.btMicTimestamp,", + " duration: 10000,", + " data: opusPayload", + " }));", + " mgr.btMicTimestamp += 10000;", + " } catch (e) {}", + " }", + " return;", + " }", + "", + " // Determine offset: USB report 0x01 (63 bytes) has offset 0; BT extended report 0x31 (77 bytes) has offset +1", + " let isUsbReport = true;", + " if (reportId === 0x31) {", + " isUsbReport = false;", + " if (mgr.connectionType !== 'Bluetooth') mgr.connectionType = 'Bluetooth';", + " } else if (reportId === 0x01 && mgr.connectionType === 'Bluetooth' && data.byteLength < 55) {", + " // Legacy 9-byte Bluetooth report before extended 0x31 switch completes:", + " if (data.byteLength >= 7) {", + " const b6 = data.getUint8(6);", + " mgr.rawPs = (b6 & 1) !== 0;", + " mgr.rawTpPressed = (b6 & 2) !== 0;", + " }", + " return;", + " }", + "", + " const num = isUsbReport ? 0 : 1;", + " if (data.byteLength < 54 + num) return;", + "", + " // Digital keys (PS, Touchpad click, Mic Mute, DualSense Edge Fn & Back Paddles)", + " const keys2 = data.getUint8(7 + num + 2);", + " mgr.rawPs = (keys2 & 1) !== 0;", + " mgr.rawTpPressed = (keys2 & 2) !== 0;", + " mgr.rawMicBtn = (keys2 & 4) !== 0;", + " mgr.rawFnL = (keys2 & 16) !== 0;", + " mgr.rawFnR = (keys2 & 32) !== 0;", + " mgr.rawPaddleL = (keys2 & 64) !== 0;", + " mgr.rawPaddleR = (keys2 & 128) !== 0;", + "", + " // Gyroscope (16-bit signed little-endian)", + " const rawGyroPitch = data.getInt16(15 + num, true);", + " const rawGyroYaw = data.getInt16(17 + num, true);", + " const rawGyroRoll = data.getInt16(19 + num, true);", + " mgr.gyroPitch = rawGyroPitch - mgr.gyroBiasPitch;", + " mgr.gyroYaw = rawGyroYaw - mgr.gyroBiasYaw;", + " mgr.gyroRoll = rawGyroRoll - mgr.gyroBiasRoll;", + "", + " // Accelerometer (16-bit signed little-endian)", + " const ax = data.getInt16(21 + num, true);", + " const ay = data.getInt16(23 + num, true);", + " const az = data.getInt16(25 + num, true);", + " mgr.accelX = ax;", + " mgr.accelY = ay;", + " mgr.accelZ = az;", + "", + " // Compute intuitive tilt angles in degrees from gravity vector", + " const norm = Math.sqrt(ax * ax + ay * ay + az * az);", + " if (norm > 100) {", + " mgr.tiltPitchDeg = Math.atan2(az, Math.sqrt(ax * ax + ay * ay)) * (180 / Math.PI);", + " mgr.tiltRollDeg = Math.atan2(-ax, ay) * (180 / Math.PI);", + " }", + "", + " // Touchpad 2-finger capacitive coordinates (1920 x 1080)", + " const td = 32 + num;", + " const idByte1 = data.getUint8(td);", + " const active1 = (idByte1 & 0x80) === 0;", + " const x1 = ((data.getUint8(td + 2) & 0x0F) << 8) | data.getUint8(td + 1);", + " const y1 = (data.getUint8(td + 3) << 4) | (data.getUint8(td + 2) >> 4);", + " mgr.touch1.rawActive = active1;", + " mgr.touch1.id = idByte1 & 0x7F;", + " if (active1) {", + " mgr.touch1.x = x1;", + " mgr.touch1.y = y1;", + " }", + "", + " const idByte2 = data.getUint8(td + 4);", + " const active2 = (idByte2 & 0x80) === 0;", + " const x2 = ((data.getUint8(td + 6) & 0x0F) << 8) | data.getUint8(td + 5);", + " const y2 = (data.getUint8(td + 7) << 4) | (data.getUint8(td + 6) >> 4);", + " mgr.touch2.rawActive = active2;", + " mgr.touch2.id = idByte2 & 0x7F;", + " if (active2) {", + " mgr.touch2.x = x2;", + " mgr.touch2.y = y2;", + " }", + "", + " // DualSense Edge hardware trigger stop sliders", + " const trigLvl = data.getUint8(49 + num);", + " mgr.triggerStopLeft = (trigLvl >> 4) & 3;", + " mgr.triggerStopRight = (trigLvl >> 6) & 3;", + "", + " // Battery & jack status", + " const status0 = data.getUint8(52 + num);", + " const status1 = data.getUint8(53 + num);", + " const chargeCode = (status0 & 0xF0) >> 4;", + " let batStep = status0 & 0x0F;", + " if (chargeCode === 2) {", + " mgr.batteryStatus = 'Full';", + " mgr.batteryLevel = 100;", + " } else if (chargeCode === 1) {", + " mgr.batteryStatus = 'Charging';", + " mgr.batteryLevel = Math.min(100, batStep * 10 + 5);", + " } else if (chargeCode === 0) {", + " mgr.batteryStatus = 'Discharging';", + " mgr.batteryLevel = Math.min(100, batStep * 10 + 5);", + " } else {", + " mgr.batteryStatus = 'Error';", + " }", + " mgr.headphonePlugged = (status1 & 1) !== 0;", + " mgr.micPlugged = (status1 & 2) !== 0;", + " }", + "", + " mgr.flushOutputReport = function(f0, f1, f2) {", + " mgr.pendingFlags0 |= (f0 || 0);", + " mgr.pendingFlags1 |= (f1 || 0);", + " mgr.pendingFlags2 |= (f2 || 0);", + " mgr.outputDirty = true;", + "", + " if (!mgr.device || !mgr.device.opened) return;", + "", + " if (!mgr._microtaskPromise) {", + " mgr._microtaskPromise = Promise.resolve().then(function() {", + " mgr._microtaskPromise = null;", + " mgr._doFlush();", + " });", + " }", + " return mgr._microtaskPromise;", + " };", + "", + " mgr._doFlush = function() {", + " if (!mgr.device || !mgr.device.opened) return;", + " if (!mgr.outputDirty && !mgr.sendQueued) return;", + "", + " if (mgr.sendPending) {", + " mgr.sendQueued = true;", + " return;", + " }", + "", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " const elapsed = now - mgr._lastOutputSentTime;", + "", + " if (elapsed < minInterval) {", + " mgr.sendQueued = true;", + " if (!mgr._flushTimer && typeof setTimeout !== 'undefined') {", + " const delay = Math.max(1, minInterval - elapsed);", + " mgr._flushTimer = setTimeout(function() {", + " mgr._flushTimer = null;", + " mgr._doFlush();", + " }, delay);", + " }", + " return;", + " }", + "", + " if (mgr._flushTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._flushTimer);", + " mgr._flushTimer = null;", + " }", + "", + " mgr.sendPending = true;", + " (async function() {", + " try {", + " mgr.sendQueued = false;", + " let useF0 = mgr.pendingFlags0;", + " let useF1 = mgr.pendingFlags1;", + " let useF2 = mgr.pendingFlags2;", + " mgr.pendingFlags0 = 0;", + " mgr.pendingFlags1 = 0;", + " mgr.pendingFlags2 = 0;", + " mgr.outputDirty = false;", + "", + " // If rumble is active, ALWAYS maintain motor values and validFlag0 bits 0 & 1 active", + " // so no other event (lightbar, trigger, LED, volume) ever starves or resets the vibration!", + " if (mgr.rumbleLeft > 0 || mgr.rumbleRight > 0) {", + " useF0 |= 0x03;", + " mgr.outStruct[2] = mgr.rumbleRight;", + " mgr.outStruct[3] = mgr.rumbleLeft;", + " }", + "", + " mgr.outStruct[0] = useF0;", + " mgr.outStruct[1] = useF1;", + " mgr.outStruct[38] = useF2;", + "", + " if (mgr.connectionType === 'USB') {", + " await mgr.device.sendReport(0x02, new Uint8Array(mgr.outStruct));", + " } else {", + " const btBuf = new Uint8Array(77);", + " btBuf[0] = (mgr.outputSeq & 0x0F) << 4;", + " mgr.outputSeq = (mgr.outputSeq + 1) & 0xFF;", + " btBuf[1] = 0x10;", + " btBuf.set(mgr.outStruct, 2);", + " fillOutputCrc32(0x31, btBuf);", + " await mgr.device.sendReport(0x31, btBuf);", + " }", + "", + " mgr._lastOutputSentTime = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + "", + " mgr.outStruct[0] = 0;", + " mgr.outStruct[1] = 0;", + " mgr.outStruct[38] = 0;", + " } catch (e) {", + " } finally {", + " mgr.sendPending = false;", + " if ((mgr.sendQueued || mgr.outputDirty) && mgr.device && mgr.device.opened) {", + " if (!mgr._flushTimer && typeof setTimeout !== 'undefined') {", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " mgr._flushTimer = setTimeout(function() {", + " mgr._flushTimer = null;", + " mgr._doFlush();", + " }, minInterval);", + " }", + " }", + " }", + " })();", + " };", + "", + " mgr.stopRumble = function() {", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " mgr._rumbleStopTimer = null;", + " }", + " mgr.rumbleLeft = 0;", + " mgr.rumbleRight = 0;", + " mgr.rumbleRemaining = 0;", + " mgr.rumbleEndTime = 0;", + " mgr.outStruct[2] = 0;", + " mgr.outStruct[3] = 0;", + " if (mgr.device && mgr.device.opened) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " };", + "", + " mgr.startRumble = function(left, right, durationSec) {", + " if (left <= 0 && right <= 0) {", + " mgr.stopRumble();", + " return;", + " }", + "", + " mgr.rumbleLeft = left;", + " mgr.rumbleRight = right;", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " const dur = Number(durationSec);", + " if (dur > 0) {", + " mgr.rumbleRemaining = dur;", + " mgr.rumbleEndTime = now + dur * 1000;", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " }", + " if (typeof setTimeout !== 'undefined') {", + " mgr._rumbleStopTimer = setTimeout(function() {", + " mgr._rumbleStopTimer = null;", + " mgr.stopRumble();", + " }, dur * 1000);", + " }", + " } else {", + " mgr.rumbleRemaining = -1;", + " mgr.rumbleEndTime = 0;", + " if (mgr._rumbleStopTimer) {", + " if (typeof clearTimeout !== 'undefined') clearTimeout(mgr._rumbleStopTimer);", + " mgr._rumbleStopTimer = null;", + " }", + " }", + "", + " mgr.outStruct[2] = right;", + " mgr.outStruct[3] = left;", + " if (mgr.device && mgr.device.opened) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " };", + "", + " mgr.sendFeatureCmd = async function(reportId, payloadBytes) {", + " if (!mgr.device || !mgr.device.opened) return false;", + " try {", + " let idealLen = 63;", + " const col = mgr.device.collections && mgr.device.collections.find(function(c) {", + " return c.usagePage === 0x0001 && c.usage === 0x0005;", + " });", + " if (col && col.featureReports) {", + " const meta = col.featureReports.find(function(r) { return r.reportId === reportId; });", + " if (meta && meta.items && meta.items[0] && meta.items[0].reportCount) {", + " idealLen = meta.items[0].reportCount;", + " }", + " }", + " const buf = new Uint8Array(idealLen);", + " buf.set(payloadBytes.subarray(0, Math.min(payloadBytes.length, idealLen)));", + " if (mgr.connectionType === 'Bluetooth') {", + " fillFeatureCrc32(reportId, buf);", + " }", + " await mgr.device.sendFeatureReport(reportId, buf);", + " return true;", + " } catch (e) {", + " return false;", + " }", + " };", + "", + " mgr.controlWaveOut = async function(enable, target) {", + " const ctrl = new Uint8Array(2 + 3);", + " ctrl[0] = 6; // DualSenseTestDeviceId.AUDIO", + " ctrl[1] = 2; // DualSenseTestActionId.WAVEOUT_CTRL", + " ctrl[2] = enable ? 1 : 0;", + " ctrl[3] = 1;", + " ctrl[4] = 0;", + "", + " if (enable) {", + " const calib = new Uint8Array(2 + 20);", + " calib[0] = 6; // AUDIO", + " calib[1] = 4; // BUILTIN_MIC_CALIB_DATA_VERIFY", + " if (target === 'headphone') {", + " calib[2 + 4] = 4;", + " calib[2 + 6] = 6;", + " } else {", + " calib[2 + 2] = 8;", + " }", + " await mgr.sendFeatureCmd(0x80, calib);", + " }", + " await mgr.sendFeatureCmd(0x80, ctrl);", + " };", + "", + " const HID_FILTERS = [", + " { vendorId: VENDOR_SONY, productId: PID_DUALSENSE, usagePage: 0x0001, usage: 0x0005 },", + " { vendorId: VENDOR_SONY, productId: PID_DUALSENSE_EDGE, usagePage: 0x0001, usage: 0x0005 }", + " ];", + "", + " function errMsg(e) {", + " if (!e) return 'Unknown error';", + " return (e.name ? e.name + ': ' : '') + (e.message || String(e));", + " }", + "", + " function pickDualSense(list) {", + " if (!list || !list.length) return null;", + " for (let i = 0; i < list.length; i++) {", + " if (isGamepadHidDevice(list[i])) return list[i];", + " }", + " return null;", + " }", + "", + " function isConnectedNow() {", + " return !!(mgr.device && mgr.device.opened);", + " }", + "", + " // ---------------------------------------------------------------", + " // GDevelop Desktop (Electron) support.", + " // WebHID permission handlers MUST live in the Electron main process:", + " // functions passed through @electron/remote become asynchronous IPC", + " // proxies that return \"undefined\" (= permission denied). So the", + " // handlers are compiled directly inside the main process instead.", + " // ---------------------------------------------------------------", + " const MAIN_PROCESS_SETUP_SRC = [", + " '(function (ses) {',", + " ' function isDs(d) { return !!d && d.vendorId === 1356 && (d.productId === 3302 || d.productId === 3570); }',", + " ' ses.setPermissionCheckHandler(function () { return true; });',", + " ' ses.setPermissionRequestHandler(function (wc, permission, callback) { callback(true); });',", + " ' ses.setDevicePermissionHandler(function (details) {',", + " ' if (details && details.deviceType === \"hid\") return isDs(details.device);',", + " ' return false;',", + " ' });',", + " ' var g = globalThis;',", + " ' if (g.__gdDualSenseSelectHid) { try { ses.removeListener(\"select-hid-device\", g.__gdDualSenseSelectHid); } catch (e) {} }',", + " ' g.__gdDualSenseSelectHid = function (event, details, callback) {',", + " ' event.preventDefault();',", + " ' var list = (details && details.deviceList) || [];',", + " ' for (var i = 0; i < list.length; i++) { if (isDs(list[i])) { callback(list[i].deviceId); return; } }',", + " ' var done = false;',", + " ' var onAdded = function (e, d) {',", + " ' if (done || !d || !isDs(d.device)) return;',", + " ' done = true;',", + " ' ses.removeListener(\"hid-device-added\", onAdded);',", + " ' callback(d.device.deviceId);',", + " ' };',", + " ' ses.on(\"hid-device-added\", onAdded);',", + " ' setTimeout(function () {',", + " ' if (done) return;',", + " ' done = true;',", + " ' ses.removeListener(\"hid-device-added\", onAdded);',", + " ' callback(\"\");',", + " ' }, 3000);',", + " ' };',", + " ' ses.on(\"select-hid-device\", g.__gdDualSenseSelectHid);',", + " ' return true;',", + " '})'", + " ].join(' ');", + "", + " function getElectronRemote(runtimeScene) {", + " let remote = null;", + " try {", + " const game = runtimeScene && runtimeScene.getGame ? runtimeScene.getGame() : null;", + " const renderer = game && game.getRenderer ? game.getRenderer() : null;", + " if (renderer && typeof renderer.getElectronRemote === 'function') {", + " remote = renderer.getElectronRemote();", + " }", + " } catch (e) {}", + " if (!remote && typeof require === 'function') {", + " try { remote = require('@electron/remote'); } catch (e) {}", + " }", + " return remote || null;", + " }", + "", + " function setupElectronPermissions(runtimeScene) {", + " if (mgr.electronChecked) return mgr.electronWebContents;", + " mgr.electronChecked = true;", + " const remote = getElectronRemote(runtimeScene);", + " if (!remote || typeof remote.getCurrentWebContents !== 'function') return null;", + " try {", + " const wc = remote.getCurrentWebContents();", + " mgr.electronWebContents = wc || null;", + " const ses = wc ? wc.session : null;", + " if (!ses) return mgr.electronWebContents;", + " let setupFn = null;", + " try { setupFn = remote.require('vm').runInThisContext(MAIN_PROCESS_SETUP_SRC); } catch (e) {}", + " if (!setupFn) {", + " try { setupFn = remote.getGlobal('eval')(MAIN_PROCESS_SETUP_SRC); } catch (e) {}", + " }", + " if (setupFn) {", + " mgr.electronPermissionsReady = !!setupFn(ses);", + " } else {", + " mgr.lastError = 'Could not install WebHID permissions in GDevelop';", + " }", + " } catch (e) {", + " mgr.lastError = 'Electron setup failed: ' + errMsg(e);", + " }", + " return mgr.electronWebContents;", + " }", + "", + " async function attachDevice(device) {", + " if (!device) return false;", + " if (mgr.device === device && device.opened) return true;", + " try {", + " if (!device.opened) {", + " await device.open();", + " }", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " mgr.status = 'Controller found but could not be opened (close Steam Input / DS4Windows / other apps using it)';", + " return false;", + " }", + " try {", + " const cType = detectConnectionType(device);", + " mgr.device = device;", + " mgr.connectionType = (cType === 'Unknown') ? 'USB' : cType;", + " mgr.model = (device.productId === PID_DUALSENSE_EDGE) ? 'DualSense Edge' : 'DualSense';", + " device.addEventListener('inputreport', handleInputReport);", + "", + " // Bluetooth: reading feature report 0x05/0x20 switches the controller from the", + " // reduced 9-byte 0x01 report to the full 77-byte 0x31 report (touchpad, gyro, ...).", + " try {", + " await device.receiveFeatureReport(0x05);", + " } catch (e) {", + " try { await device.receiveFeatureReport(0x20); } catch (e2) {}", + " }", + "", + " mgr.status = 'Connected';", + " mgr.lastError = '';", + " mgr._retryCount = 0;", + "", + " // Apply the full output state, including anything the events already set", + " // (lightbar, player LEDs, mute LED, triggers, volumes) while we were connecting.", + " await mgr.flushOutputReport(0x7F, 0x15, 0x03);", + " return true;", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " mgr.device = null;", + " mgr.connectionType = 'Disconnected';", + " mgr.model = 'None';", + " mgr.status = 'Connection failed';", + " return false;", + " }", + " }", + "", + " function detachDevice(device) {", + " if (mgr.device && (!device || mgr.device === device)) {", + " if (typeof mgr.stopSoundOnSpeaker === 'function') mgr.stopSoundOnSpeaker();", + " if (typeof mgr.stopRumble === 'function') mgr.stopRumble();", + " if (mgr._flushTimer) { clearTimeout(mgr._flushTimer); mgr._flushTimer = null; }", + " if (mgr._rumbleStopTimer) { clearTimeout(mgr._rumbleStopTimer); mgr._rumbleStopTimer = null; }", + " mgr.outputDirty = false;", + " mgr.sendQueued = false;", + " mgr.sendPending = false;", + " try {", + " mgr.device.removeEventListener('inputreport', handleInputReport);", + " } catch (e) {}", + " mgr.device = null;", + " mgr.connectionType = 'Disconnected';", + " mgr.model = 'None';", + " mgr.rawPs = false; mgr.rawMicBtn = false; mgr.rawTpPressed = false;", + " mgr.touch1.rawActive = false; mgr.touch2.rawActive = false;", + " mgr.status = mgr.wantConnection ? 'Controller lost - waiting for it to come back' : 'Disconnected';", + " }", + " }", + "", + " // Already-permitted devices (instant in GDevelop Desktop thanks to the main-process permission handler)", + " mgr.tryReconnectKnownDevices = async function() {", + " if (typeof navigator === 'undefined' || !navigator.hid) return false;", + " if (isConnectedNow()) return true;", + " try {", + " const devices = await navigator.hid.getDevices();", + " const ds = pickDualSense(devices);", + " if (ds) return await attachDevice(ds);", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " }", + " return false;", + " };", + "", + " // Must be called synchronously inside a user gesture (or Electron's simulated gesture).", + " mgr.requestDeviceNow = function() {", + " if (typeof navigator === 'undefined' || !navigator.hid) return Promise.resolve(false);", + " let p;", + " try {", + " p = navigator.hid.requestDevice({ filters: HID_FILTERS });", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " return Promise.resolve(false);", + " }", + " return p.then(function(chosen) {", + " const ds = pickDualSense(chosen);", + " if (!ds) return false;", + " return attachDevice(ds);", + " }).catch(function(e) {", + " mgr.lastError = errMsg(e);", + " return false;", + " });", + " };", + " if (typeof window !== 'undefined') {", + " window.__gdDualSenseRequestDevice = function() {", + " return mgr.requestDeviceNow().then(function(ok) { return !!ok; });", + " };", + " }", + "", + " // Browsers only show the WebHID device picker right after a click / key press.", + " function armGestureRetry() {", + " if (mgr.gestureArmed || typeof window === 'undefined') return;", + " mgr.gestureArmed = true;", + " const handler = function() {", + " window.removeEventListener('pointerdown', handler, true);", + " window.removeEventListener('keydown', handler, true);", + " mgr.gestureArmed = false;", + " if (!mgr.wantConnection || isConnectedNow()) return;", + " mgr.requestDeviceNow().then(function(ok) {", + " if (!ok && !isConnectedNow()) mgr.status = 'No DualSense selected';", + " });", + " };", + " window.addEventListener('pointerdown', handler, true);", + " window.addEventListener('keydown', handler, true);", + " }", + "", + " mgr.runConnectAttempt = async function(allowPicker) {", + " if (mgr.connecting || !mgr.wantConnection) return;", + " if (isConnectedNow()) return;", + " mgr.connecting = true;", + " if (mgr.status.indexOf('Connected') !== 0) mgr.status = 'Connecting...';", + " try {", + " if (await mgr.tryReconnectKnownDevices()) return;", + " if (!allowPicker) {", + " if (mgr.status.indexOf('could not be opened') === -1) mgr.status = 'Searching for a DualSense (plug it in or pair it via Bluetooth)';", + " return;", + " }", + " if (mgr.electronWebContents) {", + " // GDevelop Desktop: run requestDevice with a simulated user gesture.", + " // The main-process \"select-hid-device\" handler picks the DualSense automatically (no dialog).", + " try {", + " await mgr.electronWebContents.executeJavaScript(", + " '(window.__gdDualSenseRequestDevice ? window.__gdDualSenseRequestDevice() : false)',", + " true", + " );", + " } catch (e) {", + " mgr.lastError = errMsg(e);", + " }", + " if (isConnectedNow()) return;", + " // Last resort (only if the simulated gesture route was refused)", + " if (/SecurityError|gesture|activation/i.test(mgr.lastError) && await mgr.requestDeviceNow()) return;", + " if (mgr.status.indexOf('could not be opened') === -1) mgr.status = 'Searching for a DualSense (plug it in or pair it via Bluetooth)';", + " return;", + " }", + " // Web browser: try directly, otherwise wait for the next click / key press.", + " if (await mgr.requestDeviceNow()) return;", + " armGestureRetry();", + " mgr.status = 'Click or press a key in the game to choose your DualSense';", + " } finally {", + " mgr.connecting = false;", + " }", + " };", + "", + " // Called by the \"Connect DualSense controller\" action.", + " mgr.connect = function(runtimeScene) {", + " mgr.wantConnection = true;", + " if (isConnectedNow()) {", + " mgr.status = 'Connected';", + " return;", + " }", + " if (typeof navigator === 'undefined' || !navigator.hid) {", + " mgr.status = 'WebHID is not available here (use GDevelop Desktop, Chrome, Edge or Opera)';", + " return;", + " }", + " setupElectronPermissions(runtimeScene);", + " mgr._retryTimer = 0;", + " mgr.runConnectAttempt(true);", + " };", + "", + " // Called by the \"Disconnect DualSense controller\" action.", + " mgr.disconnect = async function() {", + " mgr.wantConnection = false;", + " if (typeof mgr.stopSoundOnSpeaker === 'function') mgr.stopSoundOnSpeaker();", + " if (typeof mgr.stopRumble === 'function') mgr.stopRumble();", + " const dev = mgr.device;", + " if (dev) {", + " try {", + " // Turn effects off before letting go of the controller", + " mgr.rainbowEnabled = false;", + " mgr.outStruct[2] = 0; mgr.outStruct[3] = 0;", + " mgr.outStruct[10] = 0; mgr.outStruct[21] = 0;", + " await mgr.flushOutputReport(0x0F, 0, 0);", + " } catch (e) {}", + " detachDevice(dev);", + " try { await dev.close(); } catch (e) {}", + " }", + " mgr.status = 'Disconnected';", + " };", + "", + " // Microphone volume monitoring (USB WebAudio + Bluetooth Opus 0x31/0x32)", + " mgr.startMicMonitoring = async function() {", + " if (mgr.micActive || mgr.micStarting) return;", + " mgr.micStarting = true;", + " try {", + " if (mgr.connectionType === 'Bluetooth' && mgr.device && mgr.device.opened && typeof AudioDecoder !== 'undefined') {", + " mgr.btMicTimestamp = 0;", + " mgr.btMicDecoder = new AudioDecoder({", + " output: function(audioData) {", + " try {", + " const n = audioData.numberOfFrames;", + " if (n > 0) {", + " const samples = new Float32Array(n);", + " audioData.copyTo(samples, { planeIndex: 0, format: 'f32-planar' });", + " let sumSq = 0;", + " let maxAbs = 0;", + " for (let i = 0; i < n; i++) {", + " const s = Math.abs(samples[i]);", + " sumSq += s * s;", + " if (s > maxAbs) maxAbs = s;", + " }", + " const rms = Math.sqrt(sumSq / n);", + " const vol = Math.min(100, rms * 320);", + " mgr.micVolume = mgr.micVolume * 0.65 + vol * 0.35;", + " mgr.micPeak = Math.min(100, maxAbs * 140);", + " }", + " } finally {", + " audioData.close();", + " }", + " },", + " error: function() {}", + " });", + " mgr.btMicDecoder.configure({", + " codec: 'opus',", + " sampleRate: 48000,", + " numberOfChannels: 1", + " });", + "", + " // Send BT mic enable reports (0x31 state + 0x32 control)", + " const r31 = new Uint8Array(77);", + " r31[0] = (mgr.btMicStateSeq++ & 0x0F) << 4;", + " r31[1] = 0x10;", + " r31[2] = 0xC0;", + " r31[3] = 0x83;", + " r31[8] = 0x08;", + " r31[9] = mgr.micPlugged ? 0x08 : 0x09;", + " r31[10] = 0x00;", + " r31[11] = 0x0F;", + " r31[39] = 0x01;", + " fillOutputCrc32(0x31, r31);", + " await mgr.device.sendReport(0x31, r31);", + "", + " const r32 = new Uint8Array(141);", + " const seq = mgr.btMicControlSeq++ & 0x0F;", + " r32[0] = seq << 4;", + " r32[1] = 0x91;", + " r32[2] = 0x07;", + " r32[3] = 0xFF;", + " r32.fill(0x40, 4, 9);", + " r32[9] = seq;", + " r32[10] = 0x92;", + " r32[11] = 0x40;", + " fillOutputCrc32(0x32, r32);", + " await mgr.device.sendReport(0x32, r32);", + "", + " mgr.micActive = true;", + " return;", + " }", + "", + " // USB / Standard WebAudio Microphone fallback (prefers DualSense / Wireless Controller input)", + " if (typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {", + " let targetDeviceId = undefined;", + " try {", + " const temp = await navigator.mediaDevices.getUserMedia({ audio: true });", + " temp.getTracks().forEach(function(t) { t.stop(); });", + " const devs = await navigator.mediaDevices.enumerateDevices();", + " const dsInput = devs.find(function(d) {", + " if (d.kind !== 'audioinput' || !d.label) return false;", + " const l = d.label.toLowerCase();", + " return l.indexOf('dualsense') !== -1 || l.indexOf('wireless controller') !== -1;", + " });", + " if (dsInput) targetDeviceId = dsInput.deviceId;", + " } catch (e) {}", + "", + " const constraints = targetDeviceId", + " ? { audio: { deviceId: { exact: targetDeviceId }, channelCount: { ideal: 2 }, echoCancellation: false, noiseSuppression: false, autoGainControl: false } }", + " : { audio: true };", + "", + " const stream = await navigator.mediaDevices.getUserMedia(constraints);", + " const ctx = new (window.AudioContext || window.webkitAudioContext)();", + " await ctx.resume();", + " const source = ctx.createMediaStreamSource(stream);", + " const analyser = ctx.createAnalyser();", + " analyser.fftSize = 512;", + " analyser.smoothingTimeConstant = 0.75;", + "", + " const track = stream.getAudioTracks()[0];", + " const chCount = (track && track.getSettings && track.getSettings().channelCount) || 2;", + " if (targetDeviceId && chCount >= 2) {", + " const splitter = ctx.createChannelSplitter(chCount);", + " source.connect(splitter);", + " splitter.connect(analyser, 1);", + " mgr.micSplitter = splitter;", + " } else {", + " source.connect(analyser);", + " }", + "", + " mgr.micStream = stream;", + " mgr.micAudioCtx = ctx;", + " mgr.micSource = source;", + " mgr.micAnalyser = analyser;", + " mgr.micDataArray = new Uint8Array(analyser.fftSize);", + " mgr.micActive = true;", + " }", + " } catch (e) {", + " mgr.micActive = false;", + " } finally {", + " mgr.micStarting = false;", + " }", + " };", + "", + " mgr.stopMicMonitoring = async function() {", + " mgr.micActive = false;", + " mgr.micVolume = 0;", + " mgr.micPeak = 0;", + " if (mgr.btMicDecoder) {", + " try { mgr.btMicDecoder.close(); } catch (e) {}", + " mgr.btMicDecoder = null;", + " if (mgr.device && mgr.device.opened && mgr.connectionType === 'Bluetooth') {", + " try {", + " const r32 = new Uint8Array(141);", + " const seq = mgr.btMicControlSeq++ & 0x0F;", + " r32[0] = seq << 4;", + " r32[1] = 0x91;", + " r32[2] = 0x07;", + " r32[3] = 0xFE;", + " r32.fill(0x40, 4, 9);", + " r32[9] = seq;", + " r32[10] = 0x92;", + " r32[11] = 0x40;", + " fillOutputCrc32(0x32, r32);", + " await mgr.device.sendReport(0x32, r32);", + " } catch (e) {}", + " }", + " }", + " if (mgr.micSplitter) {", + " try { mgr.micSplitter.disconnect(); } catch (e) {}", + " mgr.micSplitter = null;", + " }", + " if (mgr.micSource) {", + " try { mgr.micSource.disconnect(); } catch (e) {}", + " mgr.micSource = null;", + " }", + " if (mgr.micStream) {", + " try { mgr.micStream.getTracks().forEach(function(t) { t.stop(); }); } catch (e) {}", + " mgr.micStream = null;", + " }", + " if (mgr.micAudioCtx) {", + " try { await mgr.micAudioCtx.close(); } catch (e) {}", + " mgr.micAudioCtx = null;", + " }", + " mgr.micAnalyser = null;", + " mgr.micDataArray = null;", + " };", + "", + " // DualSense Speaker Controller (Bluetooth HID Opus Report 0x36 & USB Multi-Channel)", + " mgr.stopSoundOnSpeaker = function() {", + " if (mgr._btAudioAbort) {", + " try { mgr._btAudioAbort(); } catch (e) {}", + " mgr._btAudioAbort = null;", + " }", + " if (mgr._spkUsbSource) {", + " try { mgr._spkUsbSource.stop(); } catch (e) {}", + " mgr._spkUsbSource = null;", + " }", + " mgr._btAudioPlaying = false;", + " };", + "", + " async function loadAudioArrayBuffer(urlOrName, runtimeScene) {", + " let candidates = [urlOrName];", + " try {", + " if (runtimeScene && typeof runtimeScene.getGame === 'function') {", + " const game = runtimeScene.getGame();", + " const rl = game && typeof game.getResourceLoader === 'function' ? game.getResourceLoader() : null;", + " if (rl) {", + " const res = typeof rl.getResource === 'function' ? rl.getResource(urlOrName) : null;", + " if (res && res.file) {", + " if (typeof rl.getFullUrl === 'function') candidates.unshift(rl.getFullUrl(res.file));", + " candidates.push(res.file);", + " }", + " if (typeof rl.getFullUrl === 'function') {", + " candidates.push(rl.getFullUrl(urlOrName));", + " }", + " }", + " }", + " } catch (e) {}", + "", + " const isAbs = urlOrName.indexOf('://') !== -1 || urlOrName.indexOf('blob:') === 0 || urlOrName.indexOf('data:') === 0;", + " if (!isAbs) {", + " candidates.push('assets/' + urlOrName);", + " candidates.push('./' + urlOrName);", + " candidates.push(urlOrName);", + " }", + "", + " const uniqueUrls = Array.from(new Set(candidates));", + "", + " for (let i = 0; i < uniqueUrls.length; i++) {", + " const u = uniqueUrls[i];", + " if (!u) continue;", + " try {", + " const resp = await fetch(u);", + " if (resp && resp.ok) {", + " const buf = await resp.arrayBuffer();", + " if (buf && buf.byteLength > 0) return buf;", + " }", + " } catch (e) {}", + " try {", + " const fs = (typeof require === 'function') ? require('fs') : null;", + " if (fs && fs.readFileSync) {", + " let filePath = u;", + " if (filePath.indexOf('file:///') === 0) filePath = filePath.slice(8);", + " else if (filePath.indexOf('file://') === 0) filePath = filePath.slice(7);", + " if (fs.existsSync && fs.existsSync(filePath)) {", + " const nodeBuf = fs.readFileSync(filePath);", + " return nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);", + " }", + " }", + " } catch (e) {}", + " }", + " return null;", + " }", + "", + " async function playBtAudioStream(audioBuf, volumePct) {", + " if (!mgr.device || !mgr.device.opened) return false;", + " if (typeof AudioEncoder === 'undefined') {", + " mgr.lastError = 'WebCodecs AudioEncoder is not available for Bluetooth Opus streaming';", + " return false;", + " }", + "", + " // 1. Resample to 45,000 Hz stereo", + " // The DualSense DAC runs Bluetooth audio at ~45kHz.", + " // Nominal 48k Opus encoding of a 45k buffer results in ~10.667ms frame duration and correct pitch.", + " const sampleCount45k = Math.ceil(audioBuf.duration * 45000);", + " const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;", + " const offline45 = new OfflineCtx(2, sampleCount45k, 45000);", + " const src45 = offline45.createBufferSource();", + " src45.buffer = audioBuf;", + " src45.connect(offline45.destination);", + " src45.start(0);", + " const buffer45k = await offline45.startRendering();", + "", + " // 2. Encode to 200-byte Opus frames via WebCodecs AudioEncoder (48k, 2ch, 160k CBR, 10ms lowdelay)", + " const OPUS_FRAME_BYTES = 200;", + " const frames = [];", + " let encodeError = null;", + "", + " const encoder = new AudioEncoder({", + " output: function(chunk) {", + " const raw = new Uint8Array(chunk.byteLength);", + " chunk.copyTo(raw);", + " const frame = new Uint8Array(OPUS_FRAME_BYTES);", + " frame.set(raw.subarray(0, OPUS_FRAME_BYTES));", + " frames.push(frame);", + " },", + " error: function(err) {", + " encodeError = err;", + " }", + " });", + "", + " encoder.configure({", + " codec: 'opus',", + " sampleRate: 48000,", + " numberOfChannels: 2,", + " bitrate: 160000,", + " bitrateMode: 'constant',", + " opus: {", + " format: 'opus',", + " frameDuration: 10000,", + " application: 'lowdelay',", + " signal: 'music',", + " complexity: 1", + " }", + " });", + "", + " const ch0 = buffer45k.getChannelData(0);", + " const ch1 = buffer45k.numberOfChannels > 1 ? buffer45k.getChannelData(1) : ch0;", + " const planar = new Float32Array(ch0.length + ch1.length);", + " planar.set(ch0, 0);", + " planar.set(ch1, ch0.length);", + "", + " const audioData = new AudioData({", + " format: 'f32-planar',", + " sampleRate: 48000,", + " numberOfFrames: ch0.length,", + " numberOfChannels: 2,", + " timestamp: 0,", + " data: planar", + " });", + "", + " encoder.encode(audioData);", + " audioData.close();", + " await encoder.flush();", + " encoder.close();", + "", + " if (encodeError || frames.length === 0) {", + " if (encodeError) mgr.lastError = 'Bluetooth Opus encoding failed: ' + errMsg(encodeError);", + " return false;", + " }", + "", + " // 3. Prepare streaming clock and control", + " let aborted = false;", + " let rafId = 0;", + " let intervalId = 0;", + " let clockCtx = null;", + " let clockSource = null;", + "", + " mgr._btAudioAbort = function() {", + " aborted = true;", + " if (rafId) {", + " if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(rafId);", + " rafId = 0;", + " }", + " if (intervalId) {", + " if (typeof clearInterval !== 'undefined') clearInterval(intervalId);", + " intervalId = 0;", + " }", + " if (clockSource) {", + " try { clockSource.stop(); } catch (e) {}", + " try { clockSource.disconnect(); } catch (e) {}", + " clockSource = null;", + " }", + " if (clockCtx) {", + " try { clockCtx.close(); } catch (e) {}", + " clockCtx = null;", + " }", + " mgr._btAudioPlaying = false;", + " };", + "", + " mgr._btAudioPlaying = true;", + "", + " const FRAME_DURATION_S = 480 / 45000; // ~0.010667s (10.667ms per frame)", + " const MAX_CATCHUP_FRAMES = 8;", + " const devVol = Math.round(Math.max(0, Math.min(100, volumePct)) * 0x64 / 100);", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " clockCtx = new AudioCtx();", + " await clockCtx.resume();", + "", + " clockSource = clockCtx.createBufferSource();", + " const clockBuffer = clockCtx.createBuffer(1, Math.max(1, Math.ceil(audioBuf.duration * clockCtx.sampleRate)), clockCtx.sampleRate);", + " clockSource.buffer = clockBuffer;", + " const silentGain = clockCtx.createGain();", + " silentGain.gain.value = 0;", + " clockSource.connect(silentGain);", + " silentGain.connect(clockCtx.destination);", + "", + " let startedAt = clockCtx.currentTime;", + " let lastSentFrame = -1;", + " let sendSeq = 0;", + " let frameCounter = 0;", + " const totalFrames = frames.length;", + "", + " clockSource.onended = function() {", + " if (!aborted && mgr._btAudioAbort) {", + " mgr._btAudioAbort();", + " }", + " };", + " clockSource.start(0);", + "", + " function sendFrame(idx) {", + " if (aborted || !mgr.device || !mgr.device.opened) return;", + " const opus = frames[idx];", + " if (!opus) return;", + "", + " const payload = new Uint8Array(397);", + " payload[0] = ((sendSeq & 0x0F) << 4);", + "", + " // Control subpacket (speaker volume + audio route)", + " payload[1] = 0x90;", + " payload[2] = 0x3F;", + " payload[3] = 0xA0; // valid_flag0: bit 5 (speaker volume) + bit 7 (audio control)", + " payload[4] = 0x00;", + " payload[8] = devVol & 0xFF; // speaker volume (0x00 .. 0x64)", + " payload[10] = 0x09; // audioControl", + "", + " // Audio subpacket", + " payload[66] = 0x91;", + " payload[67] = 0x07;", + " payload[68] = 0xFE;", + " payload[69] = 0x40;", + " payload[70] = 0x40;", + " payload[71] = 0x40;", + " payload[72] = 0x40;", + " payload[73] = 0x40;", + " payload[74] = frameCounter & 0xFF;", + " payload[75] = 0x93; // 0x93 = route to speaker", + " payload[76] = 0xC8; // 200 bytes opus frame", + " payload.set(opus.subarray(0, 200), 77);", + "", + " // Haptic subpacket (silence)", + " payload[277] = 0x92;", + " payload[278] = 0x40;", + "", + " fillOutputCrc32(0x36, payload);", + "", + " sendSeq = (sendSeq + 1) & 0x0F;", + " frameCounter = (frameCounter + 1) & 0xFF;", + "", + " mgr.device.sendReport(0x36, payload).catch(function(err) {});", + " }", + "", + " function tick() {", + " if (aborted || !clockCtx) return;", + " const t = clockCtx.currentTime - startedAt;", + " const targetFrame = Math.floor(t / FRAME_DURATION_S);", + "", + " if (targetFrame - lastSentFrame > MAX_CATCHUP_FRAMES) {", + " lastSentFrame = targetFrame - 1;", + " }", + "", + " while (lastSentFrame < targetFrame && lastSentFrame < totalFrames - 1) {", + " lastSentFrame++;", + " sendFrame(lastSentFrame);", + " }", + "", + " if (lastSentFrame >= totalFrames - 1 && t >= audioBuf.duration) {", + " if (mgr._btAudioAbort) mgr._btAudioAbort();", + " }", + " }", + "", + " if (typeof requestAnimationFrame !== 'undefined') {", + " const rafLoop = function() {", + " if (aborted) return;", + " tick();", + " if (!aborted) rafId = requestAnimationFrame(rafLoop);", + " };", + " rafId = requestAnimationFrame(rafLoop);", + " }", + " tick();", + " if (typeof setInterval !== 'undefined') intervalId = setInterval(tick, 8);", + "", + " return true;", + " }", + "", + " async function playUsbAudio(audioBuf, vol) {", + " mgr.outStruct[5] = Math.round(vol * 0x64 / 100);", + " mgr.outStruct[7] = (3 << 4); // route to speaker", + " await mgr.flushOutputReport(0xA0, 0, 0);", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " if (!mgr.spkAudioCtx) {", + " mgr.spkAudioCtx = new AudioCtx();", + " }", + " const ctx = mgr.spkAudioCtx;", + " await ctx.resume();", + "", + " if (!mgr.spkSinkId && typeof ctx.setSinkId === 'function' && navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {", + " try {", + " const devs = await navigator.mediaDevices.enumerateDevices();", + " const dsOut = devs.find(function(d) {", + " if (d.kind !== 'audiooutput' || !d.label) return false;", + " const l = d.label.toLowerCase();", + " return l.indexOf('dualsense') !== -1 || l.indexOf('wireless controller') !== -1;", + " });", + " if (dsOut && dsOut.deviceId) {", + " await ctx.setSinkId(dsOut.deviceId);", + " mgr.spkSinkId = dsOut.deviceId;", + " }", + " } catch (e) {}", + " }", + "", + " const src = ctx.createBufferSource();", + " src.buffer = audioBuf;", + " const gainNode = ctx.createGain();", + " gainNode.gain.value = vol / 100;", + "", + " if (ctx.destination.maxChannelCount >= 4) {", + " ctx.destination.channelCount = 4;", + " ctx.destination.channelCountMode = 'explicit';", + " ctx.destination.channelInterpretation = 'discrete';", + " const merger = ctx.createChannelMerger(4);", + " const splitter = ctx.createChannelSplitter(2);", + " src.connect(gainNode);", + " gainNode.connect(splitter);", + " splitter.connect(merger, 0, 0);", + " splitter.connect(merger, 1, 1);", + " merger.connect(ctx.destination);", + " } else {", + " src.connect(gainNode);", + " gainNode.connect(ctx.destination);", + " }", + " mgr._spkUsbSource = src;", + " src.onended = function() {", + " if (mgr._spkUsbSource === src) mgr._spkUsbSource = null;", + " };", + " src.start(0);", + " }", + "", + " // Play audio file through DualSense Speaker (Bluetooth HID Opus Report 0x36 or USB 4-ch endpoint)", + " mgr.playSoundOnSpeaker = async function(audioResourceName, volumePct, runtimeScene) {", + " if (!mgr.device || !mgr.device.opened) {", + " mgr.lastError = 'DualSense controller is not connected';", + " return;", + " }", + " try {", + " const vol = Math.max(0, Math.min(100, volumePct));", + " mgr.speakerVolume = vol;", + "", + " const arrBuf = await loadAudioArrayBuffer(audioResourceName, runtimeScene);", + " if (!arrBuf) {", + " mgr.lastError = 'Could not load audio resource: ' + audioResourceName;", + " return;", + " }", + "", + " const AudioCtx = window.AudioContext || window.webkitAudioContext;", + " const decodeCtx = new AudioCtx();", + " let audioBuf = null;", + " try {", + " audioBuf = await decodeCtx.decodeAudioData(arrBuf);", + " } catch (e) {", + " mgr.lastError = 'Audio decoding failed: ' + errMsg(e);", + " return;", + " } finally {", + " try { await decodeCtx.close(); } catch (e) {}", + " }", + "", + " if (!audioBuf) return;", + "", + " mgr.stopSoundOnSpeaker();", + "", + " const isBt = (mgr.connectionType === 'Bluetooth') || (mgr.device && detectConnectionType(mgr.device) === 'Bluetooth');", + " if (isBt) {", + " await playBtAudioStream(audioBuf, vol);", + " } else {", + " await playUsbAudio(audioBuf, vol);", + " }", + " } catch (e) {", + " mgr.lastError = 'Speaker playback error: ' + errMsg(e);", + " }", + " };", + "", + " // Frame update (called automatically in onScenePreEvents)", + " mgr.stepFrame = function(runtimeScene) {", + " const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " if (now - mgr._lastStepNow < 4) return;", + " mgr._lastStepNow = now;", + "", + " const dt = runtimeScene ? runtimeScene.getElapsedTime() / 1000.0 : 0.016;", + "", + " // Keep looking for the controller while the events want a connection", + " if (mgr.wantConnection && !isConnectedNow() && !mgr.connecting) {", + " mgr._retryTimer += dt;", + " if (mgr._retryTimer >= 1.5) {", + " mgr._retryTimer = 0;", + " mgr._retryCount++;", + " // Electron: occasionally re-run the (automatic, dialog-free) device picker too", + " const allowPicker = !!mgr.electronWebContents && (mgr._retryCount % 4 === 0);", + " mgr.runConnectAttempt(allowPicker);", + " }", + " }", + "", + " // Update edge-triggered button states", + " mgr.psJustPressed = mgr.rawPs && !mgr.psPrev;", + " mgr.psPressed = mgr.rawPs;", + " mgr.psPrev = mgr.rawPs;", + "", + " mgr.micBtnJustPressed = mgr.rawMicBtn && !mgr.micBtnPrev;", + " mgr.micBtnPressed = mgr.rawMicBtn;", + " mgr.micBtnPrev = mgr.rawMicBtn;", + "", + " mgr.tpJustPressed = mgr.rawTpPressed && !mgr.tpPrev;", + " mgr.tpPressed = mgr.rawTpPressed;", + " mgr.tpPrev = mgr.rawTpPressed;", + "", + " mgr.fnLPressed = mgr.rawFnL;", + " mgr.fnRPressed = mgr.rawFnR;", + " mgr.paddleLPressed = mgr.rawPaddleL;", + " mgr.paddleRPressed = mgr.rawPaddleR;", + "", + " // Update Touchpad deltas & edge-triggers", + " const t1 = mgr.touch1;", + " t1.justStarted = t1.rawActive && !t1.prevActive;", + " t1.active = t1.rawActive;", + " if (t1.active) {", + " if (t1.prevActive) {", + " t1.dx = t1.x - t1.prevX;", + " t1.dy = t1.y - t1.prevY;", + " } else {", + " t1.dx = 0;", + " t1.dy = 0;", + " }", + " t1.prevX = t1.x;", + " t1.prevY = t1.y;", + " } else {", + " t1.dx = 0;", + " t1.dy = 0;", + " }", + " t1.prevActive = t1.active;", + "", + " const t2 = mgr.touch2;", + " t2.justStarted = t2.rawActive && !t2.prevActive;", + " t2.active = t2.rawActive;", + " if (t2.active) {", + " if (t2.prevActive) {", + " t2.dx = t2.x - t2.prevX;", + " t2.dy = t2.y - t2.prevY;", + " } else {", + " t2.dx = 0;", + " t2.dy = 0;", + " }", + " t2.prevX = t2.x;", + " t2.prevY = t2.y;", + " } else {", + " t2.dx = 0;", + " t2.dy = 0;", + " }", + " t2.prevActive = t2.active;", + "", + " // Update USB WebAudio Mic analyser if active", + " if (mgr.micActive && mgr.micAnalyser && mgr.micDataArray) {", + " mgr.micAnalyser.getByteTimeDomainData(mgr.micDataArray);", + " let sumSq = 0;", + " let maxAbs = 0;", + " const len = mgr.micDataArray.length;", + " for (let i = 0; i < len; i++) {", + " const norm = (mgr.micDataArray[i] - 128) / 128.0;", + " const absVal = Math.abs(norm);", + " sumSq += norm * norm;", + " if (absVal > maxAbs) maxAbs = absVal;", + " }", + " const rms = Math.sqrt(sumSq / len);", + " const vol = Math.min(100, rms * 300);", + " mgr.micVolume = mgr.micVolume * 0.7 + vol * 0.3;", + " mgr.micPeak = Math.min(100, maxAbs * 130);", + " }", + "", + " // Timed vibration update and watchdog refresh", + " const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();", + " if (mgr.rumbleEndTime > 0) {", + " if (nowMs >= mgr.rumbleEndTime) {", + " mgr.stopRumble();", + " } else {", + " mgr.rumbleRemaining = Math.max(0, (mgr.rumbleEndTime - nowMs) / 1000.0);", + " }", + " }", + " if (mgr.rumbleLeft > 0 || mgr.rumbleRight > 0) {", + " if (nowMs - mgr._lastOutputSentTime >= 35) {", + " mgr.flushOutputReport(0x03, 0, 0);", + " }", + " }", + "", + " // If output is dirty or queued, and rate-limit window has elapsed, flush it now", + " if (mgr.outputDirty || mgr.sendQueued) {", + " const minInterval = (mgr.connectionType === 'Bluetooth') ? 20 : 10;", + " if (nowMs - mgr._lastOutputSentTime >= minInterval) {", + " mgr._doFlush();", + " }", + " }", + "", + " // Timed hardware test tone auto-stop", + " if (mgr.toneRemaining > 0) {", + " mgr.toneRemaining -= dt;", + " if (mgr.toneRemaining <= 0) {", + " mgr.toneRemaining = -1;", + " mgr.controlWaveOut(false, mgr.toneTarget);", + " }", + " }", + "", + " // Rainbow lightbar cycle", + " if (mgr.rainbowEnabled && mgr.device && mgr.device.opened) {", + " mgr.rainbowHue = (mgr.rainbowHue + mgr.rainbowSpeed * dt) % 360;", + " if (mgr.rainbowHue < 0) mgr.rainbowHue += 360;", + " mgr.rainbowTimer += dt;", + " if (mgr.rainbowTimer >= 0.033) {", + " mgr.rainbowTimer = 0;", + " const rgb = hsvToRgb(mgr.rainbowHue, 1, 1);", + " mgr.lightbarR = rgb[0];", + " mgr.lightbarG = rgb[1];", + " mgr.lightbarB = rgb[2];", + " mgr.outStruct[44] = rgb[0];", + " mgr.outStruct[45] = rgb[1];", + " mgr.outStruct[46] = rgb[2];", + " mgr.flushOutputReport(0, 0x04, 0);", + " }", + " }", + " };", + "", + " // WebHID hotplug: only (re)attach when the events asked for a connection", + " if (typeof navigator !== 'undefined' && navigator.hid) {", + " navigator.hid.addEventListener('connect', function(ev) {", + " if (mgr.wantConnection && !isConnectedNow() && isGamepadHidDevice(ev.device)) {", + " attachDevice(ev.device);", + " }", + " });", + " navigator.hid.addEventListener('disconnect', function(ev) {", + " detachDevice(ev.device);", + " });", + " }", + "", + " gdjs._dualSenseManager = mgr;", + " return mgr;", + " };", + "}", + "gdjs._dualSenseEnsureInit(runtimeScene).connect(runtimeScene);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Turns off rumble/trigger effects, releases the DualSense and stops searching for it.", + "fullName": "Disconnect DualSense controller", + "functionType": "Action", + "name": "DisconnectDualSense", + "sentence": "Disconnect DualSense controller", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.disconnect();" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the extension is still looking for / connecting to a DualSense (after the Connect action, before a controller is connected).", + "fullName": "DualSense is connecting", + "functionType": "Condition", + "name": "IsConnecting", + "sentence": "DualSense controller is connecting", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!(mgr.wantConnection && !(mgr.device && mgr.device.opened));" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "A readable text explaining the current connection state (useful to display in a Text object while testing). Includes the last error, if any.", + "fullName": "DualSense connection status", + "functionType": "StringExpression", + "name": "ConnectionStatus", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = \"\";", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const connected = !!(mgr.device && mgr.device.opened);", + "let text = connected ? (\"Connected (\" + mgr.connectionType + \", \" + mgr.model + \")\") : mgr.status;", + "if (!connected && mgr.lastError) text += \" | Last error: \" + mgr.lastError;", + "eventsFunctionContext.returnValue = text;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if a DualSense controller is currently connected via USB or Bluetooth.", + "fullName": "DualSense is connected", + "functionType": "Condition", + "name": "IsConnected", + "sentence": "DualSense controller is connected", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!(mgr.device && mgr.device.opened);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check whether the DualSense controller is connected over USB or Bluetooth.", + "fullName": "DualSense connection type", + "functionType": "Condition", + "name": "IsConnectionType", + "sentence": "DualSense is connected via _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const expected = eventsFunctionContext.getArgument(\"Type\");", + "eventsFunctionContext.returnValue = !!(mgr.device && mgr.device.opened && mgr.connectionType === expected);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"USB\"", + "description": "Connection type", + "name": "Type", + "supplementaryInformation": "[\"USB\", \"Bluetooth\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the DualSense touchpad is physically clicked down.", + "fullName": "Touchpad button is pressed", + "functionType": "Condition", + "name": "IsTouchpadPressed", + "sentence": "Touchpad button is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.tpPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the DualSense touchpad was pressed down on this frame.", + "fullName": "Touchpad button was just pressed", + "functionType": "Condition", + "name": "WasTouchpadJustPressed", + "sentence": "Touchpad button was just pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.tpJustPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if Finger 1 or Finger 2 is currently touching the touchpad.", + "fullName": "Touchpad finger is touching", + "functionType": "Condition", + "name": "IsTouchpadFingerActive", + "sentence": "Touchpad finger _PARAM1_ is touching", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? !!mgr.touch2.active : !!mgr.touch1.active;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 = first finger, 2 = second finger for multi-touch)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if Finger 1 or Finger 2 just touched down on the touchpad this frame.", + "fullName": "Touchpad finger just started touching", + "functionType": "Condition", + "name": "DidTouchpadFingerJustStart", + "sentence": "Touchpad finger _PARAM1_ just started touching", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? !!mgr.touch2.justStarted : !!mgr.touch1.justStarted;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the PS button is currently held down.", + "fullName": "PS button is pressed", + "functionType": "Condition", + "name": "IsPSButtonPressed", + "sentence": "PS button is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.psPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the PS button was just pressed this frame.", + "fullName": "PS button was just pressed", + "functionType": "Condition", + "name": "WasPSButtonJustPressed", + "sentence": "PS button was just pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.psJustPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the Microphone Mute button (under the PS button) is currently held down.", + "fullName": "Microphone mute button is pressed", + "functionType": "Condition", + "name": "IsMicMuteButtonPressed", + "sentence": "Microphone mute button is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.micBtnPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the Microphone Mute button was just pressed this frame.", + "fullName": "Microphone mute button was just pressed", + "functionType": "Condition", + "name": "WasMicMuteButtonJustPressed", + "sentence": "Microphone mute button was just pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.micBtnJustPressed;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if a DualSense Edge back paddle or function (Fn) button is pressed.", + "fullName": "DualSense Edge special button is pressed", + "functionType": "Condition", + "name": "IsEdgeButtonPressed", + "sentence": "DualSense Edge _PARAM1_ is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const btn = eventsFunctionContext.getArgument(\"EdgeButton\");", + "if (btn === \"Left Back Paddle\") eventsFunctionContext.returnValue = !!mgr.paddleLPressed;", + "else if (btn === \"Right Back Paddle\") eventsFunctionContext.returnValue = !!mgr.paddleRPressed;", + "else if (btn === \"Left Function (Fn)\") eventsFunctionContext.returnValue = !!mgr.fnLPressed;", + "else if (btn === \"Right Function (Fn)\") eventsFunctionContext.returnValue = !!mgr.fnRPressed;", + "else eventsFunctionContext.returnValue = false;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"Left Back Paddle\"", + "description": "DualSense Edge Button", + "name": "EdgeButton", + "supplementaryInformation": "[\"Left Back Paddle\", \"Right Back Paddle\", \"Left Function (Fn)\", \"Right Function (Fn)\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Turns off adaptive trigger resistance/vibration on L2, R2, or Both triggers.", + "fullName": "Reset adaptive trigger to normal", + "functionType": "Action", + "name": "ResetAdaptiveTrigger", + "sentence": "Reset adaptive trigger _PARAM1_ to normal", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const side = eventsFunctionContext.getArgument(\"Trigger\");", + "let flags = 0;", + "if (side === \"L2\" || side === \"Both\") {", + " mgr.outStruct[21] = 0x00;", + " for (let i = 0; i < 10; i++) mgr.outStruct[22 + i] = 0;", + " flags |= 0x08;", + "}", + "if (side === \"R2\" || side === \"Both\") {", + " mgr.outStruct[10] = 0x00;", + " for (let i = 0; i < 10; i++) mgr.outStruct[11 + i] = 0;", + " flags |= 0x04;", + "}", + "mgr.flushOutputReport(flags, 0, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"Both\"", + "description": "Trigger", + "name": "Trigger", + "supplementaryInformation": "[\"L2\", \"R2\", \"Both\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Applies continuous physical tension/resistance to L2, R2, or Both triggers starting at a given pull position.", + "fullName": "Set adaptive trigger continuous resistance", + "functionType": "Action", + "name": "SetTriggerResistance", + "sentence": "Set _PARAM1_ adaptive trigger to continuous resistance (start: _PARAM2_, force: _PARAM3_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const side = eventsFunctionContext.getArgument(\"Trigger\");", + "const startPos = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"StartPosition\")) || 0)));", + "const force = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Force\")) || 0)));", + "let flags = 0;", + "if (side === \"L2\" || side === \"Both\") {", + " mgr.outStruct[21] = 0x01;", + " mgr.outStruct[22] = startPos;", + " mgr.outStruct[23] = force;", + " for (let i = 2; i < 10; i++) mgr.outStruct[22 + i] = 0;", + " flags |= 0x08;", + "}", + "if (side === \"R2\" || side === \"Both\") {", + " mgr.outStruct[10] = 0x01;", + " mgr.outStruct[11] = startPos;", + " mgr.outStruct[12] = force;", + " for (let i = 2; i < 10; i++) mgr.outStruct[11 + i] = 0;", + " flags |= 0x04;", + "}", + "mgr.flushOutputReport(flags, 0, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"R2\"", + "description": "Trigger", + "name": "Trigger", + "supplementaryInformation": "[\"L2\", \"R2\", \"Both\"]", + "type": "stringWithSelector" + }, + { + "defaultValue": "40", + "description": "Start Position (0 = top of pull, 255 = bottom, default: 40)", + "name": "StartPosition", + "type": "expression" + }, + { + "defaultValue": "230", + "description": "Resistance Force (0 = none to 255 = maximum stiffness, default: 230)", + "name": "Force", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Creates a trigger wall between Start Position and End Position that snaps cleanly once pulled past End Position.", + "fullName": "Set adaptive trigger resistance", + "functionType": "Action", + "name": "SetTriggerWeaponSection", + "sentence": "Set _PARAM1_ adaptive trigger to weapon snap (start: _PARAM2_, end: _PARAM3_, force: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const side = eventsFunctionContext.getArgument(\"Trigger\");", + "const startPos = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"StartPosition\")) || 0)));", + "const endPos = Math.max(startPos, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"EndPosition\")) || 0)));", + "const force = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Force\")) || 0)));", + "let flags = 0;", + "if (side === \"L2\" || side === \"Both\") {", + " mgr.outStruct[21] = 0x02;", + " mgr.outStruct[22] = startPos;", + " mgr.outStruct[23] = endPos;", + " mgr.outStruct[24] = force;", + " for (let i = 3; i < 10; i++) mgr.outStruct[22 + i] = 0;", + " flags |= 0x08;", + "}", + "if (side === \"R2\" || side === \"Both\") {", + " mgr.outStruct[10] = 0x02;", + " mgr.outStruct[11] = startPos;", + " mgr.outStruct[12] = endPos;", + " mgr.outStruct[13] = force;", + " for (let i = 3; i < 10; i++) mgr.outStruct[11 + i] = 0;", + " flags |= 0x04;", + "}", + "mgr.flushOutputReport(flags, 0, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"R2\"", + "description": "Trigger", + "name": "Trigger", + "supplementaryInformation": "[\"L2\", \"R2\", \"Both\"]", + "type": "stringWithSelector" + }, + { + "defaultValue": "15", + "description": "Start Position (0 to 255, default: 15)", + "name": "StartPosition", + "type": "expression" + }, + { + "defaultValue": "100", + "description": "End Position where trigger snaps past wall (0 to 255, default: 100)", + "name": "EndPosition", + "type": "expression" + }, + { + "defaultValue": "255", + "description": "Wall Force (0 to 255, default: 255)", + "name": "Force", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Rapidly vibrates the trigger motor when pulled past Start Position.", + "fullName": "Set adaptive trigger automatic recoil / vibration", + "functionType": "Action", + "name": "SetTriggerAutoVibration", + "sentence": "Set _PARAM1_ adaptive trigger to automatic vibration (start: _PARAM2_, force: _PARAM3_, frequency: _PARAM4_ Hz)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const side = eventsFunctionContext.getArgument(\"Trigger\");", + "const startPos = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"StartPosition\")) || 0)));", + "const force = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Force\")) || 0)));", + "const freq = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Frequency\")) || 0)));", + "let flags = 0;", + "if (side === \"L2\" || side === \"Both\") {", + " mgr.outStruct[21] = 0x06;", + " mgr.outStruct[22] = freq;", + " mgr.outStruct[23] = force;", + " mgr.outStruct[24] = startPos;", + " for (let i = 3; i < 10; i++) mgr.outStruct[22 + i] = 0;", + " flags |= 0x08;", + "}", + "if (side === \"R2\" || side === \"Both\") {", + " mgr.outStruct[10] = 0x06;", + " mgr.outStruct[11] = freq;", + " mgr.outStruct[12] = force;", + " mgr.outStruct[13] = startPos;", + " for (let i = 3; i < 10; i++) mgr.outStruct[11 + i] = 0;", + " flags |= 0x04;", + "}", + "mgr.flushOutputReport(flags, 0, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"R2\"", + "description": "Trigger", + "name": "Trigger", + "supplementaryInformation": "[\"L2\", \"R2\", \"Both\"]", + "type": "stringWithSelector" + }, + { + "defaultValue": "20", + "description": "Start Position (0 to 255, default: 20)", + "name": "StartPosition", + "type": "expression" + }, + { + "defaultValue": "255", + "description": "Vibration Force / Amplitude (0 to 255, default: 255)", + "name": "Force", + "type": "expression" + }, + { + "defaultValue": "12", + "description": "Vibration Frequency (1 to 255 Hz, default: 12)", + "name": "Frequency", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Sets the DualSense touchpad lightbar color using a color picker.", + "fullName": "Set lightbar color", + "functionType": "Action", + "name": "SetLightbarColor", + "sentence": "Set DualSense lightbar color to _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const colorStr = String(eventsFunctionContext.getArgument(\"Color\") || \"0;120;255\");", + "let r = 0, g = 120, b = 255;", + "if (colorStr.indexOf(\";\") !== -1) {", + " const parts = colorStr.split(\";\");", + " r = Math.max(0, Math.min(255, parseInt(parts[0], 10) || 0));", + " g = Math.max(0, Math.min(255, parseInt(parts[1], 10) || 0));", + " b = Math.max(0, Math.min(255, parseInt(parts[2], 10) || 0));", + "} else if (colorStr.charAt(0) === \"#\") {", + " const hex = parseInt(colorStr.slice(1), 16) || 0;", + " r = (hex >> 16) & 255;", + " g = (hex >> 8) & 255;", + " b = hex & 255;", + "}", + "mgr.rainbowEnabled = false;", + "mgr.lightbarR = r;", + "mgr.lightbarG = g;", + "mgr.lightbarB = b;", + "mgr.outStruct[44] = r;", + "mgr.outStruct[45] = g;", + "mgr.outStruct[46] = b;", + "mgr.flushOutputReport(0, 0x04, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"0;120;255\"", + "description": "Lightbar Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Sets the DualSense lightbar color using numeric Red, Green, and Blue values (0-255).", + "fullName": "Set lightbar RGB values", + "functionType": "Action", + "name": "SetLightbarRGB", + "sentence": "Set DualSense lightbar RGB to _PARAM1_, _PARAM2_, _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const r = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Red\")) || 0)));", + "const g = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Green\")) || 0)));", + "const b = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"Blue\")) || 0)));", + "mgr.rainbowEnabled = false;", + "mgr.lightbarR = r;", + "mgr.lightbarG = g;", + "mgr.lightbarB = b;", + "mgr.outStruct[44] = r;", + "mgr.outStruct[45] = g;", + "mgr.outStruct[46] = b;", + "mgr.flushOutputReport(0, 0x04, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "0", + "description": "Red (0 to 255)", + "name": "Red", + "type": "expression" + }, + { + "defaultValue": "120", + "description": "Green (0 to 255)", + "name": "Green", + "type": "expression" + }, + { + "defaultValue": "255", + "description": "Blue (0 to 255)", + "name": "Blue", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Enables or disables a smooth rainbow color cycling effect on the DualSense lightbar.", + "fullName": "Set lightbar rainbow effect", + "functionType": "Action", + "name": "SetLightbarRainbow", + "sentence": "Set DualSense lightbar rainbow effect: _PARAM1_ (speed: _PARAM2_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.rainbowEnabled = !!eventsFunctionContext.getArgument(\"Enable\");", + "mgr.rainbowSpeed = Number(eventsFunctionContext.getArgument(\"Speed\")) || 120;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "yes", + "description": "Enable rainbow effect", + "name": "Enable", + "type": "yesorno" + }, + { + "defaultValue": "120", + "description": "Cycle Speed (hue degrees per second, default: 120)", + "name": "Speed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Sets the white Player Indicator LEDs under the touchpad (Player 1 to 4 patterns, All 5 LEDs, or Off) and their brightness.", + "fullName": "Set player indicator LEDs", + "functionType": "Action", + "name": "SetPlayerIndicator", + "sentence": "Set DualSense player indicator to _PARAM1_ (brightness: _PARAM2_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const pat = eventsFunctionContext.getArgument(\"Pattern\");", + "const br = eventsFunctionContext.getArgument(\"Brightness\");", + "let mask = 0x04;", + "if (pat === \"Off\") mask = 0x00;", + "else if (pat === \"Player 1\") mask = 0x04;", + "else if (pat === \"Player 2\") mask = 0x0A;", + "else if (pat === \"Player 3\") mask = 0x15;", + "else if (pat === \"Player 4\") mask = 0x1B;", + "else if (pat === \"All 5 LEDs\") mask = 0x1F;", + "let brVal = 0;", + "if (br === \"Medium\") brVal = 1;", + "else if (br === \"Low\") brVal = 2;", + "mgr.outStruct[43] = mask;", + "mgr.outStruct[42] = brVal;", + "mgr.flushOutputReport(0, 0x10, 0x01);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"Player 1\"", + "description": "Player Indicator Pattern", + "name": "Pattern", + "supplementaryInformation": "[\"Off\", \"Player 1\", \"Player 2\", \"Player 3\", \"Player 4\", \"All 5 LEDs\"]", + "type": "stringWithSelector" + }, + { + "defaultValue": "\"High\"", + "description": "LED Brightness", + "name": "Brightness", + "supplementaryInformation": "[\"High\", \"Medium\", \"Low\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Controls each of the 5 white LEDs under the touchpad individually using a bitmask from 0 to 31 (e.g., 4 = center LED, 17 = outer two LEDs, 31 = all 5 LEDs).", + "fullName": "Set custom player indicator LED bitmask", + "functionType": "Action", + "name": "SetPlayerIndicatorBitmask", + "sentence": "Set DualSense custom player LED bitmask to _PARAM1_ (brightness: _PARAM2_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const mask = Math.max(0, Math.min(31, Math.round(Number(eventsFunctionContext.getArgument(\"Bitmask\")) || 0)));", + "const br = eventsFunctionContext.getArgument(\"Brightness\");", + "let brVal = 0;", + "if (br === \"Medium\") brVal = 1;", + "else if (br === \"Low\") brVal = 2;", + "mgr.outStruct[43] = mask;", + "mgr.outStruct[42] = brVal;", + "mgr.flushOutputReport(0, 0x10, 0x01);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "4", + "description": "5-LED Bitmask (0 to 31)", + "name": "Bitmask", + "type": "expression" + }, + { + "defaultValue": "\"High\"", + "description": "LED Brightness", + "name": "Brightness", + "supplementaryInformation": "[\"High\", \"Medium\", \"Low\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Controls the orange LED on the DualSense microphone mute button (Off, Solid Orange, or Breathing/Pulsing Orange).", + "fullName": "Set microphone mute LED state", + "functionType": "Action", + "name": "SetMicMuteLED", + "sentence": "Set DualSense microphone mute LED to _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const st = eventsFunctionContext.getArgument(\"State\");", + "let val = 0;", + "if (st === \"Solid Orange\") val = 1;", + "else if (st === \"Breathing Orange\") val = 2;", + "mgr.outStruct[8] = val;", + "mgr.flushOutputReport(0, 0x01, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"Solid Orange\"", + "description": "Mute Button LED State", + "name": "State", + "supplementaryInformation": "[\"Off\", \"Solid Orange\", \"Breathing Orange\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [] + }, + { + "description": "Vibrates the DualSense left (heavy) and right (soft/crisp) motors for a given duration.", + "fullName": "Vibrate DualSense haptic motors", + "functionType": "Action", + "name": "VibrateMotors", + "sentence": "Vibrate DualSense motors (left heavy: _PARAM1_, right soft: _PARAM2_, duration: _PARAM3_ s)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const left = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"LeftMotor\")) || 0)));", + "const right = Math.max(0, Math.min(255, Math.round(Number(eventsFunctionContext.getArgument(\"RightMotor\")) || 0)));", + "const rawDur = eventsFunctionContext.getArgument(\"Duration\");", + "const dur = Number(rawDur);", + "mgr.startRumble(left, right, isNaN(dur) ? 0 : dur);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "180", + "description": "Left Heavy Motor Intensity (0 to 255)", + "name": "LeftMotor", + "type": "expression" + }, + { + "defaultValue": "180", + "description": "Right Soft Motor Intensity (0 to 255)", + "name": "RightMotor", + "type": "expression" + }, + { + "defaultValue": "0.3", + "description": "Duration in seconds (0 = continuous until stopped)", + "name": "Duration", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Immediately stops the DualSense rumble motors.", + "fullName": "Stop DualSense motors vibration", + "functionType": "Action", + "name": "StopVibration", + "sentence": "Stop DualSense motors vibration", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.stopRumble();" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Checks if the DualSense rumble motors are currently active.", + "fullName": "DualSense motors are vibrating", + "functionType": "Condition", + "name": "IsVibrating", + "sentence": "DualSense motors are vibrating", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!(mgr.rumbleLeft > 0 || mgr.rumbleRight > 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Plays the DualSense hardware 1kHz test tone on the built-in controller speaker or headphone jack (works over both USB and Bluetooth).", + "fullName": "Play built-in test tone on controller speaker", + "functionType": "Action", + "name": "PlaySpeakerTestTone", + "sentence": "Play DualSense hardware test tone on _PARAM1_ (volume: _PARAM2_%, duration: _PARAM3_ s)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const targetStr = eventsFunctionContext.getArgument(\"Target\");", + "const isHp = targetStr === \"Headphone Jack\";", + "const volPct = Math.max(0, Math.min(100, Number(eventsFunctionContext.getArgument(\"Volume\")) || 85));", + "const dur = Math.max(0.05, Number(eventsFunctionContext.getArgument(\"Duration\")) || 0.5);", + "if (isHp) {", + " mgr.outStruct[4] = Math.round(volPct * 0x7F / 100);", + " mgr.outStruct[5] = 0;", + " mgr.outStruct[7] = 0;", + " mgr.flushOutputReport(0x90, 0, 0);", + "} else {", + " mgr.outStruct[5] = Math.round(volPct * 0x64 / 100);", + " mgr.outStruct[4] = 0;", + " mgr.outStruct[7] = (3 << 4);", + " mgr.flushOutputReport(0xA0, 0, 0);", + "}", + "mgr.toneTarget = isHp ? \"headphone\" : \"speaker\";", + "mgr.toneRemaining = dur;", + "mgr.controlWaveOut(true, mgr.toneTarget);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"Controller Speaker\"", + "description": "Audio Output Target", + "name": "Target", + "supplementaryInformation": "[\"Controller Speaker\", \"Headphone Jack\"]", + "type": "stringWithSelector" + }, + { + "defaultValue": "85", + "description": "Volume Percentage (0 to 100)", + "name": "Volume", + "type": "expression" + }, + { + "defaultValue": "0.5", + "description": "Duration in seconds (default: 0.5)", + "name": "Duration", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Stops the DualSense hardware test tone immediately.", + "fullName": "Stop built-in test tone on controller speaker", + "functionType": "Action", + "name": "StopSpeakerTestTone", + "sentence": "Stop DualSense hardware test tone", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.toneRemaining = -1;", + "mgr.controlWaveOut(false, mgr.toneTarget);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Plays an audio file routed to the DualSense controller speaker (supports both Bluetooth HID Opus streaming and USB multi-channel audio).", + "fullName": "Play audio file on DualSense speaker", + "functionType": "Action", + "name": "PlaySoundOnControllerSpeaker", + "sentence": "Play sound _PARAM1_ on DualSense speaker (volume: _PARAM2_%)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const soundName = String(eventsFunctionContext.getArgument(\"AudioResource\") || \"\");", + "const vol = Number(eventsFunctionContext.getArgument(\"Volume\")) || 90;", + "if (soundName) mgr.playSoundOnSpeaker(soundName, vol, runtimeScene);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "\"\"", + "description": "Audio file to play", + "name": "AudioResource", + "type": "audioResource" + }, + { + "defaultValue": "90", + "description": "Volume (0 to 100)", + "name": "Volume", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Adjusts the hardware volume of the DualSense internal speaker and 3.5mm headphone jack.", + "fullName": "Set DualSense speaker and headphone volume", + "functionType": "Action", + "name": "SetSpeakerVolume", + "sentence": "Set DualSense speaker volume to _PARAM1_% and headphone volume to _PARAM2_%", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const spk = Math.max(0, Math.min(100, Number(eventsFunctionContext.getArgument(\"SpeakerVol\")) || 0));", + "const hp = Math.max(0, Math.min(100, Number(eventsFunctionContext.getArgument(\"HeadphoneVol\")) || 0));", + "mgr.speakerVolume = spk;", + "mgr.headphoneVolume = hp;", + "mgr.outStruct[5] = Math.round(spk * 0x64 / 100);", + "mgr.outStruct[4] = Math.round(hp * 0x7F / 100);", + "mgr.outStruct[7] = (3 << 4);", + "mgr.flushOutputReport(0xB0, 0, 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "85", + "description": "Speaker Volume (0 to 100)", + "name": "SpeakerVol", + "type": "expression" + }, + { + "defaultValue": "75", + "description": "Headphone Volume (0 to 100)", + "name": "HeadphoneVol", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Starts listening to the DualSense built-in microphone (supports both USB audio and Bluetooth Opus stream) to measure real-time voice/blow volume.", + "fullName": "Start DualSense microphone volume monitoring", + "functionType": "Action", + "name": "StartMicrophoneTest", + "sentence": "Start DualSense microphone volume monitoring", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.startMicMonitoring();" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Stops monitoring the DualSense microphone.", + "fullName": "Stop DualSense microphone volume monitoring", + "functionType": "Action", + "name": "StopMicrophoneTest", + "sentence": "Stop DualSense microphone volume monitoring", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.stopMicMonitoring();" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Check if the DualSense microphone input volume (0 to 100) is above a threshold.", + "fullName": "Microphone volume is above threshold", + "functionType": "Condition", + "name": "IsMicrophoneVolumeAbove", + "sentence": "DualSense microphone volume is above _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const th = Number(eventsFunctionContext.getArgument(\"Threshold\")) || 0;", + "eventsFunctionContext.returnValue = !!(mgr.micActive && mgr.micVolume >= th);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "defaultValue": "25", + "description": "Volume Threshold (0 to 100, e.g. 25)", + "name": "Threshold", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if 3.5mm headphones are currently plugged into the DualSense controller.", + "fullName": "Headphones are plugged into DualSense", + "functionType": "Condition", + "name": "IsHeadphonePlugged", + "sentence": "Headphones are plugged into DualSense jack", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = false;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = !!mgr.headphonePlugged;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Zeros out any resting drift on the DualSense gyroscope using its current orientation.", + "fullName": "Calibrate DualSense gyroscope resting offset", + "functionType": "Action", + "name": "CalibrateGyroscope", + "sentence": "Calibrate DualSense gyroscope bias", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "mgr.gyroBiasPitch += mgr.gyroPitch;", + "mgr.gyroBiasYaw += mgr.gyroYaw;", + "mgr.gyroBiasRoll += mgr.gyroRoll;", + "mgr.gyroPitch = 0;", + "mgr.gyroYaw = 0;", + "mgr.gyroRoll = 0;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the raw X coordinate (0 to 1920) of Finger 1 or Finger 2 on the DualSense touchpad.", + "fullName": "Touchpad X coordinate (0 to 1920)", + "functionType": "Expression", + "name": "TouchpadX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? mgr.touch2.x : mgr.touch1.x;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns the raw Y coordinate (0 to 1080) of Finger 1 or Finger 2 on the DualSense touchpad.", + "fullName": "Touchpad Y coordinate (0 to 1080)", + "functionType": "Expression", + "name": "TouchpadY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? mgr.touch2.y : mgr.touch1.y;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns the normalized X coordinate (0.0 to 1.0) of Finger 1 or Finger 2 on the DualSense touchpad.", + "fullName": "Touchpad normalized X coordinate (0 to 1)", + "functionType": "Expression", + "name": "TouchpadNormalizedX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "const x = idx === 2 ? mgr.touch2.x : mgr.touch1.x;", + "eventsFunctionContext.returnValue = x / 1920.0;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns the normalized Y coordinate (0.0 to 1.0) of Finger 1 or Finger 2 on the DualSense touchpad.", + "fullName": "Touchpad normalized Y coordinate (0 to 1)", + "functionType": "Expression", + "name": "TouchpadNormalizedY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "const y = idx === 2 ? mgr.touch2.y : mgr.touch1.y;", + "eventsFunctionContext.returnValue = y / 1080.0;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns the horizontal swipe/movement speed of Finger 1 or Finger 2 since the last frame.", + "fullName": "Touchpad finger X movement delta", + "functionType": "Expression", + "name": "TouchpadDeltaX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? mgr.touch2.dx : mgr.touch1.dx;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns the vertical swipe/movement speed of Finger 1 or Finger 2 since the last frame.", + "fullName": "Touchpad finger Y movement delta", + "functionType": "Expression", + "name": "TouchpadDeltaY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "const idx = Math.round(Number(eventsFunctionContext.getArgument(\"Finger\")) || 1);", + "eventsFunctionContext.returnValue = idx === 2 ? mgr.touch2.dy : mgr.touch1.dy;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [ + { + "defaultValue": "1", + "description": "Finger index (1 or 2)", + "name": "Finger", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Returns how many fingers are currently touching the DualSense touchpad (0, 1, or 2).", + "fullName": "Touchpad active finger count (0, 1, or 2)", + "functionType": "Expression", + "name": "TouchpadTouchCount", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = (mgr.touch1.active ? 1 : 0) + (mgr.touch2.active ? 1 : 0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense gyroscope Pitch (up/down rotation rate).", + "fullName": "Gyroscope Pitch angular velocity", + "functionType": "Expression", + "name": "GyroPitch", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.gyroPitch;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense gyroscope Yaw (left/right rotation rate).", + "fullName": "Gyroscope Yaw angular velocity", + "functionType": "Expression", + "name": "GyroYaw", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.gyroYaw;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense gyroscope Roll (steering/banking rotation rate).", + "fullName": "Gyroscope Roll angular velocity", + "functionType": "Expression", + "name": "GyroRoll", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.gyroRoll;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense accelerometer X value.", + "fullName": "Accelerometer X axis", + "functionType": "Expression", + "name": "AccelX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.accelX;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense accelerometer Y value.", + "fullName": "Accelerometer Y axis", + "functionType": "Expression", + "name": "AccelY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.accelY;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense accelerometer Z value.", + "fullName": "Accelerometer Z axis", + "functionType": "Expression", + "name": "AccelZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.accelZ;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense forward/backward tilt angle in degrees calculated from gravity.", + "fullName": "Controller Pitch tilt angle in degrees (-90 to 90)", + "functionType": "Expression", + "name": "TiltPitchDegrees", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.tiltPitchDeg;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense left/right steering tilt angle in degrees calculated from gravity.", + "fullName": "Controller Roll steering angle in degrees (-180 to 180)", + "functionType": "Expression", + "name": "TiltRollDegrees", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.tiltRollDeg;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the current smoothed volume (0 to 100) from the DualSense microphone.", + "fullName": "Microphone input volume (0 to 100)", + "functionType": "Expression", + "name": "MicrophoneVolume", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = Math.round(mgr.micVolume * 10) / 10;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the instantaneous peak volume (0 to 100) from the DualSense microphone.", + "fullName": "Microphone peak level (0 to 100)", + "functionType": "Expression", + "name": "MicrophonePeak", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = Math.round(mgr.micPeak * 10) / 10;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns the DualSense battery charge percentage (0 to 100).", + "fullName": "DualSense battery percentage (0 to 100)", + "functionType": "Expression", + "name": "BatteryLevel", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = 0;", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.batteryLevel;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "number" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns \"USB\", \"Bluetooth\", or \"Disconnected\".", + "fullName": "DualSense connection type (\"USB\", \"Bluetooth\", or \"Disconnected\")", + "functionType": "StringExpression", + "name": "ConnectionType", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = \"\";", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = (mgr.device && mgr.device.opened) ? mgr.connectionType : \"Disconnected\";" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns \"DualSense\", \"DualSense Edge\", or \"None\".", + "fullName": "DualSense model name (\"DualSense\" or \"DualSense Edge\")", + "functionType": "StringExpression", + "name": "ControllerModel", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = \"\";", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = (mgr.device && mgr.device.opened) ? mgr.model : \"None\";" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [], + "objectGroups": [] + }, + { + "description": "Returns \"Discharging\", \"Charging\", \"Full\", or \"Unknown\".", + "fullName": "DualSense battery charging status", + "functionType": "StringExpression", + "name": "BatteryStatus", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "eventsFunctionContext.returnValue = \"\";", + "if (!gdjs._dualSenseEnsureInit) return;", + "const mgr = gdjs._dualSenseEnsureInit(runtimeScene);", + "eventsFunctionContext.returnValue = mgr.batteryStatus;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onFirstSceneLoaded" + }, + { + "functionName": "onScenePreEvents" + }, + { + "functionName": "ConnectDualSense" + }, + { + "functionName": "DisconnectDualSense" + }, + { + "functionName": "IsConnecting" + }, + { + "functionName": "ConnectionStatus" + }, + { + "functionName": "IsConnected" + }, + { + "functionName": "IsConnectionType" + }, + { + "functionName": "IsTouchpadPressed" + }, + { + "functionName": "WasTouchpadJustPressed" + }, + { + "functionName": "IsTouchpadFingerActive" + }, + { + "functionName": "DidTouchpadFingerJustStart" + }, + { + "functionName": "IsPSButtonPressed" + }, + { + "functionName": "WasPSButtonJustPressed" + }, + { + "functionName": "IsMicMuteButtonPressed" + }, + { + "functionName": "WasMicMuteButtonJustPressed" + }, + { + "functionName": "IsEdgeButtonPressed" + }, + { + "functionName": "ResetAdaptiveTrigger" + }, + { + "functionName": "SetTriggerResistance" + }, + { + "functionName": "SetTriggerWeaponSection" + }, + { + "functionName": "SetTriggerAutoVibration" + }, + { + "functionName": "SetLightbarColor" + }, + { + "functionName": "SetLightbarRGB" + }, + { + "functionName": "SetLightbarRainbow" + }, + { + "functionName": "SetPlayerIndicator" + }, + { + "functionName": "SetPlayerIndicatorBitmask" + }, + { + "functionName": "SetMicMuteLED" + }, + { + "functionName": "VibrateMotors" + }, + { + "functionName": "StopVibration" + }, + { + "functionName": "IsVibrating" + }, + { + "functionName": "PlaySpeakerTestTone" + }, + { + "functionName": "StopSpeakerTestTone" + }, + { + "functionName": "PlaySoundOnControllerSpeaker" + }, + { + "functionName": "SetSpeakerVolume" + }, + { + "functionName": "StartMicrophoneTest" + }, + { + "functionName": "StopMicrophoneTest" + }, + { + "functionName": "IsMicrophoneVolumeAbove" + }, + { + "functionName": "IsHeadphonePlugged" + }, + { + "functionName": "CalibrateGyroscope" + }, + { + "functionName": "TouchpadX" + }, + { + "functionName": "TouchpadY" + }, + { + "functionName": "TouchpadNormalizedX" + }, + { + "functionName": "TouchpadNormalizedY" + }, + { + "functionName": "TouchpadDeltaX" + }, + { + "functionName": "TouchpadDeltaY" + }, + { + "functionName": "TouchpadTouchCount" + }, + { + "functionName": "GyroPitch" + }, + { + "functionName": "GyroYaw" + }, + { + "functionName": "GyroRoll" + }, + { + "functionName": "AccelX" + }, + { + "functionName": "AccelY" + }, + { + "functionName": "AccelZ" + }, + { + "functionName": "TiltPitchDegrees" + }, + { + "functionName": "TiltRollDegrees" + }, + { + "functionName": "MicrophoneVolume" + }, + { + "functionName": "MicrophonePeak" + }, + { + "functionName": "BatteryLevel" + }, + { + "functionName": "ConnectionType" + }, + { + "functionName": "ControllerModel" + }, + { + "functionName": "BatteryStatus" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [] +}