sourceafGamepad::Gamepad.fan

using [java] fanx.interop::Interop
using [java] fanx.interop::ByteArray
using [java] purejavahidapi::HidDevice
using [java] purejavahidapi::HidDeviceInfo
using [java] purejavahidapi::PureJavaHidApi
using [java] purejavahidapi::InputReportListener

** Represents a Gamepad controller.
** Use [listHidDevices()]`listHidDevices` to obtain an instance.
class Gamepad {
    private static const Log log := Gamepad#.pod.log

    ** The 16 bit vendor ID.
    const Int   vendorId
    
    ** The 16 bit product ID.
    const Int   productId

    ** Name of the manufacturer.
    const Str?  manufacturer
    
    ** Product description. 
    const Str?  prodcutDesc
    
    ** A platform dependent path that describes the 'physical' path through hubs and ports to the device.
    const Str   path
    
    ** A number between 0 - 1, over which a button value is considered to be pressed.
    ** Used to created 'buttonUp' and 'buttonDown' event data.
    Float       buttonThreshold     := 0.6f
    
    ** Listener that's called when the Gamepad input changes.
    |GamepadEvent|? onInput {
        set {
            if (it == null) close; else if (&onInput == null) open
            &onInput = it
        }
    }
    
    ** Listener that's called when the Gamepad is disconnected / unplugged.
    |Gamepad|?  onDisconnect

    private HidDevice?      hidDevice
    private HidDeviceInfo   hidDeviceInfo
    private Enum:Float      oldValues   := Enum:Float[:]
    
    private new make(HidDeviceInfo hidDeviceInfo) {
        this.hidDeviceInfo  = hidDeviceInfo
        this.vendorId       = hidDeviceInfo.getVendorId
        this.productId      = hidDeviceInfo.getProductId
        this.manufacturer   = hidDeviceInfo.getManufacturerString
        this.prodcutDesc    = hidDeviceInfo.getProductString
        this.path           = hidDeviceInfo.getPath
    }
    
    ** Lists all USB-HID devices. Some may be Gamepad controllers, some may not be.
    static Gamepad[] listHidDevices() {
        ((HidDeviceInfo[]) Interop.toFan(PureJavaHidApi.enumerateDevices)).map { Gamepad(it) }
    }

    ** Lists all supported Gamepad controllers.
    static Gamepad[] listGamepads() {
        listHidDevices.findAll {
            (it.vendorId == 0x045E && it.productId == 0x028E) ||
            (it.vendorId == 0x0E8F && it.productId == 0x310D)
        }
    }
    
