Advertising

Correct check for Game Center availability

Apple's example code for Game Center does something strange -- in fact, we can simply go out and call those things bugs. Keep reading to see what I'm talking about. Here's the code.

        // check for presence of GKLocalPlayer API
        Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
        
        // check if the device is running iOS 4.1 or later
        NSString *reqSysVer = @"4.1";
        NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
        BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending);

        return (gcClass && osVersionSupported);

Well, that's incorrect and doesn't count on the fact that Game Center is unavailable on iPhone 3G despite iOS 4.1 and 4.2 existing there. If your goal wasn't to avoid crashes caused by Game Center classes not existing, but to check if Game Center really is available, you need to check if you are running on iPhone 3G. Update April 6th 2011: I have missed a few lines in the documentation that say you will receive GKErrorNotSupported when authenticating local player, if Game Center is not available. However, a library I'm working on needs this sort of check to return availability prior to authenticating player, to determine best achievement engine. You may need this as well, so I'm keeping this up. An excellent way to do this was posted on iOS Developer Tips and uses sysctlbyname() C function found in BSD operating systems such as OS X or iOS to query the kernel for value of hw.machine string.

The above method proposes adding a category to UIDevice that adds -(NSString*)machine. While this would be a most excellent way to do this, it is not an appropriate way to handle stuff in a C++ library; it should not add categories or anything like that unless it is absolutely necessary, so I just patched the function that checks availability of Game Center directly.

Compared to the iOS Developer Tips' solution, I also added a check whether or not sysctlbyname() fails. This should prevent any problems in case Apple decides to remove support for hw.machine (although I see no reason for them doing that).

We don't need to check for iPhone, iPod Touch (no iOS4) or iPod Touch 2G (Game Center available).

Let's take a look at the code:

    bool GameCenterAchievementsService::isGameCenterAvailable()
    {
        // check for presence of GKLocalPlayer API
        Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
        
        // check if the device is running iOS 4.1 or later
        NSString *reqSysVer = @"4.1";
        NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
        BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending);
        
        // device must not be an iphone 3g
        bool validDevice = true;
        {
            size_t size;
            if(sysctlbyname("hw.machine", NULL, &size, NULL, 0)!=-1)
            {
                char*name = (char*)malloc(size);
                if(sysctlbyname("hw.machine", name, &size, NULL, 0)!=-1)
                {
                    if (!strcmp(name, "iPhone1,1") || !strcmp(name, "iPhone1,2") || !strcmp(name, "iPod1,1")) 
                    {
                        validDevice = false;
                    }
                }
                free(name);
            }
        }
        
        return (gcClass && osVersionSupported && validDevice);
    }

Note that this is Objective-C++, but is trivial to patch for pure Objective-C. Report bugs in comments section below!


rest of the post
About me

GKTapper - Apple's buggy example

I've caught a few mistakes in GKTapper. Read on to see how the sample performs invalid caching of achievements and to see how it shows incorrect string as leaderboard description in one place. Invalid caching of achievements

Achievements in -submitAchievement:percentComplete: will never be cached. Also, the comment makes little sense; a sentence appears to be missing.

- (void) submitAchievement: (NSString*) identifier percentComplete: (double) percentComplete
{
	//GameCenter check for duplicate achievements when the achievement is submitted, but if you only want to report
	// new achievements to the user, then you need to check if it's been earned
	// before you submit.  Otherwise you'll end up with a race condition between loadAchievementsWithCompletionHandler
	// and reportAchievementWithCompletionHandler.  To avoid this, we fetch the current achievement list once,
	// then cache it and keep it updated with any new achievements.
	if(self.earnedAchievementCache == NULL)
	{
		[GKAchievement loadAchievementsWithCompletionHandler: ^(NSArray *scores, NSError *error)
		{
			if(error == NULL)
			{
				NSMutableDictionary* tempCache= [NSMutableDictionary dictionaryWithCapacity: [scores count]];
				for (GKAchievement* score in tempCache)
				{
					[tempCache setObject: score forKey: score.identifier];
				}
				self.earnedAchievementCache= tempCache;
				[self submitAchievement: identifier percentComplete: percentComplete];
			}
			else
			{
				//Something broke loading the achievement list.  Error out, and we'll try again the next time achievements submit.
				[self callDelegateOnMainThread: @selector(achievementSubmitted:error:) withArg: NULL error: error];
			}

		}];
	}
	else
}

Where's the bug? Code iterates over the just-created, empty dictionary "tempCache". Corrected:

				for (GKAchievement* score in scores) // this used to read tempCache

Leaderboard description invalid

Code in method mappedPlayerIDToPlayer:error: contains a small bug:

		self.leaderboardHighScoreDescription= @"GameCenter Scores Unavailable";
		self.leaderboardHighScoreDescription=  @"-";

It should probably read:

		self.leaderboardHighScoreDescription= @"GameCenter Scores Unavailable";
		self.leaderboardHighScoreString=  @"-";


rest of the post