Как мне использовать платформу Apple GameController из инструмента командной строки macOS?

#objective-c #macos #cocoa #core-foundation #gamecontroller

#objective-c #macos #cocoa #ядро-основа #gamecontroller

Вопрос:

Я пытаюсь заставить следующий код работать в качестве инструмента командной строки macOS. Важно, чтобы это не было приложением Cocoa, поэтому это не вариант.

Этот же код отлично работает в том же проекте с целевым приложением Cocoa и обнаруживает совместимый контроллер, но при запуске в качестве целевого инструмента командной строки ничего не происходит, и 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: %sn", 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 ?язык = objc

Кто-нибудь может пролить свет на то, как я мог бы заставить это работать?

В конечном счете, этот код действительно должен работать внутри пакета Core Foundation, поэтому, если бы он действительно мог работать с Core Foundation runloop, это было бы идеально.

— РЕДАКТИРОВАТЬ —

Я создал тестовый проект, чтобы более наглядно проиллюстрировать проблему. Есть две цели сборки. Цель сборки приложения Cocoa работает и получает событие подключения к контроллеру. Другая цель сборки, простое приложение CLI, не работает. Они оба используют один и тот же исходный файл. Она также включает в себя два пути кода, один из которых является традиционным [запуск NSApp], второй — описанный выше цикл событий вручную. Результат тот же.

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

Комментарии:

1. Никогда не использовал эту платформу, но теперь я склонен попробовать. У меня беспроводная консоль Xbox 360, ControllersLite распознает ее. Однако, похоже, я не могу обнаружить это с помощью GameController. У вас есть минимальный пример проекта, который гарантированно будет работать?

2. GameController поддерживает только официальные контроллеры MFi, сертифицированные Apple. Для macOS есть только два, о которых я знаю: SteelSeries Nimbus и Horipad Ultimate.

3. Черт возьми. Это означает, что я не могу здесь помочь. Ладно, пусть хотя бы будет вознаграждение.

Ответ №1:

Хотя каждый поток создает цикл выполнения ( NSRunLoop для приложения Cocoa) для обработки входных событий, цикл не запускается автоматически. Приведенный ниже код запускает ее с помощью [application run] вызова. Когда соответствующее событие обрабатывается циклом выполнения, выдается уведомление. Я устанавливаю observer в делегате приложения, просто чтобы убедиться, что все другие системы завершили инициализацию на этом этапе.

 #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: %sn", 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 уведомления, и это приводит к тому, что private _GCControllerManager (объект, который обрабатывает NSXPCConnection сообщения, отправленные GameControllerDaemon ) получает CBApplicationDidBecomeActive сообщение, которое заполняет controllers массив.

В любом случае, я попытался активировать приложение командной строки, чтобы оно перенаправляло эти сообщения, но это не сработало; приложению необходимо отправить didBecomeActive сообщение раньше во время запуска.

Затем я попытался создать свою собственную, _GCGameController и отправить CBApplicationDidBecomeActive вручную; это вроде сработало, за исключением того, что приложение заканчивается с двумя из этих контроллеров, и соединения дублируются.

Что мне было нужно, так это доступ к закрытому _GCGameController объекту, но я не знаю, кому он принадлежит, поэтому я не мог ссылаться на него напрямую.

Итак, в конце я выбрал метод swizzling. Приведенный ниже код изменяет последний метод, который вызывается при инициализации в приложении терминала, _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(amp;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
  

Комментарии:

1. Добро пожаловать в SO. Пожалуйста, не могли бы вы предоставить объяснение вместе со своим кодом, чтобы помочь другим.

2. К сожалению, это не работает, если не создано как пакет приложений Cocoa. Я отредактировал свой первоначальный вопрос, включив в него тестовый проект, чтобы показать проблему.

3. Я скачал ваш проект, и он работает у меня. Но я на всякий случай попробовал еще раз с нуля: я только что создал новый проект инструмента командной строки в Xcode (версия 10.3 10G8), заменил код в main.m на приведенный выше, запустил его, подключил свой контроллер PS4 через Bluetooth, и он выдает «ПРИКРЕПЛЕННЫЙ» на консоль (ваш проект также печатается ОТДЕЛЬНО, как и ожидалось). Я использую macOS 10.14.6 (18G87) на MacBook Air (13-дюймовый, середина 2012 года).

4. Это так странно. Я тоже пробовал с нуля, и у меня все еще не работает. Те же версии Xcode macOS на Mac mini (2018) с контроллером PS4 и Horipad Ultimate. Также это никогда раньше не работало у меня на MacBook Pro (я полагаю, в конце 2014 года). Опять же, все отлично работает со сборкой приложения Cocoa.

5. Я добился небольшого прогресса. Я заставил вашу версию работать … вроде как. Он не обнаруживает никаких ранее подключенных контроллеров при запуске приложения в качестве инструмента командной строки. [[Количество контроллеров GCController] равно нулю, и никаких уведомлений не происходит. Если я отключу и повторно подключу контроллер, он отобразится. В приложении Cocoa все контроллеры обнаруживаются при запуске.

Ответ №2:

У меня это работает со следующим файлом 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 %lun", [[GCController controllers] count]);

    // Do stuff here

    return 0;
}
  

Скомпилирован с: clang -framework AppKit -framework GameController main.m

Я понятия не имею, зачем, но мне нужен файл Info.plist в каталоге вывода сборки. Без этого массив controllers не заполняется. Это весь мой файл:

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

Я не уверен, какие последствия может иметь предоставление Info.plist, но если он есть, я могу запустить исполняемый файл a.out как обычно, и я получу свой массив контроллеров.