    private Void open() {
        hidDevice = PureJavaHidApi.openDevice(hidDeviceInfo)
        hidDevice.setInputReportListener    ((|HidDevice?, Int, ByteArray?, Int|) #onHidInput.func.bind([this]))
        hidDevice.setDeviceRemovalListener  ((|HidDevice?|) #onHidRemove.func.bind([this]))
    }
    
    private Void close() {
        try {
            hidDevice?.setInputReportListener(null)
            hidDevice?.setDeviceRemovalListener(null)
            hidDevice?.close
        } catch {
        } finally {
            hidDevice = null            
        }
    }
    
    private Void onHidRemove(HidDevice? source) {
        close
        onDisconnect?.call(this)
    }

    private Void onHidInput(HidDevice? source, Int id, ByteArray? data, Int len) {

        // for multi controller support, this could be converted to a generic structure, configured by pod Index:
        // 
        //   faceDown = ["byte":13, "mask":0xFF, "type":"val"]
        // Controller Mapping

        // or we could try to find a way to get and decode the HID Descriptor
        // or we could try to find a java native gamepad api

        rawValues := null as Enum:Float
        if (source.getHidDeviceInfo.getVendorId == 0x045E && source.getHidDeviceInfo.getProductId == 0x028E)
            rawValues = decodeXbox360Windows(data)
        if (source.getHidDeviceInfo.getVendorId == 0x0E8F && source.getHidDeviceInfo.getProductId == 0x310D)
            rawValues = decodeGamepad3Turbo(data)

        if (rawValues == null)
            log.warn("Gamepad not supported: 0x${source.getHidDeviceInfo.getVendorId.toHex(4)} 0x${source.getHidDeviceInfo.getProductId.toHex(4)} ${source.getHidDeviceInfo.getProductString}")
        
        changed     := false
        buttonsUp   := GamepadButton#.emptyList
        buttonsDown := GamepadButton#.emptyList

        rawValues.each |val, button| {
            if (val != oldValues[button]) {
                changed = true
                if (button is GamepadButton) {
                    oldState := oldValues[button] > buttonThreshold
                    newState := val > buttonThreshold
                    if (oldState.xor(newState)) {
                        if (newState) {
                            buttonsDown = buttonsDown.rw
                            buttonsDown.add(button)
                        } else {
                            buttonsUp = buttonsUp.rw
                            buttonsUp.add(button)
                        }
                    }
                }
            }
        }
        
        oldValues = rawValues
        if (changed) {
            event := GamepadEvent {
                it.gamepad = this
                it.axesValues   = rawValues.findAll |val, key| { key is GamepadAxis }
                it.buttonValues = rawValues.findAll |val, key| { key is GamepadButton }
                it.buttonsUp    = buttonsUp
                it.buttonsDown  = buttonsDown
            }

            try onInput?.call(event)
            catch (Err err)
                err.trace
        }
    }
    
    private Enum:Float decodeXbox360Windows(ByteArray? data) {
        rawValues   := Enum:Float[:]

        // this is a good site, but appears to be wrong!?
        // http://free60.org/wiki/GamePad
        
        rawValues[GamepadButton.faceDown]       = data[10].and(0x01) > 0 ? 1f : 0f
        rawValues[GamepadButton.faceLeft]       = data[10].and(0x04) > 0 ? 1f : 0f
        rawValues[GamepadButton.faceRight]      = data[10].and(0x02) > 0 ? 1f : 0f
        rawValues[GamepadButton.faceUp]         = data[10].and(0x08) > 0 ? 1f : 0f

        rawValues[GamepadButton.leftShoulder]   = data[10].and(0x10) > 0 ? 1f : 0f
        rawValues[GamepadButton.rightShoulder]  = data[10].and(0x20) > 0 ? 1f : 0f
        rawValues[GamepadButton.leftTrigger]    = (data[ 9].and(0xFF) - 0x80).max(0) / 0x80.toFloat         // 80-FF = left
        rawValues[GamepadButton.rightTrigger]   = (data[ 9].and(0xFF) - 0x80).negate.max(0) / 0x80.toFloat  // 00-80 = right

        rawValues[GamepadButton.select]         = data[10].and(0x40) > 0 ? 1f : 0f
        rawValues[GamepadButton.start]          = data[10].and(0x80) > 0 ? 1f : 0f
        rawValues[GamepadButton.logo]           = 0f    // ???
        
        rawValues[GamepadButton.leftAnalogue]   = data[11].and(0x01) > 0 ? 1f : 0f
        rawValues[GamepadButton.rightAnalogue]  = data[11].and(0x02) > 0 ? 1f : 0f
        
        dpad    := data[11].and(0x04) > 0
        dpadVal := data[11].and(0x18).shiftr(3)
        rawValues[GamepadButton.dpadUp]         = dpad && dpadVal == 0x00 ? 1f : 0f
        rawValues[GamepadButton.dpadLeft]       = dpad && dpadVal == 0x03 ? 1f : 0f
        rawValues[GamepadButton.dpadRight]      = dpad && dpadVal == 0x01 ? 1f : 0f
        rawValues[GamepadButton.dpadDown]       = dpad && dpadVal == 0x02 ? 1f : 0f

        rawValues[GamepadAxis.leftX]            = (data[1].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.leftY]            = (data[3].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.rightX]           = (data[5].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.rightY]           = (data[7].and(0xFF) - 0x80) / 0x80.toFloat

        return rawValues
    }
    
    private Enum:Float decodeGamepad3Turbo(ByteArray? data) {
        rawValues   := Enum:Float[:]

        rawValues[GamepadButton.faceDown]       = data[13].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.faceLeft]       = data[14].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.faceRight]      = data[12].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.faceUp]         = data[11].and(0xFF) / 0xFF.toFloat

        rawValues[GamepadButton.leftShoulder]   = data[15].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.rightShoulder]  = data[16].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.leftTrigger]    = data[17].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.rightTrigger]   = data[18].and(0xFF) / 0xFF.toFloat

        rawValues[GamepadButton.select]         = data[ 1].and(0x01) > 0 ? 1f : 0f
        rawValues[GamepadButton.start]          = data[ 1].and(0x02) > 0 ? 1f : 0f
        rawValues[GamepadButton.logo]           = data[ 1].and(0x10) > 0 ? 1f : 0f
        
        rawValues[GamepadButton.leftAnalogue]   = data[ 1].and(0x04) > 0 ? 1f : 0f
        rawValues[GamepadButton.rightAnalogue]  = data[ 1].and(0x08) > 0 ? 1f : 0f
        
        rawValues[GamepadButton.dpadUp]         = data[ 9].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.dpadLeft]       = data[ 8].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.dpadRight]      = data[ 7].and(0xFF) / 0xFF.toFloat
        rawValues[GamepadButton.dpadDown]       = data[10].and(0xFF) / 0xFF.toFloat

        rawValues[GamepadAxis.leftX]            = (data[3].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.leftY]            = (data[4].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.rightX]           = (data[5].and(0xFF) - 0x80) / 0x80.toFloat
        rawValues[GamepadAxis.rightY]           = (data[6].and(0xFF) - 0x80) / 0x80.toFloat

        return rawValues
    }

    @NoDoc
    override Str toStr() { prodcutDesc }
}

** Fired when the Gamepad input values change.
class GamepadEvent {
    ** The owning Gamepad instance.
    Gamepad             gamepad
    
    ** Thumbstick values that have been normalised between -1 and 1.
    ** 
    ** -1 means fully left / up. 1 means fully right / down. 
    GamepadAxis:Float   axesValues

    ** Analogue button values that have been normalised between 0 and 1.
    ** 
    ** 0 means not pressed, 1 means fully pressed.  
    GamepadButton:Float buttonValues
    
    ** A list of buttons whose analogue values are less than the Gamepad 'buttonThreshold'.
    GamepadButton[]     buttonsUp

    ** A list of buttons whose analogue values are more than the Gamepad 'buttonThreshold'.
    GamepadButton[]     buttonsDown
    
    @NoDoc
    new make(|This| f) { f(this) }
}