Skip to content

Commit

Permalink
Work on game controller DB
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkwhoffmann committed Dec 21, 2024
1 parent c80db5c commit 109eda1
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 87 deletions.
153 changes: 75 additions & 78 deletions GUI/Dialogs/Preferences/DeviceDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,16 @@
* vendor ID and product ID.
*/

// Device mapping scheme (HIDEvent -> Item -> Value -> [Actions])
typealias DeviceMapping = [ HIDEvent: [ Int: [ Int: [GamePadAction] ] ] ]

/*
struct MyData {

var name = "Generic"

var vendorID = 0
var productID = 0
var version = 0

// Button actions
var b: [Int: ([GamePadAction],[GamePadAction])] = [:]

// Axis actions
var a0: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])
var a1: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])
var a2: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])
var a3: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])
var a4: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])
var a5: ([GamePadAction],[GamePadAction],[GamePadAction]) = ([],[],[])

var isGeneric: Bool { return vendorID == 0 && productID == 0 && version == 0 }
}
*/
// Mapping scheme: HIDEvent -> Item -> Value -> [Actions]
typealias HIDMapping = [ HIDEvent: [ Int: [ Int: [GamePadAction] ] ] ]

class DeviceDatabase {

// Known devices
var known: [String: String] = [:]
var known: [GUID: String] = [:]

// Devices configured by the user
var custom: [String: String] = [:]
// var custom: [String: String] = [:]

//
// Initializing
Expand All @@ -83,115 +59,131 @@ class DeviceDatabase {

// Start from scratch
known = [:]
custom = [:]
// custom = [:]

// Register all known devices
parse(file: "gamecontrollerdb", mapping: &known)
parse(file: "gamecontrollerdb", withExtension: "txt")
}

//
// Creating the database
//

func parse(file: String, mapping: inout [String: String]) {
func parse(file: String, withExtension ext: String) {

if let url = Bundle.main.url(forResource: file, withExtension: "txt") {
if let url = Bundle.main.url(forResource: file, withExtension: ext) {

do {

let fileContents = try String(contentsOf: url, encoding: .utf8)
for line in fileContents.split(separator: "\n") {
self.parse(line: String(line), mapping: &mapping)
self.parse(line: String(line))
}

} catch { print("Error reading file: \(error)") }
}
}

func parse(line: String, mapping: inout [String: String]) {
func parse(line: String) {

// Eliminate newline characters (if any)
var descriptor = line.replacingOccurrences(of: "\n", with: "")

// Eliminate unneeded commas at both ends
descriptor = descriptor.trimmingCharacters(in: CharacterSet(charactersIn: ","))

// Extract the GUID and create the database entry
if let range = descriptor.range(of: ",") {

let prefix = String(descriptor[..<range.lowerBound])
if prefix.hasPrefix("0") && prefix.count > 28 {

mapping[prefix] = descriptor
}
}
// Extract the GUID and create a mapping
if let guid = GUID(string: descriptor) { known[guid] = descriptor }
}

func update(line: String) {

parse(line: line, mapping: &custom)
parse(line: line)
}

//
// Querying the database
//

func seekDevice(vendorID: String, productID: String, version: String) -> String {
func hasMatch(guid: GUID) -> Bool {

var result: String?
return hasMatch(vendorID: guid.vendorID, productID: guid.productID)
}

func parse(_ value: String?) -> Int? {
return value == nil ? nil : value == "" ? 0 : Int(value!)
func hasPerfectMatch(guid: GUID) -> Bool {

return hasMatch(vendorID: guid.vendorID, productID: guid.productID, version: guid.version)
}

func hasMatch(vendorID: Int, productID: Int, version: Int? = nil) -> Bool {

for (guid, _) in known {

if guid.vendorID == vendorID && guid.productID == productID {
if version == nil || guid.version == version { return true }
}
}

let vend = parse(vendorID)
let prod = parse(productID)
let vers = parse(version)
return false
}

// 1. Crawl through the custom database
result = seekDevice(vendorID: vend, productID: prod, version: vers, mapping: custom)
func seek(guid: GUID) -> String? {

// 2. Crawl through the known devices
if result == nil { result = seekDevice(vendorID: vend, productID: prod, version: vers, mapping: known) }
for (otherguid, result) in known {

// 3. Crawl through the known devices, ignoring the version number
if result == nil { result = seekDevice(vendorID: vend, productID: prod, mapping: known) }
// Compare vendorID, productID, and version
if !guid.match(guid: otherguid, offset: 8, length: 4) { continue }
if !guid.match(guid: otherguid, offset: 16, length: 4) { continue }
if !guid.match(guid: otherguid, offset: 24, length: 4) { continue }

return result;
}

// 4. Assign a default mapping
if result == nil { result = "Generic,a:b0,b:b1,leftx:a0,lefty:a1" }
for (otherguid, result) in known {

return result!
// Only compare the vendorID and productID
if !guid.match(guid: otherguid, offset: 8, length: 4) { continue }
if !guid.match(guid: otherguid, offset: 16, length: 4) { continue }

return result;
}

return nil
}

func seekDevice(vendorID: Int?, productID: Int?, version: Int? = nil, mapping: [String: String]) -> String? {
/*
func seekDevice(vendorID: String?, productID: String?, version: String? = nil) -> String? {

print("seekDevice(\(vendorID), \(productID), \(version))")
func parse(_ value: String?) -> Int? {
return value == nil ? nil : value == "" ? 0 : Int(value!)
}

return seekDevice(vendorID: parse(vendorID),
productID: parse(productID),
version: parse(version),
mapping: mapping)

for (key, value) in mapping {
}
*/

func hex(_ i: Int) -> Int {
func seek(vendorID: Int?, productID: Int?, version: Int? = nil) -> String? {

let start = key.index(key.startIndex, offsetBy: i)
let end = key.index(start, offsetBy: 2)
return Int(key[start..<end], radix: 16) ?? 0
}
print("seek(\(vendorID), \(productID), \(version))")

let _vendorID = hex(8) | hex(10) << 8;
let _productID = hex(16) | hex(18) << 8;
let _version = hex(24) | hex(26) << 8;
for (guid, value) in known {

if vendorID != nil && vendorID != _vendorID { continue }
if productID != nil && productID != _productID { continue }
if version != nil && version != _version { continue }
if vendorID != nil && vendorID != guid.vendorID { continue }
if productID != nil && productID != guid.productID { continue }
if version != nil && version != guid.version { continue }

return value
}
return nil
}

func query(vendorID: String, productID: String, version: String) -> DeviceMapping {
func query(guid: GUID) -> HIDMapping {

var result: DeviceMapping = [
var result: HIDMapping = [

.AXIS: [:],
.BUTTON: [:],
Expand All @@ -202,11 +194,16 @@ class DeviceDatabase {
.HATSWITCH: [:]
]

let descriptor = seekDevice(vendorID: vendorID, productID: productID, version: version)
// Crawl through the database
var descriptor = seek(guid: guid)

// Assign a default descriptor if needed
if descriptor == nil { descriptor = "Generic,a:b0,b:b1,leftx:a0,lefty:a1" }

print("\(descriptor)")

// Iterate through all key value pairs
for assignment in descriptor.split(separator: ",") {
for assignment in descriptor!.split(separator: ",") {

let pair = assignment.split(separator: ":").map { String($0) }
if pair.count != 2 { continue }
Expand Down
8 changes: 4 additions & 4 deletions GUI/Dialogs/Preferences/DevicesPrefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ extension PreferencesController {
}
}

var guid: GUID {return selectedDev?.guid ?? GUID() }

func refreshDevicesTab() {

let pad = selectedDev
Expand Down Expand Up @@ -48,9 +50,7 @@ extension PreferencesController {

// HID mapping
devHidMapping.focusRingType = .none
if let descriptor = pad?.db.seekDevice(vendorID: property(kIOHIDVendorIDKey),
productID: property(kIOHIDProductIDKey),
version: property(kIOHIDVersionNumberKey)) {
if let descriptor = pad?.db.seek(guid: guid) {

let trimmed = descriptor.trimmingCharacters(in: CharacterSet(charactersIn: ","))
devHidMapping.string = trimmed.replacingOccurrences(of: ",", with: ",\n")
Expand All @@ -69,7 +69,7 @@ extension PreferencesController {

let hide = pad == nil || pad?.isMouse == true
devImage.isHidden = hide
// devHidMappingBox.isHidden = hide
devHidMappingScrollView.isHidden = hide
devHidEvent.isHidden = hide
devAction.isHidden = hide
devAction2.isHidden = hide
Expand Down
2 changes: 1 addition & 1 deletion GUI/Dialogs/Preferences/PreferencesController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class PreferencesController: DialogController {
@IBOutlet weak var devAction2: NSTextField!
@IBOutlet weak var devHidEvent: NSTextField!
@IBOutlet weak var devHidMapping: NSTextView!
// @IBOutlet weak var devHidMappingBox: NSBox!
@IBOutlet weak var devHidMappingScrollView: NSScrollView!

//
// Methods
Expand Down
10 changes: 7 additions & 3 deletions GUI/Peripherals/GamePad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ class GamePad {
// var traits: MyData = MyData()

// HID mapping
var mapping: DeviceMapping?
var mapping: HIDMapping?

// Reference to the HID device
var device: IOHIDDevice?
var vendorID: String { return device?.vendorID ?? "" }
var productID: String { return device?.productID ?? "" }
var locationID: String { return device?.locationID ?? "" }
var version: String { return device?.versionNumberKey ?? "" }
var guid: GUID { return device?.guid ?? GUID() }

// Type of the managed device (joystick or mouse)
var type: ControlPortDevice
Expand All @@ -52,7 +53,7 @@ class GamePad {
var icon: NSImage?

// Indicates if this device is officially supported
var isKnown = false
var isKnown: Bool { return db.hasMatch(guid: guid) }

// Keymap of the managed device (only set for keyboard emulated devices)
var keyMap: Int?
Expand Down Expand Up @@ -93,7 +94,10 @@ class GamePad {

func updateMapping() {

mapping = db.query(vendorID: vendorID, productID: productID, version: version)
if device != nil {

mapping = db.query(guid: device!.guid)
}
}

func property(key: String) -> String? {
Expand Down
Loading

0 comments on commit 109eda1

Please sign in to comment.