7

I'm trying to get the following code to work as a macOS command line tool. It is important that this not be a Cocoa app, so that is not an option.

This same code works perfectly in the same project with a Cocoa App target and detects a compatible controller, but when run as a Command Line Tool target, nothing happens and the API shows no controllers connected.

Obviously, some of it is contrived... it's just the simplest I could boil it down to and have some indication of things happening when it actually works.

#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;
}

I'm guessing it's got something to do with how the Cocoa application sets up or the event loops are handled. Or maybe there's some internal trigger that initializes the GameController framework. The API doesn't appear to have any explicit way to initialize it.

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

Can anyone shed some light on how I might get this working?

Ultimately, this code really needs to work inside a Core Foundation bundle, so if it could actually work with a Core Foundation runloop that would be ideal.

-- EDIT --

I have made a test project to illustrate the problem more clearly. There are two build targets. The Cocoa app build target works and receives the controller connected event. The other build target, just a simple CLI app, does not work. They both use the same source file. It also includes two code paths, one of which is the traditional [NSApp run], the second is the manual event loop above. The result is the same.

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

3
  • Never used this framework, but now I`m inclined to try. I`ve got an Xbox 360 Wireless, ControllersLite recognizes it. However, I cannot seem to discover it using GameController. Do you have a minimal sample project which is guaranteed to work? Mar 28, 2019 at 23:07
  • GameController only supports official Apple certified MFi controllers. For macOS there are only two I know of: the SteelSeries Nimbus and the Horipad Ultimate. Apr 1, 2019 at 15:29
  • Damn it. That means I can`t help here. Okay, at least let there be a bounty. Apr 1, 2019 at 16:40

2 Answers 2

1
+500

Although every thread creates a run loop (NSRunLoop for a Cocoa app) to process input events, the loop doesn't start automatically. The code below makes it run with the [application run] call. When the proper event is processed by the run loop, the notification is raised. I install the observer in an Application delegate just to make sure all other systems have finished initializing at that point.

#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;
}

UPDATE Sorry, I misinterpreted the question. The code above works for connecting and disconnecting controllers, but it does not properly initialize the [GCController controllers] array with devices that were already connected when the application starts.

As you point out, connected devices send notifications with the same code on a Cocoa app, but not on a command line one. The difference is that Cocoa apps get didBecomeActive notifications, and that causes the private _GCControllerManager (the object that takes care of NSXPCConnections posted by the GameControllerDaemon) to receive a CBApplicationDidBecomeActive message that populates the controllers array.

Anyway, I tried making the command line app active so it routes these messages, but that didn't work; the app needs to send the didBecomeActive message early during startup.

Then I tried creating my own _GCGameController and send the CBApplicationDidBecomeActive manually; that kind of worked, except the app ends up with 2 of these controllers, and connections get duplicated.

What I needed was access to the private _GCGameController object, but I don't know who owns it, so I could not reference it directly.

So at the end, I went with method swizzling. The code below changes the last method that gets called at initialization in a terminal app, _GCGameController startIdleWatchTimer, so it sends CBApplicationDidBecomeActive afterwards.

I know is not a great solution, using all kinds of Apple's internal code, but maybe it helps somebody get to something better. Add the following code to the previous one:

#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
7
  • 1
    Welcome to SO. Please can you provide an explanation along with your code to help others.
    – Mike Poole
    Aug 8, 2019 at 16:17
  • Unfortunately, this does not work when not built as a Cocoa application bundle. I have edited my original question to include a test project to show the problem. Aug 12, 2019 at 0:41
  • I downloaded your project, and it works for me. But I tried again from zero just in case: I just created a new Command Line Tool project in Xcode (Version 10.3 10G8), replaced the code in main.m with the above, ran it, connected my PS4 controller through bluetooth, and it spits out ATTACHED on the console (your project also prints DETACHED as expected). I am running MacOS 10.14.6 (18G87) on a MacBook Air (13-inch, Mid 2012). Aug 13, 2019 at 22:53
  • That's so weird. I tried from scratch too, and it still doesn't work for me. Same versions of Xcode + macOS on a Mac mini (2018) with both a PS4 controller and Horipad Ultimate. Also it never worked for me before on a MacBook Pro (late 2014, I believe). Again, everything works fine with a Cocoa App build. Aug 13, 2019 at 23:31
  • I've made a little progress. I got your version to work... sort of. It does not detect any previously connected controllers on app startup as a command line tool. [[GCController controllers] count] is zero and no notifications occur. If I disconnect and reconnect a controller it then shows up. As a Cocoa app, all controllers are detected on startup. Aug 14, 2019 at 1:59
1

I have this working with the following main.m file:

#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;
}

Compiled with: clang -framework AppKit -framework GameController main.m

I have no idea why, but I need an Info.plist file in the build output directory. Without it, the controllers array doesn't get populated. This is my entire file:

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

I'm not sure what implications supplying an Info.plist might have, but if it's there, I can run the a.out executable as normal and I get my controllers array.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.