// Copyright Epic Games, Inc. All Rights Reserved. // Module includes #include "OnlineLeaderboardsInterfaceIOS.h" #include "OnlineSubsystemIOS.h" // GameCenter includes #include #include #include FOnlineLeaderboardsIOS::FOnlineLeaderboardsIOS(FOnlineSubsystemIOS* InSubsystem) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::FOnlineLeaderboardsIOS()")); // Cache a reference to the OSS Identity and Friends interfaces, we will need these when we are performing leaderboard actions IdentityInterface = (FOnlineIdentityIOS*)InSubsystem->GetIdentityInterface().Get(); check(IdentityInterface); FriendsInterface = (FOnlineFriendsIOS*)InSubsystem->GetFriendsInterface().Get(); check(FriendsInterface); UnreportedScores = nil; } FOnlineLeaderboardsIOS::~FOnlineLeaderboardsIOS() { if(UnreportedScores) { [UnreportedScores release]; UnreportedScores = nil; } if (CachedLeaderboard) { [CachedLeaderboard release]; CachedLeaderboard = nil; } if (LeaderboardPlayer) { [LeaderboardPlayer release]; LeaderboardPlayer = nil; } } bool FOnlineLeaderboardsIOS::ReadLeaderboardCompletionDelegate(NSArray* players, FOnlineLeaderboardReadRef& InReadObject) { auto ReadObject = InReadObject; bool bTriggeredReadRequest = true; CachedLeaderboard = [GKLeaderboard alloc]; [CachedLeaderboard loadEntriesForPlayers:players timeScope:GKLeaderboardTimeScopeAllTime completionHandler: ^(GKLeaderboardEntry *entries, NSArray *scores, NSError *Error) { bReadLeaderboardFinished = true; bool bWasSuccessful = (Error == nil) && [scores count] > 0; UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::loadScoresWithCompletionHandler() - %s"), (bWasSuccessful ? TEXT("Success!") : TEXT("Failed!, no scores retrieved"))); if (bWasSuccessful) { bWasSuccessful = [scores count] > 0; for (GKLeaderboardEntry *entry in scores) { FString PlayerIDString; if ([entry respondsToSelector:@selector(player)] == YES) { PlayerIDString = FString(FOnlineSubsystemIOS::GetPlayerId(entry.player)); LeaderboardPlayer = entry.player; } UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("----------------------------------------------------------------")); UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("PlayerId: %s"), *PlayerIDString); UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("Value: %d"), entry.score); UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("----------------------------------------------------------------")); FUniqueNetIdRef UserId = FUniqueNetIdIOS::Create(PlayerIDString); FOnlineStatsRow* UserRow = ReadObject.Get().FindPlayerRecord(UserId.Get()); if (UserRow == NULL) { UserRow = new (ReadObject->Rows) FOnlineStatsRow(PlayerIDString, UserId); } for (int32 StatIdx = 0; StatIdx < ReadObject->ColumnMetadata.Num(); StatIdx++) { const FColumnMetaData& ColumnMeta = ReadObject->ColumnMetadata[StatIdx]; switch (ColumnMeta.DataType) { case EOnlineKeyValuePairDataType::Int32: { int32 Value = entry.score; UserRow->Columns.Add(ColumnMeta.ColumnName, FVariantData(Value)); bWasSuccessful = true; break; } default: { UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("Unsupported key value pair during retrieval from GameCenter %s"), *ColumnMeta.ColumnName); break; } } } } } else if (Error) { LeaderboardPlayer = nil; // if we have failed to read the leaderboard then report this NSDictionary *userInfo = [Error userInfo]; NSString *errstr = [[userInfo objectForKey : NSUnderlyingErrorKey] localizedDescription]; UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::loadScoresWithCompletionHandler() - Failed to read leaderboard with error: [%s]"), *FString(errstr)); UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("You should check that the leaderboard name matches that of one in ITunesConnect")); } // Report back to the game thread whether this succeeded. [FIOSAsyncTask CreateTaskWithBlock : ^ bool(void) { ReadObject->ReadState = bWasSuccessful ? EOnlineAsyncTaskState::Done : EOnlineAsyncTaskState::Failed; TriggerOnLeaderboardReadCompleteDelegates(bWasSuccessful); return true; }]; }]; return bTriggeredReadRequest; } bool FOnlineLeaderboardsIOS::ReadLeaderboards(const TArray< FUniqueNetIdRef >& Players, FOnlineLeaderboardReadRef& InReadObject) { __block FOnlineLeaderboardReadRef ReadObject = InReadObject; UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::ReadLeaderboards()")); bReadLeaderboardFinished = false; ReadObject->ReadState = EOnlineAsyncTaskState::Failed; ReadObject->Rows.Empty(); if ((IdentityInterface != nullptr) && (IdentityInterface->GetLocalGameCenterUser() != NULL) && IdentityInterface->GetLocalGameCenterUser().isAuthenticated) { ReadObject->ReadState = EOnlineAsyncTaskState::InProgress; // Populate a list of id's for our friends which we want to look up. NSMutableArray* FriendIds = [NSMutableArray arrayWithCapacity: (Players.Num() + 1)]; // Add the local player to the list of ids to look up. FUniqueNetIdPtr LocalPlayerUID = IdentityInterface->GetUniquePlayerId(0); check(LocalPlayerUID.IsValid()); FriendIds[0] = [NSString stringWithFString:LocalPlayerUID->ToString()]; // Add the other requested players for (int32 FriendIdx = 0; FriendIdx < Players.Num(); FriendIdx++) { FriendIds[FriendIdx + 1] = [NSString stringWithFString:Players[FriendIdx]->ToString()]; } // Kick off a game center read request for the list of users GKLocalPlayer* GKLocalUser = [GKLocalPlayer localPlayer]; [GKLocalUser loadFriendsWithIdentifiers:(NSArray *)FriendIds completionHandler:^(NSArray *players, NSError *Error) { bool bWasSuccessful = (Error == nil) && [players count] > 0; if (bWasSuccessful) { bWasSuccessful = [players count] > 0; } // even if not successful, need to call the delegate to initialize Leaderboard and LocalPlayer ReadLeaderboardCompletionDelegate(players, ReadObject); }]; } else { return false; } return true; } bool FOnlineLeaderboardsIOS::ReadLeaderboardsForFriends(int32 LocalUserNum, FOnlineLeaderboardReadRef& ReadObject) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::ReadLeaderboardsForFriends()")); if( IdentityInterface->GetLocalGameCenterUser() != NULL && IdentityInterface->GetLocalGameCenterUser().isAuthenticated ) { // Gather the friends from the local players game center friends list and perform a read request for these TArray< TSharedRef > Friends; FriendsInterface->GetFriendsList( 0, EFriendsLists::ToString(EFriendsLists::Default), Friends ); TArray< FUniqueNetIdRef > FriendIds; for( int32 Idx = 0; Idx < Friends.Num(); Idx++ ) { FriendIds.Add( Friends[ Idx ]->GetUserId() ); } ReadLeaderboards( FriendIds, ReadObject ); } return true; } bool FOnlineLeaderboardsIOS::ReadLeaderboardsAroundRank(int32 Rank, uint32 Range, FOnlineLeaderboardReadRef& ReadObject) { UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("FOnlineLeaderboardsIOS::ReadLeaderboardsAroundRank is currently not supported.")); return false; } bool FOnlineLeaderboardsIOS::ReadLeaderboardsAroundUser(FUniqueNetIdRef Player, uint32 Range, FOnlineLeaderboardReadRef& ReadObject) { UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("FOnlineLeaderboardsIOS::ReadLeaderboardsAroundUser is currently not supported.")); return false; } void FOnlineLeaderboardsIOS::FreeStats(FOnlineLeaderboardRead& ReadObject) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::FreeStats()")); // not implemented for gc leaderboards } bool FOnlineLeaderboardsIOS::WriteLeaderboards(const FName& SessionName, const FUniqueNetId& Player, FOnlineLeaderboardWrite& WriteObject) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::WriteLeaderboards()")); bool bWroteAnyLeaderboard = false; // Make sure we have storage space for scores if (UnreportedScores == nil) { UnreportedScores = [[NSMutableArray alloc] initWithCapacity : WriteObject.Properties.Num()]; } //@TODO: Note: The array of leaderboard names is ignored, because they offer no data. // Instead the stat names are used as the leaderboard names for iOS for now. This whole API needs rework! // Queue up the leaderboard stat writes for (FStatPropertyArray::TConstIterator It(WriteObject.Properties); It; ++It) { // Access the stat and the value. const FVariantData& Stat = It.Value(); FString LeaderboardName(It.Key()); NSString* Category = [NSString stringWithFString:LeaderboardName]; bool bIsValidScore = false; int32 Value; // Setup the score with the value we are writing from the variant type switch (Stat.GetType()) { case EOnlineKeyValuePairDataType::Int32: { Stat.GetValue(Value); bIsValidScore = true; break; } default: { UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("FOnlineLeaderboardsIOS::WriteLeaderboards(Leaderboard: %s) Invalid data type (only Int32 is currently supported)"), *LeaderboardName); break; } } if (bIsValidScore) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::WriteLeaderboards() Queued score %d on leaderboard %s"), Value, *LeaderboardName); [UnreportedScores addObject:[NSNumber numberWithInteger:Value]]; bWroteAnyLeaderboard = true; } } // Return whether any stat was cached. return bWroteAnyLeaderboard; } bool FOnlineLeaderboardsIOS::FlushLeaderboards(const FName& SessionName) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::FlushLeaderboards()")); int SleepLimit = 30; while(!bReadLeaderboardFinished && SleepLimit != 0) { usleep(100000); --SleepLimit; } if (SleepLimit == 0) { UE_LOG_ONLINE_LEADERBOARD(Warning, TEXT("Leaderboard Could not be flusshed")); return false; } bool bBeganFlushingScores = false; if ((IdentityInterface->GetLocalGameCenterUser() != NULL) && IdentityInterface->GetLocalGameCenterUser().isAuthenticated) { const int32 UnreportedScoreCount = UnreportedScores.count; bBeganFlushingScores = UnreportedScoreCount > 0; if (bBeganFlushingScores) { NSMutableArray *ArrayCopy = [[NSMutableArray alloc] initWithArray:UnreportedScores]; [UnreportedScores release]; UnreportedScores = nil; if (LeaderboardPlayer != nil) { dispatch_async(dispatch_get_main_queue(), ^{ for (NSNumber* scoreReport in ArrayCopy) { NSInteger ScoreReportInt = [scoreReport integerValue]; [CachedLeaderboard submitScore:ScoreReportInt context:0 player:LeaderboardPlayer completionHandler: ^ (NSError *error) { // Tell whoever was listening that we have written (or failed to write) to the leaderboard bool bSucceeded = error == NULL; if (bSucceeded) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("Flushed %d scores to Game Center"), UnreportedScoreCount); } else { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("Error while flushing scores (code %d)"), [error code]); } // Report back to the game thread whether this succeeded. [FIOSAsyncTask CreateTaskWithBlock : ^ bool(void) { TriggerOnLeaderboardFlushCompleteDelegates(SessionName, bSucceeded); return true; }]; }]; } [ArrayCopy release]; }); } else { [ArrayCopy release]; } } // If we didn't begin writing to the leaderboard we should still notify whoever was listening. if (!bBeganFlushingScores) { TriggerOnLeaderboardFlushCompleteDelegates(SessionName, false); UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("Failed to flush scores to leaderboard")); } } return bBeganFlushingScores; } bool FOnlineLeaderboardsIOS::WriteOnlinePlayerRatings(const FName& SessionName, int32 LeaderboardId, const TArray& PlayerScores) { UE_LOG_ONLINE_LEADERBOARD(Display, TEXT("FOnlineLeaderboardsIOS::WriteOnlinePlayerRatings()")); // not implemented for gc leaderboards return false; }