Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ var package = Package(
),
.library(
name: "AndroidFileManager", targets: ["AndroidFileManager"]
),
.library(
name: "AndroidNativeActivity", targets: ["AndroidNativeActivity"]
),
.library(
name: "AndroidInput", targets: ["AndroidInput"]
)
],
dependencies: [
Expand Down Expand Up @@ -153,7 +159,9 @@ var package = Package(
"AndroidLogging",
"AndroidLooper",
"AndroidHardware",
"AndroidFileManager"
"AndroidFileManager",
"AndroidNativeActivity",
"AndroidInput"
],
swiftSettings: [
.swiftLanguageMode(.v5),
Expand Down Expand Up @@ -489,6 +497,42 @@ var package = Package(
linkerSettings: [
.linkedLibrary("android", .when(platforms: [.android]))
]
),
.target(
name: "AndroidNativeActivity",
dependencies: [
"AndroidNDK",
"AndroidLooper",
"AndroidFileManager",
"AndroidInput",
.product(
name: "SystemPackage",
package: "swift-system"
)
],
swiftSettings: [
.swiftLanguageMode(.v6),
ndkVersionDefine,
sdkVersionDefine
],
linkerSettings: [
.linkedLibrary("android", .when(platforms: [.android]))
]
),
.target(
name: "AndroidInput",
dependencies: [
"AndroidNDK",
"AndroidLooper"
],
swiftSettings: [
.swiftLanguageMode(.v6),
ndkVersionDefine,
sdkVersionDefine
],
linkerSettings: [
.linkedLibrary("android", .when(platforms: [.android]))
]
)
],
swiftLanguageModes: [.v5, .v6]
Expand Down
4 changes: 2 additions & 2 deletions Sources/AndroidHardware/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func stub() -> Never {
fatalError("Not running on Android")
}

typealias ALooper_callbackFunc = @convention(c) (Int32, Int32, UnsafeMutableRawPointer?) -> Int32
public typealias ALooper_callbackFunc = @convention(c) (Int32, Int32, UnsafeMutableRawPointer?) -> Int32

// MARK: - ASensorEvent

Expand All @@ -29,7 +29,7 @@ typealias ALooper_callbackFunc = @convention(c) (Int32, Int32, UnsafeMutableRawP
* version(4) + sensor(4) + type(4) + reserved0(4) +
* timestamp(8) + data_union(64) + flags(4) + reserved1(12)
*/
public struct ASensorEvent {
public struct ASensorEvent: Sendable {
public var version: Int32 // sizeof(struct ASensorEvent)
public var sensor: Int32 // sensor identifier
public var type: Int32 // sensor type
Expand Down
167 changes: 167 additions & 0 deletions Sources/AndroidInput/GameController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// GameController.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 2/27/26.
//

#if AGDK
#if os(Android)
import Android
import AndroidNDK
#endif

/// Swift wrapper for the Android Game Controller (Paddleboat) C API.
@MainActor
public struct GameController: ~Copyable {

let environment: JNIEnvironment

public init(context: jobject, environment: JNIEnvironment) throws {
let result = Paddleboat_init(environment, context)
guard result == 0 else {
throw GameController.Error(rawValue: result) ?? GameController.Error.notInitialized
}
guard Paddleboat_isInitialized() else {
throw GameController.Error.notInitialized
}
self.environment = environment
}

deinit {
Paddleboat_destroy(environment)
}

public func update() {
Paddleboat_update(environment)
}

// MARK: - Back button
public static func setBackButtonConsumed(_ consume: Bool) {
Paddleboat_setBackButtonConsumed(consume)
}

public static func isBackButtonConsumed() -> Bool {
Paddleboat_getBackButtonConsumed()
}

// MARK: - Controller Info / Data
public static func getControllerStatus(index: Int32) -> ControllerStatus {
ControllerStatus(rawValue: Paddleboat_getControllerStatus(index)) ?? .inactive
}

public static func getControllerName(index: Int32, bufferSize: Int = 128) -> (ErrorCode, String) {
var buffer = [CChar](repeating: 0, count: bufferSize)
let err = Paddleboat_getControllerName(index, Int32(buffer.count), &buffer)
let code = ErrorCode(rawValue: err) ?? .noError
let name = buffer.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }
return (code, name)
}

// MARK: - Lights / Vibration
@discardableResult
public func setControllerLight(index: Int32, type: LightType, data: UInt32) -> ErrorCode {
let err = Paddleboat_setControllerLight(index, type.rawValue, data, environment)
return ErrorCode(rawValue: err) ?? .noError
}

@discardableResult
public func setControllerVibration(index: Int32, vibration: VibrationData) -> ErrorCode {
var cData = Paddleboat_Vibration_Data(duration_ms: vibration.durationMs,
left_motor_intensity: vibration.intensityLeft,
right_motor_intensity: vibration.intensityRight)
let err = Paddleboat_setControllerVibrationData(index, &cData, environment)
return ErrorCode(rawValue: err) ?? .noError
}

// MARK: - Motion
public static func getIntegratedMotionSensorFlags() -> IntegratedMotionSensorFlags {
IntegratedMotionSensorFlags(rawValue: Paddleboat_getIntegratedMotionSensorFlags())
}
}

// MARK: - Supporting Types

public extension GameController {

// MARK: - Version / Constants
public static var maxControllers: Int32 { 8 }

// MARK: - Error
public enum Error: Int32, Swift.Error {
case noError = 0
case alreadyInitialized = -2000
case notInitialized = -2001
case invalidParameter = -2002
case invalidControllerIndex = -2003
case noController = -2004
case featureNotSupported = -2005
case fileIO = -2006
case incompatibleMappingData = -2007
case invalidMappingData = -2008
case noMouse = -2009
case initGCMFailure = -2010
}

typealias ErrorCode = Error

public enum ControllerStatus: Int32 {
case inactive = 0
case active = 1
case justConnected = 2
case justDisconnected = 3
}

public struct Buttons: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) { self.rawValue = rawValue }
public static let a = Buttons(rawValue: 1 << 0)
public static let b = Buttons(rawValue: 1 << 1)
public static let x = Buttons(rawValue: 1 << 2)
public static let y = Buttons(rawValue: 1 << 3)
public static let l1 = Buttons(rawValue: 1 << 4)
public static let r1 = Buttons(rawValue: 1 << 5)
public static let l2 = Buttons(rawValue: 1 << 6)
public static let r2 = Buttons(rawValue: 1 << 7)
public static let l3 = Buttons(rawValue: 1 << 8)
public static let r3 = Buttons(rawValue: 1 << 9)
public static let dpadUp = Buttons(rawValue: 1 << 10)
public static let dpadDown = Buttons(rawValue: 1 << 11)
public static let dpadLeft = Buttons(rawValue: 1 << 12)
public static let dpadRight = Buttons(rawValue: 1 << 13)
public static let start = Buttons(rawValue: 1 << 14)
public static let select = Buttons(rawValue: 1 << 15)
public static let system = Buttons(rawValue: 1 << 16)
public static let touchpad = Buttons(rawValue: 1 << 17)
public static let aux1 = Buttons(rawValue: 1 << 18)
public static let aux2 = Buttons(rawValue: 1 << 19)
public static let aux3 = Buttons(rawValue: 1 << 20)
public static let aux4 = Buttons(rawValue: 1 << 21)
}

public enum LightType: Int32 {
case playerNumber = 0
case rgb = 1
}

public struct IntegratedMotionSensorFlags: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) { self.rawValue = rawValue }
public static let none = IntegratedMotionSensorFlags([])
public static let accelerometer = IntegratedMotionSensorFlags(rawValue: 0x00000001)
public static let gyroscope = IntegratedMotionSensorFlags(rawValue: 0x00000002)
public static let indexFlag = IntegratedMotionSensorFlags(rawValue: 0x40000000)
}

public struct VibrationData {
public var durationMs: Int32
public var intensityLeft: Float
public var intensityRight: Float
public init(durationMs: Int32, intensityLeft: Float, intensityRight: Float) {
self.durationMs = durationMs
self.intensityLeft = intensityLeft
self.intensityRight = intensityRight
}
}
}
#endif
Loading