{"version":3,"sources":["../src/controller-db.ts"],"names":[],"mappings":"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","file":"controller-db.mjs","sourcesContent":["// controller-db.ts -- pure SDL_GameControllerDB parsing + GUID derivation.\n//\n// This module holds ONLY pure functions (no DOM, no ECS, no vendored data\n// import). The 554KB gamecontrollerdb.txt lives in the separate\n// `./controller-db-data` sub-export so it never enters the main-entry\n// bundle; the backend dynamic-imports both this module and the data module\n// on first sight of a non-standard gamepad (D-2 lazy-load).\n//\n// SDL GUID reference: KB source\n// .forgeax-harness/knowledge-base/sources/2026-07-07-sdl-guid-browser-gamepad-id-mapping.md\n//\n// D-13 strategy 2: build a match GUID from VID/PID with bus=USB(0x03),\n// CRC=0, version=0, driver signature/data=0. This matches the vast\n// majority of gamecontrollerdb.txt native entries directly.\n\n/** A single mapping token target: physical button / axis / hat index. */\nexport type MappingToken =\n  | { readonly kind: 'button'; readonly index: number }\n  | { readonly kind: 'axis'; readonly index: number; readonly half?: '+' | '-' }\n  | { readonly kind: 'hat'; readonly index: number; readonly mask: number };\n\n/**\n * Parsed mapping tokens for one controller entry, keyed by the SDL logical\n * name (`a`, `b`, `leftx`, `dpup`, `lefttrigger`, ...). Values are the\n * physical source (button `bN`, axis `aN`, hat `hN.M`).\n */\nexport type MappingTokens = Readonly<Record<string, MappingToken>>;\n\n/** One controller-DB row: its platform section + parsed mapping tokens. */\nexport interface ControllerDbEntry {\n  readonly platform: string | undefined;\n  readonly tokens: MappingTokens;\n}\n\n/**\n * Parsed controller DB: SDL GUID (32-char hex) -> one entry per platform\n * section. The same GUID can appear in multiple platform sections, so the\n * value is an array.\n */\nexport type ControllerDb = Readonly<Record<string, readonly ControllerDbEntry[]>>;\n\n/** Recognised web platform section labels (D-13). */\nconst CONTROLLER_PLATFORMS = ['Windows', 'Mac OS X', 'Linux', 'Android', 'iOS'] as const;\nexport type ControllerPlatform = (typeof CONTROLLER_PLATFORMS)[number];\n\n/** Parse one `key:source` mapping token into a MappingToken. */\nfunction parseMappingToken(source: string): MappingToken | undefined {\n  // Half-axis prefix: +aN / -aN.\n  let half: '+' | '-' | undefined;\n  let body = source;\n  if (body.startsWith('+') || body.startsWith('-')) {\n    half = body[0] as '+' | '-';\n    body = body.slice(1);\n  }\n  const prefix = body[0];\n  if (prefix === 'b') {\n    const index = Number.parseInt(body.slice(1), 10);\n    return Number.isNaN(index) ? undefined : { kind: 'button', index };\n  }\n  if (prefix === 'a') {\n    const index = Number.parseInt(body.slice(1), 10);\n    if (Number.isNaN(index)) return undefined;\n    return half ? { kind: 'axis', index, half } : { kind: 'axis', index };\n  }\n  if (prefix === 'h') {\n    // hN.M -- hat N, direction mask M.\n    const dot = body.indexOf('.');\n    if (dot < 0) return undefined;\n    const index = Number.parseInt(body.slice(1, dot), 10);\n    const mask = Number.parseInt(body.slice(dot + 1), 10);\n    return Number.isNaN(index) || Number.isNaN(mask) ? undefined : { kind: 'hat', index, mask };\n  }\n  return undefined;\n}\n\n/**\n * Parse a gamecontrollerdb.txt string into a ControllerDb map. Comment\n * (`#`) and blank lines are skipped. Each data line is\n * `GUID,Name,token:source,...,platform:PLATFORM,`.\n */\nexport function parseControllerDb(txt: string): ControllerDb {\n  const db: Record<string, ControllerDbEntry[]> = {};\n  for (const rawLine of txt.split('\\n')) {\n    const line = rawLine.trim();\n    if (line.length === 0 || line.startsWith('#')) continue;\n    const fields = line.split(',');\n    const guid = fields[0];\n    if (!guid || guid.length !== 32) continue;\n    let platform: string | undefined;\n    const tokens: Record<string, MappingToken> = {};\n    // fields[1] is the human-readable name; mapping tokens start at index 2.\n    for (let i = 2; i < fields.length; i++) {\n      const field = fields[i];\n      if (!field) continue;\n      const colon = field.indexOf(':');\n      if (colon < 0) continue;\n      const key = field.slice(0, colon);\n      const source = field.slice(colon + 1);\n      if (key === 'platform') {\n        platform = source;\n        continue;\n      }\n      // crc / sdk / hint fields are not physical mappings; skip non b/a/h.\n      const token = parseMappingToken(source);\n      if (token) tokens[key] = token;\n    }\n    const entry: ControllerDbEntry = { platform, tokens };\n    const bucket = db[guid];\n    if (bucket) {\n      bucket.push(entry);\n    } else {\n      db[guid] = [entry];\n    }\n  }\n  return db;\n}\n\n/** Format a 16-bit value as a 4-char little-endian hex pair. */\nfunction le16Hex(value: number): string {\n  const lo = value & 0xff;\n  const hi = (value >> 8) & 0xff;\n  return lo.toString(16).padStart(2, '0') + hi.toString(16).padStart(2, '0');\n}\n\n/**\n * Build a 32-char SDL GUID hex from a VID/PID pair (D-13 strategy 2):\n * bus (default USB 0x03) + CRC=0 + VID + fill + PID + fill + version=0 +\n * driver sig/data=0. Each 16-bit field is little-endian within the string.\n *\n * Example: buildGuidFromVidPid(0x045e, 0x028e) ->\n * '030000005e0400008e02000000000000' (Xbox 360).\n */\nexport function buildGuidFromVidPid(vid: number, pid: number, bus = 0x03): string {\n  return (\n    le16Hex(bus) + // bytes 0-1: bus type\n    '0000' + // bytes 2-3: CRC = 0\n    le16Hex(vid) + // bytes 4-5: VID (LE)\n    '0000' + // bytes 6-7: fill\n    le16Hex(pid) + // bytes 8-9: PID (LE)\n    '0000' + // bytes 10-11: fill\n    '0000' + // bytes 12-13: version = 0\n    '0000' // bytes 14-15: driver signature + data = 0\n  );\n}\n\nconst CHROME_VENDOR_RE = /Vendor:\\s*([0-9a-f]{1,4})/i;\nconst CHROME_PRODUCT_RE = /Product:\\s*([0-9a-f]{1,4})/i;\n\n/**\n * Extract an SDL GUID from a browser `Gamepad.id` string. Handles the\n * Chrome/Edge (`Vendor: XXXX Product: YYYY`) and Firefox (`VID-PID-Name`,\n * leading-zero tolerant) formats. Returns undefined when VID/PID cannot be\n * extracted (Safari name-only strings, XInput devices) so the caller keeps\n * the Feat1 empty-signal (graceful degradation, R-3).\n */\nexport function extractGuidFromGamepadId(id: string): string | undefined {\n  // XInput devices expose no VID/PID in either Chrome or Firefox.\n  if (id.toLowerCase().includes('xinput')) return undefined;\n\n  // Chrome / Edge: \"... (STANDARD GAMEPAD Vendor: 054c Product: 09cc)\".\n  const vendorMatch = id.match(CHROME_VENDOR_RE);\n  const productMatch = id.match(CHROME_PRODUCT_RE);\n  if (vendorMatch?.[1] && productMatch?.[1]) {\n    const vid = Number.parseInt(vendorMatch[1], 16);\n    const pid = Number.parseInt(productMatch[1], 16);\n    if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);\n  }\n\n  // Firefox: \"046d-c216-Logitech Dual Action\" (VID-PID-Name). The first two\n  // dash segments must be pure hex; a name-only Safari string fails this.\n  const parts = id.split('-');\n  if (parts.length >= 3 && parts[0] && parts[1]) {\n    const vidHex = parts[0];\n    const pidHex = parts[1];\n    if (/^[0-9a-f]{1,4}$/i.test(vidHex) && /^[0-9a-f]{1,4}$/i.test(pidHex)) {\n      const vid = Number.parseInt(vidHex, 16);\n      const pid = Number.parseInt(pidHex, 16);\n      if (!Number.isNaN(vid) && !Number.isNaN(pid)) return buildGuidFromVidPid(vid, pid);\n    }\n  }\n\n  return undefined;\n}\n\n/**\n * Map a `navigator.userAgent` string to a gamecontrollerdb.txt platform\n * section label (D-13). Returns undefined when the UA is unrecognised, in\n * which case the caller falls back to any-platform entry selection.\n */\nexport function platformFromUserAgent(ua: string): ControllerPlatform | undefined {\n  // Order matters: iOS + Android both contain substrings that overlap with\n  // the desktop checks, so test the mobile/specific cases first.\n  if (/iPhone|iPad|iPod/.test(ua)) return CONTROLLER_PLATFORMS[4];\n  if (/Android/.test(ua)) return CONTROLLER_PLATFORMS[3];\n  if (/Windows/.test(ua)) return CONTROLLER_PLATFORMS[0];\n  if (/Mac OS X|Macintosh/.test(ua)) return CONTROLLER_PLATFORMS[1];\n  if (/Linux/.test(ua)) return CONTROLLER_PLATFORMS[2];\n  return undefined;\n}\n\n/**\n * Select the best mapping entry for a GUID: prefer the entry whose platform\n * section matches `platform`, else fall back to the first available entry\n * (any platform). Returns undefined when the GUID is absent from the DB.\n */\nexport function selectBestMappingEntry(\n  db: ControllerDb,\n  guid: string,\n  platform: string | undefined,\n): ControllerDbEntry | undefined {\n  const entries = db[guid];\n  if (!entries || entries.length === 0) return undefined;\n  if (platform) {\n    const match = entries.find((e) => e.platform === platform);\n    if (match) return match;\n  }\n  return entries[0];\n}\n"]}