7

我正在尝试将以下代码用作 macOS 命令行工具。重要的是这不是 Cocoa 应用程序,因此这不是一个选项。

相同的代码在具有 Cocoa App 目标的同一个项目中完美运行,并检测到兼容的控制器,但是当作为命令行工具目标运行时,什么也没有发生,并且 API 显示没有连接控制器。

显然,其中一些是人为的……它只是我可以归结为最简单的一种,并在它实际起作用时有一些事情发生的迹象。

#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>


int main( int argc, const char * argv[] )
{
    @autoreleasepool
    {
        NSApplication * application = [NSApplication sharedApplication];

        NSNotificationCenter * center = [NSNotificationCenter defaultCenter];

        [center addObserverForName: GCControllerDidConnectNotification
                            object: nil
                             queue: nil
                        usingBlock: ^(NSNotification * note) {
                            GCController * controller = note.object;
                            printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
                        }
         ];

        [application finishLaunching];

        bool shouldKeepRunning = true;
        while (shouldKeepRunning)
        {
            printf( "." );

            while (true)
            {
                NSEvent * event = [application
                                   nextEventMatchingMask: NSEventMaskAny
                                   untilDate: nil
                                   inMode: NSDefaultRunLoopMode
                                   dequeue: YES];
                if (event == NULL)
                {
                    break;
                }
                else
                {
                    [application sendEvent: event];
                }
            }

            usleep( 100 * 1000 );
        }
    }

    return 0;
}

我猜这与 Cocoa 应用程序的设置方式或事件循环的处理方式有关。或者也许有一些初始化 GameController 框架的内部触发器。API 似乎没有任何明确的方式来初始化它。

https://developer.apple.com/documentation/gamecontroller?language=objc

任何人都可以阐明我如何让这个工作?

最终,这段代码确实需要在 Core Foundation 包中工作,所以如果它实际上可以与 Core Foundation 运行循环一起工作,那将是理想的。

- 编辑 -

我做了一个测试项目来更清楚地说明这个问题。有两个构建目标。Cocoa 应用程序构建目标工作并接收控制器连接事件。另一个构建目标,只是一个简单的 CLI 应用程序,不起作用。它们都使用相同的源文件。它还包括两个代码路径,一个是传统的【NSApp运行】,第二个是上面的手动事件循环。结果是一样的。

https://www.dropbox.com/s/a6fw3nuegq7bg8x/ControllerTest.zip?dl=0

4

2 回答 2

1

尽管每个线程都会创建一个运行循环(NSRunLoop对于 Cocoa 应用程序)来处理输入事件,但循环不会自动启动。下面的代码使它与[application run]调用一起运行。当运行循环处理适当的事件时,将引发通知。我在 Application 委托中安装了观察者,只是为了确保所有其他系统此时都已完成初始化。

#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>

@interface AppDelegate : NSObject <NSApplicationDelegate> @end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
    [center addObserverForName: GCControllerDidConnectNotification
                        object: nil
                         queue: nil
                    usingBlock: ^(NSNotification * note) {
                        GCController * controller = note.object;
                        printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
                    }
     ];
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSApplication * application = [NSApplication sharedApplication]; // You can get rid of the variable and just use the global NSApp below instead
        AppDelegate *delegate = [[AppDelegate alloc] init];
        [application setDelegate:delegate];
        [application run];
    }
    return 0;
}

更新 对不起,我误解了这个问题。上面的代码适用于连接和断开控制器,但它没有正确地使用[GCController controllers]应用程序启动时已经连接的设备初始化数组。

正如您所指出的,连接的设备在 Cocoa 应用程序上使用相同的代码发送通知,但不是在命令行上。不同之处在于 Cocoa 应用程序会收到didBecomeActive通知,这会导致私有_GCControllerManager(负责NSXPCConnections 发布的 s 的对象GameControllerDaemon)接收CBApplicationDidBecomeActive填充controllers数组的消息。

无论如何,我尝试使命令行应用程序处于活动状态,以便路由这些消息,但这不起作用;该应用程序需要didBecomeActive在启动期间尽早发送消息。

然后我尝试创建自己的_GCGameControllerCBApplicationDidBecomeActive手动发送;那种工作,除了应用程序最终有两个这样的控制器,并且连接被重复。

我需要的是访问私有_GCGameController对象,但我不知道谁拥有它,所以我无法直接引用它。

所以最后,我使用了方法调配。下面的代码更改了在终端应用程序初始化时调用的最后一个方法_GCGameController startIdleWatchTimer,因此它CBApplicationDidBecomeActive随后发送。

我知道这不是一个很好的解决方案,使用各种 Apple 的内部代码,但也许它可以帮助某人获得更好的东西。将以下代码添加到上一个代码中:

#import <objc/runtime.h>


@interface _GCControllerManager : NSObject
-(void) CBApplicationDidBecomeActive;
-(void) startIdleWatchTimer;
@end

@implementation _GCControllerManager (Extras)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(startIdleWatchTimer);
        SEL swizzledSelector = @selector(myStartIdleWatchTimer);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void) myStartIdleWatchTimer {
    [self myStartIdleWatchTimer];
    [self CBApplicationDidBecomeActive];
}

@end
于 2019-08-08T16:00:40.240 回答
1

我有这个使用以下 main.m 文件:

#import <AppKit/AppKit.h>
#import <GameController/GameController.h>

@interface AppDelegate : NSObject<NSApplicationDelegate> @end

@implementation AppDelegate
- (void) applicationDidFinishLaunching: (NSNotification*) notification {
    [NSApp stop: nil]; // Allows [app run] to return
}
@end

int main() {
    NSApplication* app = [NSApplication sharedApplication];
    [app setActivationPolicy: NSApplicationActivationPolicyRegular];
    [app setDelegate: [[AppDelegate alloc] init]];
    [app run];

    // 1 with a DualShock 4 plugged in
    printf("controllers %lu\n", [[GCController controllers] count]);

    // Do stuff here

    return 0;
}

编译:clang -framework AppKit -framework GameController main.m

我不知道为什么,但我需要构建输出目录中的 Info.plist 文件。没有它,控制器数组就不会被填充。这是我的整个文件:

<dict>
    <key>CFBundleIdentifier</key>
    <string>your.bundle.id</string>
</dict>

我不确定提供 Info.plist 可能会产生什么影响,但如果它在那里,我可以正常运行 a.out 可执行文件并获得我的控制器数组。

于 2019-11-08T18:38:37.957 回答