Add live Mac controller tester and refresh bilingual app screenshots
No files matched your search
@@ -28,11 +28,11 @@ Cuando abra la app:
|
||||
|
||||
**Mac:** crea una sola `.app` completa, con todos los datos del juego dentro; pulsa **Abrir juego**. No necesitas copiar una carpeta RomFS aparte. No necesitas Sideloadly ni un iPhone. Las partidas quedan en `~/Library/Application Support/nxnative/<title ID>/`.
|
||||
|
||||
**iPhone / iPad:** crea una IPA ligera sin firmar. Instálala con Sideloadly y copia los datos del juego como te indica la app.
|
||||
**iPhone / iPad:** crea una IPA ligera sin firmar. Instálala con Sideloadly y copia los datos del juego como te indica la app. [Ver pantalla de preparación para iPhone/iPad](docs/images/nxnative-ios-es.png).
|
||||
|
||||
**Necesitas:** Mac con Apple silicon (M1 o posterior), macOS 26+ y Xcode 26+. El comando instala las otras herramientas. Los juegos y las llaves los aportas tú. La primera compilación puede tardar.
|
||||
|
||||

|
||||

|
||||
|
||||
## Qué aporta esta versión
|
||||
|
||||
@@ -74,7 +74,7 @@ Para actualizar, usa la misma cuenta y el mismo bundle ID efectivo de Sideloadly
|
||||
|
||||
El carrusel muestra los perfiles de esta copia; no proporciona ni descarga juegos. Debes añadir tus propios archivos del juego. Pulsa **Ver todos** arriba a la derecha para consultar la lista completa de soporte. Busca por nombre, versión o Title ID; cada resultado enlaza a su ficha. El catálogo vuelve a leer los perfiles locales cada vez que lo abres. El carrusel rota automáticamente y se pausa al colocar el cursor encima.
|
||||
|
||||

|
||||

|
||||
|
||||
**Las pruebas propias cubren compilación y empaquetado en Mac, no una verificación automática de juego en iPhone.** Los perfiles y reportes de juego proceden de upstream; revisa [la ficha de tu juego](dm/games/README.md#games) antes de prepararlo. Un perfil no garantiza que cualquier versión funcione en cualquier iPhone.
|
||||
|
||||
@@ -206,7 +206,11 @@ Los juegos preparados usan GameController de Apple en **Mac, iPhone y iPad**:
|
||||
- **PS5:** DualSense y DualSense Edge.
|
||||
- **PS4:** DualShock 4.
|
||||
|
||||
La app muestra automáticamente el nombre del control conectado y **Listo en este Mac** cuando Apple reconoce un perfil compatible. Se actualiza al conectar o desconectar el control. Si preparas para iPhone/iPad, te recuerda conectarlo **a ese dispositivo**: estar conectado al Mac no lo conecta al teléfono.
|
||||
La app muestra automáticamente el nombre del control conectado y **Listo en este Mac** cuando Apple reconoce un perfil compatible. Se actualiza al conectar o desconectar el control. El panel de controles del Mac no aparece al elegir iPhone/iPad. Conecta el control directamente al dispositivo donde vas a jugar.
|
||||
|
||||
En modo **Mac**, pulsa **Probar control**: se iluminan botones y cruceta, se mueven ambos sticks y las barras muestran la presión de los gatillos. **¡Está respondiendo!** aparece después de recibir una pulsación o movimiento. Si hay varios controles, puedes elegir cuál probar. La prueba sólo lee entradas mientras está abierta y no reemplaza los manejadores de entrada de los juegos.
|
||||
|
||||

|
||||
|
||||
Conecta el control desde **Bluetooth del dispositivo** y abre tu juego. No necesitas instalar drivers ni activar opciones en la herramienta. Se mapean sticks, cruceta, botones, gatillos, botones superiores, clics de sticks y menús. En iPhone/iPad los controles táctiles se ocultan al conectar uno físico.
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ When the app opens:
|
||||
|
||||
**Mac:** creates one complete game `.app`, with its game data included inside; click **Open game**. No separate RomFS folder to copy. No Sideloadly or iPhone needed. Saves stay in `~/Library/Application Support/nxnative/<title ID>/`.
|
||||
|
||||
**iPhone / iPad:** creates an unsigned lightweight IPA; install it with Sideloadly, then copy the game data as shown in the app.
|
||||
**iPhone / iPad:** creates an unsigned lightweight IPA; install it with Sideloadly, then copy the game data as shown in the app. [See the iPhone/iPad preparation screen](docs/images/nxnative-ios.png).
|
||||
|
||||
**Required:** an Apple silicon Mac (M1 or newer), macOS 26+ and Xcode 26+. The command installs the other tools. You must provide your own games and keys. Compilation can take time, especially for the first game.
|
||||
|
||||

|
||||

|
||||
|
||||
## What this version adds
|
||||
|
||||
@@ -204,7 +204,11 @@ The game apps use Apple's GameController framework on **Mac, iPhone and iPad**:
|
||||
- **PS5:** DualSense and DualSense Edge.
|
||||
- **PS4:** DualShock 4.
|
||||
|
||||
The preparation app automatically shows connected controller names and **Ready on this Mac** when Apple exposes a compatible gamepad profile. It updates when you connect or disconnect a controller. For an iPhone/iPad build, it reminds you to pair the controller **on that device**; a controller connected to the Mac is not a remote connection to the phone.
|
||||
The preparation app automatically shows connected controller names and **Ready on this Mac** when Apple exposes a compatible gamepad profile. It updates when you connect or disconnect a controller. The Mac controller panel is hidden when you choose iPhone/iPad. Pair a controller directly with the device where you will play.
|
||||
|
||||
In **Mac** mode, click **Test controller** to see live input: buttons and D-pad light up, both sticks move, and trigger bars show pressure. **Input received!** appears only after input is detected. If several controllers are connected, choose which one to test. The tester reads input only while its window is visible and does not take input handlers away from games.
|
||||
|
||||

|
||||
|
||||
Pair your controller in the device's **Bluetooth settings**, then open the prepared game. No extra driver or launcher setting is needed. Sticks, D-pad, face buttons, shoulders, triggers, stick clicks and menu buttons are mapped automatically. On iPhone/iPad, touch controls hide while a physical controller is connected.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import AppKit
|
||||
import Combine
|
||||
import GameController
|
||||
|
||||
struct DetectedController: Identifiable {
|
||||
struct DetectedController: Identifiable, Equatable {
|
||||
let id: ObjectIdentifier
|
||||
let name: String
|
||||
let supported: Bool
|
||||
@@ -28,9 +28,10 @@ struct DetectedController: Identifiable {
|
||||
}
|
||||
deinit {for observer in observers {center.removeObserver(observer)}}
|
||||
func refresh() {
|
||||
controllers=query().map {
|
||||
let next=query().map {
|
||||
DetectedController(id:ObjectIdentifier($0),name:$0.vendorName ?? "",supported:$0.extendedGamepad != nil)
|
||||
}
|
||||
if next != controllers {controllers=next}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import GameController
|
||||
|
||||
struct ControllerInputState: Equatable {
|
||||
var buttons: Set<String> = []
|
||||
var lx: Float = 0, ly: Float = 0, rx: Float = 0, ry: Float = 0
|
||||
var lt: Float = 0, rt: Float = 0
|
||||
var playStation = false
|
||||
var playStation5 = false
|
||||
var active: Bool {
|
||||
!buttons.isEmpty || [lx,ly,rx,ry].contains {abs($0)>0.15} || lt>0.1 || rt>0.1
|
||||
}
|
||||
init() {}
|
||||
init(_ g:GCExtendedGamepad) {
|
||||
let inputs: [(String,GCControllerButtonInput?)] = [
|
||||
("a",g.buttonA),("b",g.buttonB),("x",g.buttonX),("y",g.buttonY),
|
||||
("lb",g.leftShoulder),("rb",g.rightShoulder),("ls",g.leftThumbstickButton),("rs",g.rightThumbstickButton),
|
||||
("menu",g.buttonMenu),("options",g.buttonOptions),
|
||||
("up",g.dpad.up),("down",g.dpad.down),("left",g.dpad.left),("right",g.dpad.right)]
|
||||
buttons=Set(inputs.compactMap {$0.1?.isPressed == true ? $0.0 : nil})
|
||||
lx=g.leftThumbstick.xAxis.value;ly=g.leftThumbstick.yAxis.value
|
||||
rx=g.rightThumbstick.xAxis.value;ry=g.rightThumbstick.yAxis.value
|
||||
lt=g.leftTrigger.value;rt=g.rightTrigger.value
|
||||
playStation5=g is GCDualSenseGamepad
|
||||
playStation=playStation5 || g is GCDualShockGamepad
|
||||
}
|
||||
}
|
||||
|
||||
// Sample only while the tester is visible. No handlers, pairing or background capture.
|
||||
@MainActor final class ControllerTester: ObservableObject {
|
||||
@Published private(set) var devices: [DetectedController] = []
|
||||
@Published var selected: ObjectIdentifier? {
|
||||
didSet {if selected != oldValue {clear();confirmed=false}}
|
||||
}
|
||||
@Published private(set) var state=ControllerInputState()
|
||||
@Published private(set) var confirmed=false
|
||||
private let query: () -> [GCController]
|
||||
init(query:(() -> [GCController])? = nil) {self.query=query ?? {GCController.controllers()};sample()}
|
||||
func clear() {state=ControllerInputState()}
|
||||
func sample() {
|
||||
let connected=query().filter {$0.extendedGamepad != nil}
|
||||
let next=connected.map {DetectedController(id:ObjectIdentifier($0),name:$0.vendorName ?? "",supported:true)}
|
||||
if next.map(\.id) != devices.map(\.id) {devices=next}
|
||||
if !next.contains(where:{$0.id == selected}) {selected=next.first?.id}
|
||||
guard let controller=connected.first(where:{ObjectIdentifier($0)==selected}),let pad=controller.extendedGamepad else {
|
||||
clear();confirmed=false;return
|
||||
}
|
||||
let current=ControllerInputState(pad)
|
||||
if current != state {state=current}
|
||||
if current.active && !confirmed {confirmed=true}
|
||||
}
|
||||
}
|
||||
|
||||
struct ControllerTestView: View {
|
||||
let text: (String) -> String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var tester=ControllerTester()
|
||||
private let lime=Color(red:0.79,green:0.95,blue:0.37)
|
||||
private let tick=Timer.publish(every:1.0/60.0,on:.main,in:.common).autoconnect()
|
||||
private var state: ControllerInputState {tester.state}
|
||||
|
||||
private func button(_ key:String,_ label:String,_ size:CGFloat=36) -> some View {
|
||||
let pressed=state.buttons.contains(key)
|
||||
return Text(label).font(.system(size:13,weight:.bold,design:.rounded)).lineLimit(1).minimumScaleFactor(0.7)
|
||||
.frame(width:size,height:size).foregroundStyle(pressed ? Color.black : Color.white.opacity(0.75))
|
||||
.background(pressed ? lime : Color.white.opacity(0.08),in:RoundedRectangle(cornerRadius:size/3))
|
||||
.overlay(RoundedRectangle(cornerRadius:size/3).stroke(pressed ? lime : Color.white.opacity(0.12)))
|
||||
.accessibilityLabel(label).accessibilityValue(pressed ? text("Presionado") : text("Suelto"))
|
||||
}
|
||||
private func stick(_ key:String,_ label:String,_ x:Float,_ y:Float) -> some View {
|
||||
VStack(spacing:8) {
|
||||
ZStack {
|
||||
Circle().fill(Color.black.opacity(0.24)).frame(width:92,height:92)
|
||||
Path {p in p.move(to:CGPoint(x:46,y:4));p.addLine(to:CGPoint(x:46,y:88));p.move(to:CGPoint(x:4,y:46));p.addLine(to:CGPoint(x:88,y:46))}
|
||||
.stroke(Color.white.opacity(0.08),lineWidth:1).frame(width:92,height:92)
|
||||
Circle().fill(state.buttons.contains(key) ? lime : Color.white.opacity(0.72)).frame(width:38,height:38)
|
||||
.offset(x:CGFloat(x)*26,y:-CGFloat(y)*26)
|
||||
}
|
||||
Text(label).font(.system(size:11,weight:.semibold)).foregroundStyle(.secondary)
|
||||
}.accessibilityElement(children:.ignore).accessibilityLabel(label)
|
||||
.accessibilityValue(String(format:"X %.2f, Y %.2f",x,y))
|
||||
}
|
||||
private func trigger(_ label:String,_ value:Float) -> some View {
|
||||
VStack(alignment:.leading,spacing:7) {
|
||||
HStack {Text(label);Spacer();Text("\(Int(value*100))%").monospacedDigit()}
|
||||
.font(.system(size:12,weight:.semibold)).foregroundStyle(value>0.1 ? lime : .secondary)
|
||||
ProgressView(value:Double(value)).tint(lime)
|
||||
}.frame(width:170)
|
||||
}
|
||||
var body: some View {
|
||||
VStack(alignment:.leading,spacing:20) {
|
||||
HStack {
|
||||
VStack(alignment:.leading,spacing:5) {
|
||||
Text(text("Prueba tu control")).font(.system(size:25,weight:.bold,design:.rounded))
|
||||
Text(text("Pulsa botones y mueve los sticks. Verás cada movimiento aquí."))
|
||||
.font(.system(size:13)).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button(text("Cerrar")){dismiss()}.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
if tester.devices.isEmpty {
|
||||
Label(text("Conecta un control a este Mac por Bluetooth."),systemImage:"gamecontroller")
|
||||
.font(.system(size:13)).foregroundStyle(.secondary)
|
||||
} else {
|
||||
HStack {
|
||||
if tester.devices.count>1 {
|
||||
Picker(text("Control"),selection:$tester.selected) {
|
||||
ForEach(tester.devices) {device in
|
||||
Text(device.name.isEmpty ? text("Control") : device.name).tag(Optional(device.id))
|
||||
}
|
||||
}.frame(maxWidth:310)
|
||||
} else {
|
||||
Text(tester.devices.first?.name.isEmpty == false ? tester.devices.first!.name : text("Control"))
|
||||
.font(.system(size:14,weight:.semibold))
|
||||
}
|
||||
Spacer()
|
||||
Label(text(tester.confirmed ? "¡Está respondiendo!" : "Esperando tu primera pulsación…"),
|
||||
systemImage:tester.confirmed ? "checkmark.circle.fill" : "hand.tap")
|
||||
.font(.system(size:12,weight:.medium)).foregroundStyle(tester.confirmed ? lime : .secondary)
|
||||
}
|
||||
}
|
||||
VStack(spacing:22) {
|
||||
HStack {
|
||||
trigger(state.playStation ? "L2" : "LT",state.lt)
|
||||
Spacer()
|
||||
trigger(state.playStation ? "R2" : "RT",state.rt)
|
||||
}
|
||||
HStack {
|
||||
button("lb",state.playStation ? "L1" : "LB",42)
|
||||
Spacer()
|
||||
button("options",state.playStation5 ? "Create" : state.playStation ? "Share" : "View",42)
|
||||
button("menu",state.playStation ? "Options" : "Menu",42)
|
||||
Spacer()
|
||||
button("rb",state.playStation ? "R1" : "RB",42)
|
||||
}
|
||||
HStack(alignment:.center) {
|
||||
stick("ls","L3",state.lx,state.ly)
|
||||
Spacer()
|
||||
VStack(spacing:3) {
|
||||
button("up","↑")
|
||||
HStack(spacing:3) {button("left","←");Color.clear.frame(width:36,height:36);button("right","→")}
|
||||
button("down","↓")
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing:3) {
|
||||
button("y",state.playStation ? "△" : "Y")
|
||||
HStack(spacing:3) {button("x",state.playStation ? "□" : "X");button("b",state.playStation ? "○" : "B")}
|
||||
button("a",state.playStation ? "×" : "A")
|
||||
}
|
||||
Spacer()
|
||||
stick("rs","R3",state.rx,state.ry)
|
||||
}
|
||||
}.padding(26).background(Color.white.opacity(0.045),in:RoundedRectangle(cornerRadius:24))
|
||||
Text(text("Esta prueba comprueba la entrada del control en este Mac, no la compatibilidad de un juego."))
|
||||
.font(.system(size:11)).foregroundStyle(.secondary)
|
||||
}.padding(26).frame(width:690).preferredColorScheme(.dark)
|
||||
.onReceive(tick) {_ in if NSApplication.shared.isActive {tester.sample()} else {tester.clear()}}
|
||||
.onDisappear {tester.clear()}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,15 @@ enum EasyLanguage: String, CaseIterable {
|
||||
|
||||
struct EasyStrings {
|
||||
static let english: [String:String] = [
|
||||
"Probar control": "Test controller",
|
||||
"Prueba tu control": "Test your controller",
|
||||
"Pulsa botones y mueve los sticks. Verás cada movimiento aquí.": "Press buttons and move the sticks. Every movement appears here.",
|
||||
"Conecta un control a este Mac por Bluetooth.": "Connect a controller to this Mac via Bluetooth.",
|
||||
"¡Está respondiendo!": "Input received!",
|
||||
"Esperando tu primera pulsación…": "Waiting for your first input…",
|
||||
"Presionado": "Pressed",
|
||||
"Suelto": "Released",
|
||||
"Esta prueba comprueba la entrada del control en este Mac, no la compatibilidad de un juego.": "This tests controller input on this Mac, not game compatibility.",
|
||||
"Sin control conectado en este Mac": "No controller connected to this Mac",
|
||||
"Conéctalo por Bluetooth y aparecerá aquí.": "Connect it via Bluetooth and it will appear here.",
|
||||
"Control": "Controller",
|
||||
|
||||
@@ -227,17 +227,19 @@ struct SupportCarousel: View {
|
||||
|
||||
// Apply the compact launch height even when macOS restores an older window size.
|
||||
struct CompactWindow: NSViewRepresentable {
|
||||
func makeNSView(context:Context) -> NSView { WindowView() }
|
||||
func updateNSView(_ nsView:NSView,context:Context) {}
|
||||
let height: CGFloat
|
||||
func makeNSView(context:Context) -> NSView {let view=WindowView();view.height=height;return view}
|
||||
func updateNSView(_ nsView:NSView,context:Context) {(nsView as? WindowView)?.height=height}
|
||||
private final class WindowView: NSView {
|
||||
private var adjusted = false
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
guard let window=window, !adjusted else {return}
|
||||
adjusted=true
|
||||
var height: CGFloat=650 {didSet {if height != oldValue {resize()}}}
|
||||
private var lastHeight: CGFloat?
|
||||
override func viewDidMoveToWindow() {super.viewDidMoveToWindow();resize()}
|
||||
private func resize() {
|
||||
guard let window=window, lastHeight != height else {return}
|
||||
let target=height;lastHeight=target
|
||||
DispatchQueue.main.async {
|
||||
let width=max(760,window.contentLayoutRect.width)
|
||||
window.setContentSize(NSSize(width:width,height:710))
|
||||
guard !window.styleMask.contains(.fullScreen) else {return}
|
||||
window.setContentSize(NSSize(width:max(760,window.contentLayoutRect.width),height:target))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,6 +256,7 @@ enum AppArtwork {
|
||||
struct ContentView: View {
|
||||
@StateObject private var model=BuildModel()
|
||||
@StateObject private var controllerMonitor=ControllerMonitor()
|
||||
@State private var showControllerTest=false
|
||||
private let ink=Color(red:0.93,green:0.94,blue:0.90)
|
||||
private let lime=Color(red:0.79,green:0.95,blue:0.37)
|
||||
var body: some View {
|
||||
@@ -293,11 +296,19 @@ struct ContentView: View {
|
||||
SupportCarousel(games:model.catalog)
|
||||
}
|
||||
VStack(alignment:.leading,spacing:18) {
|
||||
Picker(model.text("¿Dónde quieres jugar?"),selection:$model.target) {
|
||||
Text(model.text("Elige un dispositivo")).tag("")
|
||||
Text("iPhone / iPad").tag("ios")
|
||||
Text("Mac").tag("mac")
|
||||
}.pickerStyle(.menu).disabled(model.running)
|
||||
HStack {
|
||||
Text(model.text("¿Dónde quieres jugar?")).font(.system(size:13,weight:.medium))
|
||||
Spacer()
|
||||
ForEach(["ios","mac"],id:\.self) { target in
|
||||
Button {model.target=target} label: {
|
||||
Text(target == "ios" ? "iPhone / iPad" : "Mac")
|
||||
.font(.system(size:13,weight:.semibold)).padding(.horizontal,16).padding(.vertical,9)
|
||||
.foregroundStyle(model.target == target ? Color.black : ink)
|
||||
.background(model.target == target ? lime : Color.white.opacity(0.06),in:RoundedRectangle(cornerRadius:9))
|
||||
}.buttonStyle(.plain).disabled(model.running).accessibilityIdentifier("target-"+target)
|
||||
}
|
||||
}
|
||||
if model.isMac {
|
||||
HStack(alignment:.top,spacing:10) {
|
||||
Image(systemName:controllerMonitor.controllers.contains(where:{$0.supported}) ? "checkmark.circle.fill" : "gamecontroller")
|
||||
.foregroundStyle(controllerMonitor.controllers.contains(where:{$0.supported}) ? lime : .gray)
|
||||
@@ -311,12 +322,12 @@ struct ContentView: View {
|
||||
.font(.system(size:13,weight:.medium))
|
||||
}
|
||||
}
|
||||
if model.target == "ios" {
|
||||
Text(model.text("Para jugar en iPhone/iPad, conecta el control a ese dispositivo."))
|
||||
.font(.system(size:12)).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button(model.text("Probar control")){showControllerTest=true}
|
||||
.disabled(model.running).accessibilityIdentifier("test-controller")
|
||||
}.accessibilityIdentifier("controller-status")
|
||||
}
|
||||
HStack(spacing:12) {
|
||||
Image(systemName:"folder.fill").font(.system(size:24)).foregroundStyle(lime)
|
||||
VStack(alignment:.leading,spacing:4) {
|
||||
@@ -405,8 +416,9 @@ struct ContentView: View {
|
||||
}
|
||||
}.padding(28)
|
||||
}
|
||||
}.frame(minWidth:760,minHeight:530).background(CompactWindow()).preferredColorScheme(.dark)
|
||||
}.frame(minWidth:760,minHeight:530).background(CompactWindow(height:model.isMac ? 710 : 650)).preferredColorScheme(.dark)
|
||||
.sheet(isPresented:$model.showCatalog) {GameCatalogView(model:model)}
|
||||
.sheet(isPresented:$showControllerTest) {ControllerTestView(text:model.text)}
|
||||
.onReceive(NotificationCenter.default.publisher(for:NSApplication.willTerminateNotification)) {_ in model.cancel()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import GameController
|
||||
|
||||
@main struct InputChecks {
|
||||
@MainActor static func main() {
|
||||
let controller=GCController.withExtendedGamepad()
|
||||
let gamepad=controller.extendedGamepad!
|
||||
var connected=[controller]
|
||||
let tester=ControllerTester(query:{connected})
|
||||
precondition(!tester.confirmed && !tester.state.active)
|
||||
gamepad.buttonA.setValue(1)
|
||||
gamepad.leftShoulder.setValue(1)
|
||||
gamepad.dpad.setValueForXAxis(-1,yAxis:1)
|
||||
gamepad.leftThumbstick.setValueForXAxis(0.75,yAxis:-0.5)
|
||||
gamepad.rightThumbstick.setValueForXAxis(-0.5,yAxis:0.25)
|
||||
gamepad.leftTrigger.setValue(0.6)
|
||||
tester.sample()
|
||||
precondition(tester.state.buttons.contains("a") && tester.state.buttons.contains("lb"))
|
||||
precondition(tester.state.buttons.contains("left") && tester.state.buttons.contains("up"))
|
||||
precondition(abs(tester.state.lx-0.75)<0.01 && abs(tester.state.ly+0.5)<0.01)
|
||||
precondition(abs(tester.state.rx+0.5)<0.01 && abs(tester.state.ry-0.25)<0.01)
|
||||
precondition(abs(tester.state.lt-0.6)<0.01 && tester.confirmed)
|
||||
gamepad.buttonA.setValue(0);gamepad.leftShoulder.setValue(0)
|
||||
gamepad.dpad.setValueForXAxis(0,yAxis:0)
|
||||
gamepad.leftThumbstick.setValueForXAxis(0,yAxis:0)
|
||||
gamepad.rightThumbstick.setValueForXAxis(0,yAxis:0)
|
||||
gamepad.leftTrigger.setValue(0)
|
||||
tester.sample()
|
||||
precondition(!tester.state.active && tester.state.buttons.isEmpty && tester.confirmed)
|
||||
let second=GCController.withExtendedGamepad()
|
||||
connected=[controller,second];tester.sample()
|
||||
tester.selected=ObjectIdentifier(second)
|
||||
gamepad.buttonA.setValue(1);tester.sample()
|
||||
precondition(!tester.state.active && !tester.confirmed,"A different controller must not light up the selected gamepad")
|
||||
connected=[];tester.sample()
|
||||
precondition(tester.devices.isEmpty && !tester.state.active && !tester.confirmed)
|
||||
print("Live input: buttons, D-pad, both sticks, trigger pressure, release and disconnect passed")
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 474 KiB |
@@ -199,8 +199,8 @@ required. Successful packaging is not a new claim of gameplay verification.
|
||||
|
||||
`desktop/NxNativeEasy/ControllerMonitor.swift` enumerates `GCController.controllers`
|
||||
at launch and refreshes on connect, disconnect and foreground notifications. Only
|
||||
an extended gamepad profile is marked ready. This is local Mac input readiness,
|
||||
not a claim of a phone connection or a successful game boot. It does not install
|
||||
an extended gamepad profile is marked ready. The entire controller panel is displayed only for Mac builds. This is local Mac
|
||||
input readiness, not a claim of a phone connection or a successful game boot. It does not install
|
||||
input handlers or change pairing, so it cannot take input away from a game.
|
||||
Notification tests use Apple's snapshot controllers, including unsupported profiles;
|
||||
they do not claim testing physical Xbox or PlayStation hardware.
|
||||
@@ -208,3 +208,16 @@ they do not claim testing physical Xbox or PlayStation hardware.
|
||||
The launcher has one SwiftUI Window scene and activates an existing process with
|
||||
the same bundle ID rather than keeping a duplicate process. The installer and
|
||||
local opening command now use `~/Applications/nxnative Easy.app` by default.
|
||||
|
||||
|
||||
## Live controller test
|
||||
|
||||
The Mac-only Test controller sheet samples `GCExtendedGamepad` values at 60 Hz
|
||||
while visible and foreground. It draws actual button/D-pad presses, both stick
|
||||
positions, thumbstick clicks, menu buttons and analog trigger pressure. The success
|
||||
label requires input, not a connection alone. PlayStation gamepad profiles use
|
||||
PlayStation face-button symbols. Disconnecting or changing the selected controller
|
||||
clears stale input; switching to another app clears the live drawing. Sampling
|
||||
never replaces input handlers or enables background controller capture.
|
||||
Snapshot-controller tests check pressure, axes, releases, disconnect and selecting
|
||||
a second device. They are not physical-hardware or game compatibility validation.
|
||||
@@ -20,7 +20,7 @@ def main():
|
||||
if output.suffix!='.app':parser.error('Output must end with .app')
|
||||
contents=output/'Contents';macos=contents/'MacOS';macos.mkdir(parents=True,exist_ok=True)
|
||||
subprocess.run(['xcrun','swiftc','-parse-as-library','-swift-version','5',
|
||||
str(REPO/'desktop/NxNativeEasy/main.swift'),str(REPO/'desktop/NxNativeEasy/Localization.swift'),str(REPO/'desktop/NxNativeEasy/GameCatalog.swift'),str(REPO/'desktop/NxNativeEasy/ControllerMonitor.swift'),'-o',str(macos/'NxNativeEasy'),
|
||||
str(REPO/'desktop/NxNativeEasy/main.swift'),str(REPO/'desktop/NxNativeEasy/Localization.swift'),str(REPO/'desktop/NxNativeEasy/GameCatalog.swift'),str(REPO/'desktop/NxNativeEasy/ControllerMonitor.swift'),str(REPO/'desktop/NxNativeEasy/ControllerTest.swift'),'-o',str(macos/'NxNativeEasy'),
|
||||
'-framework','SwiftUI','-framework','AppKit','-framework','GameController'],check=True)
|
||||
resources=contents/'Resources';resources.mkdir(exist_ok=True)
|
||||
shutil.copy2(REPO/'desktop/NxNativeEasy/Assets/AppIcon.png',resources/'BrandIcon.png')
|
||||
@@ -30,7 +30,7 @@ def main():
|
||||
subprocess.run(['iconutil','-c','icns',str(iconset),'-o',str(resources/'AppIcon.icns')],check=True)
|
||||
info=dict(CFBundleIconFile='AppIcon', CFBundleIdentifier='com.nalancer08.nxnative.easy',CFBundleName='nxnative Easy',
|
||||
CFBundleDisplayName='nxnative Easy',CFBundleExecutable='NxNativeEasy',
|
||||
CFBundlePackageType='APPL',CFBundleShortVersionString='1.2',CFBundleVersion='3',
|
||||
CFBundlePackageType='APPL',CFBundleShortVersionString='1.3',CFBundleVersion='4',
|
||||
LSMinimumSystemVersion='26.0',CFBundleDevelopmentRegion='en',CFBundleLocalizations=['en','es'],NSHighResolutionCapable=True,NxNativeSource=str(REPO))
|
||||
(contents/'Info.plist').write_bytes(plistlib.dumps(info))
|
||||
# Explicit ad-hoc signing: '-' never selects an Apple certificate or team.
|
||||
|
||||
@@ -16,3 +16,13 @@ class DesktopController(unittest.TestCase):
|
||||
str(ROOT/'desktop/NxNativeEasy/tests/controller_check.swift'),
|
||||
'-framework','AppKit','-framework','GameController','-o',str(executable)],check=True)
|
||||
subprocess.run([str(executable)],check=True)
|
||||
|
||||
def test_live_input_and_release(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
executable=Path(tmp)/'input'
|
||||
subprocess.run(['xcrun','swiftc','-parse-as-library','-swift-version','5',
|
||||
str(ROOT/'desktop/NxNativeEasy/ControllerMonitor.swift'),
|
||||
str(ROOT/'desktop/NxNativeEasy/ControllerTest.swift'),
|
||||
str(ROOT/'desktop/NxNativeEasy/tests/controller_input_check.swift'),
|
||||
'-framework','SwiftUI','-framework','AppKit','-framework','GameController','-o',str(executable)],check=True)
|
||||
subprocess.run([str(executable)],check=True)
|
||||