From 356855575ec24b7459b6dc6ce1c99023bd48a7e3 Mon Sep 17 00:00:00 2001 From: Arves100 <4847635+arves100@users.noreply.github.com> Date: Tue, 28 Nov 2023 02:57:25 +0100 Subject: [PATCH] Add iphone sample from v2.12 sdk --- Samples/TapRace/iphone/AppDelegate.h | 268 ++ Samples/TapRace/iphone/AppDelegate.mm | 4126 +++++++++++++++++ Samples/TapRace/iphone/BuddyListCellView.h | 42 + Samples/TapRace/iphone/BuddyListCellView.mm | 280 ++ Samples/TapRace/iphone/CreateUserController.h | 34 + .../TapRace/iphone/CreateUserController.mm | 248 + Samples/TapRace/iphone/FlipsideView.h | 13 + Samples/TapRace/iphone/FlipsideView.m | 32 + .../TapRace/iphone/FlipsideViewController.h | 27 + .../TapRace/iphone/FlipsideViewController.mm | 109 + Samples/TapRace/iphone/ForgotPassword.h | 16 + Samples/TapRace/iphone/ForgotPassword.mm | 59 + Samples/TapRace/iphone/GameController.h | 49 + Samples/TapRace/iphone/GameController.mm | 297 ++ .../TapRace/iphone/GameResultsController.h | 72 + .../TapRace/iphone/GameResultsController.mm | 639 +++ .../TapRace/iphone/LeaderboardsController.h | 66 + .../TapRace/iphone/LeaderboardsController.mm | 786 ++++ .../iphone/LeadersByLocationController.h | 27 + .../iphone/LeadersByLocationController.mm | 465 ++ Samples/TapRace/iphone/LeadersController.h | 21 + Samples/TapRace/iphone/LeadersController.mm | 223 + Samples/TapRace/iphone/LoginController.h | 46 + Samples/TapRace/iphone/LoginController.mm | 729 +++ .../TapRace/iphone/MatchmakingController.h | 37 + .../TapRace/iphone/MatchmakingController.mm | 522 +++ Samples/TapRace/iphone/MenuController.h | 37 + Samples/TapRace/iphone/MenuController.mm | 230 + Samples/TapRace/iphone/MyCLController.h | 82 + Samples/TapRace/iphone/MyCLController.mm | 254 + .../TapRace/iphone/Resources/AboutGamespy.png | Bin 0 -> 14763 bytes Samples/TapRace/iphone/Resources/Default.png | Bin 0 -> 32137 bytes .../TapRace/iphone/Resources/FlipsideView.xib | 416 ++ Samples/TapRace/iphone/Resources/Gamespy.png | Bin 0 -> 2236 bytes Samples/TapRace/iphone/Resources/Globe.png | Bin 0 -> 11799 bytes .../TapRace/iphone/Resources/MainWindow.xib | 336 ++ Samples/TapRace/iphone/Resources/Top100.jpg | Bin 0 -> 2868 bytes .../TapRace/iphone/Resources/UserStats.xib | 913 ++++ Samples/TapRace/iphone/Resources/btn_down.png | Bin 0 -> 7568 bytes Samples/TapRace/iphone/Resources/btn_up.png | Bin 0 -> 7546 bytes Samples/TapRace/iphone/Resources/buddies.png | Bin 0 -> 7613 bytes .../iphone/Resources/buddies_rematch.png | Bin 0 -> 10783 bytes .../iphone/Resources/buddyListCell.xib | 359 ++ Samples/TapRace/iphone/Resources/camera.png | Bin 0 -> 8130 bytes .../TapRace/iphone/Resources/createuser.xib | 557 +++ .../iphone/Resources/forgotPassword.xib | 185 + .../iphone/Resources/gamespyFooter.png | Bin 0 -> 7308 bytes Samples/TapRace/iphone/Resources/gs_logo.gif | Bin 0 -> 3699 bytes .../TapRace/iphone/Resources/icon_29x29.png | Bin 0 -> 1263 bytes .../TapRace/iphone/Resources/icon_57x57.png | Bin 0 -> 2354 bytes .../TapRace/iphone/Resources/leaderboard.png | Bin 0 -> 6865 bytes .../iphone/Resources/leaderboardMap.png | Bin 0 -> 10977 bytes .../TapRace/iphone/Resources/leaderboards.xib | 1352 ++++++ .../iphone/Resources/leadersbylocation.xib | 314 ++ .../iphone/Resources/leadersbytime.xib | 274 ++ Samples/TapRace/iphone/Resources/login.xib | 699 +++ .../TapRace/iphone/Resources/matchmaking.xib | 519 +++ Samples/TapRace/iphone/Resources/menu.png | Bin 0 -> 1733 bytes Samples/TapRace/iphone/Resources/menu.xib | 830 ++++ Samples/TapRace/iphone/Resources/nophoto.png | Bin 0 -> 3550 bytes Samples/TapRace/iphone/Resources/people.png | Bin 0 -> 8156 bytes .../TapRace/iphone/Resources/playAgain.png | Bin 0 -> 7214 bytes .../TapRace/iphone/Resources/singlePlayer.png | Bin 0 -> 5526 bytes .../iphone/Resources/singleplayergame.xib | 645 +++ .../iphone/Resources/singleplayerresults.xib | 778 ++++ .../TapRace/iphone/Resources/takephoto.xib | 167 + Samples/TapRace/iphone/Resources/tapImage.png | Bin 0 -> 10183 bytes Samples/TapRace/iphone/Resources/trophy.png | Bin 0 -> 6344 bytes .../iphone/Resources/twoplayergame.xib | 635 +++ .../iphone/Resources/twoplayerresults.xib | 841 ++++ Samples/TapRace/iphone/TapRace-Info.plist | 26 + Samples/TapRace/iphone/UserStatsController.h | 55 + Samples/TapRace/iphone/UserStatsController.mm | 284 ++ Samples/TapRace/iphone/Utility.h | 72 + Samples/TapRace/iphone/Utility.mm | 165 + Samples/TapRace/iphone/atlas_taprace_v2.c | 225 + Samples/TapRace/iphone/atlas_taprace_v2.h | 72 + Samples/TapRace/iphone/main.mm | 19 + 78 files changed, 20574 insertions(+) create mode 100644 Samples/TapRace/iphone/AppDelegate.h create mode 100644 Samples/TapRace/iphone/AppDelegate.mm create mode 100644 Samples/TapRace/iphone/BuddyListCellView.h create mode 100644 Samples/TapRace/iphone/BuddyListCellView.mm create mode 100644 Samples/TapRace/iphone/CreateUserController.h create mode 100644 Samples/TapRace/iphone/CreateUserController.mm create mode 100644 Samples/TapRace/iphone/FlipsideView.h create mode 100644 Samples/TapRace/iphone/FlipsideView.m create mode 100644 Samples/TapRace/iphone/FlipsideViewController.h create mode 100644 Samples/TapRace/iphone/FlipsideViewController.mm create mode 100644 Samples/TapRace/iphone/ForgotPassword.h create mode 100644 Samples/TapRace/iphone/ForgotPassword.mm create mode 100644 Samples/TapRace/iphone/GameController.h create mode 100644 Samples/TapRace/iphone/GameController.mm create mode 100644 Samples/TapRace/iphone/GameResultsController.h create mode 100644 Samples/TapRace/iphone/GameResultsController.mm create mode 100644 Samples/TapRace/iphone/LeaderboardsController.h create mode 100644 Samples/TapRace/iphone/LeaderboardsController.mm create mode 100644 Samples/TapRace/iphone/LeadersByLocationController.h create mode 100644 Samples/TapRace/iphone/LeadersByLocationController.mm create mode 100644 Samples/TapRace/iphone/LeadersController.h create mode 100644 Samples/TapRace/iphone/LeadersController.mm create mode 100644 Samples/TapRace/iphone/LoginController.h create mode 100644 Samples/TapRace/iphone/LoginController.mm create mode 100644 Samples/TapRace/iphone/MatchmakingController.h create mode 100644 Samples/TapRace/iphone/MatchmakingController.mm create mode 100644 Samples/TapRace/iphone/MenuController.h create mode 100644 Samples/TapRace/iphone/MenuController.mm create mode 100644 Samples/TapRace/iphone/MyCLController.h create mode 100644 Samples/TapRace/iphone/MyCLController.mm create mode 100644 Samples/TapRace/iphone/Resources/AboutGamespy.png create mode 100644 Samples/TapRace/iphone/Resources/Default.png create mode 100644 Samples/TapRace/iphone/Resources/FlipsideView.xib create mode 100644 Samples/TapRace/iphone/Resources/Gamespy.png create mode 100644 Samples/TapRace/iphone/Resources/Globe.png create mode 100644 Samples/TapRace/iphone/Resources/MainWindow.xib create mode 100644 Samples/TapRace/iphone/Resources/Top100.jpg create mode 100644 Samples/TapRace/iphone/Resources/UserStats.xib create mode 100644 Samples/TapRace/iphone/Resources/btn_down.png create mode 100644 Samples/TapRace/iphone/Resources/btn_up.png create mode 100644 Samples/TapRace/iphone/Resources/buddies.png create mode 100644 Samples/TapRace/iphone/Resources/buddies_rematch.png create mode 100644 Samples/TapRace/iphone/Resources/buddyListCell.xib create mode 100644 Samples/TapRace/iphone/Resources/camera.png create mode 100644 Samples/TapRace/iphone/Resources/createuser.xib create mode 100644 Samples/TapRace/iphone/Resources/forgotPassword.xib create mode 100644 Samples/TapRace/iphone/Resources/gamespyFooter.png create mode 100644 Samples/TapRace/iphone/Resources/gs_logo.gif create mode 100644 Samples/TapRace/iphone/Resources/icon_29x29.png create mode 100644 Samples/TapRace/iphone/Resources/icon_57x57.png create mode 100644 Samples/TapRace/iphone/Resources/leaderboard.png create mode 100644 Samples/TapRace/iphone/Resources/leaderboardMap.png create mode 100644 Samples/TapRace/iphone/Resources/leaderboards.xib create mode 100644 Samples/TapRace/iphone/Resources/leadersbylocation.xib create mode 100644 Samples/TapRace/iphone/Resources/leadersbytime.xib create mode 100644 Samples/TapRace/iphone/Resources/login.xib create mode 100644 Samples/TapRace/iphone/Resources/matchmaking.xib create mode 100644 Samples/TapRace/iphone/Resources/menu.png create mode 100644 Samples/TapRace/iphone/Resources/menu.xib create mode 100644 Samples/TapRace/iphone/Resources/nophoto.png create mode 100644 Samples/TapRace/iphone/Resources/people.png create mode 100644 Samples/TapRace/iphone/Resources/playAgain.png create mode 100644 Samples/TapRace/iphone/Resources/singlePlayer.png create mode 100644 Samples/TapRace/iphone/Resources/singleplayergame.xib create mode 100644 Samples/TapRace/iphone/Resources/singleplayerresults.xib create mode 100644 Samples/TapRace/iphone/Resources/takephoto.xib create mode 100644 Samples/TapRace/iphone/Resources/tapImage.png create mode 100644 Samples/TapRace/iphone/Resources/trophy.png create mode 100644 Samples/TapRace/iphone/Resources/twoplayergame.xib create mode 100644 Samples/TapRace/iphone/Resources/twoplayerresults.xib create mode 100644 Samples/TapRace/iphone/TapRace-Info.plist create mode 100644 Samples/TapRace/iphone/UserStatsController.h create mode 100644 Samples/TapRace/iphone/UserStatsController.mm create mode 100644 Samples/TapRace/iphone/Utility.h create mode 100644 Samples/TapRace/iphone/Utility.mm create mode 100644 Samples/TapRace/iphone/atlas_taprace_v2.c create mode 100644 Samples/TapRace/iphone/atlas_taprace_v2.h create mode 100644 Samples/TapRace/iphone/main.mm diff --git a/Samples/TapRace/iphone/AppDelegate.h b/Samples/TapRace/iphone/AppDelegate.h new file mode 100644 index 00000000..45df6dd1 --- /dev/null +++ b/Samples/TapRace/iphone/AppDelegate.h @@ -0,0 +1,268 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import +#import + +#import "gt2/gt2.h" +#import "sc/sc.h" +#import "gp/gp.h" +#import "serverbrowsing/sb_serverbrowsing.h" +#import "sake/sake.h" + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +// global defines used in the sample + +#define SCRACE_GAMENAME "taprace" +#define SCRACE_SECRETKEY "xJRENu" +#define SCRACE_GAMEID 2431 +#define SCRACE_PRODUCTID 11675 +#define SCRACE_VERSION 1.3 +#define SCRACE_VERSION_STR "1.3" + + +#define LEADERBOARD_INVALID 0 +#define LEADERBOARD_BY_TIME 1 +#define LEADERBOARD_BY_BUDDIES 2 +#define LEADERBOARD_BY_LOCATION 3 + +#define MAX_BUDDIES 100 +#define MAX_LEADERS 25 +#define MAX_MAPPOINTS 100 +#define MAX_SERVERS 20 +#define TAP_POLL_TIME 0.01 // seconds +#define INITIAL_BEST_TIME 99999 // milliseconds + +#define THUMBNAIL_WIDTH 96 +#define THUMBNAIL_HEIGHT 96 +#define FULLSIZE_IMAGE_WIDTH 320 +#define FULLSIZE_IMAGE_HEIGHT 320 + +#define PLAYER1 0 +#define PLAYER2 1 + +// define for debugging.... +#undef USE_DUMMY_LEADERBOARD_DATA + +@class AppDelegate; +@class GameController; +@class LeaderboardObject; + +typedef struct PlayerData PlayerData; + +extern char raceDateTimeString[256]; +extern PlayerData gPlayerData; +extern PlayerData gOpponentData; +//extern LeaderboardObject gOpponentData; // current opponent +extern int gLeaderboardType; // type of leaderboard data +extern int gLeaderboardProfileIndex; // index of currently displayed profile +extern NSMutableArray* gLeaderboardData; // most recent leaderboard download (array of LeaderboardObject's) +extern AppDelegate* appDelegate; +extern bool sdkInitialized; +extern bool lostConnection; // true when we lose network connectivity +extern bool userKickedOut; // set to true if same user logs into another device + + +@interface AppDelegate : NSObject +{ + IBOutlet UIWindow* window; + IBOutlet UINavigationController* navController; + + NSTimer* loadRemoteDataTimer; + NSTimer* thinkTimer; + NSTimer* countdownTimer; + NSTimer* hostingCountdownTimer; + NSTimer* joiningCountdownTimer; + UIAlertView* alertView; + + GameController* gameController; + + GT2Socket gt2Socket; + GT2Connection gt2Connection; + SCInterfacePtr interface; + ServerBrowser serverBrowser; + SAKE sake; + + GSLoginCertificate remoteCertificate; + gsi_u8 remoteConnId[SC_CONNECTION_GUID_SIZE]; + SCReportPtr statsReport; + + uint64_t remoteTime; + uint64_t localTime; + uint64_t start; + + int state; + int currentAlert; + int playerCount; + int countdown; + int hostingCountdown; + int joiningCountdown; + int natnegCookie; + bool connected; // host connected to client + bool fromMenu; + + bool hosting; + bool racing; + bool win; + bool tie; + bool disconnected; + bool reportSent; + bool sessionCreated; + bool connIdSet; + +} + +@property(nonatomic) ServerBrowser serverBrowser; +@property(nonatomic, readonly) SAKE sake; +@property bool connected; +@property(nonatomic, readonly) int state; +@property bool fromMenu; +@property(nonatomic, readonly) uint64_t start; +@property(nonatomic, readonly) uint64_t localTime; +@property(nonatomic, readonly) uint64_t remoteTime; + +- (void)startThinkTimer; +- (void)stopThinkTimer; +- (void)delayedNetworkConnectionErrorMessage; + +- (void)loggedIn; +- (void)kickedOut; +- (bool)isUserKickedOut; +- (void)logout; +- (BOOL)isNetworkAvailableFlags:(SCNetworkReachabilityFlags *)outFlags; + +- (bool)joinGame: (unsigned int)ip port: (unsigned short)port useNatNeg: (bool)useNatNeg; +- (bool)hostGame; + + +- (void)stopHostingServer; +- (void)initGame; +- (void)initSake; +- (void)findGame; +- (void)startSinglePlayerGame; +- (void)startMultiPlayerGame: (const char*)remoteAddress; +- (void)startCountdown; +- (void)startHostingCountdownTimer; +- (void)stopHostingCountdownTimer; +- (void)startJoiningCountdownTimer; +- (void)stopJoiningCountdownTimer; +- (void)restartGame; +- (void)finishedGame; +- (void)returnToMenu; +- (void)returnToMatchMaking; +- (bool)isTwoPlayerGame; +- (void)savePlayerStatsToFile; + +- (void)showAboutInfo: (id) sender; +- (void)flipsideViewControllerDidFinish:(id) sender; +- (void)showLeaderboards; +- (void)showLeadersByTime; +- (void)showTop5LeadersByTime; +- (void)showLeadersByBuddies; +- (void)showLeadersByLocation; +- (void)showUser: (int)index; +- (void)deleteLeaderboards; +- (void)deleteBlockedImagesData; +- (void)removeFalseLeaders; +- (bool)imageIsBlocked: (int) pictureId; +- (void)getMyLeaderPosition; + +- (UIViewController*)findExistingControllerOfType: (Class)type; +- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName; + +@end + +//*** indicates is used on player stats screens +struct PlayerData +{ + // "Normal" game data + NSString* uniquenick; // player name displayed + gsi_u32 profileId; // player ID + gsi_u32 pictureFileId; // picture fileId + int rank; // player rank + UIImage *thumbNail; // picture of player (not from comrade) + UIImage *fullsizeImage; // bigger picture of player (not from comrade) + CLLocation *playerLocation; // lat/long of player, used for map view + + // login/connection info + char loginTicket[GP_LOGIN_TICKET_LEN]; + GSLoginCertificate certificate; + GSLoginPrivateData privateData; + SCPeerCipher peerSendCipher; // for fast encryption + SCPeerCipher peerRecvCipher; // for fast decryption + + // local data + NSMutableDictionary* playerStatsData; // Player stats data (stored locally) + NSString* playerDataPath; // Player data directory path on device + NSMutableArray* blockedImagesData; // player's blocked image list data (stored locally) + + // Stats connection data (stored remotely) + SCInterfacePtr statsInterface; + gsi_u8 sessionId[SC_SESSION_GUID_SIZE]; + gsi_u8 connectionId[SC_CONNECTION_GUID_SIZE]; + gsi_u8 statsAuthdata[16]; + + // 2 player game stats + int careerWins; // # of 2 player won + int careerLosses; // # of 2 player matches lost + int bestRaceTime; // Player's best 2 player matches race time (milliseconds). + int worstRaceTime; // Player's 2 player matches worst race time (milliseconds). + int totalMatches; // # of 2 player matches played + float averageRaceTime; // Player's average race time per match (milliseconds/match). + int matchWinStreak; // # of consecutive 2 player games won + int matchLossStreak; // # of consecutive 2 player games lost + int totalRaceTime; // Player's total race time for all matches (milliseconds). + int careerDisconnects; // Player's total number of times disconnected. + float disconnectRate; // Player's disconnect rate (disconnects/matches). + int careerDraws; // Player's total number of draws (tied matches). + int matchDrawStreak; // # of consecutive 2 player games ending with draw + int careerLongestWinStreak; // longest consecutive 2 player games won + int careerLongestLossStreak; // longest consecutive 2 player games lost + int careerLongestDrawStreak; // longest consecutive 2 player games ending with draw + int totalCompleteMatches; // Total number of matches where the game went to completion (all win/loss/draw results). + + // single player game stats + int spBestRaceTime; //*** Player's career best race time (milliseconds). + int spWorstRaceTime; // Player's career worst race time (milliseconds). + int spTotalPlays; // total single player games played + float spAverageRaceTime; // Player's average race time per play (milliseconds/match). + int spTotalRaceTime; // Player's total race time for all plays (milliseconds). + + // single and 2 player game stats + char* lastGameTime; // date/time string of last game stats retrieved from Sake + +}; + + +@interface LeaderboardObject : NSObject +{ + NSString* name; + gsi_u32 profileId; + + gsi_u32 pictureFileId; + UIImage *thumbNail; + + int rank; + int careerWins; //*** + int careerLosses; //*** + int bestRaceTime; //*** Player's career best race time (milliseconds). + int totalMatches; //*** + int currentWinStreak; //*** + //*** + CLLocation *playerLocation; // lat/long of player, used for map view +} +@property (nonatomic, copy) NSString * name; +@property gsi_u32 profileId; +@property gsi_u32 pictureFileId; +@property (nonatomic, retain) UIImage * thumbNail; +@property int rank; +@property int careerWins; +@property int careerLosses; +@property int bestRaceTime; +@property int totalMatches; +@property int currentWinStreak; +@property (nonatomic, retain) CLLocation * playerLocation; + +@end diff --git a/Samples/TapRace/iphone/AppDelegate.mm b/Samples/TapRace/iphone/AppDelegate.mm new file mode 100644 index 00000000..fa7cdae5 --- /dev/null +++ b/Samples/TapRace/iphone/AppDelegate.mm @@ -0,0 +1,4126 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "AppDelegate.h" +#import "Utility.h" +#include "atlas_taprace_v2.h" + +#import "LoginController.h" +#import "MenuController.h" +#import "MatchmakingController.h" +#import "GameController.h" +#import "GameResultsController.h" +#import "LeaderboardsController.h" +#import "LeadersController.h" +#import "LeadersByLocationController.h" +#import "UserStatsController.h" + +#import "qr2/qr2.h" +#import "natneg/natneg.h" +#import "gt2/gt2Encode.h" + +#import +#import +#import + + +// Pre-game messaging +#define MSG_CONNECT "is" // client-pid, nick +#define MSG_TO_HOST_CERT "ir" // host-pid, certificate +#define MSG_TO_HOST_CERT_TYPE 1 +#define MSG_TO_CLIENT_CERT "ir" // join-pid, certificate +#define MSG_TO_CLIENT_CERT_TYPE 2 +#define MSG_TO_HOST_CERTVERIFIED "" +#define MSG_TO_HOST_CERTVERIFIED_TYPE 3 // ready for keys +#define MSG_TO_CLIENT_CERTVERIFIED "" +#define MSG_TO_CLIENT_CERTVERIFIED_TYPE 4 // ready for keys +#define MSG_TO_HOST_KEYS "r" // encryption keys +#define MSG_TO_HOST_KEYS_TYPE 5 +#define MSG_TO_CLIENT_KEYS "r" // encrpytion keys +#define MSG_TO_CLIENT_KEYS_TYPE 6 +#define MSG_REPLAY "" +#define MSG_REPLAY_TYPE 7 // replay game +#define MSG_REPLAY_CANCELED "" +#define MSG_REPLAY_CANCELED_TYPE 8 // ...or not + +// Game messaging +#define MSG_COUNTDOWN "i" // count +#define MSG_COUNTDOWN_TYPE 20 +#define MSG_SESSION_ID "rr" // Host session ID, connID exchange +#define MSG_SESSION_ID_TYPE 21 +#define MSG_CONNECTION_ID "r" // connection ID exhcnage +#define MSG_CONNECTION_ID_TYPE 22 +#define MSG_START_RACE "" // race start +#define MSG_START_RACE_TYPE 23 +#define MSG_PROGRESS "i" // progress +#define MSG_PROGRESS_TYPE 24 +#define MSG_END_RACE "i" // time +#define MSG_END_RACE_TYPE 25 +#define MSG_CHAT "s" // message +#define MSG_CHAT_TYPE 26 + +#undef HOST_PORT // previously defined in mach headers +#define HOST_PORT 38466 +#define HOST_PORT2 38468 +#define CLIENT_PORT 38467 +#define HOST_PORT_STRING ":" STRINGIZE(HOST_PORT) +#define CLIENT_PORT_STRING ":" STRINGIZE(CLIENT_PORT) // so you can run both +#define COUNTDOWN_START 5 + +// Stats & SDK constants +#define SCRACE_TIMEOUT_MS 0 +#define SCRACE_SLEEP_MS 100 +#define SCRACE_AUTHORITATIVE gsi_true +#define SCRACE_COLLABORATIVE gsi_false +#define SCRACE_NUM_PLAYERS 2 +#define SCRACE_NUM_TEAMS 2 + +#define SCRACE_HOST_TEAM 7564 // fake team ids +#define SCRACE_CLIENT_TEAM 7565 // fake team ids + +// NAT negotiation +#define NN_SERVER 0 +#define NN_CLIENT 1 + +#define buttonExists( viewButtonIndex ) viewButtonIndex >= 0 + +enum AppState +{ + LOGGED_OUT, + SETTING_UP, + RACING, + FINISHED_RACE, + + HOST_LISTENING, + HOST_CONNECTED, + HOST_ERROR, + HOST_EXCHANGE_CERT, + HOST_VERIFY_CERT, + HOST_EXCHANGE_KEYS, + HOST_WAITING, + HOST_SEND_SESSID, + + JOIN_CHOOSESERVER, + JOIN_CONNECTING, + JOIN_CONNECTED, + JOIN_ERROR, + JOIN_EXCHANGE_CERT, + JOIN_VERIFY_CERT, + JOIN_EXCHANGE_KEYS, + JOIN_WAITING, + JOIN_SEND_CONNID +}; + +enum AppAlert +{ + ALERT_NONE, + ALERT_LOADINGPLAYERDATA, + ALERT_LISTENING, + ALERT_NATNEG, + ALERT_CONNECTING, + ALERT_REPLAY, + ALERT_REPLAY_CONFIRM, + ALERT_WAITING_FOR_LEADERS, + ALERT_NO_NETWORK_CONNECTION +}; + + +/////////////////////////////////////////////////////////////////////////////// +// Query and Reporting Callbacks +static void ServerKeyCallback(int keyid, qr2_buffer_t outbuf, void* userdata); +static void PlayerKeyCallback(int keyid, int index, qr2_buffer_t outbuf, void* userdata); +static void TeamKeyCallback(int keyid, int index, qr2_buffer_t outbuf, void* userdata); +static int CountCallback(qr2_key_type keytype, void* userdata); +static void KeyListCallback(qr2_key_type keytype, qr2_keybuffer_t keybuffer, void* userdata); +static void AddErrorCallback(qr2_error_t error, char* errmsg, void* userdata); +static void NatNegCallback(int cookie, void* userData); + +/////////////////////////////////////////////////////////////////////////////// +// Competition Callbacks +static void CreateSessionCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData); +static void SetReportIntentionCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData); +static void SubmitReportCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData); + +/////////////////////////////////////////////////////////////////////////////// +// Sake Callbacks +static void GetMyRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, SAKEGetMyRecordsInput* inputData, SAKEGetMyRecordsOutput* outputData, void* userData); + +/////////////////////////////////////////////////////////////////////////////// +// GT2 Callbacks +static void ConnectedCallback(GT2Connection connection, GT2Result result, GT2Byte* message, int len); +static void ReceivedCallback(GT2Connection connection, GT2Byte* message, int len, GT2Bool reliable); +static void ClosedCallback(GT2Connection connection, GT2CloseReason reason); +static void ConnectAttemptCallback(GT2Socket listener, GT2Connection connection, unsigned int ip, unsigned short port, int latency, GT2Byte* message, int len); +static void SocketErrorCallback(GT2Socket socket); +static GT2Bool UnrecognizedMessageCallback(GT2Socket socket, unsigned int ip, unsigned short port, GT2Byte* message, int len); + +/////////////////////////////////////////////////////////////////////////////// +// Leaderboard Callbacks +//static void cbGetBuddyList( GPConnection * connection, void * arg, void * param ); +static void GetMyLeaderPositionCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData); +static void SearchForLeadersTimeRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData); +static void SearchForTop5TimeRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData); +//static BOOL setTimeRecords(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData); + +/////////////////////////////////////////////////////////////////////////////// +// NAT Negotiation Callbacks +static void NNDetectionCallback(gsi_bool success, NAT nat); +static void NNProgressCallback(NegotiateState state, void* userdata); +static void NNCompletedCallback(NegotiateResult result, SOCKET gamesocket, sockaddr_in* remoteaddr, void* userDdata); + +static const char PLAYER_STATS_TABLE[] = "PlayerStats_v" STRINGIZE(ATLAS_RULE_SET_VERSION); + +@implementation LeaderboardObject +@synthesize name; +@synthesize profileId; +@synthesize pictureFileId; +@synthesize rank; +@synthesize careerWins; +@synthesize careerLosses; +@synthesize bestRaceTime; +@synthesize totalMatches; +@synthesize currentWinStreak; +@synthesize playerLocation; +@synthesize thumbNail; + +- (CLLocationCoordinate2D)coordinate { + return playerLocation.coordinate; +} + +- (NSString *)title { + return name; +} + +@end + +// GLOBAL VARIABLES +//================= +char raceDateTimeString[256]; +PlayerData gPlayerData; +PlayerData gOpponentData; +//LeaderboardObject gOpponentData; // current opponent +int gLeaderboardType; // type of most recently downloaded leaderboard data +int gLeaderboardProfileIndex; // index of currently displayed profile +NSMutableArray* gLeaderboardData; // array of PlayerData for leaderboard +AppDelegate* appDelegate; +bool sdkInitialized; // set true at end of applicationDidFinishLaunching +bool userKickedOut; // set to true if same user logs into another device +bool lostConnection; // true when we lose network connectivity + +@interface AppDelegate(Private) + +//- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName; +- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName animated: (BOOL)animated; + +- (bool)setupHosting; +- (bool)setupJoining: (unsigned int)ip port: (unsigned short)port useNatNeg: (bool)useNatNeg; +- (void)setupMatch: (bool)hostMatch; + +- (void)onLoadRemoteDataTimer: (NSTimer*)timer; +- (void)onThinkTimer: (NSTimer*)timer; +- (void)onCountdownTimer: (NSTimer*)timer; +- (void)onHostingCountdownTimer: (NSTimer*)timer; + +- (void)saveTwoPlayerGameStatsToFile; + +- (void)reportTwoPlayerStats; +- (void)reportSinglePlayerStats; +- (void)countdown; +- (void)startRace; + +- (void)addErrorCallback: (qr2_error_t)error errorMessage: (const char*)errmsg; + +- (void)createSessionCallback: (const SCInterfacePtr)interface httpResult: (GHTTPResult)httpResult result: (SCResult)result; +- (void)setReportIntentionCallback: (const SCInterfacePtr)interface httpResult: (GHTTPResult)httpResult result: (SCResult)result; +- (void)submitReportCallback: (const SCInterfacePtr)interface httpResult: (GHTTPResult)httpResult result: (SCResult)result; + +- (void)getMyRecordsCallback: (SAKERequestResult) result inputData: (SAKEGetMyRecordsInput*)inputData outputData: (SAKEGetMyRecordsOutput*)outputData; + +- (void)connectedCallback: (GT2Connection)connection result: (GT2Result)result message: (GT2Byte*)message length: (int)length; +- (void)receivedCallback: (GT2Connection)connection message: (GT2Byte*)message length: (int)length reliable: (GT2Bool)reliable; +- (void)closedCallback: (GT2Connection)connection reason: (GT2CloseReason)reason; +- (void)connectAttemptCallback: (GT2Socket)listener connection: (GT2Connection)connection ip: (unsigned int)ip port: (unsigned short)port latency: (int)latency message: (GT2Byte*)message length: (int)length; + +- (void)natnegCallback: (int)cookie; +- (void)natnegCompletedCallback: (NegotiateResult)result socket: (SOCKET)gamesocket remoteaddr: (sockaddr_in*)remoteaddr; + +@end + + +@implementation AppDelegate + +@synthesize serverBrowser; +@synthesize sake; + +@synthesize connected; +@synthesize state; +@synthesize fromMenu; + +@synthesize start; +@synthesize localTime; +@synthesize remoteTime; + + +// Called when leaving TapRace +// =========================== +- (void)dealloc +{ + [self deleteLeaderboards]; + [self deleteBlockedImagesData]; + [window release]; + [navController release]; + [super dealloc]; +} + +#ifdef GSI_COMMON_DEBUG + +// Called to display SDK debug messages +// ==================================== +static void DebugCallback(GSIDebugCategory theCat, GSIDebugType theType, GSIDebugLevel theLevel, const char * theTokenStr, va_list theParamList) +{ + GSI_UNUSED(theLevel); + + printf("[%s][%s] ", gGSIDebugCatStrings[theCat], gGSIDebugTypeStrings[theType]); + + vprintf(theTokenStr, theParamList); +} +#endif + + +// Called when race is finished or terminated +- (void)stopHostingServer +{ + qr2_shutdown(NULL); + +/* if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + gt2CloseSocket(tempsocket); +*/ + connected = false; + hosting = false; + NSLog(@"stopHostingServer qr2_shutdown"); +} + + +// Called when click login or into login page +// ========================================== +- (void)initGame +{ + sessionCreated = false; + NSLog(@"initGame: null gt2 socket"); + gt2Socket = NULL; + state = LOGGED_OUT; + countdown = 0; + racing = false; + connIdSet = false; + gt2Connection = NULL; + gLeaderboardData = nil; + gLeaderboardType = LEADERBOARD_INVALID; +} + + +// Called when click login or into login page +// Initialize Sake - used to read stored data from the backend +// =========================================================== +- (void)initSake +{ + GSIACResult aResult = GSIACWaiting; + + if(sdkInitialized) return; + + // Do Availability Check - Make sure backend is available + GSIStartAvailableCheck(SCRACE_GAMENAME); + while( aResult == GSIACWaiting ) + { + aResult = GSIAvailableCheckThink(); + msleep(5); + } + + if ( aResult == GSIACUnavailable ) + { + MessageBox( @"Online service for TapRace is no longer available." ); + return; + //exit(EXIT_FAILURE); + } + + if ( aResult == GSIACTemporarilyUnavailable ) + { + MessageBox(@"Online service for TapRace is temporarily down for maintenance."); + return; + //exit(EXIT_FAILURE); + } + + // Initialize SDK core object - used for both the AuthService and the Competition SDK + gsCoreInitialize(); + + + // Initialize the Competition SDK - all users submit a snapshot + SCResult result = scInitialize(SCRACE_GAMEID, &interface); + + if (result != SCResult_NO_ERROR) + { + NSLog(@"SCResult result = %d", result ); + + switch (result) + { + case SCResult_OUT_OF_MEMORY: + MessageBox(@"Application failed to initialize.", @"Out of memory"); + break; + + case SCResult_NOT_INITIALIZED: + MessageBox(@"Application failed to initialize.", @"Competition"); + break; + + case SCResult_NO_AVAILABILITY_CHECK: + MessageBox(@"Application failed to initialize.", @"Availability"); + break; + + default: + MessageBox(@"Application failed to initialize.", @"Unknown Error"); + break; + } + return; + //exit(EXIT_FAILURE); + } + + gPlayerData.statsInterface = interface; + gPlayerData.thumbNail = nil; + gPlayerData.fullsizeImage = nil; + + sakeStartup(&sake); + sakeSetGame(sake, SCRACE_GAMENAME, SCRACE_GAMEID, SCRACE_SECRETKEY); + sdkInitialized = true; +} + + +// called after a delay to upload a new image from the image picker... +// gives the mainloop a chance to switch views and render the leaders menu again before the upload starts +// ======================================================================================================= +- (void)delayedNetworkConnectionErrorMessage +{ + MessageBox(@"Unable to establish connection. Please connect to a different WiFi network or switch to 3G.", + @"Network Error"); +} + + +// called after TapRace finishes loading +// ======================================================================================================= +- (void)applicationDidFinishLaunching: (UIApplication*)application +{ +#ifdef GSI_COMMON_DEBUG + // gsSetDebugFile(stdout); // output to console + // gsSetDebugCallback(DebugCallback); // or output to a function for logging + + // Set output debug levels + gsSetDebugLevel(GSIDebugCat_All, GSIDebugType_All, GSIDebugLevel_Verbose); // all SDKs +#endif + + appDelegate = self; + + [window addSubview: navController.view]; + [window makeKeyAndVisible]; +} + +// Perform a reachability query for the address 0.0.0.0. If that address is reachable without +// requiring a connection, a network interface is available. We'll have to do more work to +// determine which network interface is available. +// ======================================================================================================= +- (BOOL)isNetworkAvailableFlags:(SCNetworkReachabilityFlags *)outFlags +{ + SCNetworkReachabilityRef defaultRouteReachability; + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress); + + SCNetworkReachabilityFlags flags; + BOOL gotFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags); + if (!gotFlags) + return NO; + + BOOL isReachable = flags & kSCNetworkReachabilityFlagsReachable; + + // This flag indicates that the specified nodename or address can + // be reached using the current network configuration, but a + // connection must first be established. + // + // If the flag is false, we don't have a connection. But because CFNetwork + // automatically attempts to bring up a WWAN connection, if the WWAN reachability + // flag is present, a connection is not required. + BOOL noConnectionRequired = !(flags & kSCNetworkReachabilityFlagsConnectionRequired); + if ((flags & kSCNetworkReachabilityFlagsIsWWAN)) { + noConnectionRequired = YES; + } + + isReachable = (isReachable && noConnectionRequired) ? YES : NO; + + // Callers of this method might want to use the reachability flags, so if an 'out' parameter + // was passed in, assign the reachability flags to it. + if (outFlags) { + *outFlags = flags; + } + + return isReachable; +} + + +- (void)applicationWillTerminate: (UIApplication*)application +{ + if( hosting ) + { + qr2_shutdown(NULL); + } + + if (gPlayerData.thumbNail != nil) { + [gPlayerData.thumbNail release]; + gPlayerData.thumbNail = nil; + } + if (gPlayerData.fullsizeImage != nil) { + [gPlayerData.fullsizeImage release]; + gPlayerData.fullsizeImage = nil; + } + + // Note: gOpponentData does not use fullsizeImage + if (gOpponentData.thumbNail != nil) { + [gOpponentData.thumbNail release]; + gOpponentData.thumbNail = nil; + } + + [appDelegate deleteLeaderboards]; + + [self logout]; +} + +- (void)navigationController: (UINavigationController*)navigationController willShowViewController: (UIViewController*)viewController animated: (BOOL)animated +{ + if ([viewController isMemberOfClass: [LoginController class]]) { + if (state != LOGGED_OUT) + { + state = LOGGED_OUT; + NSLog(@"navigationController state=LOGGED_OUT"); + + LoginController* loginController = (LoginController*)viewController; + assert(loginController != nil); + [loginController reset]; + } + } + else if ([viewController isMemberOfClass: [MenuController class]]) { + if (state != SETTING_UP) + { + racing = false; + if (gt2Connection != NULL) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + + [self stopThinkTimer]; + + state = SETTING_UP; + NSLog(@"navigationController state=SETTING_UP"); + + MenuController* menuController = (MenuController*)viewController; + assert(menuController != nil); + [menuController reset]; + } + } + else if ([viewController isMemberOfClass: [MatchmakingController class]]) { + if (state != JOIN_CHOOSESERVER) { + if (playerCount > 1) { + if (countdownTimer != nil) + { + [countdownTimer invalidate]; + countdownTimer = nil; + } + if (hostingCountdownTimer != nil) + { + [hostingCountdownTimer invalidate]; + hostingCountdownTimer = nil; + } + racing = false; + if (gt2Connection != NULL) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + + state = JOIN_CHOOSESERVER; + NSLog(@"navigationController state=JOIN_CHOOSESERVER"); + + MatchmakingController* matchmakingController = (MatchmakingController*)viewController; + assert(matchmakingController != nil); + [matchmakingController reset]; + } + } + } + else if ([viewController isMemberOfClass: [GameController class]]) { + if (state != RACING) { + state = RACING; + NSLog(@"navigationController state=RACING"); + [gameController reset]; + } + } +} + +- (void)startThinkTimer +{ + if (thinkTimer != nil) + return; // already running + + thinkTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onThinkTimer:) userInfo: nil repeats: YES]; + + [UIApplication sharedApplication].idleTimerDisabled = NO; +} + +- (void)stopThinkTimer +{ + [thinkTimer invalidate]; + thinkTimer = nil; + + [UIApplication sharedApplication].idleTimerDisabled = NO; +} + + +/** + * Once logged in, load the local file stats and initiate loading of server stats + */ +- (void)loggedIn +{ + NSString* dataPath; + + // Load locally saved player stats data. + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + + if ([paths count] == 0) { + // Something's very wrong; this should never happen. + MessageBox(@"Unable to access private data path in the file system.", @"Error"); + exit(EXIT_FAILURE); + } + + NSString* documentsPath = (NSString*)[paths objectAtIndex: 0]; + gPlayerData.playerDataPath = [[documentsPath stringByAppendingPathComponent: [NSString stringWithFormat: @"%d", gPlayerData.profileId]] retain]; + + // Check if diretory exists + BOOL isDir = NO; + NSFileManager* fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath: gPlayerData.playerDataPath isDirectory: &isDir] || !isDir) + { + if (!isDir) + { + // Don't know how this got here, but we don't want it: + [fileManager removeItemAtPath: gPlayerData.playerDataPath error: NULL]; + } + } + + //*** Get the player's block image list from file + dataPath = [gPlayerData.playerDataPath stringByAppendingPathComponent: blockedImagesDataFile]; + gPlayerData.blockedImagesData = [[NSMutableArray alloc] initWithContentsOfFile: dataPath]; // will be nil if the file doesn't exist + if( gPlayerData.blockedImagesData == nil ) + gPlayerData.blockedImagesData = [[NSMutableArray alloc] init]; + + //*** Get the player stats from file + dataPath = [gPlayerData.playerDataPath stringByAppendingPathComponent: playerStatsDataFile]; + gPlayerData.playerStatsData = [[NSMutableDictionary alloc] initWithContentsOfFile: dataPath]; // will be nil if the file doesn't exist + + // Load single & multi-player data from server + static char* fields[] = + { + ATLAS_GET_STAT_NAME(CAREER_WINS), + ATLAS_GET_STAT_NAME(CAREER_LOSSES), + ATLAS_GET_STAT_NAME(BEST_RACE_TIME), + ATLAS_GET_STAT_NAME(WORST_RACE_TIME), + ATLAS_GET_STAT_NAME(TOTAL_MATCHES), + ATLAS_GET_STAT_NAME(AVERAGE_RACE_TIME), + ATLAS_GET_STAT_NAME(CURRENT_WIN_STREAK), + ATLAS_GET_STAT_NAME(CURRENT_LOSS_STREAK), + ATLAS_GET_STAT_NAME(TOTAL_RACE_TIME), + ATLAS_GET_STAT_NAME(CAREER_DISCONNECTS), + ATLAS_GET_STAT_NAME(DISCONNECT_RATE), + ATLAS_GET_STAT_NAME(CAREER_DRAWS), + ATLAS_GET_STAT_NAME(CURRENT_DRAW_STREAK), + ATLAS_GET_STAT_NAME(CAREER_LONGEST_WIN_STREAK), + ATLAS_GET_STAT_NAME(CAREER_LONGEST_LOSS_STREAK), + ATLAS_GET_STAT_NAME(CAREER_LONGEST_DRAW_STREAK), + ATLAS_GET_STAT_NAME(TOTAL_COMPLETE_MATCHES), + + ATLAS_GET_STAT_NAME(SP_BEST_RACE_TIME), + ATLAS_GET_STAT_NAME(SP_WORST_RACE_TIME), + ATLAS_GET_STAT_NAME(SP_TOTAL_PLAYS), + ATLAS_GET_STAT_NAME(SP_AVERAGE_RACE_TIME), + ATLAS_GET_STAT_NAME(SP_TOTAL_RACE_TIME), + ATLAS_GET_STAT_NAME(LAST_TIME_PLAYED) + }; + static SAKEGetMyRecordsInput input = { (char*)PLAYER_STATS_TABLE, fields, sizeof(fields) / sizeof(fields[0]) }; + + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Loading player data..." delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + + currentAlert = ALERT_LOADINGPLAYERDATA; + sakeSetProfile(sake, gPlayerData.profileId, gPlayerData.loginTicket); + sakeGetMyRecords(sake, &input, (SAKERequestCallback)GetMyRecordsCallback, self); + state = SETTING_UP; + NSLog(@"loggedIn state=SETTING_UP"); + + // display Menu page + [self pushNewControllerOfType: [MenuController class] nib: @"menu"]; + loadRemoteDataTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onLoadRemoteDataTimer:) userInfo: nil repeats: YES]; +} + + +// Called when user receives message from server that +// - the login has been is disconnected on the server (due to same user login on other device) +// - the connection closed +- (void)kickedOut +{ + userKickedOut = true; +} + +- (bool)isUserKickedOut +{ + return userKickedOut; +} + + +// Called when logging user out +- (void)logout +{ + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + if (gt2Socket != NULL) + { + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"logout: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + } + + NSLog(@"logout: qr2 shutdown"); + qr2_shutdown(NULL); + + sakeShutdown(sake); + scShutdown(interface); + gsCoreShutdown(); + + [gPlayerData.playerDataPath release]; + gPlayerData.playerDataPath = nil; + + LoginController* loginController = (LoginController*)[self findExistingControllerOfType: [LoginController class]]; + + [loginController reset]; + + [navController popToViewController: loginController animated: YES]; +} + +- (bool)joinGame: (unsigned int)ip port: (unsigned short)port useNatNeg: (bool)useNatNeg; +{ + if (![self setupJoining: ip port: port useNatNeg: useNatNeg]) + { + //MessageBox(@"Error setting up the match"); + MessageBox(@"Connection Error", @"Error setting up the match. Login when network connection is up"); +// was commented ***************** + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + if (gt2Socket != NULL) + { + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"joinGame: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + } +// up to here ******************* + //[self logout]; // from old code + return false; + } + + return true; +} + +- (bool)hostGame +{ + if (![self setupHosting]) + { + // If timed out waiting for player + if (hostingCountdownTimer == nil || hostingCountdown != 0 ) + { + MessageBox(@"Error setting up the match."); + } + + return false; + } + return true; +} + +- (void)stopHostingCountdownTimer +{ + NSLog(@"stopHostingCountdownTimer"); + + if (hostingCountdownTimer != nil) + { + [hostingCountdownTimer invalidate]; + hostingCountdownTimer = nil; + } + +} + +- (void)tellHostToQuitGame +{ + // 2 player match - Let opponent know we finished. + ///////////////////////////// + char buffer[32]; + + int rcode = gtEncode(MSG_END_RACE_TYPE, MSG_END_RACE, buffer, sizeof(buffer), (uint32_t)(localTime * absolute_to_millis)); + assert(rcode != -1); + + NSLog(@"Protocol send MSG_END_RACE_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); +} + +- (void)findGame +{ + state = JOIN_CHOOSESERVER; + NSLog(@"findGame state=JOIN_CHOOSESERVER"); + playerCount = 2; + + [self pushNewControllerOfType: [MatchmakingController class] nib: @"matchmaking"]; + + [self startThinkTimer]; +} + +- (void)startSinglePlayerGame +{ + fromMenu = true; + state = RACING; + NSLog(@"startSinglePlayerGame state=RACING"); + reportSent = gsi_false; + playerCount = 1; + + gameController = (GameController*)[self pushNewControllerOfType: [GameController class] nib: @"singleplayergame"]; +} + +- (void)startMultiPlayerGame: (const char*)remoteAddress +{ + const int buflen = 2 + 4 + 2 + GP_UNIQUENICK_LEN; // 2 bytes for each message type, 4 bytes for int, GP_UNIQUENICK_LEN for max uniquenick length + char buf[buflen]; + gtEncodeNoType(MSG_CONNECT, buf, buflen, gPlayerData.profileId, gPlayerData.uniquenick); + + GT2ConnectionCallbacks callbacks = { + ConnectedCallback, + ReceivedCallback, + ClosedCallback, + NULL + }; + + NSLog(@"startMultiPlayerGame: connect gt2 socket"); + GT2Result result = gt2Connect(gt2Socket, >2Connection, remoteAddress, (GT2Byte*)buf, buflen, 0, &callbacks, GT2False); + NSLog(@"startMultiPlayerGame remoteAddress=%d", remoteAddress); + NSLog(@"startMultiPlayerGame gt2Connect:gt2Socket=%d result=%d\n", (int)gt2Socket, (int)result); + if (result != GT2Success) + { + MessageBox(@"Unable to connect to remote player.", @"Connection Error"); + return; + } + + gt2SetConnectionData(gt2Connection, self); + state = JOIN_CONNECTING; + NSLog(@"startMultiPlayerGame state=JOIN_CONNECTING"); + + // show that we are connecting + alertView = [[UIAlertView alloc] initWithTitle: @"Connecting to remote player" message: @"30 seconds" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_CONNECTING; + + [appDelegate startJoiningCountdownTimer]; +} + +- (bool)isTwoPlayerGame +{ + return (playerCount == 2); +} + +- (void)startCountdown +{ + if (playerCount == 1) + { + reportSent = false; + scCreateMatchlessSession(interface, &gPlayerData.certificate, &gPlayerData.privateData, CreateSessionCallback, SCRACE_TIMEOUT_MS, self); + + // Single-player game, no countdown. + [self startRace]; + return; + } + + // release joining game countdown + [self stopJoiningCountdownTimer]; + + if (hosting) + { + // The countdown here could be thought of as a loading screen. + // During this loading phase - the Host will create the game session and notify + // the other players of the session ID. All players will set their report intentions. + scCreateSession(interface, &gPlayerData.certificate, &gPlayerData.privateData, CreateSessionCallback, SCRACE_TIMEOUT_MS, self); + state = HOST_SEND_SESSID; + NSLog(@"startCountdown state=HOST_SEND_SESSID"); + + // Start the countdown. + countdown = COUNTDOWN_START; + countdownTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(onCountdownTimer:) userInfo: nil repeats: YES]; + [self countdown]; + } +} + +- (void)startHostingCountdownTimer +{ + // release hosting countdown + [self stopHostingCountdownTimer]; //if any + + // Start the countdown. + NSLog(@"startHostingCountdownTimer starts hostingCountdownTimer"); + hostingCountdown = 30; + hostingCountdownTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(onHostingCountdownTimer:) userInfo: self repeats: YES]; + +} + +- (void)stopJoiningCountdownTimer +{ + NSLog(@"stopJoiningCountdownTimer"); + if (joiningCountdownTimer != nil) + { + [joiningCountdownTimer invalidate]; + joiningCountdownTimer = nil; + } + +} + +- (void)startJoiningCountdownTimer +{ + [self stopJoiningCountdownTimer]; // if running + + NSLog(@"startJoiningCountdownTimer"); + + // Start the countdown. + joiningCountdown = 30; + joiningCountdownTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(onJoiningCountdownTimer:) userInfo: nil repeats: YES]; + +} + + +- (void)restartGame +{ + if (playerCount > 1) + { + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_TYPE, MSG_REPLAY, buffer, sizeof(buffer)); + + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + alertView = [[UIAlertView alloc] initWithTitle: @"Waiting for reply" message: @"30 seconds" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_REPLAY; + + [self startHostingCountdownTimer]; + } + else + { + assert(gameController != nil); + + [navController popToViewController: gameController animated: YES]; + } +} + +- (void)finishedGame +{ + fromMenu = false; + + BOOL newBestTime = NO; + localTime = mach_absolute_time() - start; + + + // set last game date/time + NSString *raceTimeString = [[NSDate date] description]; + strcpy( raceDateTimeString, [raceTimeString cStringUsingEncoding: NSASCIIStringEncoding] ); + gPlayerData.lastGameTime = raceDateTimeString; + + if (playerCount == 1) + { + int oldBestTime; + GameResultsController* resultsController = (GameResultsController*)[self pushNewControllerOfType: [GameResultsController class] nib: @"singleplayerresults"]; + state = FINISHED_RACE; + NSLog(@"finishedGame state=FINISHED_RACE"); + + // Update single player data. + int localTimeMillis = localTime * absolute_to_millis; + float avgTapsPerSec = 60.0 / (localTimeMillis / 1000.0); + + if (gPlayerData.playerStatsData != nil) + { + // data from file + int bestTimeMillis = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerBestTimeKey] intValue]; + int worstTimeMillis = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerWorstTimeKey] intValue]; + int averageTimeMillis = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerAverageTimeKey] intValue]; + int gamesPlayed = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerGamesPlayedKey] intValue]; + + averageTimeMillis = ((averageTimeMillis * gamesPlayed) + localTimeMillis) / (gamesPlayed + 1); + gamesPlayed++; + + + if (bestTimeMillis > localTimeMillis) + { + newBestTime = YES; + oldBestTime = bestTimeMillis; + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: localTimeMillis] forKey: singlePlayerBestTimeKey]; + } + else + { + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: bestTimeMillis] forKey: singlePlayerBestTimeKey]; + } + + if (worstTimeMillis < localTimeMillis) + worstTimeMillis = localTimeMillis; + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: worstTimeMillis] forKey: singlePlayerWorstTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: averageTimeMillis] forKey: singlePlayerAverageTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gamesPlayed] forKey: singlePlayerGamesPlayedKey]; + } + else + { + gPlayerData.playerStatsData = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + [NSNumber numberWithInt: localTimeMillis], singlePlayerBestTimeKey, + [NSNumber numberWithInt: localTimeMillis], singlePlayerWorstTimeKey, + [NSNumber numberWithInt: localTimeMillis], singlePlayerAverageTimeKey, + [NSNumber numberWithInt: 1], singlePlayerGamesPlayedKey, + nil]; + } + + [gPlayerData.playerStatsData setObject: raceTimeString forKey: lastTimePlayedKey]; + + [gPlayerData.playerStatsData writeToFile: [gPlayerData.playerDataPath stringByAppendingPathComponent: playerStatsDataFile] atomically: YES]; + + // Report the stats. + //////////////////// + if (!reportSent) + { + disconnected = gsi_false; + [self reportSinglePlayerStats]; + } + + [resultsController setGameTime: localTimeMillis]; + [resultsController setGamesPlayed: [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerGamesPlayedKey] intValue]]; + [resultsController setBestTime: [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerBestTimeKey] intValue]]; + [resultsController setAverageTime: [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerAverageTimeKey] intValue]]; + [resultsController setAverageTaps: avgTapsPerSec ]; + if (newBestTime) + { + [resultsController setNewBestTime: oldBestTime]; + } + } + else + { + // 2 player match - Let opponent know we finished. + ///////////////////////////// + char buffer[32]; + + int rcode = gtEncode(MSG_END_RACE_TYPE, MSG_END_RACE, buffer, sizeof(buffer), (uint32_t)(localTime * absolute_to_millis)); + assert(rcode != -1); + + NSLog(@"Protocol send MSG_END_RACE_TYPE\n"); + GT2Result result = gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + if( result == GT2NetworkError ) + { + MessageBox(@"Lost connection to remote player. Game canceled", @"Connection Error"); + [appDelegate returnToMatchMaking]; + } + } + +} + + +// save single player and two player game stats for logged in player +- (void)savePlayerStatsToFile +{ + if (gPlayerData.playerStatsData == nil) + { + // Copy stats from server to new file + gPlayerData.playerStatsData = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + // single player game stats + [NSNumber numberWithInt: gPlayerData.spBestRaceTime], singlePlayerBestTimeKey, + [NSNumber numberWithInt: gPlayerData.spWorstRaceTime], singlePlayerWorstTimeKey, + [NSNumber numberWithInt: gPlayerData.spTotalPlays], singlePlayerTotalPlaysKey, + [NSNumber numberWithInt: gPlayerData.spTotalRaceTime], singlePlayerTotalRaceTimeKey, + [NSNumber numberWithInt:(int) (gPlayerData.spAverageRaceTime *1000)], singlePlayerAverageTimeKey, + + // two player game stats + [NSNumber numberWithInt: gPlayerData.bestRaceTime], matchBestTimeKey, + [NSNumber numberWithInt: gPlayerData.worstRaceTime], matchWorstTimeKey, + [NSNumber numberWithInt: (int)(gPlayerData.averageRaceTime * 1000)], matchAverageTimeKey, + [NSNumber numberWithInt: gPlayerData.careerWins], matchWonKey, + [NSNumber numberWithInt: gPlayerData.careerLosses], matchLossKey, + [NSNumber numberWithInt: gPlayerData.matchWinStreak], matchWinStreakKey, + [NSNumber numberWithInt: gPlayerData.totalMatches], matchGamesPlayedKey, + [NSNumber numberWithInt: gPlayerData.matchLossStreak], matchLossStreakKey, + [NSNumber numberWithInt: gPlayerData.totalRaceTime], matchTotalRaceTimeKey, + [NSNumber numberWithInt: gPlayerData.careerDisconnects], matchCareerDisconnectsKey, + [NSNumber numberWithInt: gPlayerData.careerDraws], matchDrawsKey, + [NSNumber numberWithInt: gPlayerData.matchDrawStreak], matchDrawStreakKey, + [NSNumber numberWithInt: gPlayerData.careerLongestWinStreak], matchCareerLongestWinStreakKey, + [NSNumber numberWithInt: gPlayerData.careerLongestLossStreak], matchCareerLongestLossStreakKey, + [NSNumber numberWithInt: gPlayerData.careerLongestDrawStreak], matchCareerLongestDrawStreakKey, + [NSNumber numberWithInt: gPlayerData.totalCompleteMatches], matchTotalCompleteMatchesKey, + [NSNumber numberWithFloat: gPlayerData.disconnectRate], matchDisconnectRateKey, + [NSString stringWithCString: gPlayerData.lastGameTime encoding: NSASCIIStringEncoding], lastTimePlayedKey, + nil]; + } + else + { + // single player game stats + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.spBestRaceTime] forKey: singlePlayerBestTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.spWorstRaceTime] forKey: singlePlayerWorstTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.spTotalPlays] forKey: singlePlayerTotalPlaysKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.spTotalRaceTime] forKey: singlePlayerTotalRaceTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: gPlayerData.spAverageRaceTime] forKey: singlePlayerAverageTimeKey]; + + // two player game stats + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.bestRaceTime] forKey: matchBestTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.worstRaceTime] forKey: matchWorstTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: gPlayerData.averageRaceTime] forKey: matchAverageTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerWins] forKey: matchWonKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLosses] forKey: matchLossKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchWinStreak] forKey: matchWinStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.totalMatches] forKey: matchGamesPlayedKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchLossStreak] forKey: matchLossStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.totalRaceTime] forKey: matchTotalRaceTimeKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerDisconnects] forKey: matchCareerDisconnectsKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerDraws] forKey: matchDrawsKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchDrawStreak] forKey: matchDrawStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestWinStreak] forKey: matchCareerLongestWinStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestLossStreak] forKey: matchCareerLongestLossStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestDrawStreak] forKey: matchCareerLongestDrawStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.totalCompleteMatches] forKey: matchTotalCompleteMatchesKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: gPlayerData.disconnectRate] forKey: matchDisconnectRateKey]; + [gPlayerData.playerStatsData setObject: [NSString stringWithCString: gPlayerData.lastGameTime encoding: NSASCIIStringEncoding] forKey: lastTimePlayedKey]; + + } + + [gPlayerData.playerStatsData writeToFile: [gPlayerData.playerDataPath stringByAppendingPathComponent: playerStatsDataFile] atomically: YES]; +} + + +- (void)returnToMenu +{ + MenuController* controller = (MenuController*)[self findExistingControllerOfType: [MenuController class]]; + assert(controller != nil); + [navController popToViewController: controller animated: YES]; +} + + +- (void)returnToMatchMaking +{ + MatchmakingController* controller = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + assert(controller != nil); + [navController popToViewController: controller animated: YES]; +} + + + + - (void)showAboutInfo: (id) sender + { + FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:@"FlipsideView" bundle:nil]; + controller.delegate = sender; + + controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal; + [sender presentModalViewController:controller animated:YES]; + + [controller release]; + } + + - (void)flipsideViewControllerDidFinish:(id) sender + { + [sender dismissModalViewControllerAnimated:YES]; + } + + +- (void)showLeaderboards +{ + [self deleteLeaderboards]; // delete leaderboards unconditionally, as data may have changed since last time + [self pushNewControllerOfType: [LeaderboardsController class] nib: @"leaderboards"]; +} + + + +- (void)downloadTop5Leaders +{ + static SAKESearchForRecordsInput input; + static SAKERequest request; + static char *fieldNames[] = { + (char *) "ownerid" , + (char *) "row", + ATLAS_GET_STAT_NAME( NICK ), + ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), + ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), + ATLAS_GET_STAT_NAME( CAREER_WINS ), + ATLAS_GET_STAT_NAME( CAREER_LOSSES ), + ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), + ATLAS_GET_STAT_NAME( LATITUDE ), + ATLAS_GET_STAT_NAME( LONGITUDE) + }; + SAKEStartRequestResult startRequestResult; + + // empty leader board + [self deleteLeaderboards]; + gLeaderboardType = LEADERBOARD_BY_TIME; + + // get Top 5 leaders + input.mTableId = (char *) PLAYER_STATS_TABLE; + input.mFieldNames = fieldNames; + input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); + input.mOffset = 0; + input.mMaxRecords = 5; + input.mCacheFlag = gsi_true; // caches the TOP 5 query + input.mFilter = (char *) "BEST_RACE_TIME > 0 and BEST_RACE_TIME != 99999"; + input.mSort = (char *) "BEST_RACE_TIME asc"; + + request = sakeSearchForRecords(sake, &input, SearchForTop5TimeRecordsCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(sake); + NSLog(@"Failed to start request: %d\n", startRequestResult); + } + + currentAlert = ALERT_WAITING_FOR_LEADERS; +} + +- (void)getMyLeaderPosition +{ + static char aFilter[128]; + static SAKEGetRecordCountInput input; + static SAKERequest request; + + input.mTableId = (char *) PLAYER_STATS_TABLE; + input.mCacheFlag = gsi_true; // caches the TOP 10 query + sprintf( aFilter, "BEST_RACE_TIME < %d", gPlayerData.bestRaceTime ); + input.mFilter = (char *) aFilter; + + request = sakeGetRecordCount(sake, &input, GetMyLeaderPositionCallback, self); + if(!request) + { + gPlayerData.rank = -1; + + SAKEStartRequestResult startRequestResult; + startRequestResult = sakeGetStartRequestResult(sake); + NSLog( @"Failed to start request: %d\n", startRequestResult ); + } +} + + +- (void)downloadLeaders +{ + static SAKESearchForRecordsInput input; + static SAKERequest request; + static char *fieldNames[] = { + (char *) "ownerid" , + (char *) "row", + ATLAS_GET_STAT_NAME( NICK ), + ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), + ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), + ATLAS_GET_STAT_NAME( CAREER_WINS ), + ATLAS_GET_STAT_NAME( CAREER_LOSSES ), + ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), + ATLAS_GET_STAT_NAME( LATITUDE ), + ATLAS_GET_STAT_NAME( LONGITUDE) + }; + SAKEStartRequestResult startRequestResult; + input.mTableId = (char *) PLAYER_STATS_TABLE; + input.mFieldNames = fieldNames; + input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); + input.mOffset = 0; + input.mMaxRecords = MAX_LEADERS; + input.mCacheFlag = gsi_true; // caches the TOP 10 query + + switch (gLeaderboardType) { + case LEADERBOARD_BY_TIME: + input.mFilter = (char *) "BEST_RACE_TIME > 0 and BEST_RACE_TIME != 99999"; + input.mSort = (char *) "BEST_RACE_TIME asc"; + + request = sakeSearchForRecords(sake, &input, SearchForLeadersTimeRecordsCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(sake); + NSLog(@"Failed to start request: %d\n", startRequestResult); + } + break; + } + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Waiting for leaderboards..." delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_WAITING_FOR_LEADERS; +} + +- (BOOL)setTimeRecords: (SAKE)sake request: (SAKERequest)request result: (SAKERequestResult)result inputData: (void*)inputData outputData: (void*)outputData +{ + float lat,lng; + + SAKESearchForRecordsOutput * records = (SAKESearchForRecordsOutput*)outputData; + + if (result == SAKERequestResult_SUCCESS) + { + if (gLeaderboardData != nil) + { + int lbtype = gLeaderboardType; + [self deleteLeaderboards]; + gLeaderboardType = lbtype; + } + + if (records->mNumRecords > 0) + { + gLeaderboardData = [[NSMutableArray arrayWithCapacity: records->mNumRecords] retain]; + for (int i=0; imNumRecords; i++) + { + LeaderboardObject * pData = [[LeaderboardObject alloc] init]; + + pData.pictureFileId = 0; // init picture file id + + SAKEField * field = sakeGetFieldByName( "ownerid", records->mRecords[i], 10 ); + if (field != NULL) + pData.profileId = field->mValue.mInt; + else + pData.profileId = 0; + + field = sakeGetFieldByName( "row", records->mRecords[i], 10); + if (field != NULL) + pData.rank = field->mValue.mInt; + else + pData.rank = -1; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( NICK ), records->mRecords[i], 10 ); + if (field != NULL) + { + pData.name = [NSString stringWithFormat: @"%s", (char*)( field->mValue.mAsciiString ) ]; + } + else + pData.name = nil; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), records->mRecords[i], 10 ); + if (field != NULL) + pData.bestRaceTime = (gsi_u32)( field->mValue.mInt ); + else + pData.bestRaceTime = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), records->mRecords[i], 10 ); + if (field != NULL) + pData.totalMatches = (gsi_u32)( field->mValue.mInt ); + else + pData.totalMatches = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_WINS ), records->mRecords[i], 10 ); + if (field != NULL) + pData.careerWins = (gsi_u32)( field->mValue.mInt ); + else + pData.careerWins = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_LOSSES ), records->mRecords[i], 10 ); + if (field != NULL) + pData.careerLosses = (gsi_u32)( field->mValue.mInt ); + else + pData.careerLosses = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), records->mRecords[i], 10 ); + if (field != NULL) + pData.currentWinStreak = (gsi_u32)( field->mValue.mInt ); + else + pData.currentWinStreak = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LATITUDE ), records->mRecords[i], 10 ); + if (field != NULL) + lat = field->mValue.mFloat; + else + lat = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LONGITUDE ), records->mRecords[i], 10 ); + if (field != NULL) + lng = field->mValue.mFloat; + else + lng = 0; + + pData.playerLocation = [[CLLocation alloc] initWithLatitude: lat longitude: lng]; + [gLeaderboardData insertObject: pData atIndex: i]; + NSLog(@"leader[%d] = %d\n",i, pData.profileId); + } + } + + return YES; + } + else + { + // error + return NO; + } +} + +- (void)searchForTop5TimeRecordsCallback: (SAKE)sake request: (SAKERequest)request result: (SAKERequestResult)result inputData: (void*)inputData outputData: (void*)outputData +{ + float lat,lng; + + SAKESearchForRecordsOutput * records = (SAKESearchForRecordsOutput*)outputData; + + if (result == SAKERequestResult_SUCCESS) { + if (gLeaderboardData != nil) + { + int lbtype = gLeaderboardType; + [self deleteLeaderboards]; + gLeaderboardType = lbtype; + } + + if (records->mNumRecords <= 0) + { + MenuController* menuController = (MenuController*)[self findExistingControllerOfType: [MenuController class]]; + [ menuController noTop5Leaders]; + } + else + { + gLeaderboardData = [[NSMutableArray arrayWithCapacity: records->mNumRecords] retain]; + for (int i=0; imNumRecords; i++) + { + LeaderboardObject * pData = [[LeaderboardObject alloc] init]; + + SAKEField * field = sakeGetFieldByName( "ownerid", records->mRecords[i], 10 ); + if (field != NULL) + pData.profileId = field->mValue.mInt; + else + pData.profileId = 0; + + field = sakeGetFieldByName( "row", records->mRecords[i], 10); + if (field != NULL) + pData.rank = field->mValue.mInt; + else + pData.rank = -1; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( NICK ), records->mRecords[i], 10 ); + if (field != NULL) + { + pData.name = [NSString stringWithFormat: @"%s", (char*)( field->mValue.mAsciiString ) ]; + } + else + pData.name = nil; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), records->mRecords[i], 10 ); + if (field != NULL) + pData.bestRaceTime = (gsi_u32)( field->mValue.mInt ); + else + pData.bestRaceTime = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), records->mRecords[i], 10 ); + if (field != NULL) + pData.totalMatches = (gsi_u32)( field->mValue.mInt ); + else + pData.totalMatches = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_WINS ), records->mRecords[i], 10 ); + if (field != NULL) + pData.careerWins = (gsi_u32)( field->mValue.mInt ); + else + pData.careerWins = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_LOSSES ), records->mRecords[i], 10 ); + if (field != NULL) + pData.careerLosses = (gsi_u32)( field->mValue.mInt ); + else + pData.careerLosses = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), records->mRecords[i], 10 ); + if (field != NULL) + pData.currentWinStreak = (gsi_u32)( field->mValue.mInt ); + else + pData.currentWinStreak = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LATITUDE ), records->mRecords[i], 10 ); + if (field != NULL) + lat = field->mValue.mFloat; + else + lat = 0; + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LONGITUDE ), records->mRecords[i], 10 ); + if (field != NULL) + lng = field->mValue.mFloat; + else + lng = 0; + + pData.playerLocation = [[CLLocation alloc] initWithLatitude: lat longitude: lng]; + [gLeaderboardData insertObject: pData atIndex: i]; + } + } + } + else + { + // error + } + // get data + //[self setTimeRecords: sake request: request result: result inputData: inputData outputData: outputData ]; +} + + +- (void) getMyLeaderPositionCallback: (SAKE) aSake request: (SAKERequest)request result: (SAKERequestResult)result inputData: (void*)inputData outputData: (void*)outputData +{ + SAKEGetRecordCountOutput * records = (SAKEGetRecordCountOutput*)outputData; + + // need to pass in pointer to rank variable (int *)rankVar + if( result != SAKERequestResult_SUCCESS ) + gPlayerData.rank = 0; + else + gPlayerData.rank = records->mCount + 1; // 1 + number of records before my best time +} + +- (void)searchForLeadersTimeRecordsCallback: (SAKE) aSake request: (SAKERequest)request result: (SAKERequestResult)result inputData: (void*)inputData outputData: (void*)outputData +{ + // Got data - hide popup + if (alertView != nil) + { + //[alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView dismissWithClickedButtonIndex: alertView.firstOtherButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + } + currentAlert = ALERT_NONE; + + if( [self setTimeRecords: aSake request: request result: result inputData: inputData outputData: outputData ] ) + { + // display leaderboard page + [self pushNewControllerOfType: [LeadersController class] nib: @"leadersbytime"]; + } + else + { + // error + } +} + + +- (void)showTop5LeadersByTime +{ + [self downloadTop5Leaders]; +} + +- (void)showLeadersByTime +{ + [self deleteLeaderboards]; + gLeaderboardType = LEADERBOARD_BY_TIME; + [self downloadLeaders]; +} + + +- (void)showLeadersByBuddies +{ + GPBuddyStatus status; + int numBuddies; + + [self deleteLeaderboards]; + gLeaderboardType = LEADERBOARD_BY_BUDDIES; + + GPResult res; + LoginController* loginController = (LoginController*)[self findExistingControllerOfType: [LoginController class]]; + + res = gpGetNumBuddies( [loginController getGPConnectionPtr], &numBuddies ); + if ( (res == GP_NO_ERROR) && (numBuddies == 0)) { + // just go to other pager where a no-buddies message will pop up + [self pushNewControllerOfType: [LeadersController class] nib: @"leadersbytime"]; + + } else { + if (res == GP_NO_ERROR) { + // create an array of buddies...since we only want the 1st MAX_BUDDIES, use a static array + static GPProfile buddyProfileIds[MAX_BUDDIES]; + for (int i=0; (i < MAX_BUDDIES) && (i < numBuddies); i++) + { + if (GP_NO_ERROR == gpGetBuddyStatus( [loginController getGPConnectionPtr], i, &status )) + { + buddyProfileIds[i] = status.profile; + } + else + break; + } + + static SAKESearchForRecordsInput input; + SAKERequest request; + static char *fieldNames[] = { + (char *) "ownerid" , + (char *) "row" , + ATLAS_GET_STAT_NAME( NICK ) , + ATLAS_GET_STAT_NAME( BEST_RACE_TIME ) , + ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ) , + ATLAS_GET_STAT_NAME( CAREER_WINS ) , + ATLAS_GET_STAT_NAME( CAREER_LOSSES ) , + ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ) , + ATLAS_GET_STAT_NAME( LATITUDE ) , + ATLAS_GET_STAT_NAME( LONGITUDE) + }; + + SAKEStartRequestResult startRequestResult; + + input.mTableId = (char *) PLAYER_STATS_TABLE; + input.mFieldNames = fieldNames; + input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); + input.mFilter = (char *) "BEST_RACE_TIME > 0 and BEST_RACE_TIME != 99999"; + input.mSort = (char *) "BEST_RACE_TIME asc"; + //input.mSort = (char *) "recordid asc"; + input.mOffset = 0; + input.mMaxRecords = MAX_BUDDIES; + input.mOwnerIds = buddyProfileIds; + input.mNumOwnerIds = numBuddies; + //input.mCacheFlag = gsi_true; + + request = sakeSearchForRecords(sake, &input, SearchForLeadersTimeRecordsCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(sake); + NSLog(@"Failed to start buddylist request: %d\n", startRequestResult); + } + } + } +} + +- (void)showLeadersByLocation +{ + [self deleteLeaderboards]; + gLeaderboardType = LEADERBOARD_BY_LOCATION; + LeadersByLocationController * newview = (LeadersByLocationController *)[self pushNewControllerOfType: [LeadersByLocationController class] nib: @"leadersbylocation"]; + [newview searchLeaderboardsByLocation]; +} + +// show the profile of a user in the leaderboard array, indexed by index +- (void)showUser: (int) index +{ + gLeaderboardProfileIndex = index; + [self pushNewControllerOfType: [UserStatsController class] nib: @"UserStats"]; +} + +- (UIViewController*)findExistingControllerOfType: (Class)type +{ + NSArray* viewControllers = navController.viewControllers; + NSUInteger index = [viewControllers count]; + + while (index > 0) + { + UIViewController* viewController = [viewControllers objectAtIndex: --index]; + + if ([viewController isKindOfClass: type]) + return viewController; + } + + return nil; +} + +- (void)deleteLeaderboards +{ + if (gLeaderboardData != nil) { + for (int i=[gLeaderboardData count]-1; i>=0; i--) + { + LeaderboardObject * pData = (LeaderboardObject*)[gLeaderboardData objectAtIndex: i]; + if (pData.playerLocation != nil) + { + pData.playerLocation = nil; // this is a property, auto-releases + } + if (pData.name != nil) + { + pData.name = nil; // this is a property, auto-releases + } + if (pData.thumbNail != nil) + { + pData.thumbNail = nil; // this is a property, auto-releases + } + [gLeaderboardData removeObjectAtIndex: i]; + } + + [gLeaderboardData release]; + gLeaderboardData = nil; + } + gLeaderboardType = LEADERBOARD_INVALID; +} + +- (void)deleteBlockedImagesData +{ + if( gPlayerData.blockedImagesData != nil ) + { + for( int i = 0; i < (int)[gPlayerData.blockedImagesData count]; i++ ) + { + [gPlayerData.blockedImagesData removeObjectAtIndex: i]; + } + + [gPlayerData.blockedImagesData release]; + gPlayerData.blockedImagesData = nil; + + MessageBox(@"Cleared all blocked images."); + } +} + + +// Remove leaders that have a default time of 99.999 +- (void)removeFalseLeaders +{ + if (gLeaderboardData != nil) + { + for (int i=[gLeaderboardData count]-1; i>=0; i--) + { + LeaderboardObject * pData = (LeaderboardObject*)[gLeaderboardData objectAtIndex: i]; + + // Remove entry if player has default best time + if (pData.bestRaceTime == INITIAL_BEST_TIME ) + { + if (pData.playerLocation != nil) + pData.playerLocation = nil; // this is a property, auto-releases + + if (pData.name != nil) + pData.name = nil; // this is a property, auto-releases + + if (pData.thumbNail != nil) + pData.thumbNail = nil; // this is a property, auto-releases + + [gLeaderboardData removeObjectAtIndex: i]; + } + + if( [gLeaderboardData count] == 0 ) + { + [gLeaderboardData release]; + gLeaderboardData = nil; + gLeaderboardType = LEADERBOARD_INVALID; + } + } + } +} + +- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName +{ + return [self pushNewControllerOfType: type nib: nibName animated: YES]; +} + +- (bool)imageIsBlocked: (int) pictureId +{ + int numBlockedImages = [gPlayerData.blockedImagesData count]; + if( pictureId == 0 || numBlockedImages == 0 ) + return false; + + NSNumber* fileIdNum = [[NSNumber alloc] initWithUnsignedInt: pictureId ]; + + for (int i = 0; i < numBlockedImages; i++ ) + { + if( [[gPlayerData.blockedImagesData objectAtIndex: i ] isEqualToNumber:fileIdNum ] ) + return true; + } + + return false; +} + + +@end + + +@implementation AppDelegate(Private) + +// Called when an alertView button is tapped. +/*- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + // 0 index is "Cancel" button + if(alertView.cancelButtonIndex >= 0 && buttonIndex == alertView.cancelButtonIndex) + { + [self stopHostingCountdownTimer]; + } +} +*/ + +// Called by system mainloop when animation of alert view is finished +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ + switch (currentAlert) + { + case ALERT_LOADINGPLAYERDATA: + if( buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + [loadRemoteDataTimer invalidate]; + [self logout]; + } + break; + + case ALERT_LISTENING: + NSLog(@"ALERT_LISTENING"); + if( buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + NSLog(@"ALERT_LISTENING: qr2 shutdown"); + qr2_shutdown(NULL); // I would like this to de-list the host from the master server, but it doesn't seem to do that. + + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"ALERT_LISTENING: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + + MatchmakingController* matchmakingcontroller = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + [matchmakingcontroller hostingCanceled]; + } + break; + + case ALERT_NATNEG: + NSLog(@"ALERT_NATNEG"); + if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + NNCancel(natnegCookie); + } + break; + + case ALERT_CONNECTING: + NSLog(@"ALERT_CONNECTING"); + if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + + if (gt2Socket != NULL) + { + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"ALERT_CONNECTING: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + } + } + break; + + case ALERT_REPLAY: + NSLog(@"ALERT_REPLAY"); + if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_CANCELED_TYPE, MSG_REPLAY_CANCELED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_CANCELED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + } + break; + + case ALERT_REPLAY_CONFIRM: + NSLog(@"ALERT_REPLAY_CONFIRM"); + if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_CANCELED_TYPE, MSG_REPLAY_CANCELED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_CANCELED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + } + else if (buttonExists(alertView.firstOtherButtonIndex) >= 0 && buttonIndex == alertView.firstOtherButtonIndex) + { + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_TYPE, MSG_REPLAY, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + [self setupMatch: hosting]; + [navController popToViewController: gameController animated: YES]; + } + break; + + case ALERT_WAITING_FOR_LEADERS: + NSLog(@"ALERT_WAITING_FOR_LEADERS"); + /*if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + // what can we do to cancel a sake search? + // nothing for now. + }*/ + break; + + + case ALERT_NO_NETWORK_CONNECTION: + NSLog(@"ALERT_NO_NETWORK_CONNECTION"); + if (buttonExists(alertView.cancelButtonIndex) && buttonIndex == alertView.cancelButtonIndex) + { + NSLog(@"No network connection"); + //exit(0); + [self logout]; + } + break; + + default: + if( currentAlert == ALERT_NONE ) + NSLog(@"ALERT_NONE"); + else + NSLog(@"ERROR: UNKNOWN ALERT"); + break; + } + + currentAlert = ALERT_NONE; +} + +//- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName +//{ +// return [self pushNewControllerOfType: type nib: nibName animated: YES]; +//} + +- (UIViewController*)pushNewControllerOfType: (Class)type nib: (NSString*)nibName animated: (BOOL)animated +{ + UIViewController* viewController = [type alloc]; + [viewController initWithNibName: nibName bundle: nil]; + [navController pushViewController: viewController animated: animated]; + [viewController release]; + return viewController; +} + + +- (void)setupMatch: (bool)hostMatch +{ + start = 0; + countdown = 0; + localTime = 0; + remoteTime = 0; + racing = false; + reportSent = gsi_false; + hosting = hostMatch; +} + + +- (bool)setupHosting +{ + [self setupMatch: true]; + + + if( gt2Socket == NULL ) + { + NSLog(@"setupHosting: create gt2 socket"); + GT2Result gt2Result = gt2CreateSocket(>2Socket, HOST_PORT_STRING, 0, 0, SocketErrorCallback); + NSLog(@"gt2socket = %d", gt2Socket ); + if (gt2Result != GT2Success) + { + NSLog(@"gt2socket creation failed"); + hosting = false; + return false; + } + } + gt2SetSocketData(gt2Socket, self); + gt2Listen(gt2Socket, ConnectAttemptCallback); + gt2SetUnrecognizedMessageCallback(gt2Socket, UnrecognizedMessageCallback); + + + state = HOST_LISTENING; + NSLog(@"setupHosting state=HOST_LISTENING"); + + //qr2_error_t qr2Result = qr2_init( NULL, NULL, HOST_PORT /*2*/, SCRACE_GAMENAME, SCRACE_SECRETKEY, TRUE, TRUE, + // ServerKeyCallback, PlayerKeyCallback, TeamKeyCallback, KeyListCallback, CountCallback, AddErrorCallback, self); + + qr2_error_t qr2Result = qr2_init_socketA(NULL, gt2GetSocketSOCKET(gt2Socket), HOST_PORT, SCRACE_GAMENAME, SCRACE_SECRETKEY, TRUE, TRUE, ServerKeyCallback, PlayerKeyCallback, TeamKeyCallback, KeyListCallback, CountCallback, AddErrorCallback, self); + + if (qr2Result != e_qrnoerror) + { + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + if( gt2Socket ) + { + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"setupHosting: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + } + + return false; + } + + qr2_register_natneg_callback(NULL, NatNegCallback); + + alertView = [[UIAlertView alloc] initWithTitle: @"Waiting for player" message: @"30 seconds" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_LISTENING; + + NSLog(@"setupHosting starts hostingCountdownTimer"); + [self startHostingCountdownTimer]; + + return true; +} + +- (bool)setupJoining: (unsigned int)ip port: (unsigned short)port useNatNeg: (bool)useNatNeg; +{ + [self setupMatch: false]; + + // create socket if new match + if( gt2Socket == NULL ) + { + NSLog(@"setupJoining: create gt2 socket"); + GT2Result result = gt2CreateSocket(>2Socket, CLIENT_PORT_STRING, 0, 0, SocketErrorCallback); + if (result != GT2Success) + return false; + } + + gt2SetSocketData(gt2Socket, self); + + // Do NAT negotiation if needed + if (useNatNeg) + { + const char* ip_addr = inet_ntoa((in_addr){ ip }); + + while (natnegCookie == 0) + { + natnegCookie = rand(); + } + + SBError sbret = ServerBrowserSendNatNegotiateCookieToServerA(serverBrowser, ip_addr, port, natnegCookie); + + if (sbret != sbe_noerror) + { + // What if somebody else is already doing a transaction with this cookie value? Astronomically improbable, but does the server return an error value? + NSLog(@"NN transaction cancelled due to cookie in use."); + return false; + } + + NegotiateError error = NNStartNatDetection(NNDetectionCallback); + + + gt2SetUnrecognizedMessageCallback(gt2Socket, UnrecognizedMessageCallback); + NegotiateError nnret = NNBeginNegotiationWithSocket(gt2GetSocketSOCKET(gt2Socket), natnegCookie, NN_CLIENT, NNProgressCallback, NNCompletedCallback, self); + + if (nnret != ne_noerror) + { + // ne_noerror, ne_allocerror, ne_socketerror, ne_dnserror + /*if( nnret == ne_socketerror ) + NSLog(@"NN socket error."); + else if( nnret == ne_dnserror ) + NSLog(@"NN DNS error."); + else + NSLog(@"NN allocation error."); + */ + return false; + } + + alertView = [[UIAlertView alloc] initWithTitle: @"Connection" message: @"Negotiating..." delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_NATNEG; + } + + else // if not using NatNeg + { + NSString* remoteHost = [NSString stringWithFormat: @"%s:%d", inet_ntoa((in_addr){ ip }), port]; + [self startMultiPlayerGame: [remoteHost cStringUsingEncoding: NSASCIIStringEncoding]]; + } + + return true; +} + +- (void)onLoadRemoteDataTimer: (NSTimer*)timer +{ + gsCoreThink(0); +} + +- (void)onThinkTimer: (NSTimer*)timer +{ + static bool thinking; + + if (!thinking) + { + thinking = true; + + // Think so SDKs can process + ///////////////////////////////////////////// + if (gt2Socket != NULL) + gt2Think(gt2Socket); + + ghttpThink(); + scThink(interface); + + if (hosting) + qr2_think(NULL); + + if (serverBrowser != NULL) + ServerBrowserThink(serverBrowser); + + NNThink(); + + switch (state) + { + // ********************************************************** // + // ************************* HOST LOGIC ********************* // + // ********************************************************** // + + case HOST_LISTENING: + // Wait for client to join + ///////////////////////////////////////////// + break; + + case HOST_EXCHANGE_CERT: + { + char buffer[520]; + char cert[512]; + int rcode; + gsi_u32 certLen; + + // Store cert in a binary buffer for easy exchange + ///////////////////////////////////////////// + wsLoginCertWriteBinary(&gPlayerData.certificate, cert, sizeof(cert), &certLen); + + // Exchange certificates with the other player to validate (step 1) + ///////////////////////////////////////////// + rcode = gtEncode(MSG_TO_CLIENT_CERT_TYPE, MSG_TO_CLIENT_CERT, buffer, sizeof(buffer), gPlayerData.profileId, cert, certLen); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_CLIENT_CERT_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + // Wait for a reply + ///////////////////////////////////////////// + state = HOST_WAITING; + NSLog(@"onThinkTimer state=HOST_WAITING from HOST_EXCHANGE_CERT"); + break; + } + + case HOST_VERIFY_CERT: + // Validate authentication certificates (step 2) + ///////////////////////////////////////////// + remoteCertificate.mIsValid = wsLoginCertIsValid(&remoteCertificate); + if (gsi_is_false(remoteCertificate.mIsValid)) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + MessageBox(@"Remote player has an invalid certificate, cancelling game."); + } + else + { + char buffer[32]; + int rcode = gtEncode(MSG_TO_CLIENT_CERTVERIFIED_TYPE, MSG_TO_CLIENT_CERTVERIFIED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_CLIENT_CERTVERIFIED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + // Wait for a reply + ///////////////////////////////////////////// + state = HOST_WAITING; + NSLog(@"onThinkTimer state=HOST_WAITING from HOST_VERIFY_CERT"); + } + break; + + case HOST_EXCHANGE_KEYS: + { + char buffer[512]; + int rcode; + SCPeerKeyExchangeMsg exchangeMsg; + + // P2P encryption exchange keys (step 3) + ///////////////////////////////////////////// + + // Each player should create a key for receiving data from the remote player + // For extra security, we use a different encryption key for each channel + ///////////////////////////////////////////// + scPeerCipherInit(&gPlayerData.certificate, &gPlayerData.peerRecvCipher); + + // Create a key exchange message for transmitting the key to the other player + // using the remote player's certificate to encrypt the cipher + ///////////////////////////////////////////// + scPeerCipherCreateKeyExchangeMsg(&remoteCertificate, &gPlayerData.peerRecvCipher, exchangeMsg); + + // Now send the key to the other player + ///////////////////////////////////////////// + rcode = gtEncode(MSG_TO_CLIENT_KEYS_TYPE, MSG_TO_CLIENT_KEYS, buffer, sizeof(buffer), exchangeMsg, GS_CRYPT_RSA_BYTE_SIZE); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_CLIENT_KEYS_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + // Wait for a reply + ///////////////////////////////////////////// + state = HOST_WAITING; + NSLog(@"onThinkTimer state=HOST_WAITING from HOST_EXCHANGE_KEYS"); + break; + } + + case HOST_CONNECTED: + if (alertView != nil) + { + //[alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView dismissWithClickedButtonIndex: alertView.firstOtherButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + } + + state = RACING; + NSLog(@"onThinkTimer state=RACING on HOST_CONNECTED"); + + gameController = (GameController*)[self pushNewControllerOfType: [GameController class] nib: @"twoplayergame"]; + [gameController setRemoteNick: [NSString stringWithCString: remoteCertificate.mUniqueNick encoding: NSASCIIStringEncoding]]; + gOpponentData.profileId = remoteCertificate.mProfileId; + localTime = 0; + remoteTime = 0; + break; + + case HOST_SEND_SESSID: + if(sessionCreated) + { + int rcode; + char buffer[256]; + char sessionCrypt[SC_SESSION_GUID_SIZE]; + char connCrypt[SC_CONNECTION_GUID_SIZE]; + + // Encrypt the connID/session ID to send using P2P encryption + ///////////////////////////////////////////////////////// + memcpy(sessionCrypt, gPlayerData.sessionId, SC_SESSION_GUID_SIZE); + scPeerCipherEncryptBufferIV(&gPlayerData.peerSendCipher, 1, (gsi_u8*)sessionCrypt, SC_SESSION_GUID_SIZE); + + memcpy(connCrypt, gPlayerData.connectionId, SC_CONNECTION_GUID_SIZE); + scPeerCipherEncryptBufferIV(&gPlayerData.peerSendCipher, 2, (gsi_u8*)connCrypt, SC_CONNECTION_GUID_SIZE); + + OutputDebugString([NSString stringWithFormat: @"[HOST_SEND_SESSID] sessionCrypt: %.40s\n", sessionCrypt]); + OutputDebugString([NSString stringWithFormat: @"[HOST_SEND_SESSID] connCrypt: %.40s\n", connCrypt]); + + + // Now the host sends the session ID & his conn ID to the client + ///////////////////////////////////////////// + rcode = gtEncode(MSG_SESSION_ID_TYPE, MSG_SESSION_ID, buffer, sizeof(buffer), + sessionCrypt, SC_SESSION_GUID_SIZE, connCrypt, SC_CONNECTION_GUID_SIZE); + assert(rcode != -1); + NSLog(@"Protocol send MSG_SESSION_ID_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + + // Once session is created, set the session ID and report intention + ///////////////////////////////////////////// + scSetSessionId(interface, gPlayerData.sessionId); + scSetReportIntention(interface, gPlayerData.connectionId, SCRACE_AUTHORITATIVE, &gPlayerData.certificate, &gPlayerData.privateData, + SetReportIntentionCallback, SCRACE_TIMEOUT_MS, self); + + sessionCreated = gsi_false; + + // Go back to racing state + ///////////////////////////////////////////// + state = RACING; + NSLog(@"onThinkTimer state=RACING on HOST_SEND_SESSID"); + } + break; + + case HOST_ERROR: + if (alertView != nil) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + } + + MessageBox(@"Error setting up hosting"); + state = JOIN_CHOOSESERVER; + NSLog(@"onThinkTimer state=JOIN_CHOOSESERVER on HOST_ERROR"); + break; + + + // ********************************************************** // + // **************** JOIN (CLIENT) LOGIC ********************* // + // ********************************************************** // + + case JOIN_EXCHANGE_CERT: + // Wait for host to send cert first + ///////////////////////////////////////////// + break; + + case JOIN_VERIFY_CERT: + // Validate authentication certificates (step 2) + ///////////////////////////////////////////// + remoteCertificate.mIsValid = wsLoginCertIsValid(&remoteCertificate); + if (gsi_is_false(remoteCertificate.mIsValid)) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + MessageBox(@"Remote player has an invalid certificate, cancelling game."); + [self logout]; + } + else + { + state = JOIN_EXCHANGE_KEYS; + NSLog(@"onThinkTimer state=JOIN_EXCHANGE_KEYS on JOIN_VERIFY_CERT"); + } + break; + + case JOIN_EXCHANGE_KEYS: + // Wait for host to send keys first + ///////////////////////////////////////////// + break; + + case JOIN_CONNECTED: + if (alertView != nil) + { + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + //[alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + } + state = RACING; + NSLog(@"onThinkTimer state=RACING on JOIN_CONNECTED"); + + gameController = (GameController*)[self pushNewControllerOfType: [GameController class] nib: @"twoplayergame"]; + [gameController setRemoteNick: [NSString stringWithCString: remoteCertificate.mUniqueNick encoding: NSASCIIStringEncoding]]; + localTime = 0; + remoteTime = 0; + break; + + case JOIN_SEND_CONNID: + // Once connection ID has been set, relay it to the host + ///////////////////////////////////////////// + if (connIdSet) + { + int rcode; + char buffer[128]; + char connCrypt[SC_CONNECTION_GUID_SIZE]; + + // Encrypt the connection ID to send using P2P encryption + ///////////////////////////////////////////////////////// + memcpy(connCrypt, gPlayerData.connectionId, SC_CONNECTION_GUID_SIZE); + scPeerCipherEncryptBufferIV(&gPlayerData.peerSendCipher, 1, (gsi_u8*)connCrypt, SC_CONNECTION_GUID_SIZE); + + OutputDebugString([NSString stringWithFormat: @"[JOIN_SEND_CONNID] connCrypt: %.40s\n", connCrypt]); + + + // Client needs to send the host his/her connection ID + ///////////////////////////////////////////// + rcode = gtEncode(MSG_CONNECTION_ID_TYPE, MSG_CONNECTION_ID, buffer, sizeof(buffer), connCrypt, SC_CONNECTION_GUID_SIZE); + assert(rcode != -1); + NSLog(@"Protocol send MSG_CONNECTION_ID_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + connIdSet = gsi_false; + + // set profile id of opponent + gOpponentData.profileId = remoteCertificate.mProfileId; + + // Go back to the racing state + ///////////////////////////////////////////// + state = RACING; + NSLog(@"onThinkTimer state=RACING on JOIN_SEND_CONNID"); + } + break; + + case JOIN_ERROR: + if (alertView != nil) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + } + + MessageBox(@"Error joining a game"); + state = JOIN_CHOOSESERVER; + NSLog(@"onThinkTimer state=JOIN_CHOOSESERVER on JOIN_ERROR"); + break; + } + + thinking = false; + } + + // Are we racing? + ///////////////// + if(racing) + { + // Did we finish? + ///////////////// + if(localTime) + { + // Let our opponent know we are done. + //////////////////////////////////////// + char buffer[64]; + int rcode; + int steps = gameController.numSteps; + if ( playerCount > 1 && steps != gameController.prevNumSteps ) + { + gameController.prevNumSteps = steps; + rcode = gtEncode(MSG_PROGRESS_TYPE, MSG_PROGRESS, buffer, sizeof(buffer), steps); + assert(rcode != -1); + NSLog(@"Protocol send MSG_PROGRESS_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2False); + } + + // Did we both finish? + ////////////////////// + if(remoteTime) + { + // Done racing. + /////////////// + racing = false; + state = FINISHED_RACE; + NSLog(@"onThinkTimer state=FINISHED_RACE"); + + [gameController doneRacing]; + + // Show the times. + ////////////////// + NSString* message; + if(localTime < remoteTime) + { + message = [NSString stringWithFormat: @"You won!\n%0.2fs to %0.2fs", localTime * absolute_to_seconds, remoteTime * absolute_to_seconds]; + win = gsi_true; + } + else if(remoteTime < localTime) + { + message = [NSString stringWithFormat: @"You lost!\n%0.2fs to %0.2fs", localTime * absolute_to_seconds, remoteTime * absolute_to_seconds]; + } + else + { + message = [NSString stringWithFormat: @"You tied!\n%0.2fs", localTime * absolute_to_seconds]; + tie = gsi_true; + } + + MessageBox(message); + + // Report the stats. + //////////////////// + if (!reportSent) + { + disconnected = gsi_false; + [self reportTwoPlayerStats]; + [self saveTwoPlayerGameStatsToFile]; + } + + GameResultsController* controller = (GameResultsController*)[self pushNewControllerOfType: [GameResultsController class] nib: @"twoplayerresults" animated: YES]; + int localTimeMillis = localTime * absolute_to_millis; + int remoteTimeMillis = remoteTime * absolute_to_millis; + //[controller setGameTime: std::max(localTimeMillis, remoteTimeMillis)]; + + [controller setLocalPlayer: gPlayerData.uniquenick time: localTimeMillis]; + [controller setRemotePlayer: [NSString stringWithCString: remoteCertificate.mUniqueNick encoding: NSASCIIStringEncoding] time: remoteTimeMillis]; + [controller setWinner ]; // call only after setting local & remote + [controller getImagesForResultsPage]; + } + } + else + { + char buffer[64]; + int rcode; + int steps = gameController.numSteps; + + // Let our opponent know how far we are if we tapped. + //////////////////////////////////////// + if ( playerCount > 1 && steps != gameController.prevNumSteps ) + { + gameController.prevNumSteps = steps; + rcode = gtEncode(MSG_PROGRESS_TYPE, MSG_PROGRESS, buffer, sizeof(buffer), steps); + assert(rcode != -1); + NSLog(@"Protocol send MSG_PROGRESS_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2False); + } + } + } +} + +- (void)onCountdownTimer: (NSTimer*)timer +{ + --countdown; + [self countdown]; + + if (countdown == 0) + { + [countdownTimer invalidate]; + countdownTimer = nil; + [self startRace]; + } +} + +// Countdown popup when 2 player game is connecting +- (void)onJoiningCountdownTimer: (NSTimer*)timer +{ + --joiningCountdown; + alertView.message = [NSString stringWithFormat: @"%d seconds", joiningCountdown]; + + if (joiningCountdown == 0) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + + NSLog(@"onJoiningCountdownTimer stop timer"); + + [joiningCountdownTimer invalidate]; + joiningCountdownTimer = nil; + + if( currentAlert == ALERT_REPLAY ) + { + // tell opponent rematch is cancelled due to taking too long to reply + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_CANCELED_TYPE, MSG_REPLAY_CANCELED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_CANCELED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + MessageBox(@"Failed to connect with remote player. Match canceled."); + } + else + { + NSLog( @"onJoiningCountdownTimer timed out with currentAlert=%d", currentAlert ); + MessageBox(@"Error establishing connection.", @"Connection Error"); + } + + // cancel hosting + MatchmakingController* matchmakingController = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + [ matchmakingController hostingCanceled ]; + + [self stopHostingServer]; + hosting = false; + racing = false; + state = JOIN_CHOOSESERVER; + NSLog(@"onJoiningCountdownTimer state=JOIN_CHOOSESERVER"); + + } +} + +- (void)onHostingCountdownTimer: (NSTimer*)timer +{ + --hostingCountdown; + alertView.message = [NSString stringWithFormat: @"%d seconds", hostingCountdown]; + + if (hostingCountdown == 0) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: YES]; + [alertView release]; + alertView = nil; + + [hostingCountdownTimer invalidate]; + hostingCountdownTimer = nil; + + // remove hosting from server + [self stopHostingServer]; + + //NSLog(@"onHostingCountdownTimer: qr2 shutdown"); + //qr2_shutdown(NULL); + + if( currentAlert == ALERT_REPLAY ) + { + // tell opponent rematch is cancelled due to taking too long to reply + char buffer[32]; + int rcode = gtEncode(MSG_REPLAY_CANCELED_TYPE, MSG_REPLAY_CANCELED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_REPLAY_CANCELED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + + MessageBox(@"Player has not responded. Rematch canceled."); + } + + // cancel hosting + MatchmakingController* matchmakingController = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + [ matchmakingController hostingCanceled ]; + + hosting = false; + racing = false; + state = JOIN_CHOOSESERVER; + NSLog(@"onHostingCountdownTimer state=JOIN_CHOOSESERVER"); + + [self returnToMatchMaking]; + } +} + +// Called after each 2 player matches +- (void)saveTwoPlayerGameStatsToFile +{ + // get my race time + int myRaceTimeMillis = (uint32_t)(localTime * absolute_to_millis); + + + if (gPlayerData.playerStatsData == nil) // no local stats file + { + // get here if no server stats also so init first ever stats + + // set win, loss, draw and disconnect + if (!disconnected) + { + gPlayerData.totalCompleteMatches = 1; + + gPlayerData.bestRaceTime = gPlayerData.worstRaceTime = + gPlayerData.totalRaceTime = myRaceTimeMillis; + gPlayerData.averageRaceTime = localTime; + + if( win ) + { + gPlayerData.careerWins = gPlayerData.matchWinStreak = + gPlayerData.careerLongestWinStreak = 1; + } + else if( !tie ) + { + gPlayerData.careerLosses = gPlayerData.matchLossStreak = + gPlayerData.careerLongestLossStreak = 1; + } + else + { + gPlayerData.careerDraws = gPlayerData.matchDrawStreak = + gPlayerData.careerLongestDrawStreak = 1; + } + + gPlayerData.disconnectRate = 0.0; + gPlayerData.careerDisconnects = 0; + } + else + { + gPlayerData.disconnectRate = 1.0; + gPlayerData.careerDisconnects = 1; + } + + gPlayerData.totalMatches = 1; + + // Save first 2 player game stats to new file + gPlayerData.playerStatsData = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + [NSNumber numberWithInt: gPlayerData.bestRaceTime], matchBestTimeKey, + [NSNumber numberWithInt: gPlayerData.worstRaceTime], matchWorstTimeKey, + [NSNumber numberWithInt: (int)(gPlayerData.averageRaceTime * 1000)], matchAverageTimeKey, + + [NSNumber numberWithInt: gPlayerData.careerWins], matchWonKey, + [NSNumber numberWithInt: gPlayerData.careerLosses], matchLossKey, + [NSNumber numberWithInt: gPlayerData.careerDraws], matchDrawsKey, + + [NSNumber numberWithInt: gPlayerData.matchWinStreak], matchWinStreakKey, + [NSNumber numberWithInt: gPlayerData.matchLossStreak], matchLossStreakKey, + [NSNumber numberWithInt: gPlayerData.matchDrawStreak], matchDrawStreakKey, + + [NSNumber numberWithInt: gPlayerData.totalMatches], matchGamesPlayedKey, + [NSNumber numberWithInt: gPlayerData.totalRaceTime], matchTotalRaceTimeKey, + [NSNumber numberWithInt: gPlayerData.careerDisconnects], matchCareerDisconnectsKey, + [NSNumber numberWithInt: gPlayerData.totalCompleteMatches], matchTotalCompleteMatchesKey, + [NSNumber numberWithFloat: gPlayerData.disconnectRate], matchDisconnectRateKey, + + + [NSNumber numberWithInt: gPlayerData.careerLongestWinStreak], matchCareerLongestWinStreakKey, + [NSNumber numberWithInt: gPlayerData.careerLongestLossStreak], matchCareerLongestLossStreakKey, + [NSNumber numberWithInt: gPlayerData.careerLongestDrawStreak], matchCareerLongestDrawStreakKey, + + [NSString stringWithCString: gPlayerData.lastGameTime encoding: NSASCIIStringEncoding], lastTimePlayedKey, + nil]; + } + else if (disconnected) + { + int discon = getLocalIntegerStat(matchCareerDisconnectsKey); + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: discon+1] forKey: matchCareerDisconnectsKey]; + + int totalMatches = getLocalIntegerStat(matchGamesPlayedKey); + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: totalMatches+1] forKey: matchGamesPlayedKey]; + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: 1.0*discon/totalMatches] forKey: matchDisconnectRateKey]; + + } + else + { + int bestRaceTime = getLocalIntegerStat(matchBestTimeKey); + if( myRaceTimeMillis < bestRaceTime ) + { + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: myRaceTimeMillis] forKey: matchBestTimeKey]; + } + + int worstRaceTime = getLocalIntegerStat(matchWorstTimeKey); + if( myRaceTimeMillis > worstRaceTime ) + { + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: myRaceTimeMillis] forKey: matchWorstTimeKey]; + } + + int totalRaceTime = getLocalIntegerStat(matchTotalRaceTimeKey) + myRaceTimeMillis; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: totalRaceTime] forKey: matchTotalRaceTimeKey]; + + int totalMatches = getLocalIntegerStat(matchGamesPlayedKey) + 1; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: totalMatches] forKey: matchGamesPlayedKey]; + + int discon = getLocalIntegerStat(matchCareerDisconnectsKey); + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: 1.0*discon/totalMatches] forKey: matchDisconnectRateKey]; + + int newAvgRaceTime = totalRaceTime / totalMatches; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithFloat: newAvgRaceTime] forKey: matchAverageTimeKey]; + + int totalCompletedMatches = getLocalIntegerStat(matchTotalCompleteMatchesKey) + 1; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: totalCompletedMatches] forKey: matchTotalCompleteMatchesKey]; + + //int wins = getLocalIntegerStat(matchWonKey) ; + //int loss = getLocalIntegerStat(matchLossKey); + //int draws = getLocalIntegerStat(matchDrawsKey); + + if( win ) + { + gPlayerData.careerWins++; + gPlayerData.matchWinStreak++; + gPlayerData.matchLossStreak = gPlayerData.matchDrawStreak = 0; + + if( gPlayerData.matchWinStreak > gPlayerData.careerLongestWinStreak ) + { + gPlayerData.careerLongestWinStreak = gPlayerData.matchWinStreak; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestWinStreak] forKey: matchCareerLongestWinStreakKey]; + } + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerWins] forKey: matchWonKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchWinStreak] forKey: matchWinStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchLossStreak] forKey: matchLossStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchDrawStreak] forKey: matchDrawStreakKey]; + } + else if( !tie ) + { + gPlayerData.careerLosses++; + gPlayerData.matchLossStreak++; + gPlayerData.matchWinStreak = gPlayerData.matchDrawStreak = 0; + + if( gPlayerData.matchLossStreak > gPlayerData.careerLongestLossStreak ) + { + gPlayerData.careerLongestLossStreak = gPlayerData.matchLossStreak; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestLossStreak ] forKey: matchCareerLongestLossStreakKey]; + } + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLosses] forKey: matchLossKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchWinStreak ] forKey: matchWinStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchLossStreak ] forKey: matchLossStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchDrawStreak ] forKey: matchDrawStreakKey]; + } + else + { + gPlayerData.careerDraws++; + gPlayerData.matchDrawStreak++; + gPlayerData.matchWinStreak = gPlayerData.matchLossStreak = 0; + + if( gPlayerData.matchDrawStreak > gPlayerData.careerLongestDrawStreak ) + { + gPlayerData.careerLongestDrawStreak = gPlayerData.matchDrawStreak; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerLongestDrawStreak ] forKey: matchCareerLongestDrawStreakKey]; + } + + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.careerDraws ] forKey: matchDrawsKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchWinStreak ] forKey: matchWinStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchLossStreak ] forKey: matchLossStreakKey]; + [gPlayerData.playerStatsData setObject: [NSNumber numberWithInt: gPlayerData.matchDrawStreak ] forKey: matchDrawStreakKey]; + } + + [gPlayerData.playerStatsData setObject: [NSString stringWithCString: gPlayerData.lastGameTime encoding: NSASCIIStringEncoding] forKey: lastTimePlayedKey]; + } + + [gPlayerData.playerStatsData writeToFile: [gPlayerData.playerDataPath stringByAppendingPathComponent: playerStatsDataFile] atomically: YES]; + + // scReportAddFloatValue(statsReport, KEY_LONGITUDE, gPlayerData.playerLocation.coordinate.longitude); + // scReportAddFloatValue(statsReport, KEY_LATITUDE, gPlayerData.playerLocation.coordinate.latitude); + +} + + +- (void)reportTwoPlayerStats +{ + SCResult aResult = SCResult_NO_ERROR; + SCGameResult myGameResult, opponentGameResult; + int myTeam, opponentTeam; + int numTeams = SCRACE_NUM_TEAMS; + + + // Determine winners and losers + ///////////////////////////////////////////// + if (!disconnected) + { + if (win) + { + myGameResult = SCGameResult_WIN; + opponentGameResult = SCGameResult_LOSS; + } + else if (!tie) + { + myGameResult = SCGameResult_LOSS; + opponentGameResult = SCGameResult_WIN; + } + else + { + myGameResult = SCGameResult_DRAW; + opponentGameResult = SCGameResult_DRAW; + } + } + else + { + //report disconnected game - don't report any keys + myGameResult = SCGameResult_DISCONNECT; + opponentGameResult = SCGameResult_DISCONNECT; + } + + + // Determine teams, and who is on which + ///////////////////////////////////////////// + if (hosting) + { + myTeam = SCRACE_HOST_TEAM; + opponentTeam = SCRACE_CLIENT_TEAM; + } + else + { + myTeam = SCRACE_CLIENT_TEAM; + opponentTeam = SCRACE_HOST_TEAM; + } + + + // Create the report and begin building it + ///////////////////////////////////////////// + aResult = scCreateReport(interface, ATLAS_RULE_SET_VERSION, playerCount, numTeams, &statsReport); + if (aResult != SCResult_NO_ERROR) + { + MessageBox(@"Failed to Create Report - Out of memory"); + return; + } + + // Non-player data + ///////////////////////////////////////////// + scReportBeginGlobalData(statsReport); + // no global data reported + + // Player data + ///////////////////////////////////////////// + scReportBeginPlayerData(statsReport); + + // Report your data + //////////////////// + scReportBeginNewPlayer(statsReport); + scReportSetPlayerData(statsReport, PLAYER1, gPlayerData.connectionId, myTeam, + myGameResult, gPlayerData.profileId, &gPlayerData.certificate, gPlayerData.statsAuthdata); + if (!disconnected) + { + scReportAddIntValue(statsReport, KEY_MULTI_PLAY, (uint32_t)(TRUE)); + scReportAddIntValue(statsReport, RACE_TIME, (uint32_t)(localTime * absolute_to_millis)); + scReportAddFloatValue(statsReport, KEY_LONGITUDE, gPlayerData.playerLocation.coordinate.longitude); + scReportAddFloatValue(statsReport, KEY_LATITUDE, gPlayerData.playerLocation.coordinate.latitude); + + static char nickString[256]; + strcpy( nickString, [gPlayerData.uniquenick cStringUsingEncoding: NSASCIIStringEncoding]); + scReportAddStringValue(statsReport, KEY_NICK, nickString); + + // get current time and save as last game date/time + //gPlayerData.lastGameTime = [[NSDate date] description]; + //char raceDateTimeString[256]; + NSString *raceTimeString = [[NSDate date] description]; + strcpy( raceDateTimeString, [raceTimeString cStringUsingEncoding: NSASCIIStringEncoding] ); + gPlayerData.lastGameTime = raceDateTimeString; + scReportAddStringValue(statsReport, KEY_RACE_DATE_TIME, gPlayerData.lastGameTime ); + } + + // Report opponent data + //////////////////// + scReportBeginNewPlayer(statsReport); + scReportSetPlayerData(statsReport, PLAYER2, remoteConnId, opponentTeam, + opponentGameResult, remoteCertificate.mProfileId, &remoteCertificate, gPlayerData.statsAuthdata); + if (!disconnected) + scReportAddIntValue(statsReport, RACE_TIME, (uint32_t)(remoteTime * absolute_to_millis)); + + + // Team data + ///////////////////////////////////////////// + scReportBeginTeamData(statsReport); + + // Report your team data + //////////////////////// + scReportBeginNewTeam(statsReport); + scReportSetTeamData(statsReport, myTeam, myGameResult); + + // Report opponent team data + //////////////////////// + scReportBeginNewTeam(statsReport); + scReportSetTeamData(statsReport, opponentTeam, opponentGameResult); + + + // End the report and set GameStatus + if (!disconnected) + scReportEnd(statsReport, SCRACE_AUTHORITATIVE, SCGameStatus_COMPLETE); + else + scReportEnd(statsReport, SCRACE_AUTHORITATIVE, SCGameStatus_BROKEN); + + // Submit the Report + ///////////////////////////////////////////// + if (SCResult_NO_ERROR != scSubmitReport(interface, statsReport, SCRACE_AUTHORITATIVE, &gPlayerData.certificate, + &gPlayerData.privateData, SubmitReportCallback, SCRACE_TIMEOUT_MS, self)) + { + MessageBox(@"Failed to submit Stats Report."); + return; + } +} + + +- (void)reportSinglePlayerStats +{ + SCResult aResult = SCResult_NO_ERROR; +// SCGameResult myGameResult, opponentGameResult; +// int myTeam; +// int numTeams = SCRACE_NUM_TEAMS; + + + // Create the report and begin building it + ///////////////////////////////////////////// + aResult = scCreateReport(interface, ATLAS_RULE_SET_VERSION, 1, 0, &statsReport); + if (aResult != SCResult_NO_ERROR) + { + MessageBox(@"Failed to create Single Player Game stats report - Out of memory"); + return; + } + + scReportSetAsMatchless(statsReport); + + // Non-player data + ///////////////////////////////////////////// + scReportBeginGlobalData(statsReport); + // no global data reported + + // Player data + ///////////////////////////////////////////// + scReportBeginPlayerData(statsReport); + + // Report your data + //////////////////// + scReportBeginNewPlayer(statsReport); + scReportSetPlayerData(statsReport, PLAYER1, gPlayerData.connectionId, SCRACE_HOST_TEAM, SCGameResult_WIN, + gPlayerData.profileId, &gPlayerData.certificate, gPlayerData.statsAuthdata); + if (!disconnected) + { + scReportAddIntValue(statsReport, KEY_SINGLE_PLAY, (uint32_t)(TRUE)); + scReportAddIntValue(statsReport, SP_RACE_TIME, (uint32_t)(localTime * absolute_to_millis)); + scReportAddFloatValue(statsReport, KEY_LONGITUDE, gPlayerData.playerLocation.coordinate.longitude); + scReportAddFloatValue(statsReport, KEY_LATITUDE, gPlayerData.playerLocation.coordinate.latitude); + + static char nickString[256]; + strcpy( nickString, [gPlayerData.uniquenick cStringUsingEncoding: NSASCIIStringEncoding]); + scReportAddStringValue(statsReport, KEY_NICK, nickString); + + // get current time and save as last game date/time + //gPlayerData.lastGameTime = [[NSDate date] description]; + //char raceDateTimeString[256]; + //NSString *raceTimeString = [[NSDate date] description]; + //strcpy( raceDateTimeString, [raceTimeString cStringUsingEncoding: NSASCIIStringEncoding] ); + //gPlayerData.lastGameTime = raceDateTimeString; + scReportAddStringValue(statsReport, KEY_RACE_DATE_TIME, gPlayerData.lastGameTime ); + } + + // Team data + ///////////////////////////////////////////// + scReportBeginTeamData(statsReport); + + // End the report and set GameStatus + if (!disconnected) + scReportEnd(statsReport, gsi_false, SCGameStatus_COMPLETE); + else + scReportEnd(statsReport, gsi_false, SCGameStatus_BROKEN); + + // Submit the Report + ///////////////////////////////////////////// + if (SCResult_NO_ERROR != scSubmitReport(interface, statsReport, gsi_false, &gPlayerData.certificate, + &gPlayerData.privateData, SubmitReportCallback, SCRACE_TIMEOUT_MS, self)) + { + MessageBox(@"Failed to submit Single Player Game stats report."); + return; + } +} + + +- (void)countdown +{ + // If report was just recently submitted, reset the submission flag + /////////////////////////////////////////////////////////////////// + if (reportSent) + reportSent = gsi_false; + + if (hosting) + { + int rcode; + char message[32]; + + rcode = gtEncode(MSG_COUNTDOWN_TYPE, MSG_COUNTDOWN, message, sizeof(message), countdown); + assert(rcode != -1); + NSLog(@"Protocol send MSG_COUNTDOWN_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)message, rcode, GT2True); + } + + [gameController countdown: countdown]; +} + +- (void)hostingCountdown +{ +/* if (hosting) + { + alertView.message = @"qwe qwe"; + } +*/ + //[alertView countdown: countdown]; +} + +- (void)startRace +{ + if(hosting) + { + int rcode; + char buffer[32]; + rcode = gtEncode(MSG_START_RACE_TYPE, MSG_START_RACE, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_START_RACE_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + } + + // initialize stats markers + ///////////////////////////////////////////// + win = gsi_false; + tie = gsi_false; + disconnected = gsi_false; + + localTime = 0; + remoteTime = 0; + [gameController raceStarted]; + racing = true; + start = mach_absolute_time(); +} + +- (void)addErrorCallback: (qr2_error_t)error errorMessage: (const char*)errmsg +{ + if (alertView != nil) + { + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex animated: TRUE]; + [alertView release]; + alertView = nil; + } + + MessageBox([NSString stringWithCString: errmsg encoding: NSASCIIStringEncoding]); +} + +- (void)createSessionCallback: (const SCInterfacePtr)interfacePtr httpResult: (GHTTPResult)httpResult result: (SCResult)result +{ + if (httpResult == GHTTPSuccess && result == SCResult_NO_ERROR) + { + // Retrieve the Session/Connection ID to be used later + memcpy(gPlayerData.sessionId, scGetSessionId(interfacePtr), SC_SESSION_GUID_SIZE); + memcpy(gPlayerData.connectionId, scGetConnectionId(interfacePtr), SC_CONNECTION_GUID_SIZE); + + sessionCreated = gsi_true; + + OutputDebugString([NSString stringWithFormat: @"[createSessionCB] Session ID: %s\n", gPlayerData.sessionId]); + OutputDebugString([NSString stringWithFormat: @"[createSessionCB] Connection ID: [%s]\n", gPlayerData.connectionId]); + + if( playerCount == 1 ) + scSetReportIntention(interface, gPlayerData.connectionId, gsi_false, &gPlayerData.certificate, &gPlayerData.privateData, + SetReportIntentionCallback, SCRACE_TIMEOUT_MS, self); + } + else + { + OutputDebugString([NSString stringWithFormat: @"[createSessionCB] Error. HTTPResult: %d, SCResult: %d\n", httpResult, result]); + + [gameController stopRaceClock]; + MessageBox(@"Error Creating Stats Session"); + [self logout]; + } +} + +- (void)setReportIntentionCallback: (const SCInterfacePtr)interfacePtr httpResult: (GHTTPResult)httpResult result: (SCResult)result +{ + if (httpResult == GHTTPSuccess && result == SCResult_NO_ERROR) + { + // Retrieve the connection ID to be used later + ///////////////////////////////////////////////////////// + const char * connectionId = scGetConnectionId(interfacePtr); + memcpy(gPlayerData.connectionId, connectionId, SC_CONNECTION_GUID_SIZE); + connIdSet = gsi_true; + + OutputDebugString([NSString stringWithFormat: @"[setIntentionCB] Connection ID: [%s]\n", gPlayerData.connectionId]); + } + else + { + OutputDebugString([NSString stringWithFormat: @"[setIntentionCB] Error. HTTPResult: %d, SCResult: %d\n", httpResult, result]); + + MessageBox(@"Error initializing stats system. Login when network connection is up", @"Connection Error"); + [self logout]; + return; + } +} + +- (void)submitReportCallback: (const SCInterfacePtr)interface httpResult: (GHTTPResult)httpResult result: (SCResult)result +{ + if (httpResult != GHTTPSuccess || result != SCResult_NO_ERROR) + { + OutputDebugString([NSString stringWithFormat: @"[submitReportCB] Error. HTTPResult: %d, SCResult: %d\n", httpResult, result]); + + //MessageBox(@"Error submitting stats report.", @"Connection Error"); + //return; + } + + reportSent = gsi_true; //mark that we've submitted a report for this session + + // Cleanup + ///////////////////////////////////////////// + scDestroyReport(statsReport); + statsReport = NULL; +} + +- (void)getMyRecordsCallback: (SAKERequestResult) result inputData: (SAKEGetMyRecordsInput*)inputData outputData: (SAKEGetMyRecordsOutput*)outputData +{ + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + [loadRemoteDataTimer invalidate]; + loadRemoteDataTimer = nil; + + if (result != SAKERequestResult_SUCCESS) + { + MessageBox(@"Attempt to retrieve data failed."); + return; + } + + if (outputData->mNumRecords == 0) + { + // New user. + gPlayerData.bestRaceTime = INITIAL_BEST_TIME; + gPlayerData.spBestRaceTime = INITIAL_BEST_TIME; + //gPlayerData.lastGameTime = '\0'; + return; + } + + // Assign vars: + SAKEField* fields = outputData->mRecords[0]; + + for (int n = 0; n < inputData->mNumFields; n++) { + int stat = ATLAS_GET_STAT(fields[n].mName); + + switch (stat) { + case CAREER_WINS: + gPlayerData.careerWins = fields[n].mValue.mInt; + break; + + case CAREER_LOSSES: + gPlayerData.careerLosses = fields[n].mValue.mInt; + break; + + case BEST_RACE_TIME: + gPlayerData.bestRaceTime = fields[n].mValue.mInt; + break; + + case WORST_RACE_TIME: + gPlayerData.worstRaceTime = fields[n].mValue.mInt; + break; + + case TOTAL_MATCHES: + gPlayerData.totalMatches = fields[n].mValue.mInt; + break; + + case AVERAGE_RACE_TIME: + gPlayerData.averageRaceTime = fields[n].mValue.mFloat; + break; + + case CURRENT_WIN_STREAK: + gPlayerData.matchWinStreak = fields[n].mValue.mInt; + break; + + case CURRENT_LOSS_STREAK: + gPlayerData.matchLossStreak = fields[n].mValue.mInt; + break; + + case TOTAL_RACE_TIME: + gPlayerData.totalRaceTime = fields[n].mValue.mInt; + break; + + case CAREER_DISCONNECTS: + gPlayerData.careerDisconnects = fields[n].mValue.mInt; + break; + + case DISCONNECT_RATE: + gPlayerData.disconnectRate = fields[n].mValue.mFloat; + break; + + case CAREER_DRAWS: + gPlayerData.careerDraws = fields[n].mValue.mInt; + break; + + case CURRENT_DRAW_STREAK: + gPlayerData.matchDrawStreak = fields[n].mValue.mInt; + break; + + case CAREER_LONGEST_WIN_STREAK: + gPlayerData.careerLongestWinStreak = fields[n].mValue.mInt; + break; + + case CAREER_LONGEST_LOSS_STREAK: + gPlayerData.careerLongestLossStreak = fields[n].mValue.mInt; + break; + + case CAREER_LONGEST_DRAW_STREAK: + gPlayerData.careerLongestDrawStreak = fields[n].mValue.mInt; + break; + + case TOTAL_COMPLETE_MATCHES: + gPlayerData.totalCompleteMatches = fields[n].mValue.mInt; + break; + + case LAST_TIME_PLAYED: + gPlayerData.lastGameTime = fields[n].mValue.mAsciiString; + break; + + case SP_AVERAGE_RACE_TIME: + gPlayerData.spAverageRaceTime = fields[n].mValue.mFloat; + break; + + case SP_BEST_RACE_TIME: + gPlayerData.spBestRaceTime = fields[n].mValue.mInt; + break; + + case SP_WORST_RACE_TIME: + gPlayerData.spWorstRaceTime = fields[n].mValue.mInt; + break; + + case SP_TOTAL_PLAYS: + gPlayerData.spTotalPlays = fields[n].mValue.mInt; + break; + + case SP_TOTAL_RACE_TIME: + gPlayerData.spTotalRaceTime = fields[n].mValue.mInt; + break; + } + } + + // Check if player stats from server are more up-to-date than the local stats saved on file + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; + + NSString* aTime = (NSString*)[gPlayerData.playerStatsData objectForKey: lastTimePlayedKey ]; + + // if no stat timestamp is found in file, save server data to file + if( aTime == nil ) + { + [self savePlayerStatsToFile ]; + return; + } + + NSDate* fileDate = [dateFormatter dateFromString: aTime ]; + + aTime = [NSString stringWithCString: gPlayerData.lastGameTime encoding: NSASCIIStringEncoding]; + // if no stat timestamp is found from server, log and ignore + if( aTime == nil ) + { + NSLog(@"Stats from server does not have a timestamp."); + return; + } + NSDate* serverDate = [dateFormatter dateFromString: aTime]; + + NSComparisonResult dateResult = [ fileDate compare: serverDate ]; + if( dateResult == NSOrderedSame || dateResult == NSOrderedAscending ) + { + // File stats are older or same time as server stats + // Save server data to file + NSLog(@"Save server data to file"); + [self savePlayerStatsToFile ]; + } + else + { + // File stats are more up to date than server stats + // Use file data for display stats + NSLog(@"Use file data for display stats"); + } + +} + +- (void)connectedCallback: (GT2Connection)connection result: (GT2Result)result message: (GT2Byte*)message length: (int)length +{ + if (result == GT2Success) + { + state = JOIN_EXCHANGE_CERT; + NSLog(@"connectedCallback state=JOIN_EXCHANGE_CERT - (success)"); + } + else + { + state = JOIN_ERROR; + NSLog(@"connectedCallback state=JOIN_ERROR (fail)"); + gt2Connection = NULL; + } +} + +- (void)receivedCallback: (GT2Connection)connection message: (GT2Byte*)message length: (int)length reliable: (GT2Bool)reliable +{ + if(!message || !length) + return; + + switch (gtEncodedMessageType((char*)message)) + { + case MSG_COUNTDOWN_TYPE: + { + assert(!hosting); +NSLog(@"receivedCallback(MSG_COUNTDOWN_TYPE)"); + if(gtDecode(MSG_COUNTDOWN, (char*)message, length, &countdown) == -1) + { + state = JOIN_ERROR; + NSLog(@"state=JOIN_ERROR - MSG_COUNTDOWN_TYPE in receivedCallback"); + return; + } + + if( state == RACING ) + [self countdown]; + + break; + } + + case MSG_START_RACE_TYPE: +NSLog(@"receivedCallback(MSG_START_RACE_TYPE)"); + assert(!hosting); + [self startRace]; + break; + + case MSG_PROGRESS_TYPE: +NSLog(@"receivedCallback(MSG_PROGRESS_TYPE)"); + if(racing) + { + int progress; + + if(gtDecode(MSG_PROGRESS, (char*)message, length, &progress) != -1) + { + [gameController remoteProgress: progress]; + } + } + break; + + case MSG_END_RACE_TYPE: +NSLog(@"receivedCallback(MSG_END_RACE_TYPE)"); + if(racing) + { + uint32_t millis; + gtDecode(MSG_END_RACE, (char*)message, length, &millis); + remoteTime = millis * millis_to_absolute; + } + break; + + case MSG_TO_CLIENT_CERT_TYPE: + { +NSLog(@"receivedCallback(MSG_TO_CLIENT_CERT_TYPE)"); + char remoteCert[512]; + int remoteCertLen = 512; + int pid; + + if(gtDecode(MSG_TO_CLIENT_CERT, (char*)message, length, &pid, remoteCert, &remoteCertLen) == -1) + { + state = JOIN_ERROR; + NSLog(@"state=JOIN_ERROR - MSG_TO_CLIENT_CERT_TYPE in receivedCallback"); + return; + } + + // Parse the certificate + ///////////////////////////////////////////////////////// + wsLoginCertReadBinary(&remoteCertificate, remoteCert, remoteCertLen); + + // Build up and send the certificate response + ///////////////////////////////////////////////////////// + char buffer[520]; + char cert[512]; + int rcode; + gsi_u32 certLen; + + wsLoginCertWriteBinary(&gPlayerData.certificate, cert, sizeof(cert), &certLen); + + rcode = gtEncode(MSG_TO_HOST_CERT_TYPE, MSG_TO_HOST_CERT, buffer, sizeof(buffer), gPlayerData.profileId, cert, certLen); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_HOST_CERT_TYPE\n"); + gt2Send(connection, (const unsigned char*)buffer, rcode, GT2True); + + state = JOIN_VERIFY_CERT; + NSLog(@"receivedCallback state=JOIN_VERIFY_CERT"); + break; + } + + case MSG_TO_HOST_CERT_TYPE: + { +NSLog(@"receivedCallback(MSG_TO_HOST_CERT_TYPE)"); + char remoteCert[512]; + int remoteCertLen = 512; + int pid; + + if(gtDecode(MSG_TO_HOST_CERT, (char*)message, length, &pid, remoteCert, &remoteCertLen) == -1) + { + state = HOST_ERROR; + NSLog(@"MSG_TO_HOST_CERT_TYPE - state=HOST_ERROR"); + return; + } + + // Parse the certificate + ///////////////////////////////////////////////////////// + wsLoginCertReadBinary(&remoteCertificate, remoteCert, remoteCertLen); + + state = HOST_VERIFY_CERT; + NSLog(@"state=JOIN_VERIFY_CERT"); + break; + } + + case MSG_TO_CLIENT_CERTVERIFIED_TYPE: +NSLog(@"receivedCallback(MSG_TO_CLIENT_CERTVERIFIED_TYPE)"); + char buffer[32]; + int rcode; + assert(!hosting); + rcode = gtEncode(MSG_TO_HOST_CERTVERIFIED_TYPE, MSG_TO_HOST_CERTVERIFIED, buffer, sizeof(buffer)); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_HOST_CERTVERIFIED_TYPE\n"); + gt2Send(gt2Connection, (const unsigned char*)buffer, rcode, GT2True); + break; + + case MSG_TO_HOST_CERTVERIFIED_TYPE: +NSLog(@"receivedCallback(MSG_TO_HOST_CERTVERIFIED_TYPE)"); + assert(hosting); + state = HOST_EXCHANGE_KEYS; + NSLog(@"state=HOST_EXCHANGE_KEYS"); + break; + + case MSG_TO_CLIENT_KEYS_TYPE: + { +NSLog(@"receivedCallback(MSG_TO_CLIENT_KEYS_TYPE)"); + SCPeerKeyExchangeMsg recvMsg; + int recvMsgLen = GS_CRYPT_RSA_BYTE_SIZE; + + if(gtDecode(MSG_TO_CLIENT_KEYS, (char*)message, length, recvMsg, &recvMsgLen) == -1) + { + state = JOIN_ERROR; + NSLog(@"state=JOIN_ERROR - MSG_TO_CLIENT_KEYS_TYPE in receivedCallback"); + return; + } + + // Stop Join countdown + [self stopJoiningCountdownTimer]; // if running + + // Receiving player should parse the cipher key out of it. + // - decrypting the msg requires the local player's private data + ///////////////////////////////////////////////////////// + scPeerCipherParseKeyExchangeMsg(&gPlayerData.certificate, &gPlayerData.privateData, + recvMsg, &gPlayerData.peerSendCipher); + + + // Send response to host + ///////////////////////////////////////////////////////// + char buffer[512]; + int rcode; + SCPeerKeyExchangeMsg exchangeMsg; + + scPeerCipherInit(&gPlayerData.certificate, &gPlayerData.peerRecvCipher); + scPeerCipherCreateKeyExchangeMsg(&remoteCertificate, &gPlayerData.peerRecvCipher, exchangeMsg); + + // Now send the key to the other player + ///////////////////////////////////////////////////////// + rcode = gtEncode(MSG_TO_HOST_KEYS_TYPE, MSG_TO_HOST_KEYS, buffer, sizeof(buffer), exchangeMsg, GS_CRYPT_RSA_BYTE_SIZE); + assert(rcode != -1); + NSLog(@"Protocol send MSG_TO_HOST_KEYS_TYPE\n"); + gt2Send(connection, (const unsigned char*)buffer, rcode, GT2True); + + state = JOIN_CONNECTED; + NSLog(@"state=JOIN_CONNECTED"); + break; + } + + case MSG_TO_HOST_KEYS_TYPE: + { +NSLog(@"receivedCallback(MSG_TO_HOST_KEYS_TYPE)"); + SCPeerKeyExchangeMsg exchangeMsg; + int exchangeMsgLen = GS_CRYPT_RSA_BYTE_SIZE; + + if(gtDecode(MSG_TO_HOST_KEYS, (char*)message, length, exchangeMsg, &exchangeMsgLen) == -1) + { + state = HOST_ERROR; + NSLog(@"state=HOST_ERROR"); + return; + } + + // release hosting countdown + [self stopHostingCountdownTimer]; // if running + + // Receiving player should parse the cipher key out of it. + // - decrypting the msg requires the local player's private data + ///////////////////////////////////////////////////////// + scPeerCipherParseKeyExchangeMsg(&gPlayerData.certificate, &gPlayerData.privateData, + exchangeMsg, &gPlayerData.peerSendCipher); + + + state = HOST_CONNECTED; + NSLog(@"receivedCallback(MSG_TO_HOST_KEYS_TYPE) state=HOST_CONNECTED"); + + break; + } + + case MSG_SESSION_ID_TYPE: + { +NSLog(@"receivedCallback(MSG_SESSION_ID_TYPE)"); + assert(!hosting); + char sessionCrypt[SC_SESSION_GUID_SIZE]; + char connCrypt[SC_CONNECTION_GUID_SIZE]; + int sidLen = SC_SESSION_GUID_SIZE; + int ccidLen = SC_CONNECTION_GUID_SIZE; + + + // Client decodes sessionID / remote connID + ///////////////////////////////////////////////////////// + if(gtDecode(MSG_SESSION_ID, (char*)message, length, sessionCrypt, &sidLen, connCrypt, &ccidLen) == -1) + { + state = JOIN_ERROR; + NSLog(@"state=JOIN_ERROR - MSG_SESSION_ID_TYPE in receivedCallback"); + return; + } + + OutputDebugString([NSString stringWithFormat: @"[MSG_SESSION_ID_TYPE] sessionCrypt: %.40s\n", sessionCrypt]); + OutputDebugString([NSString stringWithFormat: @"[MSG_SESSION_ID_TYPE] connCrypt: %.40s\n", connCrypt]); + + // Decrypt the sessionID / remote connID + ///////////////////////////////////////////////////////// + scPeerCipherDecryptBufferIV(&gPlayerData.peerRecvCipher, 1, (gsi_u8*)sessionCrypt, SC_SESSION_GUID_SIZE); + memcpy(gPlayerData.sessionId, sessionCrypt, SC_SESSION_GUID_SIZE); + + scPeerCipherDecryptBufferIV(&gPlayerData.peerRecvCipher, 2, (gsi_u8*)connCrypt, SC_CONNECTION_GUID_SIZE); + memcpy(remoteConnId, connCrypt, SC_CONNECTION_GUID_SIZE); + + + OutputDebugString([NSString stringWithFormat: @"[MSG_SESSION_ID_TYPE] mSessionId: %s\n", gPlayerData.sessionId]); + OutputDebugString([NSString stringWithFormat: @"[MSG_SESSION_ID_TYPE] remoteConnId: %s\n", remoteConnId]); + + + // Joining player sets the session ID and the report intention + ///////////////////////////////////////////////////////// + scSetSessionId(gPlayerData.statsInterface, gPlayerData.sessionId); + scSetReportIntention(gPlayerData.statsInterface, gPlayerData.connectionId, SCRACE_AUTHORITATIVE, &gPlayerData.certificate, &gPlayerData.privateData, + SetReportIntentionCallback, SCRACE_TIMEOUT_MS, self); + + state = JOIN_SEND_CONNID; + NSLog(@"state=JOIN_SEND_CONNID"); + break; + } + + case MSG_CONNECTION_ID_TYPE: + { +NSLog(@"receivedCallback(MSG_CONNECTION_ID_TYPE)"); + assert(hosting); + char connCrypt[SC_CONNECTION_GUID_SIZE]; + int ccidLen = SC_CONNECTION_GUID_SIZE; + + // Hosting player decodes the remote conn ID for use in reporting + ///////////////////////////////////////////////////////// + if(gtDecode(MSG_CONNECTION_ID, (char*)message, length, connCrypt, &ccidLen) == -1) + { + state = HOST_ERROR; + NSLog(@"MSG_CONNECTION_ID_TYPE - state=HOST_ERROR"); + return; + } + + OutputDebugString([NSString stringWithFormat: @"[MSG_CONNECTION_ID_TYPE] connCrypt: %.40s\n", connCrypt]); + + + // Decrypt the remote conn ID + ///////////////////////////////////////////////////////// + scPeerCipherDecryptBufferIV(&gPlayerData.peerRecvCipher, 1, (gsi_u8*)connCrypt, SC_CONNECTION_GUID_SIZE); + memcpy(remoteConnId, connCrypt, SC_CONNECTION_GUID_SIZE); + + + OutputDebugString([NSString stringWithFormat: @"[MSG_CONNECTION_ID_TYPE] remoteConnId: %s\n", remoteConnId]); + + // at this point all of our exchanges are complete - ready for game start + ///////////////////////////////////////////////////////// + break; + } + + case MSG_REPLAY_TYPE: +NSLog(@"receivedCallback(MSG_REPLAY_TYPE) state=%d", state); + assert(state == FINISHED_RACE); + if (currentAlert == ALERT_REPLAY) + { + // Remote player agreed to a replay. + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + [self stopHostingCountdownTimer]; + + [self setupMatch: hosting]; + [navController popToViewController: gameController animated: YES]; + } + else + { + // Remote player asked for a replay. + alertView = [[UIAlertView alloc] initWithTitle: @"You have been challenged to a rematch!" message: @"Accept?" delegate: self cancelButtonTitle: @"No" otherButtonTitles: @"Yes", nil]; + [alertView show]; + currentAlert = ALERT_REPLAY_CONFIRM; + } + break; + + case MSG_REPLAY_CANCELED_TYPE: +NSLog(@"receivedCallback(MSG_REPLAY_CANCELED_TYPE)"); + [self stopHostingCountdownTimer]; + + assert(state == FINISHED_RACE); + if (currentAlert == ALERT_REPLAY) { + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + MessageBox(@"Your replay request was declined."); + } + else if (currentAlert == ALERT_REPLAY_CONFIRM) { + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + MessageBox(@"Your opponent canceled the replay request."); + } + } +} + +// Called when the connection to remote player is closed +- (void)closedCallback: (GT2Connection)connection reason: (GT2CloseReason)reason +{ + // Logout triggers this function when it's a local close + // so we need to make sure we don't loop + NSLog(@"closedCallback reason = %d state=%d", reason, state ); + + // Stop host countdown + [self stopHostingCountdownTimer]; + + if (reason != GT2LocalClose) + { + NSLog(@"closedCallback reason=%d - NOT GT2LocalClose", reason); + + if( state == RACING ) + { + // stop the game-start countdown timer + if (countdownTimer != nil) + { + [countdownTimer invalidate]; + countdownTimer = nil; + } + + // stop game race clock + [gameController stopRaceClock]; + + // If the connection was closed remotely, or connection errors occured + // we should report stats anyways to report disconnects + MessageBox(@"Opponent canceled match"); + + [self stopHostingServer]; + disconnected = gsi_true; + + [self reportTwoPlayerStats]; + [self saveTwoPlayerGameStatsToFile]; + + MatchmakingController* matchmakingController = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + [navController popToViewController: matchmakingController animated: YES]; + } + else if ( state == HOST_CONNECTED ) + { + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + [self stopJoiningCountdownTimer]; + [self stopHostingServer]; + disconnected = gsi_true; + + [self reportTwoPlayerStats]; + [self saveTwoPlayerGameStatsToFile]; + + // set state so that onThinkTimer does not go to game page + state = JOIN_CHOOSESERVER; + + //MatchmakingController* matchmakingController = (MatchmakingController*)[self findExistingControllerOfType: [MatchmakingController class]]; + //[navController popToViewController: matchmakingController animated: YES]; + + MessageBox(@"Opponent canceled" ); + } + + else + { + NSLog(@"closedCallback currentAlert=%d - NOT GT2LocalClose", currentAlert); + + GameResultsController* resultsController = (GameResultsController*)[self findExistingControllerOfType: [GameResultsController class]]; + if (resultsController != nil) + { + [resultsController connectionDropped]; + } + + if ( state != FINISHED_RACE ) + { + switch (currentAlert) + { + case ALERT_REPLAY: + case ALERT_REPLAY_CONFIRM: + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + [self stopHostingServer]; + disconnected = gsi_true; + + MessageBox(@"The connection to the other player was terminated."); + break; + + default: + // stop game race clock + [gameController stopRaceClock]; + + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + [self stopHostingServer]; + disconnected = gsi_true; + + MessageBox(@"Opponent quit race" ); + [self returnToMatchMaking]; + break; + } + } + } + } + + + racing = false; + if (gt2Connection) + { + gt2CloseConnection(gt2Connection); + gt2Connection = NULL; + } + if (gt2Socket) + { + GT2Socket tempsocket = gt2Socket; + gt2Socket = NULL; // prevent recursion + NSLog(@"closeCallback: null & close gt2 socket"); + gt2CloseSocket(tempsocket); + } +} + +- (void)connectAttemptCallback: (GT2Socket)listener connection: (GT2Connection)connection ip: (unsigned int)ip port: (unsigned short)port latency: (int)latency message: (GT2Byte*)message length: (int)length +{ + NSLog(@"start connectAttemptCallback"); + + int pid = 0; + char nick[128]; + + // Only allow one connection. + ///////////////////////////// + if(gt2Connection) + { + gt2Reject(connection, NULL, 0); + return; + } + + // Decode the pid. + ////////////////// + if(message && length) + { + if(gtDecodeNoType(MSG_CONNECT, (char*)message, length, &pid, nick) == -1) + pid = 0; + } + + // If we didn't/couldn't get the pid, reject the attempt. + ///////////////////////////////////////////////////////// + if(!pid) + { + gt2Reject(connection, NULL, 0); + return; + } + + // Accept the connection. + ///////////////////////// + GT2ConnectionCallbacks callbacks = { + NULL, + ReceivedCallback, + ClosedCallback, + NULL + }; + + if(!gt2Accept(connection, &callbacks)) + return; + + // Set some states. + /////////////////// + gt2Connection = connection; + gt2SetConnectionData(gt2Connection, self); + state = HOST_EXCHANGE_CERT; //once connected, exchange certifications + NSLog(@"connectAttemptCallback state=HOST_EXCHANGE_CERT"); + + if( hosting ) + { + // remove countdown pop-up + [alertView dismissWithClickedButtonIndex: 0 animated: YES]; + [alertView release]; + alertView = nil; + + // stop hosting countdown + [self stopHostingCountdownTimer]; + + // show that we are connecting + alertView = [[UIAlertView alloc] initWithTitle: @"Initiating race with player" message: @"30 seconds" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_CONNECTING; //NEW ??? + + [self startJoiningCountdownTimer]; + } + + connected = true; + qr2_send_statechanged(NULL); +} + + +// Called by NN SDK for host only +- (void)natnegCallback: (int)cookie +{ + if( hosting ) + { + // remove countdown pop-up +// [alertView dismissWithClickedButtonIndex: 0 animated: YES]; + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + // stop hosting countdown + [self stopHostingCountdownTimer]; + + // show that we are connecting + alertView = [[UIAlertView alloc] initWithTitle: @"Connecting to player" message: @"30 seconds" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + currentAlert = ALERT_CONNECTING; //NEW ??? + + [self startJoiningCountdownTimer]; + } + + NNBeginNegotiationWithSocket(gt2GetSocketSOCKET(gt2Socket), cookie, NN_SERVER, NNProgressCallback, NNCompletedCallback, self); +} + +- (void)natnegCompletedCallback: (NegotiateResult)result socket: (SOCKET)gamesocket remoteaddr: (sockaddr_in*)remoteaddr +{ + NSLog(@"natnegCompletedCallback start (state=%d) (result=%d)", state, result); + + if (state == HOST_LISTENING) + { + NSLog(@"natnegCompletedCallback host waiting for client\n"); + // Server side doesn't have to do anything here. + return; + } + + // Close the Connection popup didDismissWithButtonIndex + [alertView dismissWithClickedButtonIndex: -1 animated: YES]; + [alertView release]; + alertView = nil; + + if (result != nr_success) + { + switch (result) + { + case nr_inittimeout: + MessageBox(@"Unable to communicate with NAT Negotiation server."); + break; + + case nr_pingtimeout: + MessageBox(@"Couldn't establish a direct connection to the other player."); + break; + + case nr_deadbeatpartner: + MessageBox(@"The other player did not register with the NAT Negotiation Server."); + break; + + default: + MessageBox(@"An error occurred connecting to the remote player."); + break; + } + + return; + } + + NSString* remoteAddress = [NSString stringWithFormat: @"%s:%d", inet_ntoa(remoteaddr->sin_addr), ntohs(remoteaddr->sin_port)]; + NSLog(@"remote Address = %s", remoteAddress); + + [self startMultiPlayerGame: [remoteAddress cStringUsingEncoding: NSASCIIStringEncoding]]; +} + + +// System needs more memory +// Remove any data we can retrieve from server +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application +{ + MessageBox(@"System is running low on memory. Logging out", @"Low memory"); + [self logout]; + NSLog( @"WARNING: applicationDidReceiveMemoryWarning" ); +} + +@end + + +void ServerKeyCallback(int keyid, qr2_buffer_t outbuf, void* userdata) +{ + switch (keyid) + { + case HOSTNAME_KEY: + qr2_buffer_addA(outbuf, [gPlayerData.uniquenick cStringUsingEncoding: NSASCIIStringEncoding]); + break; + + case GAMEVER_KEY: + qr2_buffer_addA(outbuf, SCRACE_VERSION_STR); + break; + + case HOSTPORT_KEY: + qr2_buffer_add_int(outbuf, HOST_PORT2); + break; + + case GAMEMODE_KEY: + { + switch([ appDelegate state ]) + { + RACING: + qr2_buffer_addA(outbuf, "closedplaying"); + break; + + HOST_CONNECTED: + JOIN_CONNECTED: + qr2_buffer_addA(outbuf, "closedwaiting"); // accept no more players + break; + + HOST_LISTENING: + JOIN_ERROR: + qr2_buffer_addA(outbuf, "openwaiting"); + break; + + default: + qr2_buffer_addA(outbuf, "openwaiting"); // hosting and waiting + break; + } + break; + } + + case TIMEELAPSED_KEY: + qr2_buffer_add_int(outbuf, 0); + break; + + case NUMPLAYERS_KEY: + qr2_buffer_add_int(outbuf, ( [appDelegate connected] ? 2 : 1) ); + break; + + case MAXPLAYERS_KEY: + qr2_buffer_add_int(outbuf, 2); + break; + + default: + qr2_buffer_addA(outbuf, ""); + break; + } +} + +void PlayerKeyCallback(int keyid, int index, qr2_buffer_t outbuf, void* userdata) +{ + switch (keyid) + { + case PLAYER__KEY: + qr2_buffer_addA(outbuf, [gPlayerData.uniquenick cStringUsingEncoding: NSASCIIStringEncoding]); + break; + + case PING__KEY: + qr2_buffer_add_int(outbuf, 0); + break; + + case PID__KEY: + qr2_buffer_add_int(outbuf, gPlayerData.profileId); + break; + + default: + qr2_buffer_addA(outbuf, ""); + break; + } +} + +void TeamKeyCallback(int keyid, int index, qr2_buffer_t outbuf, void* userdata) +{ + qr2_buffer_addA(outbuf, ""); +} + +int CountCallback(qr2_key_type keytype, void* userdata) +{ + if( keytype == key_player ) + return 1; + + return 0; +} + +void KeyListCallback(qr2_key_type keytype, qr2_keybuffer_t keybuffer, void* userdata) +{ + switch (keytype) + { + case key_server: + qr2_keybuffer_add(keybuffer, HOSTNAME_KEY); + qr2_keybuffer_add(keybuffer, GAMEVER_KEY); + qr2_keybuffer_add(keybuffer, HOSTPORT_KEY); + qr2_keybuffer_add(keybuffer, GAMEMODE_KEY); + qr2_keybuffer_add(keybuffer, TIMEELAPSED_KEY); + qr2_keybuffer_add(keybuffer, NUMPLAYERS_KEY); + qr2_keybuffer_add(keybuffer, MAXPLAYERS_KEY); + break; + + case key_player: + qr2_keybuffer_add(keybuffer, PLAYER__KEY); + qr2_keybuffer_add(keybuffer, PING__KEY); + qr2_keybuffer_add(keybuffer, PID__KEY); + break; + + case key_team: + case key_type_count: + break; + } +} + +void AddErrorCallback(qr2_error_t error, char* errmsg, void* userdata) +{ + [(AppDelegate*)userdata addErrorCallback: error errorMessage: errmsg]; +} + +void NatNegCallback(int cookie, void* userData) +{ + [(AppDelegate*)userData natnegCallback: cookie]; +} + +void CreateSessionCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData) +{ + [(AppDelegate*)userData createSessionCallback: interface httpResult: httpResult result: result]; +} + +void SetReportIntentionCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData) +{ + [(AppDelegate*)userData setReportIntentionCallback: interface httpResult: httpResult result: result]; +} + +void SubmitReportCallback(const SCInterfacePtr interface, GHTTPResult httpResult, SCResult result, void* userData) +{ + [(AppDelegate*)userData submitReportCallback: interface httpResult: httpResult result: result]; +} + +void GetMyRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, SAKEGetMyRecordsInput* inputData, SAKEGetMyRecordsOutput* outputData, void* userData) +{ + [(AppDelegate*)userData getMyRecordsCallback: result inputData: inputData outputData: outputData]; +} + +void ConnectedCallback(GT2Connection connection, GT2Result result, GT2Byte* message, int len) +{ + AppDelegate* delegate = (AppDelegate*)gt2GetConnectionData(connection); + [delegate connectedCallback: connection result: result message: message length: len]; +} + +void ReceivedCallback(GT2Connection connection, GT2Byte* message, int len, GT2Bool reliable) +{ + AppDelegate* delegate = (AppDelegate*)gt2GetConnectionData(connection); + [delegate receivedCallback: connection message: message length: len reliable: reliable]; +} + +void ClosedCallback(GT2Connection connection, GT2CloseReason reason) +{ + AppDelegate* delegate = (AppDelegate*)gt2GetConnectionData(connection); + [delegate closedCallback: connection reason: reason]; +} + +void ConnectAttemptCallback(GT2Socket listener, GT2Connection connection, unsigned int ip, unsigned short port, int latency, GT2Byte* message, int len) +{ + AppDelegate* delegate = (AppDelegate*)gt2GetSocketData(listener); + [delegate connectAttemptCallback: listener connection: connection ip: ip port: port latency: latency message: message length: len]; +} + +void SocketErrorCallback(GT2Socket socket) +{ +} + +GT2Bool UnrecognizedMessageCallback(GT2Socket socket, unsigned int ip, unsigned short port, GT2Byte* message, int len) +{ + static GT2Byte qrHeader[] = { QR_MAGIC_1, QR_MAGIC_2 }; + static GT2Byte nnHeader[] = { NN_MAGIC_0, NN_MAGIC_1, NN_MAGIC_2, NN_MAGIC_3, NN_MAGIC_4, NN_MAGIC_5 }; + + if (memcmp(message, qrHeader, sizeof(qrHeader)) == 0) + { + sockaddr_in address = { sizeof(sockaddr_in), AF_INET, htons(port), { ip }, { 0 } }; + qr2_parse_queryA(NULL, (char*)message, len, (sockaddr*)&address); + return GT2True; + } + else if (memcmp(message, nnHeader, sizeof(nnHeader)) == 0) + { + sockaddr_in address = { sizeof(sockaddr_in), AF_INET, htons(port), { ip }, { 0 } }; + NNProcessData((char*)message, len, &address); + return GT2True; + } + + NSLog(@"UnrecognizedMessageCallback: unknown message type with header[%d, %d]", message[0], message[1]); + return GT2False; +} + +void NNDetectionCallback(gsi_bool success, NAT nat) +{ + int charsWritten; + char aString[256]; + + NSLog(@"****************************************************************************\n"); + if(success == gsi_false) + { + NSLog(@"NNDetectionCallback: Failed to fully detect the NAT. Please try the detection again.\n"); + return; + } + + memset(aString, 0, 255); + charsWritten = sprintf(aString, "NNDetectionCallback: The detected NAT is a "); + switch(nat.natType) + { + case no_nat: + charsWritten = sprintf(aString, "NNDetectionCallback: No NAT detected."); + break; + case firewall_only: + charsWritten = sprintf(aString, "NNDetectionCallback: No NAT detected, but firewall may be present."); + break; + case full_cone: + charsWritten += sprintf(aString + charsWritten, "full cone "); + break; + case restricted_cone: + charsWritten += sprintf(aString + charsWritten, "restricted cone "); + break; + case port_restricted_cone: + charsWritten += sprintf(aString + charsWritten, "port restricted cone "); + break; + case symmetric: + charsWritten += sprintf(aString + charsWritten, "symmetric "); + break; + case unknown: + default: + charsWritten = sprintf(aString, "NNDetectionCallback: Unknown NAT type detected "); + break; + } + + if(nat.natType != no_nat && nat.natType != firewall_only) + switch(nat.mappingScheme) + { + case private_as_public: + charsWritten += sprintf(aString + charsWritten, "and is using the private port as the public port."); + break; + case consistent_port: + charsWritten += sprintf(aString + charsWritten, "and is using the same public port for all requests from the same private port."); + break; + case incremental: + charsWritten += sprintf(aString + charsWritten, "and is using an incremental port mapping scheme."); + break; + case mixed: + charsWritten += sprintf(aString + charsWritten, "and is using a mixed port mapping scheme."); + break; + case unrecognized: + default: + charsWritten += sprintf(aString + charsWritten, "and is using an unrecognized port mapping scheme."); + break; + } + + charsWritten += sprintf(aString + charsWritten, "\n"); + + printf("NNDetectionCallback: [%s]", aString ); + gsDebugFormat(GSIDebugCat_App, GSIDebugType_Misc, GSIDebugLevel_Notice, aString); +} + + +void NNProgressCallback(NegotiateState state, void* userdata) +{ + NSLog(@"NN progress status: %d", (int) state ); +} + +void NNCompletedCallback(NegotiateResult result, SOCKET gamesocket, sockaddr_in* remoteaddr, void* userdata) +{ + AppDelegate* delegate = (AppDelegate*)userdata; + [delegate natnegCompletedCallback: result socket: gamesocket remoteaddr: remoteaddr]; +} + +// C callback bridges to Objective C +static void SearchForLeadersTimeRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData) +{ + [(AppDelegate*)userData searchForLeadersTimeRecordsCallback: sake request: request result: result inputData: inputData outputData: outputData]; +} + +static void SearchForTop5TimeRecordsCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData) +{ + [(AppDelegate*)userData searchForTop5TimeRecordsCallback: sake request: request result: result inputData: inputData outputData: outputData]; +} + +static void GetMyLeaderPositionCallback(SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData) +{ + [(AppDelegate*)userData getMyLeaderPositionCallback: sake request: request result: result inputData: inputData outputData: outputData]; +} diff --git a/Samples/TapRace/iphone/BuddyListCellView.h b/Samples/TapRace/iphone/BuddyListCellView.h new file mode 100644 index 00000000..09f2f0b9 --- /dev/null +++ b/Samples/TapRace/iphone/BuddyListCellView.h @@ -0,0 +1,42 @@ +// +// BuddyListCellView.h +// +// Created by CW Hicks 10/21/08. +// Copyright 2008 IGN. All rights reserved. +// + +#import + +@interface BuddyListCellView : UIView +{ + IBOutlet UIView* cellView; + IBOutlet UIImageView* thumbnail; + IBOutlet UILabel* nameLabel; + IBOutlet UILabel* indexLabel; + IBOutlet UILabel* timeLabel; + + int profileId; + unsigned int pictureFileId; + int index; + + int cannotDie; // ref count for number of outstanding callbacks + bool mustDie; + + IBOutlet UIActivityIndicatorView* busy; +} + +@property (nonatomic, retain) UIView * cellView; +@property int profileId; +@property unsigned int pictureFileId; +@property int index; + +- (void)setName: (NSString*)name; +- (void)setIndex: (int)newIndex; +- (void)setImage: (UIImage*)image; +- (void)setTime: (int)newTime; + +- (id)initCellView; // call to load from xib +- (void)getImageFromNetwork; + +@end + diff --git a/Samples/TapRace/iphone/BuddyListCellView.mm b/Samples/TapRace/iphone/BuddyListCellView.mm new file mode 100644 index 00000000..bdd4fad7 --- /dev/null +++ b/Samples/TapRace/iphone/BuddyListCellView.mm @@ -0,0 +1,280 @@ +// +// BuddyListCellView.mm +// +// Created by CW Hicks on 10/21/08. +// Copyright 2008 IGN. All rights reserved. +// + +#import "BuddyListCellView.h" +#import "Utility.h" +#import "AppDelegate.h" +#import "sake/sake.h" + +void SakeSearchForBuddyRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +GHTTPBool DownloadBuddyImageCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param); + +@implementation BuddyListCellView + +@synthesize cellView; +@synthesize profileId; +@synthesize pictureFileId; +@synthesize index; + +- (id) initCellView +{ + [[NSBundle mainBundle] loadNibNamed: @"buddyListCell" owner: self options: nil]; + return self; +} + + +- (id)initWithFrame:(CGRect)frame +{ + if ( (self = [super initWithFrame:frame]) ) + { + // Initialization code + } + return self; +} + +- (void)awakeFromNib +{ +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + // Configure the view for the selected state +} + + +- (void)dealloc +{ + if (cannotDie > 0) + { + // this must be preserved until the last callback returns + mustDie = YES; + [self retain]; // bump the ref count so won't be released + return; + } + + [cellView release]; + [thumbnail release]; + [nameLabel release]; + [indexLabel release]; + [timeLabel release]; + [busy release]; + + [super dealloc]; +} + +- (void)setName: (NSString*)name +{ + nameLabel.text = [name copy]; +} + +- (void)setIndex: (int)newIndex +{ + index = newIndex-1; + indexLabel.text = [NSString stringWithFormat: @"%d", newIndex]; +} + +- (void)setImage: (UIImage*)image +{ + [thumbnail setImage:image]; +} + +- (void)setTime: (int)newTime +{ + SetTimeLabel( timeLabel, newTime ); +} + +- (void) drawThumbnailFromImage: (UIImage*)image +{ + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: index]; + + // resize the image to represent the profile size and get the current colorspace + int imgh = image.size.height; + int imgw = image.size.width; + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB( ); + void * data = calloc(imgh, 4*imgw); + CGContextRef thumbnailContextRef = CGBitmapContextCreate ( data, // void *data, + imgw, // size_t width, + imgh, // size_t height, + 8, // size_t bitsPerComponent, + 4*imgw, // size_t bytesPerRow, + colorspace, // CGColorSpaceRef colorspace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big // CGBitmapInfo bitmapInfo + ); + CGRect tnrect = CGRectMake( 0, 0, imgw, imgh ); + CGContextDrawImage ( thumbnailContextRef, tnrect, image.CGImage ); // draw (and resize) image + + + if (leader.thumbNail != nil) + [leader.thumbNail release]; + + NSLog(@"setting thumbnail for player[%d] = %d\n", index, leader.profileId); + + // set thumbnail for leader + leader.thumbNail = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( thumbnailContextRef )] retain]; + + CGContextRelease(thumbnailContextRef); + CGColorSpaceRelease(colorspace); + free(data); + + [busy stopAnimating]; + busy.hidden=YES; + [thumbnail setImage: leader.thumbNail]; + [thumbnail setNeedsDisplay]; +} + +- (GHTTPBool) downloadCompletedCallbackRequest: (GHTTPRequest) request result: (GHTTPResult) result buffer: (char *) buffer bufferLen: (GHTTPByteCount) bufferLen +{ + cannotDie--; + if (mustDie && (cannotDie == 0)) { + [self dealloc]; + return GHTTPTrue; + } + + if(result != GHTTPSuccess) + { + NSLog(@"File Download: GHTTP Error: %d\n", result); + ghttpCleanup(); + return GHTTPTrue; + } + // create a new thumbnail using the image in the buffer, then release the buffer + NSData * jpegImage = [NSData dataWithBytes:(const void *)buffer length: bufferLen]; + if( bufferLen ) + { + [self drawThumbnailFromImage: [UIImage imageWithData: jpegImage]]; + } + + ghttpCleanup(); + [busy stopAnimating]; + busy.hidden=YES; + return GHTTPTrue; // frees the buffer +} + +- (void) downloadImageToThumbnail: (int) fileId forProfile: (int) aProfileId +{ + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: index]; + leader.pictureFileId = fileId; + NSLog(@"setting fileid[%d] for player[%d]=%d\n", fileId, index, leader.profileId); + + // if image is blocked, do not load it + if( [ appDelegate imageIsBlocked: fileId] ) + { + //leader.thumbNail = nil; + [busy stopAnimating]; + busy.hidden=YES; + return; + } + + GHTTPRequest request; + gsi_char url[SAKE_MAX_URL_LENGTH]; + + ghttpStartup(); + if (sakeGetFileDownloadURL(appDelegate.sake, fileId, url)) + { + NSLog(@"Download image for player[%d] URL: %s\n", leader.profileId, url); + + // download the file + request = ghttpGetEx(url, NULL, NULL, 0, NULL, GHTTPFalse, GHTTPFalse, NULL, DownloadBuddyImageCompletedCallback, self); + if (request == -1) + { + NSLog(@"Error starting image file download\n"); + } + else + { + cannotDie++; + [appDelegate startThinkTimer]; + return; + } + } + else + { + NSLog(@"Failed to get download url!\n"); + } + + ghttpCleanup(); + [busy stopAnimating]; + busy.hidden=YES; +} + +- (void) sakeSearchForRecordsRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + cannotDie--; + if (mustDie && (cannotDie == 0)) + { + [self dealloc]; + return; + } + + SAKESearchForRecordsInput* inData = (SAKESearchForRecordsInput*)inputData; + if(result == SAKERequestResult_SUCCESS) + { + if ( ((SAKESearchForRecordsOutput*)outputData)->mNumRecords > 0) + { + [self downloadImageToThumbnail: ((SAKEGetMyRecordsOutput*)outputData)->mRecords[0]->mValue.mInt + forProfile: (inData)->mOwnerIds[0] ]; + return; + } + else + { + //XNSLog(@"No existing profile for player[%d]\n", *((SAKESearchForRecordsInput*)(request)->mInput)->mOwnerIds[0]); + NSLog(@"No existing profile for player[%d]\n", (inData)->mOwnerIds[0]); + } + } + else + { + NSLog(@"Failed to search for player[%d] record, error: %d\n", *((inData)->mOwnerIds) ,result); + } + [busy stopAnimating]; + busy.hidden=YES; +} + +- (void)getImageFromNetwork +{ + SAKERequest request; + SAKEStartRequestResult startRequestResult; + static SAKESearchForRecordsInput sfr_input; + static char * fieldNames[] = { (char *) "picture" }; + int ownerids[] = { profileId }; + + sfr_input.mTableId = (char *) "Profiles"; + sfr_input.mFieldNames = fieldNames; + sfr_input.mNumFields = sizeof(fieldNames) / sizeof(fieldNames[0]); + sfr_input.mSort = (char *) "recordid asc"; + sfr_input.mOffset = 0; + sfr_input.mMaxRecords = 1; + sfr_input.mOwnerIds = ownerids; + sfr_input.mNumOwnerIds = 1; + + NSLog(@"get fileid for [%d]\n", profileId); + request = sakeSearchForRecords(appDelegate.sake, &sfr_input, SakeSearchForBuddyRecordsCallback, self ); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start search for records request: %d\n", startRequestResult); + [busy stopAnimating]; + + } + else + { + NSLog(@"Searching for picture fileid record for player [%d]\n", profileId); + cannotDie++; + } +} + + +@end + +void SakeSearchForBuddyRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + [(BuddyListCellView *)userData sakeSearchForRecordsRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + +GHTTPBool DownloadBuddyImageCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param) +{ + return [(BuddyListCellView *)param downloadCompletedCallbackRequest: request result: result buffer: buffer bufferLen: bufferLen]; +} + + diff --git a/Samples/TapRace/iphone/CreateUserController.h b/Samples/TapRace/iphone/CreateUserController.h new file mode 100644 index 00000000..ba45eee0 --- /dev/null +++ b/Samples/TapRace/iphone/CreateUserController.h @@ -0,0 +1,34 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import "gp/gp.h" + +@class LoginController; + +@interface CreateUserController : UIViewController +{ + IBOutlet UITextField* emailField; + IBOutlet UITextField* userNameField; + IBOutlet UITextField* passwordField; + IBOutlet UISwitch* autoLoginSwitch; + + LoginController* loginController; + + NSTimer* loginTimer; + + GPConnection connection; + GPProfile profile; +} + +@property(nonatomic, assign) LoginController* loginController; +@property(nonatomic, readonly) GPConnection connection; +@property(nonatomic, readonly) GPProfile profile; +@property(nonatomic, readonly) NSString* uniquenick; +@property(nonatomic, readonly) NSString* password; +@property(nonatomic, retain) UISwitch * autoLoginSwitch; + +- (IBAction)createButtonClicked: (id)sender; +- (IBAction)returnClicked: (id)sender; + +@end diff --git a/Samples/TapRace/iphone/CreateUserController.mm b/Samples/TapRace/iphone/CreateUserController.mm new file mode 100644 index 00000000..e6a3eb4b --- /dev/null +++ b/Samples/TapRace/iphone/CreateUserController.mm @@ -0,0 +1,248 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "CreateUserController.h" +#import "LoginController.h" +#import "AppDelegate.h" +#import "Utility.h" + + +static void ConnectNewUserCallback(GPConnection* connection, GPConnectResponseArg* connectResponse, CreateUserController* controller); +static void ConnectCallback(GPConnection* connection, GPConnectResponseArg* connectResponse, CreateUserController* controller); +static void RegisterUniqueNickCallback(GPConnection* connection, GPRegisterUniqueNickResponseArg* registerResponse, CreateUserController* controller); + + +@interface CreateUserController(Private) + +- (void)doneClicked: (id)sender; +- (void)onLoginTimer: (NSTimer*)timer; +- (void)connectNewUserCallback: (GPResult)result profile: (GPProfile)profile uniquenick: (const char[])uniquenick; +- (void)connectCallback: (GPResult)result profile: (GPProfile)profile uniquenick: (const char[])uniquenick; +- (void)registerUniqueNickCallback: (GPResult)result; + +@end + + +@implementation CreateUserController + +@synthesize loginController; +@synthesize connection; +@synthesize profile; +@synthesize autoLoginSwitch; + +- (void)dealloc +{ + [autoLoginSwitch release]; + [emailField release]; + [userNameField release]; + [passwordField release]; + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + +- (NSString*)uniquenick +{ + return userNameField.text; +} + +- (NSString*)password +{ + return passwordField.text; +} + +- (IBAction)createButtonClicked: (id)sender +{ + // Check for no account info + //////////////////////////// + if(([userNameField.text length] == 0) || ([passwordField.text length] == 0)) + { + MessageBox(@"Please fill in all the account information."); + return; + } + + if (gpInitialize(&connection, SCRACE_PRODUCTID, WSLogin_NAMESPACE_SHARED_UNIQUE, GP_PARTNERID_GAMESPY) != GP_NO_ERROR) + { + MessageBox(@"Error initializing the login system."); + return; + } + + const char* nick = [userNameField.text cStringUsingEncoding: NSASCIIStringEncoding]; + const char* email = [emailField.text cStringUsingEncoding: NSASCIIStringEncoding]; + const char* password = [passwordField.text cStringUsingEncoding: NSASCIIStringEncoding]; + + GPResult result = gpConnectNewUserA(&connection, nick, nick, email, password, NULL, GP_FIREWALL, GP_NON_BLOCKING, (GPCallback)ConnectNewUserCallback, self); + + if (result != GP_NO_ERROR) { + char errorString[GP_ERROR_STRING_LEN]; + + switch (result) { + case GP_PARAMETER_ERROR: + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding], @"Invalid Data"); + //MessageBox(@"One of the input fields contains invalid data."); + break; + + default: + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding]); + } + + gpDisconnect(&connection); + gpDestroy(&connection); + return; + } + + loginTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onLoginTimer:) userInfo: nil repeats: YES]; +} + +- (IBAction)returnClicked: (id)sender +{ + UITextField* textFields[] = { emailField, userNameField, passwordField }; + size_t textFieldCount = sizeof(textFields) / sizeof(textFields[0]); + size_t textFieldIndex = 0; + + // Find the text field that generated the event. + while (textFields[textFieldIndex] != sender) + textFieldIndex++; + + // Cycle focus to the next text field. + if (++textFieldIndex < textFieldCount) { + [textFields[textFieldIndex] becomeFirstResponder]; + } + else { + [(UIResponder*)sender resignFirstResponder]; + } +} + +@end + + +@implementation CreateUserController(Private) + +- (void)textFieldDidBeginEditing: (UITextField*)textField +{ + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(doneClicked:)]; +} + +- (void)textFieldDidEndEditing:(UITextField *)textField +{ + self.navigationItem.rightBarButtonItem = nil; +} + +- (void)doneClicked: (id)sender +{ + UITextField* textFields[] = { emailField, userNameField, passwordField }; + size_t textFieldCount = sizeof(textFields) / sizeof(textFields[0]); + + for (size_t n = 0; n < textFieldCount; n++) { + [textFields[n] resignFirstResponder]; + } +} + +- (void)onLoginTimer: (NSTimer*)timer +{ + gpProcess(&connection); +} + +- (void)connectNewUserCallback: (GPResult)result profile: (GPProfile)userProfile uniquenick: (const char[])uniquenick +{ + [loginTimer invalidate]; + loginTimer = nil; + + if (result != GP_NO_ERROR) { + GPErrorCode errorCode; + char errorString[GP_ERROR_STRING_LEN]; + gpGetErrorCode(&connection, &errorCode); + + switch (errorCode) { + case GP_NEWUSER_BAD_PASSWORD: + MessageBox(@"There is an existing account with the given e-mail address, but the password does not match. To use your existing " + @"GameSpy account, provide the correct password for the account.", @"Invalid Password"); + break; + + case GP_NEWUSER_BAD_NICK: + { + // This may mean that the user already has a profile with this nick; try to connect to it, and then try + // to register a corresponding uniquenick. + const char* nick = [userNameField.text cStringUsingEncoding: NSASCIIStringEncoding]; + const char* email = [emailField.text cStringUsingEncoding: NSASCIIStringEncoding]; + const char* password = [passwordField.text cStringUsingEncoding: NSASCIIStringEncoding]; + + gpDestroy(&connection); + gpInitialize(&connection, SCRACE_PRODUCTID, WSLogin_NAMESPACE_SHARED_UNIQUE, GP_PARTNERID_GAMESPY); + gpConnectA(&connection, nick, email, password, GP_FIREWALL, GP_NON_BLOCKING, (GPCallback)ConnectCallback, self); + loginTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onLoginTimer:) userInfo: nil repeats: YES]; + return; + } + + default: + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding], @"Try Again" ); + } + + gpDisconnect(&connection); + gpDestroy(&connection); + return; + } + + profile = userProfile; + [loginController userProfileCreated: self autoLogin: autoLoginSwitch.on]; +} + +- (void)connectCallback: (GPResult)result profile: (GPProfile)userProfile uniquenick: (const char[])uniquenick +{ + if (result != GP_NO_ERROR) { + char errorString[GP_ERROR_STRING_LEN]; + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding]); + gpDisconnect(&connection); + gpDestroy(&connection); + [loginTimer invalidate]; + loginTimer = nil; + return; + } + + profile = userProfile; + gpRegisterUniqueNickA(&connection, [userNameField.text cStringUsingEncoding: NSASCIIStringEncoding], NULL, GP_NON_BLOCKING, (GPCallback)RegisterUniqueNickCallback, self); +} + +- (void)registerUniqueNickCallback: (GPResult)result +{ + [loginTimer invalidate]; + loginTimer = nil; + + if (result != GP_NO_ERROR) { + char errorString[GP_ERROR_STRING_LEN]; + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding]); + gpDisconnect(&connection); + gpDestroy(&connection); + return; + } + + [loginController userProfileCreated: self autoLogin: autoLoginSwitch.on]; +} + +@end + + +void ConnectNewUserCallback(GPConnection* connection, GPConnectResponseArg* connectResponse, CreateUserController* controller) +{ + [controller connectNewUserCallback: connectResponse->result profile: connectResponse->profile uniquenick: connectResponse->uniquenick]; +} + +void ConnectCallback(GPConnection* connection, GPConnectResponseArg* connectResponse, CreateUserController* controller) +{ + [controller connectCallback: connectResponse->result profile: connectResponse->profile uniquenick: connectResponse->uniquenick]; +} + +void RegisterUniqueNickCallback(GPConnection* connection, GPRegisterUniqueNickResponseArg* registerResponse, CreateUserController* controller) +{ + [controller registerUniqueNickCallback: registerResponse->result]; +} + diff --git a/Samples/TapRace/iphone/FlipsideView.h b/Samples/TapRace/iphone/FlipsideView.h new file mode 100644 index 00000000..77da06e7 --- /dev/null +++ b/Samples/TapRace/iphone/FlipsideView.h @@ -0,0 +1,13 @@ +// +// FlipsideView.h +// flipview +// +// Created by Paul Berlinguette on 6/11/09. +// Copyright __MyCompanyName__ 2009. All rights reserved. +// + +@interface FlipsideView : UIView { + +} + +@end diff --git a/Samples/TapRace/iphone/FlipsideView.m b/Samples/TapRace/iphone/FlipsideView.m new file mode 100644 index 00000000..27356c40 --- /dev/null +++ b/Samples/TapRace/iphone/FlipsideView.m @@ -0,0 +1,32 @@ +// +// FlipsideView.m +// flipview +// +// Created by Paul Berlinguette on 6/11/09. +// Copyright __MyCompanyName__ 2009. All rights reserved. +// + +#import "FlipsideView.h" + +@implementation FlipsideView + + +- (id)initWithFrame:(CGRect)frame { + if ( (self = [super initWithFrame:frame]) ) { + // Initialization code + } + return self; +} + + +- (void)drawRect:(CGRect)rect { + // Drawing code +} + + +- (void)dealloc { + [super dealloc]; +} + + +@end diff --git a/Samples/TapRace/iphone/FlipsideViewController.h b/Samples/TapRace/iphone/FlipsideViewController.h new file mode 100644 index 00000000..05f99bfa --- /dev/null +++ b/Samples/TapRace/iphone/FlipsideViewController.h @@ -0,0 +1,27 @@ +// +// FlipsideViewController.h +// flipview +// +// Created by Paul Berlinguette on 6/11/09. +// Copyright __MyCompanyName__ 2009. All rights reserved. +// +#import + +@protocol FlipsideViewControllerDelegate; + +@interface FlipsideViewController : UIViewController { + id delegate; + IBOutlet UIButton* clearBlockedImagesButton; +} + +@property (nonatomic, assign) id delegate; +- (IBAction)done; +- (IBAction)contactUs:(id)sender; +- (IBAction)clearBlockedImagesFile; + +@end +@protocol FlipsideViewControllerDelegate + +- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; +@end + diff --git a/Samples/TapRace/iphone/FlipsideViewController.mm b/Samples/TapRace/iphone/FlipsideViewController.mm new file mode 100644 index 00000000..0da18687 --- /dev/null +++ b/Samples/TapRace/iphone/FlipsideViewController.mm @@ -0,0 +1,109 @@ +// +// FlipsideViewController.m +// flipview +// +// Created by Paul Berlinguette on 6/11/09. +// Copyright __MyCompanyName__ 2009. All rights reserved. +// +#import "Utility.h" +#import "AppDelegate.h" +#import "FlipsideViewController.h" + + + +@implementation FlipsideViewController + +@synthesize delegate; + + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor]; +} + + +- (IBAction)done +{ + [ appDelegate flipsideViewControllerDidFinish: self.delegate]; +} + +#pragma mark User Interface Actions + +- (IBAction)contactUs:(id)sender { + + // Warn if cannot send mail + if(! [MFMailComposeViewController canSendMail] ) + { + MessageBox( @"Device is unable to send an email at the moment."); + return; + } + + MFMailComposeViewController *composeVC = [[MFMailComposeViewController alloc] init]; + + composeVC.mailComposeDelegate = self; + + [composeVC setSubject:@"GameSpy TapRace - Request info on iPhone tech"]; + + NSString *messageBody = [NSString stringWithFormat:@"Hi,\n\n" + "I am a developer and I have played TapRace on the iPhone and would like more information on GameSpy's social gaming and multi-player services\n\n" + "Please contact me at xxx-xxx-xxxx or reply to my email.\n\n" + "Thanks you,\n\n" + "\n\n" + ];//"Thanks, - %@ by %@\n", title, artist]; + [composeVC setMessageBody:messageBody isHTML:NO]; + + [composeVC setToRecipients: [NSArray arrayWithObject:@"devrelations@gamespy.com"]]; + + [self presentModalViewController:composeVC animated:YES]; + [composeVC release]; +} + +- (IBAction)clearBlockedImagesFile +{ + NSString* dataPath; + + //*** Remove player's block image file + dataPath = [gPlayerData.playerDataPath stringByAppendingPathComponent: blockedImagesDataFile]; + + BOOL isDir = NO; + NSFileManager* fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath: dataPath isDirectory: &isDir]) + { + [fileManager removeItemAtPath: dataPath error: NULL]; + + } + + [appDelegate deleteBlockedImagesData ]; +} + + +- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { + [self dismissModalViewControllerAnimated:YES]; +} + +/* + // Override to allow orientations other than the default portrait orientation. + - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); + } + */ + +- (void)didReceiveMemoryWarning { + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + + // Release any cached data, images, etc that aren't in use. +} + +- (void)viewDidUnload { + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; +} + + +- (void)dealloc { + [super dealloc]; +} + +@end diff --git a/Samples/TapRace/iphone/ForgotPassword.h b/Samples/TapRace/iphone/ForgotPassword.h new file mode 100644 index 00000000..c6228e44 --- /dev/null +++ b/Samples/TapRace/iphone/ForgotPassword.h @@ -0,0 +1,16 @@ +// +// ForgotPassword.h +// Gamespy +// +// Created by Puap Puap on 10/27/08. +// Copyright 2008 Pick Up And Play. All rights reserved. +// + +#import + + +@interface ForgotPassword : UIViewController { + IBOutlet UIWebView * webView; +} + +@end diff --git a/Samples/TapRace/iphone/ForgotPassword.mm b/Samples/TapRace/iphone/ForgotPassword.mm new file mode 100644 index 00000000..32aabc22 --- /dev/null +++ b/Samples/TapRace/iphone/ForgotPassword.mm @@ -0,0 +1,59 @@ +// +// ForgotPassword.mm +// Gamespy +// +// Created by Puap Puap on 10/27/08. +// Copyright 2008 Pick Up And Play. All rights reserved. +// + +#import "ForgotPassword.h" + + +@implementation ForgotPassword + +// Override initWithNibName:bundle: to load the view using a nib file then perform additional customization that is not appropriate for viewDidLoad. +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if ( (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) ) + { + NSString* stringurl = @"https://login.gamespyid.com/lostpassword.aspx"; + NSURL * url = [NSURL URLWithString: stringurl]; +// NSURLRequest * urlrequest = [NSURLRequest requestWithURL: url]; +// [webView loadRequest: urlrequest]; + + [[UIApplication sharedApplication] openURL:url]; + } + return self; +} + +/* +// Implement loadView to create a view hierarchy programmatically. +- (void)loadView { +} +*/ + +/* +// Implement viewDidLoad to do additional setup after loading the view. +- (void)viewDidLoad { + [super viewDidLoad]; +} +*/ + + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview + // Release anything that's not essential, such as cached data +} + + +- (void)dealloc { + [super dealloc]; +} + + +@end diff --git a/Samples/TapRace/iphone/GameController.h b/Samples/TapRace/iphone/GameController.h new file mode 100644 index 00000000..ca258bea --- /dev/null +++ b/Samples/TapRace/iphone/GameController.h @@ -0,0 +1,49 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import + + +@interface GameController : UIViewController +{ + IBOutlet UIButton* tapButton1; + IBOutlet UIButton* tapButton2; + IBOutlet UIProgressView* localProgress; + IBOutlet UIProgressView* remoteProgress; + IBOutlet UILabel* localPlayerLabel; + IBOutlet UILabel* remotePlayerLabel; + IBOutlet UILabel* gameTimeLabel; + IBOutlet UILabel* previousBestLabel; + IBOutlet UILabel* previousBestTimeLabel; + IBOutlet UILabel* messageLabel; + IBOutlet UIActivityIndicatorView* activityIndicator; + + NSTimer* updateTimer; + NSArray* gameControls; + + NSString* initialMessageText; + + int numSteps, prevNumSteps; + + bool opponentFinished; +} + +@property(nonatomic, readonly) int numSteps; +@property(nonatomic, readwrite ) int prevNumSteps; + +- (IBAction)startButtonClicked: (id)sender; +- (IBAction)tapButtonPressed: (id)sender; +- (IBAction)showInfo; +- (IBAction)gotoMenu; + +//- (void)gotoMenu: (id)sender; +- (void)countdown: (int)count; +- (void)stopRaceClock; +- (void)raceStarted; +- (void)doneRacing; +- (void)remoteProgress: (int)progress; +- (void)setRemoteNick: (NSString*)nick; + +- (void)reset; + +@end diff --git a/Samples/TapRace/iphone/GameController.mm b/Samples/TapRace/iphone/GameController.mm new file mode 100644 index 00000000..33825a14 --- /dev/null +++ b/Samples/TapRace/iphone/GameController.mm @@ -0,0 +1,297 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "GameController.h" +#import "AppDelegate.h" +#import "Utility.h" + +#import + +#if TARGET_IPHONE_SIMULATOR +static const int TOTAL_STEPS = 3; +#else +static const int TOTAL_STEPS = 60; +#endif + + + +@interface GameController(Private) + +- (void)updateTime: (NSTimer*)timer; + +@end + + +@implementation GameController + +@synthesize numSteps; +@synthesize prevNumSteps; + +- (id)initWithCoder: (NSCoder*)decoder +{ + self = [super initWithCoder: decoder]; + + if (self != nil) + { + numSteps = prevNumSteps = 0; + opponentFinished = false; + } + + return self; +} + +- (void)dealloc +{ + [tapButton1 release]; + [tapButton2 release]; + [localProgress release]; + [remoteProgress release]; + [localPlayerLabel release]; + [remotePlayerLabel release]; + [gameTimeLabel release]; + [previousBestLabel release]; + [previousBestTimeLabel release]; + [messageLabel release]; + [activityIndicator release]; + [gameControls release]; + [initialMessageText release]; + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + +- (void)viewDidLoad +{ + gameControls = [[NSArray alloc] initWithObjects: tapButton1, tapButton2, nil]; + + if (messageLabel != nil) + { + initialMessageText = [messageLabel.text copy]; + } + + // if Two-player game, set button back to matchmaking + if ( [appDelegate isTwoPlayerGame] ) + { + // set right button to go to menu page + UIBarButtonItem* aBarButton = [ [UIBarButtonItem alloc] initWithTitle:@"Menu" + style:UIBarButtonItemStyleBordered + target:self + action:@selector(gotoMenu)]; + self.navigationItem.rightBarButtonItem = aBarButton; + [aBarButton release]; + + aBarButton = [ [UIBarButtonItem alloc] initWithTitle:@"MatchMaking" + style:UIBarButtonItemStyleBordered + target:self + action:@selector(gotoMatchMaking)]; + self.navigationItem.leftBarButtonItem = aBarButton; + [aBarButton release]; + } +} + +- (void)viewWillAppear: (BOOL)animated +{ + // if Single-player game + if (! [appDelegate isTwoPlayerGame] ) + { + self.title = @"Single Player Game"; + + // show previous best time + if (gPlayerData.playerStatsData != nil) + { + int previousBestTimeMillis = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerBestTimeKey] intValue]; + + previousBestTimeLabel.text = [NSString stringWithFormat: timeFormatString, previousBestTimeMillis / (60 * 1000), (previousBestTimeMillis % (60 * 1000)) / 1000.0f]; + previousBestLabel.hidden = NO; + previousBestTimeLabel.hidden = NO; + + messageLabel.text= @"Start tapping the keys to begin!"; + messageLabel.hidden = NO; + } + else + { + previousBestLabel.hidden = YES; + previousBestTimeLabel.hidden = YES; + } + } +} + +- (void)viewWillDisappear: (BOOL)animated +{ + if (updateTimer != nil) + { + [updateTimer invalidate]; + updateTimer = nil; + } +} + +- (void)viewDidAppear: (BOOL)animated +{ + // Two-player game + if ( [appDelegate isTwoPlayerGame] ) + { + // start immediately + [appDelegate startCountdown]; + } + else if( appDelegate.fromMenu ) // Single player game + { + MessageBoxNoBlock(@"Alternate tapping the keys. The faster you tap, the better your time will be! ", @"How To Play", @"Continue" ); + } + +} + +// Display the About page +- (IBAction)showInfo +{ + [ appDelegate showAboutInfo: self ]; +} + +- (IBAction)startButtonClicked: (id)sender +{ + // Single-player game; tap button + [tapButton1 removeTarget: self action: @selector(startButtonClicked:) forControlEvents: UIControlEventAllEvents]; + [tapButton1 addTarget: self action: @selector(tapButtonPressed:) forControlEvents: UIControlEventTouchDown]; + + tapButton1.enabled = NO; + tapButton2.enabled = YES; + + messageLabel.hidden = YES; + + numSteps = 1; + localProgress.progress = numSteps / (float)TOTAL_STEPS; + + [appDelegate startCountdown]; +} + +- (IBAction)tapButtonPressed: (id)sender +{ + tapButton1.enabled = !tapButton1.enabled; + tapButton2.enabled = !tapButton2.enabled; + + ++numSteps; + localProgress.progress = numSteps / (float)TOTAL_STEPS; + + if(numSteps == TOTAL_STEPS) + { + if(!opponentFinished) + messageLabel.text = @"Waiting for opponent to finish race"; + + [appDelegate finishedGame]; + + [updateTimer invalidate]; + updateTimer = nil; + + tapButton1.enabled = NO; + tapButton2.enabled = NO; + } +} + +- (IBAction)gotoMenu +{ + [appDelegate returnToMenu]; +} + +- (IBAction)gotoMatchMaking +{ + [appDelegate returnToMatchMaking]; +} + +// stop race clock when disconnected or race canceled +- (void)stopRaceClock +{ + [updateTimer invalidate]; + updateTimer = nil; +} + + +// 5 second start of race countdown +- (void)countdown: (int)count +{ + if(count) + messageLabel.text = [NSString stringWithFormat: @"Race starts in %ds", count]; +} + +- (void)raceStarted +{ + updateTimer = [NSTimer scheduledTimerWithTimeInterval: TAP_POLL_TIME target: self selector: @selector(updateTime:) userInfo: nil repeats: YES]; + if (remoteProgress == nil) { + // If in single-player mode, the game is already active. + return; + } + + /*for (UIControl* control in gameControls) { + control.enabled = YES; + }*/ + tapButton1.enabled = YES; + tapButton2.enabled = NO; + + messageLabel.text = @"Go!"; + [activityIndicator stopAnimating]; +} + +- (void)doneRacing +{ +// uint64_t localTime = mach_absolute_time() - appDelegate.start; +// gameTimeLabel.text = [NSString stringWithFormat: timeFormatString, localTime / (60 * 1000), (localTime % (60 * 1000)) / 1000.0f]; +// gameTimeLabel.text = [NSString stringWithFormat: @"%0.2f", localTime * absolute_to_seconds ]; + + messageLabel.text = @"Race Complete"; + + localProgress.progress = 0.0; + remoteProgress.progress = 0.0; +} + +- (void)remoteProgress: (int)progress +{ + remoteProgress.progress = progress / (float)TOTAL_STEPS; +} + +- (void)setRemoteNick: (NSString*)nick +{ + remotePlayerLabel.text = nick; +} + +- (void)reset +{ + gameTimeLabel.text = [NSString stringWithFormat: timeFormatString, 0, 0.0]; + + if (remoteProgress == nil) + { + tapButton1.enabled = YES; + tapButton2.enabled = NO; + + // First touch starts the game. + [tapButton1 removeTarget: self action: NULL forControlEvents: UIControlEventAllEvents]; + [tapButton1 addTarget: self action: @selector(startButtonClicked:) forControlEvents: UIControlEventTouchDown]; + + numSteps = 0; + localProgress.progress = 0.0; + } + else + { + numSteps = 0; + + localProgress.progress = 0.0; + remoteProgress.progress = 0.0; + + [activityIndicator startAnimating]; + messageLabel.text = initialMessageText; + } +} + +@end + + +@implementation GameController(Private) + +- (void)updateTime: (NSTimer*)timer; +{ + unsigned int timeElapsed = (mach_absolute_time() - appDelegate.start) * absolute_to_millis; + gameTimeLabel.text = [NSString stringWithFormat: timeFormatString, timeElapsed / (60 * 1000), (timeElapsed % (60 * 1000)) / 1000.0f]; +} + +@end diff --git a/Samples/TapRace/iphone/GameResultsController.h b/Samples/TapRace/iphone/GameResultsController.h new file mode 100644 index 00000000..2dd1d728 --- /dev/null +++ b/Samples/TapRace/iphone/GameResultsController.h @@ -0,0 +1,72 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import + + +@interface GameResultsController : UIViewController +{ + // Single-player controls: + IBOutlet UILabel* gameTimeLabel; + IBOutlet UILabel* gamesPlayedLabel; + IBOutlet UILabel* bestTimeLabel; + IBOutlet UILabel* averageTimeLabel; + IBOutlet UILabel* congratsLabel; + IBOutlet UILabel* newbestLabel; + IBOutlet UILabel* newbestTimeLabel; + IBOutlet UILabel* oldbestLabel; + IBOutlet UILabel* avgTapsPerSecLabel; + + // Multi-player controls: + IBOutlet UILabel* tieLabel; + + IBOutlet UILabel* localWinnerLabel; + IBOutlet UILabel* localPlayerLabel; + IBOutlet UILabel* localTimeLabel; + IBOutlet UIButton* localThumbnailButton; // so that we can grey out loser + //IBOutlet UIImageView* localThumbnail; + + IBOutlet UILabel* remoteWinnerLabel; + IBOutlet UILabel* remotePlayerLabel; + IBOutlet UILabel* remoteTimeLabel; + IBOutlet UIButton* remoteThumbnailButton; + //IBOutlet UIImageView* remoteThumbnail; + + // Common controls: + IBOutlet UIButton* leaderboardsButton; + IBOutlet UIButton* playAgainButton; + IBOutlet UIButton* menuButton; + + IBOutlet UIActivityIndicatorView* busyLocal; + IBOutlet UIActivityIndicatorView* busyRemote; + + int localTime; + int remoteTime; +} + +- (IBAction)leaderboardsButtonClicked: (id)sender; +- (IBAction)playAgainButtonClicked: (id)sender; +- (IBAction)menuButtonClicked: (id)sender; +- (IBAction)viewOpponentClicked: (id)sender; +- (IBAction)viewMyStatsClicked: (id)sender; + +- (IBAction)showInfo; +- (IBAction)gotoMenu; +- (IBAction)gotoMatchMaking; + +- (void)setGamesPlayed: (int)gamesPlayed; +- (void)setBestTime: (int)bestTime; +- (void)setAverageTime: (int)averageTime; +- (void)setAverageTaps: (float) avgTaps; + +- (void)setLocalPlayer: (NSString*)playerName time: (int)time; +- (void)setRemotePlayer: (NSString*)playerName time: (int)time; + +- (void)setGameTime: (int)gameTime; +- (void)setWinner; +- (void)setNewBestTime: (int)oldTime; +- (void) getImagesForResultsPage; +- (void) drawThumbnailImage: (UIImage*)image forPlayer: (int) profileId; + +- (void)connectionDropped; +@end diff --git a/Samples/TapRace/iphone/GameResultsController.mm b/Samples/TapRace/iphone/GameResultsController.mm new file mode 100644 index 00000000..00068c14 --- /dev/null +++ b/Samples/TapRace/iphone/GameResultsController.mm @@ -0,0 +1,639 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "GameResultsController.h" +#import "AppDelegate.h" +#import "Utility.h" + +#import + +typedef struct serverParam_struct +{ + GameResultsController * controller; + int profileId; +} ServerParam; + +// callback prototypes +// http stuff to upload and download image +//void postCallback( GHTTPRequest request, int bytesPosted, int totalBytes, int objectsPosted, int totalObjects, void * param ); +//GHTTPBool UploadCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param); +GHTTPBool DownloadImageCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param); + +// sake stuff to access and create persistent server storage +//void SakeCreateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +//void SakeUpdateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +//void SakeTestRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +void SakeSearchForImageFileIdRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); + +//static SAKEField fields[2]; + +static bool discardCallbacks = true; + + + + +@interface GameResultsController(Private) + +- (void)determineWinner; + +@end + + +@implementation GameResultsController + +- (void)dealloc +{ + discardCallbacks = true; + [oldbestLabel release]; + [avgTapsPerSecLabel release]; + [congratsLabel release]; + [newbestLabel release]; + [newbestTimeLabel release]; + [gamesPlayedLabel release]; + [bestTimeLabel release]; + [averageTimeLabel release]; + [localWinnerLabel release]; + [remoteWinnerLabel release]; + [tieLabel release]; + [localPlayerLabel release]; + [localTimeLabel release]; + [localThumbnailButton release]; + [remotePlayerLabel release]; + [remoteTimeLabel release]; + [remoteThumbnailButton release]; + [gameTimeLabel release]; + [leaderboardsButton release]; + [playAgainButton release]; + [menuButton release]; + [super dealloc]; +} + + +// Common controls: +IBOutlet UIButton* leaderboardsButton; +IBOutlet UIButton* playAgainButton; +IBOutlet UIButton* menuButton; + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + +- (void)viewWillAppear: (BOOL)animated +{ + [super viewWillAppear: animated]; + self.title = @"Race Result"; + //self.backBarButtonItem.title = @"Menu"; + + //[localThumbnail setImage: gPlayerData.thumbNail ]; + //[remoteThumbnail setImage: gPlayerData.thumbNail ]; + self.navigationItem.hidesBackButton = YES; + discardCallbacks = false; +} + + +- (void)viewDidLoad +{ + // set right button to go to menu page + UIBarButtonItem* aBarButton = [ [UIBarButtonItem alloc] initWithTitle:@"Menu" + style:UIBarButtonItemStyleBordered + target:self + action:@selector(gotoMenu)]; + + // if Two-player game, set left button back to matchmaking + if( [ appDelegate isTwoPlayerGame ] ) + { + self.navigationItem.rightBarButtonItem = aBarButton; + [aBarButton release]; + + aBarButton = [ [UIBarButtonItem alloc] initWithTitle:@"MatchMaking" + style:UIBarButtonItemStyleBordered + target:self + action:@selector(gotoMatchMaking)]; + self.navigationItem.leftBarButtonItem = aBarButton; + [aBarButton release]; + } + else + { + self.navigationItem.leftBarButtonItem = aBarButton; + [aBarButton release]; + } +} + + +- (IBAction)viewOpponentClicked: (id)sender +{ + //[appDelegate showLeaderboards]; +} + +- (IBAction)viewMyStatsClicked: (id)sender +{ + [appDelegate showLeaderboards]; +} + + +- (IBAction)leaderboardsButtonClicked: (id)sender +{ + [appDelegate showLeaderboards]; +} + +- (IBAction)playAgainButtonClicked: (id)sender +{ + [appDelegate restartGame]; +} + +- (IBAction)menuButtonClicked: (id)sender +{ + [appDelegate returnToMenu]; +} + + +- (IBAction)gotoMatchMaking +{ + [appDelegate returnToMatchMaking]; +} + +- (IBAction)gotoMenu +{ + [appDelegate returnToMenu]; +} + +// Display the About page +- (IBAction)showInfo +{ + [ appDelegate showAboutInfo: self ]; +} + +- (void)setGamesPlayed: (int)gamesPlayed +{ + gamesPlayedLabel.text = [NSString stringWithFormat: @"%d", gamesPlayed]; +} + + +- (void)setBestTime: (int)bestTime +{ + SetTimeLabel(bestTimeLabel, bestTime); +} + +- (void)setNewBestTime: (int)oldTime +{ + SetTimeLabel(newbestTimeLabel, oldTime); + congratsLabel.hidden = NO; + newbestLabel.hidden = NO; + newbestTimeLabel.hidden = NO; + oldbestLabel.hidden = NO; +} + + +- (void) getImagesForResultsPage +{ + SAKERequest request; + SAKEStartRequestResult startRequestResult; + static SAKESearchForRecordsInput sfr_input; + static char * fieldNames[] = { (char *) "ownerid", (char *) "picture" }; + //int ownerids[3]; + static int ownerids[2] = { gPlayerData.profileId, gOpponentData.profileId }; + + if( gPlayerData.thumbNail == nil && gOpponentData.thumbNail == nil ) + { + ownerids[0] = gPlayerData.profileId; + ownerids[1] = gOpponentData.profileId; +// ownerids[2] = '\0'; + + [busyLocal startAnimating ]; + [busyRemote startAnimating ]; + busyLocal.hidden=NO; + busyRemote.hidden=NO; + + sfr_input.mNumOwnerIds = 2; + sfr_input.mMaxRecords = 2; + NSLog(@"getImagesForResultsPage for player[%d] and opponent[%d]",gPlayerData.profileId,gOpponentData.profileId); + } + else if( gPlayerData.thumbNail != nil ) + { + // display player picture + [localThumbnailButton setBackgroundImage:gPlayerData.thumbNail forState: UIControlStateNormal ]; + busyLocal.hidden=YES; + [busyLocal stopAnimating]; + + // get opponent picture + ownerids[0] = gOpponentData.profileId; + ownerids[1] = '\0'; + sfr_input.mNumOwnerIds = 1; + sfr_input.mMaxRecords = 1; + [busyRemote startAnimating ]; + busyLocal.hidden=NO; + NSLog(@"getImagesForResultsPage for opponent"); + } + else if( gOpponentData.thumbNail != nil ) + { + // display opponent picture + [remoteThumbnailButton setBackgroundImage:gOpponentData.thumbNail forState: UIControlStateNormal ]; + busyRemote.hidden=YES; + [busyRemote stopAnimating]; + + // get player picture + ownerids[0] = gPlayerData.profileId; + ownerids[1] = '\0'; + sfr_input.mNumOwnerIds = 1; + sfr_input.mMaxRecords = 1; + busyLocal.hidden=NO; + [busyLocal startAnimating]; + NSLog(@"getImagesForResultsPage for player"); + } + else + { + // get no pictures + busyLocal.hidden=YES; + busyRemote.hidden=YES; + [busyLocal stopAnimating]; + [busyRemote stopAnimating]; + + [localThumbnailButton setBackgroundImage:gPlayerData.thumbNail forState: UIControlStateNormal ]; + [remoteThumbnailButton setBackgroundImage:gOpponentData.thumbNail forState: UIControlStateNormal ]; + NSLog(@"getImagesForResultsPage for no one"); + return; + } + + sfr_input.mTableId = (char *) "Profiles"; + sfr_input.mFieldNames = fieldNames; + sfr_input.mNumFields = 2; + sfr_input.mSort = (char *) "recordid asc"; + sfr_input.mOffset = 0; + sfr_input.mOwnerIds = ownerids; + + request = sakeSearchForRecords(appDelegate.sake, &sfr_input, SakeSearchForImageFileIdRecordsCallback, self ); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start search for records request: %d\n", startRequestResult); + } + else + { + NSLog(@"Searching for player record\n"); + } +} + +- (void) downloadThumbnail: (int) fileId forPlayer: (int) profileId +{ + GHTTPRequest request; + gsi_char url[SAKE_MAX_URL_LENGTH]; + + ghttpStartup(); + if (sakeGetFileDownloadURL(appDelegate.sake, fileId, url)) + { + NSLog(@"Download image URL: %s\n", url); + + ServerParam *param = (ServerParam*) gsimalloc(sizeof(ServerParam)); + param->controller = self; + param->profileId = profileId; + + // download the file + request = ghttpGetEx(url, NULL, NULL, 0, NULL, GHTTPFalse, GHTTPFalse, NULL, DownloadImageCompletedCallback, (void *) param); + if (request == -1) + { + gsifree(param); + NSLog(@"Error starting image file download\n"); + if( (unsigned int)fileId == gPlayerData.pictureFileId ) + [busyLocal stopAnimating]; + else + [busyRemote stopAnimating]; + } + else + { + [appDelegate startThinkTimer]; + return; + } + } + else + { + NSLog(@"Failed to get download url!\n"); + + if( (unsigned int)fileId == gPlayerData.pictureFileId ) + [busyLocal stopAnimating]; + else + [busyRemote stopAnimating]; + } + ghttpCleanup(); +} + + +- (void)setAverageTime: (int)averageTime +{ + SetTimeLabel(averageTimeLabel, averageTime); +} + +- (void)setAverageTaps: (float) avgTaps +{ + avgTapsPerSecLabel.text = [NSString stringWithFormat: @"%5.3f", avgTaps]; +} +- (void)setLocalPlayer: (NSString*)playerName time: (int)time +{ + localPlayerLabel.text = playerName; + SetTimeLabel(localTimeLabel, time); + localTime = time; +} + +- (void)setRemotePlayer: (NSString*)playerName time: (int)time +{ + remotePlayerLabel.text = playerName; + SetTimeLabel(remoteTimeLabel, time); + remoteTime = time; +} + +- (void)setGameTime: (int)gameTime +{ + SetTimeLabel(gameTimeLabel, gameTime); +} + +- (void)setWinner +{ + [self determineWinner]; +} + +- (void)connectionDropped +{ + playAgainButton.enabled = NO; +} + +// Resize the downloaded image and save in gPlayerData.fullsizeImage to represent the profile size +// Resize the fullsizeimage to and save in gPlayerData.thumbnail to represent the leaderboard thumbnail size +// Then draw thumbnail +- (void) drawThumbnailImage: (UIImage*)image forPlayer: (int) profileId +{ + + // get the current colorspace + int imgh = image.size.height; + int imgw = image.size.width; + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB( ); + void * data = calloc(imgh, 4*imgw); + CGContextRef bmpContextRef = CGBitmapContextCreate ( data, // void *data, + imgw, // size_t width, + imgh, // size_t height, + 8, // size_t bitsPerComponent, + 4*imgw, // size_t bytesPerRow, + colorspace, // CGColorSpaceRef colorspace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big // CGBitmapInfo bitmapInfo + ); + CGRect fsrect = CGRectMake( 0, 0, imgw, imgh ); + CGContextDrawImage ( bmpContextRef, fsrect, image.CGImage ); // draw (and resize) image + //if (gPlayerData.fullsizeImage != nil) [gPlayerData.fullsizeImage release]; + //gPlayerData.fullsizeImage = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( bmpContextRef )] retain]; + CGContextRelease(bmpContextRef); + free(data); + + // now redraw into thumbnail + int imageOrientation = image.imageOrientation; + int th,tw; + float hscale = ((float)imgh)/float(THUMBNAIL_HEIGHT); + float wscale = ((float)imgw)/float(THUMBNAIL_WIDTH); + if (hscale > wscale) { + // scale to height + th = THUMBNAIL_HEIGHT; + tw = (int)((float)imgw/hscale); + } else { + tw = THUMBNAIL_WIDTH; + th = (int)((float)imgh/wscale); + } + data = calloc(th, 4*tw); + CGContextRef thumbnailContextRef = CGBitmapContextCreate ( NULL, // void *data, + tw, // size_t width, + th, // size_t height, + 8, // size_t bitsPerComponent, + 4*tw, // size_t bytesPerRow, + colorspace, // CGColorSpaceRef colorspace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big // CGBitmapInfo bitmapInfo + ); + CGRect tnrect; + // rotate the context + switch (imageOrientation) { + case 0: // no rotation + tnrect = CGRectMake( 0, 0, tw, th ); + break; + case 3: // rotate -90 + tnrect = CGRectMake( 0, 0, th, tw ); + CGContextTranslateCTM (thumbnailContextRef, 0.0f, float(th) ); + CGContextRotateCTM (thumbnailContextRef, -3.1415926f/2.0f); + break; + case 2: // rotate 180 + tnrect = CGRectMake( 0, 0, tw, th ); + CGContextTranslateCTM (thumbnailContextRef, float(tw), float(th) ); + CGContextRotateCTM (thumbnailContextRef, -3.1415926f); + break; + case 1: // rotate 90 + tnrect = CGRectMake( 0, 0, th, tw ); + CGContextTranslateCTM (thumbnailContextRef, float(tw), 0.0f ); + CGContextRotateCTM (thumbnailContextRef, 3.1415926f/2.0f); + break; + } + CGContextDrawImage ( thumbnailContextRef, tnrect, image.CGImage ); // draw image + + if( (unsigned int)profileId == gPlayerData.profileId ) + { + if (gPlayerData.thumbNail != nil) [gPlayerData.thumbNail release]; + gPlayerData.thumbNail = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( thumbnailContextRef )] retain]; + CGContextRelease(thumbnailContextRef); + CGColorSpaceRelease(colorspace); + + [busyLocal stopAnimating]; + busyLocal.hidden=YES; + [localThumbnailButton setBackgroundImage:gPlayerData.thumbNail forState: UIControlStateNormal ]; + //[localThumbnailButton setImage:gPlayerData.thumbNail forState: UIControlStateNormal ]; + + [localThumbnailButton setNeedsDisplay]; + } + else + { + if (gOpponentData.thumbNail != nil) [gOpponentData.thumbNail release]; + gOpponentData.thumbNail = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( thumbnailContextRef )] retain]; + CGContextRelease(thumbnailContextRef); + CGColorSpaceRelease(colorspace); + + [busyRemote stopAnimating]; + busyRemote.hidden=YES; + [remoteThumbnailButton setBackgroundImage:gOpponentData.thumbNail forState: UIControlStateNormal ]; + + [remoteThumbnailButton setNeedsDisplay]; + } + + free(data); +} + +@end + + +@implementation GameResultsController(Private) + +- (GHTTPBool) downloadImageCompletedCallbackRequest: (GHTTPRequest) request + result: (GHTTPResult) result + buffer: (char *) buffer + bufferLen: (GHTTPByteCount) bufferLen + profileId: (int) profileId +{ + if (discardCallbacks) return GHTTPFalse; + if(result != GHTTPSuccess) + { + NSLog(@"File Download: GHTTP Error: %d\n", result); + if( (unsigned int)profileId == gPlayerData.profileId ) + { + busyLocal.hidden=YES; + [busyLocal stopAnimating]; + } + else + { + busyRemote.hidden=YES; + [busyRemote stopAnimating]; + } + + ghttpCleanup(); + return GHTTPTrue; + } + // if picture is found, create a new thumbnail using the image in the buffer, then release the buffer + if( bufferLen ) + { + NSData * jpegImage = [NSData dataWithBytes:(const void *)buffer length: bufferLen]; + [self drawThumbnailImage: [UIImage imageWithData: jpegImage] forPlayer: profileId ]; + NSLog(@"Thumbnail downloaded for %d \n", profileId); + } + + + ghttpCleanup(); + [busyLocal stopAnimating]; + busyLocal.hidden=YES; + [busyRemote stopAnimating]; + busyRemote.hidden=YES; + + return GHTTPTrue; // frees the buffer +} + + + +- (void) sakeSearchForImageFileIdRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + if (discardCallbacks) return; + + if(result == SAKERequestResult_SUCCESS) + { + if ( ((SAKESearchForRecordsOutput*)outputData)->mNumRecords > 0) + { + int aProfileId, aFileId; + SAKEField* aField; + + NSLog(@"sakeSearchForImageFileIdRequestCallback get %d ids", ((SAKESearchForRecordsOutput*)outputData)->mNumRecords ); + for( int i = 0; i < ((SAKESearchForRecordsOutput*)outputData)->mNumRecords; i++ ) + { + aField = ( (SAKEField*) ((SAKESearchForRecordsOutput*)outputData)->mRecords[i]); + aProfileId = aField[0].mValue.mInt; + aFileId = aField[1].mValue.mInt; + + if ( (unsigned int)aProfileId == gPlayerData.profileId ) + { + gPlayerData.pictureFileId = aFileId; + [self downloadThumbnail: aFileId forPlayer: gPlayerData.profileId ]; + } + else + { + gOpponentData.pictureFileId = aFileId; + [self downloadThumbnail: aFileId forPlayer: gOpponentData.profileId ]; + } + + } + } + else + { + [busyLocal stopAnimating]; + [busyRemote stopAnimating]; + busyLocal.hidden=YES; + busyRemote.hidden=YES; + NSLog(@"No existing profiles\n"); + } + } + else + { + [busyLocal stopAnimating]; + [busyRemote stopAnimating]; + busyLocal.hidden=YES; + busyRemote.hidden=YES; + NSLog(@"Failed to search for this player record, error: %d\n", result); + } +} + + +- (void)determineWinner +{ + if ((localTime > 0) && (remoteTime > 0)) { + // Multi-player game. + if (localTime == remoteTime) + { + tieLabel.hidden = NO; + localWinnerLabel.hidden = YES; + remoteWinnerLabel.hidden = YES; + //localThumbnailButton.enabled = YES; + //remoteThumbnailButton.enabled = YES; + } + else if (localTime < remoteTime) + { + tieLabel.hidden = YES; + localWinnerLabel.hidden = NO; + remoteWinnerLabel.hidden = YES; + //localThumbnailButton.enabled = YES; + //remoteThumbnailButton.enabled = NO; + } + else + { + tieLabel.hidden = YES; + localWinnerLabel.hidden = YES; + remoteWinnerLabel.hidden = NO; + //localThumbnailButton.enabled = NO; + //remoteThumbnailButton.enabled = YES; + } + } +} + +/* +- (GHTTPBool) downloadImageCompletedCallbackRequest: (GHTTPRequest) request result: (GHTTPResult) result buffer: (char *) buffer bufferLen: (GHTTPByteCount) bufferLen +{ + if (discardCallbacks) return GHTTPFalse; + if(result != GHTTPSuccess) + { + NSLog(@"File Download: GHTTP Error: %d\n", result); + ghttpCleanup(); + return GHTTPTrue; + } + // if picture is found, create a new thumbnail using the image in the buffer, then release the buffer + if( bufferLen ) + { + NSData * jpegImage = [NSData dataWithBytes:(const void *)buffer length: bufferLen]; + [self drawThumbnailImage: [UIImage imageWithData: jpegImage] forProfileID: ]; + NSLog(@"Thumbnail downloaded\n"); + } + + + ghttpCleanup(); + [busy stopAnimating]; + return GHTTPTrue; // frees the buffer +} +*/ +@end + +void SakeSearchForImageFileIdRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + if (discardCallbacks) return; + [(GameResultsController *)userData sakeSearchForImageFileIdRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; + +} + + +GHTTPBool DownloadImageCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param) +{ + if (discardCallbacks) return GHTTPFalse; + + GHTTPBool res = [(GameResultsController *)( ((ServerParam*)param)->controller) downloadImageCompletedCallbackRequest: request + result: result + buffer: buffer + bufferLen: bufferLen + profileId: (int)( ((ServerParam*)param)->profileId) ]; + gsifree(param); + return res; +} diff --git a/Samples/TapRace/iphone/LeaderboardsController.h b/Samples/TapRace/iphone/LeaderboardsController.h new file mode 100644 index 00000000..e29446f1 --- /dev/null +++ b/Samples/TapRace/iphone/LeaderboardsController.h @@ -0,0 +1,66 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import + + +@interface LeaderboardsController : UIViewController +{ + IBOutlet UIButton* setPhotoButton; + IBOutlet UIButton* top100Button; + IBOutlet UIButton* mySpotButton; + IBOutlet UIButton* byLocationButton; + + IBOutlet UILabel* myTotalRaces; + IBOutlet UILabel* myRank; + IBOutlet UILabel* myTotalWins; + IBOutlet UILabel* myTotalLosses; + IBOutlet UILabel* myRatio; + IBOutlet UILabel* myBestTime; + IBOutlet UILabel* myStreak; + IBOutlet UILabel* myStreakLabel; + + + IBOutlet UILabel* mySPTotalRaces; + IBOutlet UILabel* mySPBestTime; + IBOutlet UILabel* mySPAvgTime; + + IBOutlet UIImageView* thumbNail; + + IBOutlet UIActivityIndicatorView* busy; + + UIImagePickerController* imagePickerController; + + int gUploadedFileId; + UIImage * newImage; +} + +@property (nonatomic, retain) UILabel* myTotalRaces; +@property (nonatomic, retain) UILabel* myRank; +@property (nonatomic, retain) UILabel* myTotalWins; +@property (nonatomic, retain) UILabel* myTotalLosses; +@property (nonatomic, retain) UILabel* myRatio; +@property (nonatomic, retain) UILabel* myBestTime; +@property (nonatomic, retain) UILabel* myStreak; +@property (nonatomic, retain) UILabel* myStreakLabel; +@property (nonatomic, retain) UILabel* mySPTotalRaces; +@property (nonatomic, retain) UILabel* mySPBestTime; +@property (nonatomic, retain) UILabel* mySPAvgTime; + +@property (nonatomic, retain) UIImageView* thumbNail; + + +- (IBAction)setPhotoButtonClicked: (id)sender; +- (IBAction)top100ButtonClicked: (id)sender; +- (IBAction)myBuddiesButtonClicked: (id)sender; +- (IBAction)byLocationButtonClicked: (id)sender; +- (IBAction)showInfo; + +- (void) testForExistingImageRecord; +- (void) getImageFromProfile; +- (void) drawAndUploadImage: (UIImage*)image; +- (void) drawThumbnailFromImage: (UIImage*)image; +- (void) delayedDraw; +- (void) delayedRank; + +@end diff --git a/Samples/TapRace/iphone/LeaderboardsController.mm b/Samples/TapRace/iphone/LeaderboardsController.mm new file mode 100644 index 00000000..520a8888 --- /dev/null +++ b/Samples/TapRace/iphone/LeaderboardsController.mm @@ -0,0 +1,786 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "LeaderboardsController.h" +#import "LoginController.h" +#import "MyCLController.h" +#import "AppDelegate.h" +#import "Utility.h" +#import "sake/sakeRequest.h" +#import + +//#define getLocalIntegerStat(key) [(NSNumber*)[gPlayerData.playerStatsData objectForKey: key ] intValue] +//#define getLocalStringStat(key) [(NSString*)[gPlayerData.playerStatsData objectForKey: key ]] + +// callback prototypes +// http stuff to upload and download image +void postCallback( GHTTPRequest request, int bytesPosted, int totalBytes, int objectsPosted, int totalObjects, void * param ); +GHTTPBool UploadCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param); +GHTTPBool DownloadCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param); +// sake stuff to access and create persistent server storage +void SakeCreateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +void SakeUpdateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +void SakeTestRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +void SakeSearchForRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); + +static SAKEField fields[2]; + +static bool discardCallbacks = true; + +// Exists for the simulator, not for the device: #import +@interface LeaderboardsController(Private) + +@end + + +@implementation LeaderboardsController + +@synthesize myTotalRaces; +@synthesize myRank; +@synthesize myTotalWins; +@synthesize myTotalLosses; +@synthesize myRatio; +@synthesize myBestTime; +@synthesize myStreak; +@synthesize myStreakLabel; +@synthesize mySPTotalRaces; +@synthesize mySPBestTime; +@synthesize mySPAvgTime; +@synthesize thumbNail; + + +- (void)dealloc +{ + discardCallbacks = true; + if (imagePickerController != nil) + { + [imagePickerController release]; + imagePickerController = nil; + } + + [thumbNail release]; + [setPhotoButton release]; + [top100Button release]; + [mySpotButton release]; + [byLocationButton release]; + [myTotalRaces release]; + [myTotalWins release]; + [myTotalLosses release]; + [myRatio release]; + [myBestTime release]; + [myStreak release]; + [mySPTotalRaces release]; + [mySPBestTime release]; + [mySPAvgTime release]; + [busy release]; + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + +- (void)viewWillAppear: (BOOL)animated +{ + [super viewWillAppear: animated]; + + [appDelegate getMyLeaderPosition ]; + + NSString* aTitle = [NSString stringWithFormat: @"%@'s Profile", gPlayerData.uniquenick ]; + self.title = aTitle; //[leader.name copy]; // index of currently displayed profile + + // Hide Buddies leaderboard button if no buddies + int numBuddies; + LoginController* loginController = (LoginController*)[appDelegate findExistingControllerOfType: [LoginController class]]; + GPResult res = gpGetNumBuddies( [loginController getGPConnectionPtr], &numBuddies ); + + if ( (res == GP_NO_ERROR) && (numBuddies == 0)) + mySpotButton.hidden= YES; + else + mySpotButton.hidden = NO; + + // disable Location button if GPS is not available + NSString *modelType = [UIDevice currentDevice].model; + if( ! [modelType isEqualToString: (NSString *) @"iPod touch" ] ) + { + // disable Location button on iPhone if GPS is not available + if( [MyCLController sharedInstance].locationManager.locationServicesEnabled ) + byLocationButton.hidden = NO; + else + byLocationButton.hidden = YES; + } + + setPhotoButton.hidden = ![UIImagePickerController isSourceTypeAvailable: UIImagePickerControllerSourceTypePhotoLibrary]; + + // hide buddy list button for v1 + mySpotButton.hidden = YES; + + int matchesPlayed = getLocalIntegerStat(matchGamesPlayedKey); + myTotalRaces.text = [NSString stringWithFormat: @"%d", matchesPlayed ]; + mySPTotalRaces.text = [NSString stringWithFormat: @"%d", getLocalIntegerStat(singlePlayerGamesPlayedKey)]; + + int wins = getLocalIntegerStat(matchWonKey); + int loss = getLocalIntegerStat(matchLossKey); + + myTotalWins.text = [NSString stringWithFormat: @"%d", wins]; + myTotalLosses.text = [NSString stringWithFormat: @"%d", loss ]; + + if( matchesPlayed > 0 ) + { + myRatio.text = [NSString stringWithFormat: @"%d\%%", (wins*100)/matchesPlayed ]; + } + else + { + myRatio.text = [NSString stringWithString: @"0"]; + } + + int bestRaceTime = getLocalIntegerStat(matchBestTimeKey); + if( (bestRaceTime <= 0) || (bestRaceTime >= INITIAL_BEST_TIME) ) + { + myBestTime.text = [NSString stringWithString: @"none"]; + } + else + { + SetTimeLabel( myBestTime, bestRaceTime); + } + + //int spBestTime = [(NSNumber*)[gPlayerData.playerStatsData objectForKey: singlePlayerBestTimeKey] intValue]; + int spBestRaceTime = getLocalIntegerStat(singlePlayerBestTimeKey); + int spAvgPlayTime = getLocalIntegerStat(singlePlayerAverageTimeKey); + + if( (spBestRaceTime <= 0) || (spBestRaceTime >= INITIAL_BEST_TIME) ) + { + mySPBestTime.text = [NSString stringWithString: @"none"]; + mySPAvgTime.text = [NSString stringWithString: @"none"]; + } + else + { + SetTimeLabel( mySPBestTime, spBestRaceTime ); + SetTimeLabel(mySPAvgTime, spAvgPlayTime ); + } + + // determine whether to display win, loss or draw streak + int tmpStreak = getLocalIntegerStat(matchDrawStreakKey); + NSMutableString* tmpStreakLabel = @"Draw Streak:"; + + if( tmpStreak == 0 ) + { + tmpStreak = getLocalIntegerStat(matchLossStreakKey); + tmpStreakLabel = @"Loss Streak:"; + + if( tmpStreak == 0 ) + { + tmpStreak = getLocalIntegerStat(matchWinStreakKey); + tmpStreakLabel = @"Win Streak:"; + } + } + + myStreak.text = [NSString stringWithFormat: @"%d", tmpStreak ]; + myStreakLabel.text = tmpStreakLabel; + + discardCallbacks = false; + + // display current rank + if( gPlayerData.rank > 0 ) + myRank.text = [NSString stringWithFormat: @"#%d", gPlayerData.rank ]; + else + { + // go get rank retrieved from server + [self performSelector:@selector(delayedRank) withObject:nil afterDelay:1.0f]; + } + + // if coming back from picking a new image... + if (newImage != nil ) + { + // do a delayed request to allow this view to draw, then upload the image + [busy startAnimating]; + [self performSelector:@selector(delayedDraw) withObject:nil afterDelay:0.02f]; + } + else if (gPlayerData.thumbNail != nil) // if no thumbnail, just leave as the default 'No Photo' + { + [busy stopAnimating]; + [self.thumbNail setImage: gPlayerData.thumbNail]; + } + else + { + // check to see if there is a player record + [busy startAnimating]; + [self getImageFromProfile]; + } +} + +// Display the About page + - (IBAction)showInfo + { + [ appDelegate showAboutInfo: self ]; + } + +// Display the iPhone Photo Album to select a picture +- (IBAction)setPhotoButtonClicked: (id)sender +{ + imagePickerController = [[UIImagePickerController alloc] init]; + imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + imagePickerController.delegate = self; +// self.navigationController.navigationBarHidden = YES; +// [self.navigationController pushViewController: imagePickerController animated: NO]; + [self presentModalViewController:imagePickerController animated:YES]; + //[imagePickerController release]; + +} + +// Display the table to Top 30 leaders +- (IBAction)top100ButtonClicked: (id)sender +{ + [appDelegate showLeadersByTime]; +} + +- (IBAction)myBuddiesButtonClicked: (id)sender +{ + [appDelegate showLeadersByBuddies]; +} + +- (IBAction)byLocationButtonClicked: (id)sender +{ + [appDelegate showLeadersByLocation]; +} + +- (void) getImageFromProfile +{ + SAKERequest request; + SAKEStartRequestResult startRequestResult; + static SAKESearchForRecordsInput sfr_input; + static char * fieldNames[] = { (char *) "picture" }; + int ownerids[] = { gPlayerData.profileId }; + + sfr_input.mTableId = (char *) "Profiles"; + sfr_input.mFieldNames = fieldNames; + sfr_input.mNumFields = 1; + sfr_input.mSort = (char *) "recordid asc"; + sfr_input.mOffset = 0; + sfr_input.mMaxRecords = 1; + sfr_input.mOwnerIds = ownerids; + sfr_input.mNumOwnerIds = 1; + + request = sakeSearchForRecords(appDelegate.sake, &sfr_input, SakeSearchForRecordsCallback, self ); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start search for records request: %d\n", startRequestResult); + } + else + { + NSLog(@"Searching for player record\n"); + } +} + + +- (void) testForExistingImageRecord +{ + SAKERequest request; + SAKEStartRequestResult testRequestResult; + static SAKEGetMyRecordsInput gmr_input; + static char *fieldnames[] = { (char *) "recordId" }; + gmr_input.mTableId = (char *) "Profiles"; + gmr_input.mFieldNames = fieldnames; + gmr_input.mNumFields = 1; + + request = sakeGetMyRecords(appDelegate.sake, &gmr_input, SakeTestRecordCallback, self); + if(!request) + { + testRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start test for record request: %d\n", testRequestResult); + [busy stopAnimating]; + } + else + { + NSLog(@"Testing for existing image record\n"); + } +} + +- (void) drawAndUploadImage: (UIImage*)image +{ + GHTTPPost post; + GHTTPRequest request; + gsi_char url[SAKE_MAX_URL_LENGTH]; + + int imgh = image.size.height; + int imgw = image.size.width; + + NSLog(@"drawAndUploadImage full image height=%d width=%d", imgh, imgw); + + [busy startAnimating]; + [self drawThumbnailFromImage: image]; + + ghttpStartup(); + + // get an upload url + if(sakeGetFileUploadURL(appDelegate.sake, url)) + { + NSLog(@"Upload URL: %s\n", url); + + // upload a file + post = ghttpNewPost(); + ghttpPostSetCallback(post, postCallback, self); + + // get a jpeg version of the image + NSLog(@"drawAndUploadImage full image height=%d width=%d", gPlayerData.thumbNail.size.height, gPlayerData.thumbNail.size.width); + NSData * jpeg = UIImageJPEGRepresentation ( gPlayerData.thumbNail, 1.0f ); + ghttpPostAddFileFromMemory(post, "picture.image", (const char *)[jpeg bytes], [jpeg length], "picture.image", NULL); + + request = ghttpPostEx(url, NULL, post, GHTTPFalse, GHTTPTrue, NULL, UploadCompletedCallback, self); + + if(request == -1) + { + NSLog(@"Error starting file upload\n"); + } + else + { + [appDelegate startThinkTimer]; + return; // wait for callback before cleaning up ghttp + } + } + else + { + NSLog(@"Failed to get upload url!\n"); + } + + ghttpCleanup(); + [busy stopAnimating]; +} + +// Resize the downloaded image and save in gPlayerData.fullsizeImage to represent the profile size +// Resize the fullsizeimage to and save in gPlayerData.thumbnail to represent the leaderboard thumbnail size +// Then draw thumbnail +- (void) drawThumbnailFromImage: (UIImage*)image +{ + + // get the current colorspace + int imgh = image.size.height; + int imgw = image.size.width; + + NSLog(@"drawThumbnailFromImage h=%d w=%d", imgh, imgw); + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB( ); + void * data = calloc(imgh, 4*imgw); + CGContextRef bmpContextRef = CGBitmapContextCreate ( data, // void *data, + imgw, // size_t width, + imgh, // size_t height, + 8, // size_t bitsPerComponent, + 4*imgw, // size_t bytesPerRow, + colorspace, // CGColorSpaceRef colorspace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big // CGBitmapInfo bitmapInfo + ); + CGRect fsrect = CGRectMake( 0, 0, imgw, imgh ); + CGContextDrawImage ( bmpContextRef, fsrect, image.CGImage ); // draw (and resize) image + if (gPlayerData.fullsizeImage != nil) [gPlayerData.fullsizeImage release]; + gPlayerData.fullsizeImage = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( bmpContextRef )] retain]; + CGContextRelease(bmpContextRef); + free(data); + + // now redraw into thumbnail + int imageOrientation = image.imageOrientation; + int th,tw; + float hscale = ((float)imgh)/float(THUMBNAIL_HEIGHT); + float wscale = ((float)imgw)/float(THUMBNAIL_WIDTH); + if (hscale > wscale) { + // scale to height + th = THUMBNAIL_HEIGHT; + tw = (int)((float)imgw/hscale); + } else { + tw = THUMBNAIL_WIDTH; + th = (int)((float)imgh/wscale); + } + data = calloc(th, 4*tw); + CGContextRef thumbnailContextRef = CGBitmapContextCreate ( NULL, // void *data, + tw, // size_t width, + th, // size_t height, + 8, // size_t bitsPerComponent, + 4*tw, // size_t bytesPerRow, + colorspace, // CGColorSpaceRef colorspace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big // CGBitmapInfo bitmapInfo + ); + CGRect tnrect; + // rotate the context + switch (imageOrientation) { + case 0: // no rotation + tnrect = CGRectMake( 0, 0, tw, th ); + break; + case 3: // rotate -90 + tnrect = CGRectMake( 0, 0, th, tw ); + CGContextTranslateCTM (thumbnailContextRef, 0.0f, float(th) ); + CGContextRotateCTM (thumbnailContextRef, -3.1415926f/2.0f); + break; + case 2: // rotate 180 + tnrect = CGRectMake( 0, 0, tw, th ); + CGContextTranslateCTM (thumbnailContextRef, float(tw), float(th) ); + CGContextRotateCTM (thumbnailContextRef, -3.1415926f); + break; + case 1: // rotate 90 + tnrect = CGRectMake( 0, 0, th, tw ); + CGContextTranslateCTM (thumbnailContextRef, float(tw), 0.0f ); + CGContextRotateCTM (thumbnailContextRef, 3.1415926f/2.0f); + break; + } + CGContextDrawImage ( thumbnailContextRef, tnrect, image.CGImage ); // draw image + if (gPlayerData.thumbNail != nil) [gPlayerData.thumbNail release]; + gPlayerData.thumbNail = [[UIImage imageWithCGImage: CGBitmapContextCreateImage ( thumbnailContextRef )] retain]; + + imgh = gPlayerData.thumbNail.size.height; + imgw = gPlayerData.thumbNail.size.width; + NSLog(@"drawThumbnailFromImage thumbnail h=%d w=%d", imgh, imgw); + + CGContextRelease(thumbnailContextRef); + CGColorSpaceRelease(colorspace); + free(data); + + [busy stopAnimating]; + [self.thumbNail setImage: gPlayerData.thumbNail]; + [thumbNail setNeedsDisplay]; +} + +// called after a delay to upload a new image from the image picker... +// gives the mainloop a chance to switch views and render the leaders menu again before the upload starts +- (void)delayedDraw +{ + int imgh = newImage.size.height; + int imgw = newImage.size.width; + + NSLog(@"delayedDraw full image height=%d width=%d", imgh, imgw); + + [self drawAndUploadImage: newImage]; + [newImage release]; + newImage = nil; +} + +// called after a delay to show rank +// gives the mainloop a chance for callback to set server value for rank +- (void)delayedRank +{ + if( gPlayerData.rank != 0 ) + myRank.text = [NSString stringWithFormat: @"#%d", gPlayerData.rank ]; +} + +@end + + +@implementation LeaderboardsController(Private) + +// +//- (void) drawAndUploadImage: (UIImage*)image +//{ +// GHTTPPost post; +// GHTTPRequest request; +// gsi_char url[SAKE_MAX_URL_LENGTH]; +// +// [busy startAnimating]; +// [self drawThumbnailFromImage: image]; +// +// ghttpStartup(); +// +// // get an upload url +// if(sakeGetFileUploadURL(appDelegate.sake, url)) { +// printf("Upload URL: %s\n", url); +// // upload a file +// post = ghttpNewPost(); +// ghttpPostSetCallback(post, postCallback, self); +// // get a jpeg version of the image +// NSData * jpeg = UIImageJPEGRepresentation ( gPlayerData.thumbnail, 1.0f ); +// ghttpPostAddFileFromMemory(post, "picture.image", (const char *)[jpeg bytes], [jpeg length], "picture.image", NULL); +// +// request = ghttpPostEx(url, NULL, post, GHTTPFalse, GHTTPTrue, NULL, UploadCompletedCallback, self); +// +// if(request == -1) +// { +// printf("Error starting file upload\n"); +// } else { +// [appDelegate startThinkTimer]; +// return; // wait for callback before cleaning up ghttp +// } +// } else { +// printf("Failed to get upload url!\n"); +// } +// +// ghttpCleanup(); +// [busy stopAnimating]; +//} + +- (void)imagePickerController: (UIImagePickerController*)picker didFinishPickingImage: (UIImage*)image editingInfo: (NSDictionary*)editingInfo +{ +// [self.navigationController popToViewController: self animated: NO]; +// self.navigationController.navigationBarHidden = NO; + newImage = [image retain]; // set the new image field + [self dismissModalViewControllerAnimated:YES]; + [imagePickerController release]; + imagePickerController = nil; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker +{ + [self dismissModalViewControllerAnimated:YES]; + [imagePickerController release]; + imagePickerController = nil; +// [self.navigationController popToViewController: self animated: YES]; +// self.navigationController.navigationBarHidden = NO; +} + +- (void) postCallBackRequest: (GHTTPRequest) request bytesPosted: (int) bytesPosted totalBytes: (int) totalBytes objectsPosted: (int) objectsPosted totalObjects: (int) totalObjects { + // progress of post +} + +- (void) setupRecordRequest: (char**) tableId fields: (SAKEField**) fieldsPtr fieldCount: (int*)numFields { + *tableId = (char *) "Profiles"; + fields[0].mName = (char *) "nick"; + fields[0].mType = SAKEFieldType_ASCII_STRING; + fields[0].mValue.mAsciiString = (char*) [gPlayerData.uniquenick cStringUsingEncoding: NSASCIIStringEncoding]; // player nick goes in here + fields[1].mName = (char *) "picture"; + fields[1].mType = SAKEFieldType_INT; + fields[1].mValue.mInt = gUploadedFileId; // fileid of image + *fieldsPtr = fields; + *numFields = 2; +} + +- (void) updateImageToProfile: (int)recordId { + SAKERequest request; + SAKEStartRequestResult startRequestResult; + static SAKEUpdateRecordInput ur_input; + + [self setupRecordRequest: &ur_input.mTableId fields: &ur_input.mFields fieldCount: &ur_input.mNumFields]; + ur_input.mRecordId = recordId; + request = sakeUpdateRecord(appDelegate.sake, &ur_input, SakeUpdateRecordCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start request: %d\n", startRequestResult); + [busy stopAnimating]; + } else { + NSLog(@"Updating image to profile\n"); + } +} + +- (void) addImageToProfile { + SAKERequest request; + SAKEStartRequestResult startRequestResult; + static SAKECreateRecordInput cr_input; + + [self setupRecordRequest: &cr_input.mTableId fields: &cr_input.mFields fieldCount: &cr_input.mNumFields]; + request = sakeCreateRecord(appDelegate.sake, &cr_input, SakeCreateRecordCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start request: %d\n", startRequestResult); + } else { + NSLog(@"Adding image to profile\n"); + } +} + +- (void) downloadImageToThumbnail: (int) fileId +{ + GHTTPRequest request; + gsi_char url[SAKE_MAX_URL_LENGTH]; + + ghttpStartup(); + if (sakeGetFileDownloadURL(appDelegate.sake, fileId, url)) + { + NSLog(@"Download image URL: %s\n", url); + + // download the file + request = ghttpGetEx(url, NULL, NULL, 0, NULL, GHTTPFalse, GHTTPFalse, NULL, DownloadCompletedCallback, self); + if (request == -1) + { + NSLog(@"Error starting image file download\n"); + [busy stopAnimating]; + } else { + [appDelegate startThinkTimer]; + return; + } + } else { + NSLog(@"Failed to get download url!\n"); + [busy stopAnimating]; + } + ghttpCleanup(); +} + +- (void) sakeCreateRecordRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + if (discardCallbacks) return; + if(result != SAKERequestResult_SUCCESS) + { + NSLog(@"Failed to add image to profile, error: %d\n", result); + } else { + NSLog(@"Image added to profile\n"); + } + [busy stopAnimating]; +} + +- (void) sakeUpdateRecordRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + if (discardCallbacks) return; + if(result != SAKERequestResult_SUCCESS) + { + NSLog(@"Failed to update image to profile, error: %d\n", result); + } else { + NSLog(@"Image updated to profile\n"); + } + [busy stopAnimating]; +} + +- (void) sakeTestRecordRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + if (discardCallbacks) return; + if(result == SAKERequestResult_SUCCESS) { + if ( ((SAKEGetMyRecordsOutput*)outputData)->mNumRecords > 0) + { + + [self updateImageToProfile: ((SAKEGetMyRecordsOutput*)outputData)->mRecords[0]->mValue.mInt]; + } else { + [self addImageToProfile ]; + } + } else { + NSLog(@"Failed to test for image record, error: %d\n", result); + } + [busy stopAnimating]; +} + +- (void) sakeSearchForRecordsRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + if (discardCallbacks) return; + + if(result == SAKERequestResult_SUCCESS) + { + if ( ((SAKESearchForRecordsOutput*)outputData)->mNumRecords > 0) + { + gPlayerData.pictureFileId = ((SAKEGetMyRecordsOutput*)outputData)->mRecords[0]->mValue.mInt; + [self downloadImageToThumbnail: gPlayerData.pictureFileId ]; + } + else + { + [busy stopAnimating]; + NSLog(@"No existing profile for this player\n"); + } + } + else + { + [busy stopAnimating]; + NSLog(@"Failed to search for this player record, error: %d\n", result); + } +} + +- (GHTTPBool) uploadCompletedCallbackRequest: (GHTTPRequest) request result: (GHTTPResult) result buffer: (char *) buffer bufferLen: (GHTTPByteCount) bufferLen +{ + SAKEFileResult fileResult; + gsi_bool gUploadResult; + gUploadResult = gsi_false; + + if (discardCallbacks) return GHTTPFalse; + [busy stopAnimating]; + if(result != GHTTPSuccess) + { + NSLog(@"File Upload: GHTTP Error: %d\n", result); + ghttpCleanup(); + return GHTTPTrue; + } + + if(!sakeGetFileResultFromHeaders(ghttpGetHeaders(request), &fileResult)) + { + NSLog(@"File Upload: Failed to find Sake-File-Result header\n"); + ghttpCleanup(); + return GHTTPTrue; + } + + if(fileResult != SAKEFileResult_SUCCESS) + { + NSLog(@"File Upload: SakeFileResult != success: %d\n", fileResult); + ghttpCleanup(); + return GHTTPTrue; + } + + if(!sakeGetFileIdFromHeaders(ghttpGetHeaders(request), &gUploadedFileId)) + { + NSLog(@"File Upload: Unable to parse Sake-File-Id header\n"); + ghttpCleanup(); + return GHTTPTrue; + } + + gPlayerData.pictureFileId = gUploadedFileId; // save new picture file id + + NSLog(@"File Upload: Uploaded fileId: %d\n", gUploadedFileId); + gUploadResult = gsi_true; + ghttpCleanup(); + + // see if record already existed + [busy startAnimating]; + [self testForExistingImageRecord ]; + + return GHTTPTrue; +} + +- (GHTTPBool) downloadCompletedCallbackRequest: (GHTTPRequest) request result: (GHTTPResult) result buffer: (char *) buffer bufferLen: (GHTTPByteCount) bufferLen +{ + if (discardCallbacks) return GHTTPFalse; + if(result != GHTTPSuccess) + { + NSLog(@"File Download: GHTTP Error: %d\n", result); + ghttpCleanup(); + return GHTTPTrue; + } + // if picture is found, create a new thumbnail using the image in the buffer, then release the buffer + if( bufferLen ) + { + NSData * jpegImage = [NSData dataWithBytes:(const void *)buffer length: bufferLen]; + [self drawThumbnailFromImage: [UIImage imageWithData: jpegImage]]; + NSLog(@"Thumbnail downloaded\n"); + } + + + ghttpCleanup(); + [busy stopAnimating]; + return GHTTPTrue; // frees the buffer +} + + +@end + +// callbacks +void postCallback( GHTTPRequest request, int bytesPosted, int totalBytes, int objectsPosted, int totalObjects, void * param ) +{ + if (discardCallbacks) return; + [(LeaderboardsController *)param postCallBackRequest: request bytesPosted: bytesPosted totalBytes: totalBytes objectsPosted: objectsPosted totalObjects: totalObjects ]; +} + +GHTTPBool UploadCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param) +{ + if (discardCallbacks) return GHTTPFalse; + return [(LeaderboardsController *)param uploadCompletedCallbackRequest: request result: result buffer: buffer bufferLen: bufferLen]; +} + +GHTTPBool DownloadCompletedCallback(GHTTPRequest request, GHTTPResult result, char * buffer, GHTTPByteCount bufferLen, void * param) +{ + if (discardCallbacks) return GHTTPFalse; + return [(LeaderboardsController *)param downloadCompletedCallbackRequest: request result: result buffer: buffer bufferLen: bufferLen]; +} + +void SakeCreateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + if (discardCallbacks) return; + [(LeaderboardsController *)userData sakeCreateRecordRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + +void SakeUpdateRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + if (discardCallbacks) return; + [(LeaderboardsController *)userData sakeUpdateRecordRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + +void SakeTestRecordCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + if (discardCallbacks) return; + [(LeaderboardsController *)userData sakeTestRecordRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + +void SakeSearchForRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + if (discardCallbacks) return; + [(LeaderboardsController *)userData sakeSearchForRecordsRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + diff --git a/Samples/TapRace/iphone/LeadersByLocationController.h b/Samples/TapRace/iphone/LeadersByLocationController.h new file mode 100644 index 00000000..ccac5b45 --- /dev/null +++ b/Samples/TapRace/iphone/LeadersByLocationController.h @@ -0,0 +1,27 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import + +@interface LeadersByLocationController : UIViewController +{ + NSString * myName; + NSTimer* updateTimer; + + IBOutlet UIWebView* webView; + IBOutlet MKMapView *mapView; + IBOutlet UIActivityIndicatorView *spinner; + + BOOL isCurrentlyUpdating; + UIAlertView* alertView; +} + +@property (nonatomic, retain) UIActivityIndicatorView *spinner; +@property (nonatomic, retain) NSString * myName; + +- (IBAction)showInfo; + +- (void)searchLeaderboardsByLocation; + +@end diff --git a/Samples/TapRace/iphone/LeadersByLocationController.mm b/Samples/TapRace/iphone/LeadersByLocationController.mm new file mode 100644 index 00000000..f05fe949 --- /dev/null +++ b/Samples/TapRace/iphone/LeadersByLocationController.mm @@ -0,0 +1,465 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "LeadersByLocationController.h" +#import "AppDelegate.h" +#import "Utility.h" +#include "atlas_taprace_v2.h" +#import "UserStatsController.h" + +#define LOCATION_DEGREES_RADIUS 1.1f +#define METER_RADIUS_FOR_SELECT (6000.0f*LOCATION_DEGREES_RADIUS) // approximate meters radius of map/10 +#define MAP_ZOOM 7 + +// structure to hold the buddy info +@interface buddy : NSObject +{ + NSString * name; + CLLocationDegrees latitude; + CLLocationDegrees longitude; +} + +@property (nonatomic, copy) NSString * name; +@property CLLocationDegrees latitude; +@property CLLocationDegrees longitude; + +@end + +//************************************** +@implementation buddy +@synthesize name, latitude, longitude; +@end + + +void SearchForLocationRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ); +static const char PLAYER_STATS_TABLE[] = "PlayerStats_v" STRINGIZE(ATLAS_RULE_SET_VERSION); + + +//************************************** +@implementation LeadersByLocationController + +@synthesize spinner, myName; + +- (void)dealloc +{ + [webView release]; + [spinner release]; + [updateTimer invalidate]; + [updateTimer release]; + [super dealloc]; +} + +//************************************** +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if ( (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) ) { + self->isCurrentlyUpdating = NO; + self.myName = [gPlayerData.uniquenick copy]; + } + return self; +} + +//************************************** +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + +//************************************** +- (void)updateTime: (NSTimer*)timer; +{ + // try to issue javascript to grab the latest lat/long + NSString * touched = [webView stringByEvaluatingJavaScriptFromString: @"touched"]; + if ([touched intValue]) { + [webView stringByEvaluatingJavaScriptFromString: @"touched = 0;"]; + NSString * lastLat = [webView stringByEvaluatingJavaScriptFromString: @"lastLat"]; + NSString * lastLon = [webView stringByEvaluatingJavaScriptFromString: @"lastLon"]; + + CLLocation * loc = [[CLLocation alloc] initWithLatitude: [lastLat floatValue] longitude: [lastLon floatValue]]; + int budindex = -1; + + // locate the closest individual + CLLocationDistance distance = 99999.99f; + for (unsigned int i = 0; i < [gLeaderboardData count]; i++) + { + LeaderboardObject * nextbuddy = (LeaderboardObject*)[gLeaderboardData objectAtIndex: i]; + CLLocationDistance buddyDist = [loc getDistanceFrom: nextbuddy.playerLocation]; + if (buddyDist < distance) { + budindex = i; + distance = buddyDist; + } + } + // if not within 1000 meters, ignore the touch + if ((distance < METER_RADIUS_FOR_SELECT) && (budindex >= 0)) { + [appDelegate showUser: budindex]; // show the user profile + } + } +} + +//************************************** +- (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id )annotation +{ + MKPinAnnotationView *av = (MKPinAnnotationView *)[aMapView dequeueReusableAnnotationViewWithIdentifier:@"myID"]; + if ( nil == av ) + av = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"myId"]; + + UIButton *button = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; + av.rightCalloutAccessoryView = button; + av.canShowCallout = YES; + return [av autorelease]; +} + + +//************************************** +- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control +{ + gLeaderboardProfileIndex = 0; + + // get the selected uniquenick and set user to display from list + NSString* selectedLeader = [[ view annotation] title ]; + + for( unsigned int index = 0; index < [gLeaderboardData count]; index++ ) + { + if( [ selectedLeader isEqualToString: ((LeaderboardObject*)[gLeaderboardData objectAtIndex: index ]).name ] ) + { + gLeaderboardProfileIndex = index; + break; + } + } + + [appDelegate pushNewControllerOfType: [UserStatsController class] nib: @"UserStats"]; +} + + +//************************************** +- (void)viewDidLoad { + unsigned int i; + CLLocationDegrees myLat, buddyLat, myLon, buddyLon; + + myLat = gPlayerData.playerLocation.coordinate.latitude; + myLon = gPlayerData.playerLocation.coordinate.longitude; + NSLog(@"myLat %f myLon %f", myLat, myLon ); + + // calculate area to display + // All leaders with no position are set to my position + for( i = 0; i < [gLeaderboardData count]; i++ ) + { + LeaderboardObject * nextbuddy = (LeaderboardObject*)[gLeaderboardData objectAtIndex: i]; + + buddyLat = nextbuddy.playerLocation.coordinate.latitude; + buddyLon = nextbuddy.playerLocation.coordinate.longitude; + if( buddyLat == 0.0 && buddyLon == 0.0 ) + { + NSLog(@"Unknown location for %s\n", nextbuddy.name ); + nextbuddy.playerLocation.coordinate.latitude = myLat; + nextbuddy.playerLocation.coordinate.longitude = myLon; + continue; + } + + NSLog(@"bddy Lat %lf Lon %lf", buddyLat, buddyLon ); + } + + MKCoordinateSpan span = MKCoordinateSpanMake( LOCATION_DEGREES_RADIUS, LOCATION_DEGREES_RADIUS); + MKCoordinateRegion location = { gPlayerData.playerLocation.coordinate, span }; + [mapView setRegion:location]; + + // set pins for each player + [mapView addAnnotations:gLeaderboardData]; +} + +// Display the About page +- (IBAction)showInfo +{ + [ appDelegate showAboutInfo: self ]; +} + + +/* +-(void)viewDidLoadOld { + + [spinner startAnimating]; + // TODO: Parse and save lat/long + NSMutableString *update = [[[NSMutableString alloc] init] autorelease]; + [update appendString:@"\n\n\n\n
\n"]; + [update appendString:@"\n"]; + + [webView loadHTMLString:update baseURL: nil]; + + if (alertView != nil) { + // remove the previous alert + [alertView dismissWithClickedButtonIndex: alertView.cancelButtonIndex+1 animated: NO]; + [alertView release]; + alertView = nil; + } + + if ([gLeaderboardData count] == 0) { + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Searching for nearby players" delegate: self cancelButtonTitle: nil otherButtonTitles: nil]; + [alertView show]; + return; // don't look for tagTouch events + } + // kick off a timer to test for tagTouch events + if (updateTimer != nil) { + [updateTimer invalidate]; + } + updateTimer = [NSTimer scheduledTimerWithTimeInterval: 0.01 target: self selector: @selector(updateTime:) userInfo: nil repeats: YES]; + + [spinner stopAnimating]; +} +*/ + +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ + if ([gLeaderboardData count] == 0) + { + // same as exit + [self.navigationController popViewControllerAnimated: YES]; + } +} + +-(void)newError:(NSString *)text { + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Error finding location" delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + [alertView show]; +} + +- (void) searchLeaderboardsByLocation +{ + static SAKESearchForRecordsInput input; + SAKERequest request; + static char *fieldNames[] = { + (char *) "ownerid" , + (char *) "row", + ATLAS_GET_STAT_NAME( NICK ), + ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), + ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), + ATLAS_GET_STAT_NAME( CAREER_WINS ), + ATLAS_GET_STAT_NAME( CAREER_LOSSES ), + ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), + ATLAS_GET_STAT_NAME( LATITUDE ), + ATLAS_GET_STAT_NAME( LONGITUDE) + }; + + SAKEStartRequestResult startRequestResult; + + char filter[512]; + float myLat = gPlayerData.playerLocation.coordinate.latitude; + float myLong = gPlayerData.playerLocation.coordinate.longitude; + float offset = LOCATION_DEGREES_RADIUS; + + sprintf(filter, "BEST_RACE_TIME != 99999 and LATITUDE > %f and LATITUDE < %f and LONGITUDE > %f and LONGITUDE < %f", + myLat-offset, myLat+offset, myLong-offset, myLong+offset); + + input.mTableId = (char *) PLAYER_STATS_TABLE; + input.mFieldNames = fieldNames; + input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); + input.mFilter = filter; + input.mSort = (char *) "BEST_RACE_TIME asc"; + input.mOffset = 0; + input.mMaxRecords = 30; + + request = sakeSearchForRecords(appDelegate.sake, &input, SearchForLocationRecordsCallback, self); + if(!request) + { + startRequestResult = sakeGetStartRequestResult(appDelegate.sake); + NSLog(@"Failed to start location search request: %d\n", startRequestResult); + } +} + +- (void) sakeSearchForLocationRecordsRequestCallback:(SAKE) sake request:(SAKERequest) request result:(SAKERequestResult) result inputData:(void *) inputData outputData:(void *) outputData +{ + float lat; + float lng; + + if(result == SAKERequestResult_SUCCESS) { + SAKESearchForRecordsOutput* records = (SAKESearchForRecordsOutput*)outputData; + if ( records->mNumRecords > 0) { + // extract the info and put it into the buddy list and display it + if (gLeaderboardData != nil) + { + int lbtype = gLeaderboardType; + [appDelegate deleteLeaderboards]; + gLeaderboardType = lbtype; + } + + if (records->mNumRecords > 0) + { + gLeaderboardData = [[NSMutableArray arrayWithCapacity: records->mNumRecords] retain]; + + for (int i=0; imNumRecords; i++) + { + SAKEField * field = sakeGetFieldByName( "ownerid", records->mRecords[i], 10 ); + + // if this guy is me, skip it + if ((field != NULL) && ( (int)(gPlayerData.profileId) == field->mValue.mInt)) + { + /* // but extract my rank, just in case + field = sakeGetFieldByName( "row", records->mRecords[i], 10); + if (field != NULL) + { + gPlayerData.rank = field->mValue.mInt; + } + */ + continue; + } + + // create a new leaderboard object + LeaderboardObject * pData = [[LeaderboardObject alloc] init]; + + if (field != NULL) { + pData.profileId = field->mValue.mInt; + } else { + pData.profileId = 0; + } + field = sakeGetFieldByName( "row", records->mRecords[i], 10); + if (field != NULL) { + pData.rank = field->mValue.mInt; + } else { + pData.rank = -1; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( NICK ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.name = [NSString stringWithFormat: @"%s", (char*)( field->mValue.mAsciiString ) ]; + } else { + pData.name = nil; + } + NSLog(@"Found player[%d]: %s ", pData.rank, field->mValue.mAsciiString); + + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( BEST_RACE_TIME ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.bestRaceTime = (gsi_u32)( field->mValue.mInt ); + } else { + pData.bestRaceTime = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( TOTAL_COMPLETE_MATCHES ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.totalMatches = (gsi_u32)( field->mValue.mInt ); + } else { + pData.totalMatches = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_WINS ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.careerWins = (gsi_u32)( field->mValue.mInt ); + } else { + pData.careerWins = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CAREER_LOSSES ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.careerLosses = (gsi_u32)( field->mValue.mInt ); + } else { + pData.careerLosses = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( CURRENT_WIN_STREAK ), records->mRecords[i], 10 ); + if (field != NULL) { + pData.currentWinStreak = (gsi_u32)( field->mValue.mInt ); + } else { + pData.currentWinStreak = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LATITUDE ), records->mRecords[i], 10 ); + if (field != NULL) { + lat = field->mValue.mFloat; + } else { + NSLog(@"- no lat "); + lat = 0; + } + field = sakeGetFieldByName( ATLAS_GET_STAT_NAME( LONGITUDE ), records->mRecords[i], 10 ); + if (field != NULL) { + lng = field->mValue.mFloat; + } else { + NSLog(@" - no long - "); + lng = 0; + } + NSLog(@" at lat=%f, long=%f\n", lat, lng); + + pData.playerLocation = [[CLLocation alloc] initWithLatitude: lat longitude: lng]; + [gLeaderboardData addObject: pData]; + } + + // redraw the map + [self viewDidLoad]; + } else { + if (alertView != nil) { + // remove the previous alert + [alertView dismissWithClickedButtonIndex: alertView.firstOtherButtonIndex animated: NO]; + [alertView release]; + alertView = nil; + } + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"No nearby leaders" delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + [alertView show]; + } + } + } else { + NSLog(@"Failed to search by location, error: %d\n", result); + } +} + +@end + +void SearchForLocationRecordsCallback( SAKE sake, SAKERequest request, SAKERequestResult result, void * inputData, void * outputData, void * userData ) +{ + [(LeadersByLocationController *)userData sakeSearchForLocationRecordsRequestCallback: sake request: request result: result inputData: inputData outputData: outputData ]; +} + diff --git a/Samples/TapRace/iphone/LeadersController.h b/Samples/TapRace/iphone/LeadersController.h new file mode 100644 index 00000000..2557af4a --- /dev/null +++ b/Samples/TapRace/iphone/LeadersController.h @@ -0,0 +1,21 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import + +@interface LeadersController : UIViewController +{ + UITableView* leadersList; // list of leader info + IBOutlet UILabel* tableContainer; // table GUI + NSMutableDictionary* buddyList; // preconstructed list of cell views + + UIAlertView* alertView; +} + +@property (nonatomic, retain) NSMutableDictionary* buddyList; + +- (IBAction)showInfo; + +- (void)updatePictureInTable; + +@end diff --git a/Samples/TapRace/iphone/LeadersController.mm b/Samples/TapRace/iphone/LeadersController.mm new file mode 100644 index 00000000..4fcbb71a --- /dev/null +++ b/Samples/TapRace/iphone/LeadersController.mm @@ -0,0 +1,223 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "LeadersController.h" +#import "AppDelegate.h" +#import "BuddyListCellView.h" + + +static NSString * kCellIdentifier1 = @"gobbletygoop"; + + +@implementation LeadersController + +@synthesize buddyList; + + +- (void)dealloc +{ + [tableContainer release]; + [leadersList release]; + [buddyList release]; + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + +- (void)constructView +{ + // Create table. + CGRect rect = tableContainer.frame; + + // add the table view to it + leadersList = [[UITableView alloc] initWithFrame:tableContainer.frame style: UITableViewStylePlain]; + leadersList.backgroundColor = self.view.backgroundColor; + leadersList.delegate = self; + leadersList.dataSource = self; + leadersList.separatorStyle = UITableViewCellSeparatorStyleSingleLine; + leadersList.separatorColor = [UIColor blackColor]; + [self.view addSubview: leadersList]; + leadersList.frame = rect; + + if (gLeaderboardType == LEADERBOARD_BY_TIME) + { + self.title = [NSString stringWithString: @"Leaderboard"]; + } + else + { + self.title = [NSString stringWithString: @"Leaders by Buddies"]; + } + + if ([gLeaderboardData count] == 0) + { + if (gLeaderboardType == LEADERBOARD_BY_TIME) + { + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"No leaders!" delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + } + else + { + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"No buddies have played!" delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + } + + [alertView show]; + } +} + +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ + // same as exit + [self.navigationController popViewControllerAnimated: YES]; +} + +// Set each thumbnail that is blocked to default picture +-(void)updatePictureInTable +{ + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]; + if( [appDelegate imageIsBlocked:leader.pictureFileId ] ) + { + // get default image + NSString* defaultImage = [[NSBundle mainBundle] pathForResource:@"singlePlayer" ofType:@"png"]; + UIImage* imageObj = [[UIImage alloc] initWithContentsOfFile:defaultImage]; + + // set cell picture to default & refresh + NSNumber * key = [[NSNumber alloc] initWithInt:gLeaderboardProfileIndex]; + BuddyListCellView * view = [buddyList objectForKey: key]; + [view setImage: imageObj]; + [view setNeedsDisplay]; + } +} + +- (void)viewWillAppear: (BOOL)animated +{ + if (leadersList == nil) + { + [self constructView]; + } + else + { + [self updatePictureInTable ]; + } +} + + +// Display the About page +- (IBAction)showInfo +{ + [ appDelegate showAboutInfo: self ]; +} + + +- (UITableViewCell *)tableView:(UITableView *)tblView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = nil; + NSNumber * key = [NSNumber numberWithInteger: indexPath.row]; + unsigned int keyint = [key intValue]; + + if (keyint > [buddyList count]) + return nil; + + BuddyListCellView * view = [buddyList objectForKey: key]; + CGRect rect; + if (view == nil) + return nil; + + rect = view.cellView.frame; + cell = [tblView dequeueReusableCellWithIdentifier: kCellIdentifier1]; + if (cell == nil) + { + cell = [[[UITableViewCell alloc] initWithFrame: rect reuseIdentifier:kCellIdentifier1] autorelease]; + } + + // ??? need to change cell = initWithStyle:reuseIdentifier + //UITableViewCellAccessoryDisclosureIndicator ]; + + // modify the data for this cellview + UIView * uiview = nil; + for (uiview in cell.contentView.subviews) + break; // just get the first one (only one) + + if (uiview != nil) + { + // remove old one + [uiview removeFromSuperview]; // releases it and its contents + } + + // go ahead and add the current view + [cell.contentView addSubview: view.cellView]; // only add the 1st time it is visible + + return cell; +} + + +- (NSInteger)tableView:(UITableView *)tblView numberOfRowsInSection:(NSInteger)section +{ + if ((buddyList == nil) && (gLeaderboardData != nil) && ([gLeaderboardData count] > 0)) + { + // make the list before the table actually starts needing it + buddyList = [[NSMutableDictionary dictionaryWithCapacity: [gLeaderboardData count]] retain]; + } + return [gLeaderboardData count]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tblView +{ + return 1; +} + +- (CGFloat)tableView:(UITableView *)tblView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSNumber * key = [NSNumber numberWithInteger: indexPath.row]; + int keyint = [key intValue]; + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: keyint]; + + // now construct the cell for this entry in the table + BuddyListCellView * newcell = [[BuddyListCellView alloc] initCellView]; + + // fill in the fields + [newcell setName: leader.name]; + newcell.profileId = leader.profileId; + [newcell setIndex: keyint+1]; + UIImage * img = leader.thumbNail; + + // if no thumbnail, leave as default 'No Photo' image + if (img != nil && ![appDelegate imageIsBlocked: leader.pictureFileId]) + { + [newcell setImage: img]; + } + else + { + // kick off the process of getting the image + [newcell getImageFromNetwork]; + } + + [newcell setTime: leader.bestRaceTime]; + + // add to array + [buddyList setObject: newcell forKey: key]; + + CGRect rect = newcell.cellView.frame; + [newcell release]; + return rect.size.height+1; +} + +//Managing Accessory Views +- (UITableViewCellAccessoryType)tableView:(UITableView *)tblView accessoryTypeForRowWithIndexPath:(NSIndexPath *)indexPath +{ + return UITableViewCellAccessoryDisclosureIndicator; +} + +- (void)tableView:(UITableView *)tblView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSNumber * key = [NSNumber numberWithInteger: indexPath.row]; + int keyint = [key intValue]; + + [tblView deselectRowAtIndexPath:indexPath animated:YES]; + [appDelegate showUser: keyint]; +} + + +@end diff --git a/Samples/TapRace/iphone/LoginController.h b/Samples/TapRace/iphone/LoginController.h new file mode 100644 index 00000000..4c098ac6 --- /dev/null +++ b/Samples/TapRace/iphone/LoginController.h @@ -0,0 +1,46 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import + +#import "GP/gp.h" + +@class CreateUserController; + +@interface LoginController : UIViewController +{ + IBOutlet UITextField* userNameField; + IBOutlet UIPickerView* userNamePicker; + IBOutlet UITextField* passwordField; + IBOutlet UISwitch* autoLoginSwitch; + IBOutlet UIActivityIndicatorView* activityIndicator; + IBOutlet UIButton* forgotPassword; + IBOutlet UIView* userNamePickerView; + IBOutlet UIButton* info; + + UIAlertView* alertView; + + NSTimer* loginTimer; + + NSArray* rememberedLogins; + + NSDictionary* rememberedPasswordsByLogin; + + GPConnection connection; +} + +- (IBAction)loginClicked: (id)sender; +- (IBAction)returnClicked: (id)sender; +- (IBAction)createClicked: (id)sender; +- (IBAction)forgotPasswordClicked: (id)sender; +- (IBAction)autoLoginChanged: (id)sender; + +- (void)userProfileCreated: (CreateUserController*)createUserController autoLogin: (BOOL) autoLoginValue; +- (void)stopLoginTimer; +- (void)reset; +- (void)delayedNetworkConnectionErrorMessage; +- (GPConnection *)getGPConnectionPtr; +- (BOOL)isNetworkAvailableFlags:(SCNetworkReachabilityFlags *)outFlags; + +@end diff --git a/Samples/TapRace/iphone/LoginController.mm b/Samples/TapRace/iphone/LoginController.mm new file mode 100644 index 00000000..c05920a9 --- /dev/null +++ b/Samples/TapRace/iphone/LoginController.mm @@ -0,0 +1,729 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "LoginController.h" +#import "AppDelegate.h" +#import "Utility.h" +#import "CreateUserController.h" +#import "ForgotPassword.h" +#import "InfoController.h" + +#import "common/gsAvailable.h" +#import "webservices/AuthService.h" + +static NSString* animationId = @"UserNamePickerAnimation"; + +//static NSString* loginFileName = @"login.plist"; +//static NSString* lastLoginKey = @"lastLogin"; +//static NSString* passwordsByLoginKey = @"passwordsByLogin"; + +static NSString* useridKey = @"useridKey"; +static NSString* passwordKey = @"passwordKey"; +static NSString* autologinKey = @"autologinKey"; + + + + +@interface LoginController(Private) + +- (void)doneClicked: (id)sender; +- (void)onLoginTimer: (NSTimer*)timer; +- (void)loginCallback: (GPResult)result profile: (GPProfile)profile uniquenick: (const char[])uniquenick; +- (void)loginCallback: (GHTTPResult)result certificate: (GSLoginCertificate*)certificate privateData: (GSLoginPrivateData*)privateData; +- (void)animationDidStop: (NSString*)animId finished: (BOOL)finished context: (id)context; +- (void)closeUserNamePicker; + +@end + +static void gpLoginCallback(GPConnection* connection, GPConnectResponseArg* arg, void* param); +static void gpUserKickedOutCallback(GPConnection* connection, void* arg, void* param); +static void wsLoginCallback(GHTTPResult httpResult, WSLoginResponse* response, void* userData); + +@implementation LoginController + +- (void)dealloc +{ + [userNameField release]; + [passwordField release]; + [autoLoginSwitch release]; + [activityIndicator release]; + [userNamePicker release]; + + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + +/*- (void)viewDidLoad +{ +/* if (![self isNetworkAvailableFlags:nil]) + { + MessageBox( @"A network connection is not available. Application will close.", @"OK" ); + exit(0); + } +*/ + +//static NSMutableString* errorMsg = @"error"; +// called after a delay to upload a new image from the image picker... +// gives the mainloop a chance to switch views and render the leaders menu again before the upload starts +- (void)delayedNetworkConnectionErrorMessage +{ + lostConnection = true; + //MessageBox( errMsg, @"Network Error" ); + MessageBox(@"Unable to establish connection. Please connect to a different WiFi network or switch to 3G.", @"Network Error"); +} + + +- (void)viewDidLoad +{ + // Basic initialization. + alertView = nil; + sdkInitialized = false; + lostConnection = false; + + [appDelegate initGame]; + + // load login info from user defaults + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + userNameField.text = [ defaults stringForKey: useridKey ]; + passwordField.text = [ defaults stringForKey: passwordKey ]; + autoLoginSwitch.on = [ defaults boolForKey: autologinKey ]; + + // Seed the random number generator (for generating cookie values for nat negotiation). + time_t currentTime; + time(¤tTime); + srand(currentTime); + + // Check Network connectivity + if (![self isNetworkAvailableFlags:nil]) + { + // display error message in a second to give the system time to display the login page + [self performSelector:@selector(delayedNetworkConnectionErrorMessage) withObject:nil afterDelay:1.0f]; + return; + } + + // Init gsCore, Competition & Sake SDKs + [appDelegate initSake ]; + + // Login if auto login is true + if( autoLoginSwitch.on ) + { + [ self loginClicked: nil ]; + } +} + + +/*- (void)viewWillAppear: (BOOL)animated +{ + if (![self isNetworkAvailableFlags:nil]) + { + MessageBox( @"A network connection is not available. Application will close.", @"OK" ); + exit(0); + } +} +*/ + +/* +- (void)viewDidLoad +{ + if (![self isNetworkAvailableFlags:nil]) { + MessageBox( @"A network connection is not available. Application will close.", @"No Network" ); + exit(0); + } + // load login info + ////////////////// + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + if ([paths count] > 0) { + NSString* documentsPath = (NSString*)[paths objectAtIndex: 0]; + NSString* loginFilePath = [documentsPath stringByAppendingPathComponent: loginFileName]; + NSDictionary* loginDictionary = [NSDictionary dictionaryWithContentsOfFile: loginFilePath]; + + if (loginDictionary != nil) { + NSString* lastLogin = (NSString*)[loginDictionary objectForKey: lastLoginKey]; + + if (lastLogin != nil) { + autoLoginSwitch.on = YES; + NSDictionary* passwordsByLogin = (NSDictionary*)[loginDictionary objectForKey: passwordsByLoginKey]; + + if (passwordsByLogin != nil) { + NSString* password = [passwordsByLogin objectForKey: lastLogin]; + + if (password != nil) { + userNameField.text = lastLogin; + passwordField.text = password; + } + } + } + } + } +} +*/ + +- (NSInteger)numberOfComponentsInPickerView: (UIPickerView*)pickerView +{ + return 1; +} + +- (NSInteger)pickerView: (UIPickerView*)pickerView numberOfRowsInComponent: (NSInteger)component +{ + return [rememberedLogins count] + 1; +} + +- (NSString*)pickerView: (UIPickerView*)pickerView titleForRow: (NSInteger)row forComponent: (NSInteger)component +{ + if (row < (int) ([rememberedLogins count]) ) + + { + return (NSString*)[rememberedLogins objectAtIndex: row]; + } + + return @""; +} + +- (void)pickerView: (UIPickerView*)pickerView didSelectRow: (NSInteger)row inComponent: (NSInteger)component +{ + if (row < (int) ([rememberedLogins count]) ) + { + NSString* login = (NSString*)[rememberedLogins objectAtIndex: row]; + userNameField.text = login; + passwordField.text = (NSString*)[rememberedPasswordsByLogin objectForKey: login]; + [autoLoginSwitch setOn: YES animated: YES]; + } + else { + userNameField.text = nil; + passwordField.text = nil; + [autoLoginSwitch setOn: NO animated: YES]; + } + +} + +// Perform a reachability query for the address 0.0.0.0. If that address is reachable without +// requiring a connection, a network interface is available. We'll have to do more work to +// determine which network interface is available. +- (BOOL)isNetworkAvailableFlags:(SCNetworkReachabilityFlags *)outFlags +{ + SCNetworkReachabilityRef defaultRouteReachability; + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress); + + SCNetworkReachabilityFlags flags; + BOOL gotFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags); + if (!gotFlags) + return NO; + + BOOL isReachable = flags & kSCNetworkReachabilityFlagsReachable; + + // This flag indicates that the specified nodename or address can + // be reached using the current network configuration, but a + // connection must first be established. + // + // If the flag is false, we don't have a connection. But because CFNetwork + // automatically attempts to bring up a WWAN connection, if the WWAN reachability + // flag is present, a connection is not required. + BOOL noConnectionRequired = !(flags & kSCNetworkReachabilityFlagsConnectionRequired); + if ((flags & kSCNetworkReachabilityFlagsIsWWAN)) { + noConnectionRequired = YES; + } + + isReachable = (isReachable && noConnectionRequired) ? YES : NO; + + // Callers of this method might want to use the reachability flags, so if an 'out' parameter + // was passed in, assign the reachability flags to it. + if (outFlags) { + *outFlags = flags; + } + + return isReachable; +} + +- (IBAction)loginClicked: (id)sender +{ + [appDelegate initGame ]; + [appDelegate initSake ]; + + // Check for no account info + if(([userNameField.text length] == 0) || ([passwordField.text length] == 0)) + { + MessageBox(@"Please fill in all the account information."); + return; + } + + // Initialize GP + GPResult gpresult = gpInitialize(&connection, SCRACE_PRODUCTID, WSLogin_NAMESPACE_SHARED_UNIQUE, GP_PARTNERID_GAMESPY); + if( gpresult == GP_MEMORY_ERROR ) + { + MessageBox(@"Could not initialize the login system", @"Memory Error"); + return; + } + else if( gpresult == GP_PARAMETER_ERROR) + { + MessageBox(@"Could not initialize the login system", @"Parameter Error"); + return; + } + else if( gpresult == GP_SERVER_ERROR) + { + MessageBox(@"Could not initialize the login system", @"Server Error"); + return; + } + else if( gpresult == GP_NETWORK_ERROR) + { + MessageBox(@"Could not initialize the login system", @"Network Error"); + return; + } + else if( gpresult != GP_NO_ERROR) + { + MessageBox(@"Error initializing the login system"); + return; + } + + // set callback to be notified if same user logs into another device + gpSetCallback(&connection, GP_ERROR , (GPCallback)gpUserKickedOutCallback, self); + + // Connect to the account specified + const char* userName = [userNameField.text cStringUsingEncoding: NSASCIIStringEncoding]; + const char* password = [passwordField.text cStringUsingEncoding: NSASCIIStringEncoding]; + + GPResult result = gpConnectUniqueNickA(&connection, userName, password, GP_FIREWALL, GP_NON_BLOCKING, (GPCallback)gpLoginCallback, self); + if (result != GP_NO_ERROR) + { + switch (result) + { + case GP_PARAMETER_ERROR: + MessageBox(@"Please enter a valid user name and password."); + return; + + default: + MessageBox(@"Login failed. Try again when network connection is up", @"Connection Error"); + return; + } + } + + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Logging in..." delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; + [alertView show]; + loginTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onLoginTimer:) userInfo: nil repeats: YES]; + [activityIndicator startAnimating]; +} + +- (IBAction)returnClicked: (id)sender +{ + UITextField* textFields[] = { userNameField, passwordField }; + size_t textFieldCount = sizeof(textFields) / sizeof(textFields[0]); + size_t textFieldIndex = 0; + + // Find the text field that generated the event. + while (textFields[textFieldIndex] != sender) + textFieldIndex++; + + // Cycle focus to the next text field. + if (++textFieldIndex < textFieldCount) { + [textFields[textFieldIndex] becomeFirstResponder]; + } + else { + // [(UIResponder*)sender resignFirstResponder]; + } +} + + +- (IBAction)createClicked: (id)sender +{ + CreateUserController* createUserController = [[CreateUserController alloc] initWithNibName: @"createuser" bundle: nil]; + [self.navigationController pushViewController: createUserController animated: YES]; + createUserController.loginController = self; + createUserController.autoLoginSwitch.on = autoLoginSwitch.on; // use same setting as on login +} + +- (IBAction)forgotPasswordClicked: (id)sender +{ + ForgotPassword* forgotPasswordController = [[ForgotPassword alloc] initWithNibName: @"forgotPassword" bundle: nil]; + [self.navigationController pushViewController: forgotPasswordController animated: YES]; +} + +- (IBAction)autoLoginChanged: (id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [ defaults setBool: autoLoginSwitch.on forKey: autologinKey ]; +} + +/* +- (IBAction)autoLoginChanged: (id)sender +{ + if (!autoLoginSwitch.on) { + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + + if ([paths count] > 0) { + NSString* documentsPath = (NSString*)[paths objectAtIndex: 0]; + NSString* loginFilePath = [documentsPath stringByAppendingPathComponent: loginFileName]; + NSMutableDictionary* loginDictionary = [NSMutableDictionary dictionaryWithContentsOfFile: loginFilePath]; + + if (loginDictionary != nil) { + [loginDictionary removeObjectForKey: lastLoginKey]; + + NSMutableDictionary* passwordsByLogin = [NSMutableDictionary dictionaryWithDictionary: (NSDictionary*)[loginDictionary objectForKey: passwordsByLoginKey]]; + + if (passwordsByLogin != nil) { + [passwordsByLogin removeObjectForKey: userNameField.text]; + [loginDictionary setObject: passwordsByLogin forKey: passwordsByLoginKey]; + } + + [loginDictionary writeToFile: loginFilePath atomically: YES]; + } + } + } +} +*/ +/*- (IBAction)autoLoginChanged: (id)sender +{ + if (!rememberMeSwitch.on) { + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + + if ([paths count] > 0) { + NSString* documentsPath = (NSString*)[paths objectAtIndex: 0]; + NSString* loginFilePath = [documentsPath stringByAppendingPathComponent: loginFileName]; + NSMutableDictionary* loginDictionary = [NSMutableDictionary dictionaryWithContentsOfFile: loginFilePath]; + + if (loginDictionary != nil) { + [loginDictionary removeObjectForKey: lastLoginKey]; + + NSMutableDictionary* passwordsByLogin = [NSMutableDictionary dictionaryWithDictionary: (NSDictionary*)[loginDictionary objectForKey: passwordsByLoginKey]]; + + if (passwordsByLogin != nil) { + [passwordsByLogin removeObjectForKey: userNameField.text]; + [loginDictionary setObject: passwordsByLogin forKey: passwordsByLoginKey]; + } + + [loginDictionary writeToFile: loginFilePath atomically: YES]; + } + } + } +} +*/ + + +- (void)userProfileCreated: (CreateUserController*)createUserController autoLogin: (BOOL) autoLoginValue +{ + // if the autoLogin flag is true, turn on the switch first + autoLoginSwitch.on = autoLoginValue; + + // These objects disappear as soon as we pop the view controller. + // Popping the view conroller will trigger the viewWillAppear message, which calls reset, which + // destroys the login controller's connection object. Since we want to preserve the connection + // object created by the createUserController, assign it to a temporary variable rather than to + // self->connection. + GPConnection tempConnection = createUserController.connection; + userNameField.text = createUserController.uniquenick; + passwordField.text = createUserController.password; + + // This line destroys createUserController and triggers the viewWillAppear message. + [self.navigationController popToViewController: self animated: YES]; + + // By this point, viewWillAppear has been called, so it should now be safe to assign the connection + // object. + connection = tempConnection; + + // Continue the login process as usual. + loginTimer = [NSTimer scheduledTimerWithTimeInterval: 0.050 target: self selector: @selector(onLoginTimer:) userInfo: nil repeats: YES]; + [self loginCallback: GP_NO_ERROR profile: createUserController.profile uniquenick: [userNameField.text cStringUsingEncoding: NSASCIIStringEncoding]]; +} + +- (void)reset +{ + gpDisconnect(&connection); + gpDestroy(&connection); + + // Basic initialization. + alertView = nil; + sdkInitialized = false; + lostConnection = false; + + // clean up gPlayerData ... PlayerData should be promoted to a full object! + [gPlayerData.uniquenick release]; + gPlayerData.uniquenick = nil; + [gPlayerData.thumbNail release]; + gPlayerData.thumbNail = nil; + [gPlayerData.fullsizeImage release]; + gPlayerData.fullsizeImage = nil; + [gPlayerData.playerLocation release]; + gPlayerData.playerLocation = nil; + [gPlayerData.playerDataPath release]; + gPlayerData.playerDataPath = nil; + [gPlayerData.playerStatsData release]; + gPlayerData.playerStatsData = nil; + + // clean up gOpponentData + [gOpponentData.uniquenick release]; + gOpponentData.uniquenick = nil; + [gOpponentData.thumbNail release]; + gOpponentData.thumbNail = nil; + [gOpponentData.fullsizeImage release]; + gOpponentData.fullsizeImage = nil; + [gOpponentData.playerLocation release]; + gOpponentData.playerLocation = nil; + [gOpponentData.playerDataPath release]; + gOpponentData.playerDataPath = nil; + [gOpponentData.playerStatsData release]; + gOpponentData.playerStatsData = nil; + + // clean up leaderboards + [appDelegate deleteLeaderboards]; +} + +- (GPConnection *)getGPConnectionPtr +{ + return &connection; +} + + +- (void)stopLoginTimer +{ + if (loginTimer != nil) + { + [loginTimer invalidate]; + loginTimer = nil; + } +} + +@end + + +@implementation LoginController(Private) + +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ + if (buttonIndex == alertView.cancelButtonIndex) + { + [loginTimer invalidate]; + gpDisconnect(&connection); + gpDestroy(&connection); + + [activityIndicator stopAnimating]; + } +} + +- (BOOL)textFieldShouldBeginEditing: (UITextField*)textField +{ +/* if (textField == userNameField) { + if (userNamePickerView.hidden) { + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + + if ([paths count] > 0) { + NSString* documentsPath = (NSString*)[paths objectAtIndex: 0]; + NSString* loginFilePath = [documentsPath stringByAppendingPathComponent: loginFileName]; + NSDictionary* loginDictionary = [NSDictionary dictionaryWithContentsOfFile: loginFilePath]; + + if (loginDictionary != nil) { + // Released when the user disimisses the picker view. + rememberedPasswordsByLogin = [(NSDictionary*)[loginDictionary objectForKey: passwordsByLoginKey] retain]; + + if (rememberedPasswordsByLogin != nil) { + // Released when the user disimisses the picker view. + rememberedLogins = [[rememberedPasswordsByLogin allKeys] retain]; + + if ([rememberedLogins count] > 0) { + [userNamePicker reloadAllComponents]; + NSUInteger currentIndex = [rememberedLogins indexOfObject: userNameField.text]; + + if (currentIndex == NSNotFound) { + currentIndex = [rememberedLogins count]; + } + + [userNamePicker selectRow: currentIndex inComponent: 0 animated: NO]; + + [passwordField resignFirstResponder]; + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(closeUserNamePicker)] autorelease]; + userNamePickerView.alpha = 0.0f; + userNamePickerView.hidden = NO; + + [UIView beginAnimations: animationId context: nil]; + userNamePickerView.alpha = 1.0f; + [UIView commitAnimations]; + return NO; + } + } + } + } + } + } +*/ + return YES; +} + +- (void)textFieldDidBeginEditing: (UITextField*)textField +{ + //self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(doneClicked:)] autorelease]; +} + +- (void)textFieldDidEndEditing:(UITextField *)textField +{ + //self.navigationItem.rightBarButtonItem = nil; +} + +- (void)doneClicked: (id)sender +{ + UITextField* textFields[] = { userNameField, passwordField }; + size_t textFieldCount = sizeof(textFields) / sizeof(textFields[0]); + + for (size_t n = 0; n < textFieldCount; n++) { + [textFields[n] resignFirstResponder]; + } +} + +- (void)animationDidStop: (NSString*)animId finished: (BOOL)finished context: (id)context +{ + if ([userNamePicker selectedRowInComponent: 0] == [userNamePicker numberOfRowsInComponent: 0] - 1) { + [userNameField becomeFirstResponder]; + } + + userNamePickerView.hidden = YES; +} + +- (void)closeUserNamePicker +{ + [rememberedLogins release]; + [rememberedPasswordsByLogin release]; + + //self.navigationItem.rightBarButtonItem = nil; + + [UIView beginAnimations: animationId context: nil]; + [UIView setAnimationDelegate: self]; + [UIView setAnimationDidStopSelector: @selector(animationDidStop:finished:context:)]; + userNamePickerView.alpha = 0.0f; + [UIView commitAnimations]; +} + +- (void)onLoginTimer: (NSTimer*)timer +{ + //GPEnum connected; + + // We are still using core! + // The logic here is to think when the core is valid + // the core is valid + // 1) GSCore_IN_USE + // 2) GSCore_SHUTTING_DOWN + + // We may not have started using core also + if (gsCoreIsShutdown() != GSCore_SHUTDOWN_COMPLETE) + gsCoreThink(0); + + if (connection) + gpProcess(&connection); +} + +- (void)loginCallback: (GPResult)result profile: (GPProfile)profile uniquenick: (const char[])uniquenick +{ + [alertView dismissWithClickedButtonIndex: alertView.firstOtherButtonIndex animated: YES]; + + if (result != GP_NO_ERROR) { + [loginTimer invalidate]; + loginTimer = nil; + [activityIndicator stopAnimating]; + char errorString[GP_ERROR_STRING_LEN]; + gpGetErrorStringA(&connection, errorString); + MessageBox([NSString stringWithCString: errorString encoding: NSASCIIStringEncoding], @"Login Failed"); + return; + } + + // Save the login info and autosave flag for next time + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [ defaults setObject: userNameField.text forKey: useridKey ]; + [ defaults setObject: passwordField.text forKey: passwordKey ]; + [ defaults setBool: autoLoginSwitch.on forKey: autologinKey ]; + + // Store the profile ID for the player + ////////////////////////////////////// + gPlayerData.uniquenick = [[NSString alloc] initWithCString: uniquenick encoding: NSASCIIStringEncoding]; + gPlayerData.profileId = profile; + gpGetLoginTicket(&connection, gPlayerData.loginTicket); + + NSLog(@"login ticket[%s]", gPlayerData.loginTicket); + + const char* password = [passwordField.text cStringUsingEncoding: NSASCIIStringEncoding]; + gsi_u32 wsLoginResult = wsLoginUnique(WSLogin_PARTNERCODE_GAMESPY, WSLogin_NAMESPACE_SHARED_UNIQUE, uniquenick, password, NULL, wsLoginCallback, self); + + if (wsLoginResult != WSLogin_Success) + { + switch (wsLoginResult) + { + // Just output same error message for all login error type + default: + MessageBox(@"Failed to login to authentication service.", @"Login Failed"); + break; + } + + [loginTimer invalidate]; + loginTimer = nil; + [activityIndicator stopAnimating]; + gpDisconnect(&connection); + gpDestroy(&connection); + } +} + +- (void)loginCallback: (GHTTPResult)result certificate: (GSLoginCertificate*)certificate privateData: (GSLoginPrivateData*)privateData +{ + + if (result != GHTTPSuccess) + { + [activityIndicator stopAnimating]; + switch (result) + { + // Just output same error message for all login error type + default: + MessageBox(@"Failed to login to authentication service.", @"Login Failed"); + break; + } + + [loginTimer invalidate]; + loginTimer = nil; + + gpDisconnect(&connection); + gpDestroy(&connection); + + return; + } + + // Store the Login Certificate & Private Data for later use w/ Competition + ////////////////////////////////////////////////////////////////////////// + memcpy(&gPlayerData.certificate, certificate, sizeof(GSLoginCertificate)); + memcpy(&gPlayerData.privateData, privateData, sizeof(GSLoginPrivateData)); + + [activityIndicator stopAnimating]; + [appDelegate loggedIn]; +} + +@end + + +void gpUserKickedOutCallback( GPConnection* connection, void* arg, void* param ) +{ + GPErrorCode err = ((GPErrorArg*)arg)->errorCode; + if( err == GP_FORCED_DISCONNECT || err == GP_CONNECTION_CLOSED ) + { + // LoginController* loginController = (LoginController*)[appDelegate findExistingControllerOfType: [LoginController class]]; + // [loginController stopLoginTimer]; + + [appDelegate kickedOut]; + NSLog(@"User logged into other device.\n"); + MessageBox( @"You have been disconnected because another player has logged in with your ID.", @"Logged Out" ); + [appDelegate logout]; + } +} + + +void gpLoginCallback(GPConnection* connection, GPConnectResponseArg* arg, void* param) +{ + LoginController* loginController = (LoginController*)param; + [loginController loginCallback: arg->result profile: arg->profile uniquenick: arg->uniquenick]; +} + +void wsLoginCallback(GHTTPResult httpResult, WSLoginResponse* response, void* userData) +{ + LoginController* loginController = (LoginController*)userData; + [loginController loginCallback: httpResult certificate: &response->mCertificate privateData: &response->mPrivateData]; +} diff --git a/Samples/TapRace/iphone/MatchmakingController.h b/Samples/TapRace/iphone/MatchmakingController.h new file mode 100644 index 00000000..45147cb1 --- /dev/null +++ b/Samples/TapRace/iphone/MatchmakingController.h @@ -0,0 +1,37 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import + +@interface MatchmakingController : UIViewController +{ + IBOutlet UIPickerView *gamePicker; + IBOutlet UIButton *joinButton; + IBOutlet UIButton *hostButton; + IBOutlet UIButton *RefreshButton; + IBOutlet UILabel *shakeLabel; + IBOutlet UILabel *listCountLabel; + + UIAccelerationValue myAccelerometer[3]; // high pass shake detection + + std::vector servers; +} + +- (IBAction)joinButtonClicked: (id)sender; +- (IBAction)hostButtonClicked: (id)sender; +- (IBAction)refreshButtonClicked: (id)sender; + +//- (void)hostCountdown: (int)count; +- (void)hostingCanceled; +- (void)enableShake: (bool) enable; + +- (void)reset; + +- (void)addServer: (NSString*)name address: (unsigned int)address port: (unsigned short)port useNatNeg: (bool)useNatNeg; +- (void)updateServer: (NSString*)name address: (unsigned int)address port: (unsigned short)port; +- (void)removeServer: (unsigned int)address port: (unsigned short)port; + +- (void)updatePicker; + +@end diff --git a/Samples/TapRace/iphone/MatchmakingController.mm b/Samples/TapRace/iphone/MatchmakingController.mm new file mode 100644 index 00000000..13648bb1 --- /dev/null +++ b/Samples/TapRace/iphone/MatchmakingController.mm @@ -0,0 +1,522 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +// Matchmaking is for selecting/looking for a server (aka player) to play a game with + +#import "MatchmakingController.h" +#import "LoginController.h" +#import "AppDelegate.h" +#import "Utility.h" + +#import "qr2/qr2.h" + +// Values for detecting shake +#define kAccelerometerFrequency 25 //Hz +#define kFilteringFactor 0.1 +#define kMinShakeInterval 0.5 +#define kShakeAccelerationThreshold 3.0 + +// Structure to hold server connection info +struct ServerData +{ + NSString* name; // Server name + unsigned int address; // IP address + unsigned short port; // port to connect on + bool useNatNeg; // true if IP is not local and NAT negotiation is required +}; + + +// Server Browsing Callback +static void SBCallback(ServerBrowser sb, SBCallbackReason reason, SBServer server, void* instance); + +static bool sbeError = false; + + +@interface MatchmakingController(Private) + +@end + + +@implementation MatchmakingController + +static gsi_bool sbUpdateFinished = gsi_false; // true when query for list of servers is complete + +// Release objects in page & server list +// ===================================== +- (void)dealloc +{ + for (std::vector::iterator i = servers.begin(); i != servers.end(); ++i) { + [i->name release]; + } + + [gamePicker release]; + [joinButton release]; + [hostButton release]; + [shakeLabel release]; + + [super dealloc]; +} + + +// Do not support auto-rotate +// ===================================== +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Support portrait orientation only + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + + +// When page will appear +// - setup GUI logic +// - start query for list of servers +// ===================================== +- (void)viewWillAppear: (BOOL)animated +{ + // tell host to quit game if I just came from gameController + [appDelegate stopHostingServer]; + + // setup GUI + self.title = @"MatchMaking"; + joinButton.enabled = NO; + //[RefreshButton setTitle: @"Refresh" forState: UIControlStateNormal ]; + //[RefreshButton setTitle: @"Refreshing" forState: UIControlStateHighlighted ]; + + // Query server list + ServerBrowser serverBrowser = ServerBrowserNewA(SCRACE_GAMENAME, SCRACE_GAMENAME, SCRACE_SECRETKEY, 0, MAX_SERVERS, QVERSION_QR2, SBFalse, SBCallback, self); + appDelegate.serverBrowser = serverBrowser; + + unsigned char basicFields[] = { HOSTNAME_KEY, HOSTPORT_KEY }; + size_t basicFieldCount = sizeof(basicFields) / sizeof(basicFields[0]); + sbUpdateFinished = gsi_false; + + // Get available servers + const char freeServersFilter[] = {"numplayers < maxplayers" }; + sbeError = false; + SBError result = ServerBrowserLimitUpdate(serverBrowser, SBTrue, SBTrue, basicFields, basicFieldCount, freeServersFilter, MAX_SERVERS); + + if (result != sbe_noerror) + { + sbeError = true; + + // MessageBox(@"Error communicating with GameSpy master server."); + // ServerBrowserFree(serverBrowser); + return; + } + + //Configure and enable the accelerometer + [UIAccelerometer sharedAccelerometer].updateInterval = (1.0 / kAccelerometerFrequency); + [UIAccelerometer sharedAccelerometer].delegate = self; +} + +- (void)viewDidAppear: (BOOL)animated +{ + if ( sbeError ) + { + //MessageBox(@"Error retrieving Games.", @"Connection Error"); + //ServerBrowserFree(appDelegate.serverBrowser); + return; + } + +} + + + +// When the page will disappear +// - release servers +// - stop hosting timer if any +// - stop check for shake +// ===================================== +- (void)viewWillDisappear: (BOOL)animated +{ + // Clear server list in GUI + for (std::vector::iterator i = servers.begin(); i != servers.end(); ++i) + { + [i->name release]; + } + servers.clear(); + + // Stop hosting if it was + [appDelegate stopHostingCountdownTimer]; + + // Clear server list in structure + ServerBrowser serverBrowser = appDelegate.serverBrowser; + if (serverBrowser != NULL) + { + ServerBrowserFree(serverBrowser); + appDelegate.serverBrowser = NULL; + } + + // Stop checking for shake when close matchmaking page + UIAccelerometer* accelerometer = [UIAccelerometer sharedAccelerometer]; + if (accelerometer.delegate == self) + accelerometer.delegate = nil; +} + + +// When Join button is pressed +// - update GUI logic +// - update server list to see is selected server is still available +// - if it is, initiate join to remote server +// ===================================== +- (IBAction)joinButtonClicked: (id)sender +{ + // disable join & accelarometer + [self enableShake:NO]; + joinButton.enabled = NO; + + // get selected player name from list + NSInteger index = [gamePicker selectedRowInComponent: 0]; + NSString *selectedServerName = servers.at(index).name; ; + NSString *serverName = [[NSString alloc] initWithString: selectedServerName ] ; + + // renew server list + [self refreshButtonClicked: sender ]; + + // wait for server list to update + while ((ServerBrowserThink(appDelegate.serverBrowser) == sbe_noerror) && (sbUpdateFinished == gsi_false)) + msleep(10); + + index = -1; + BOOL foundServer = NO; + + // get selected server name from renewed list & highlight selected server + for( std::vector::iterator i = servers.begin(); i != servers.end(); ++i) + { + index++; + if( [serverName compare: i->name] == NSOrderedSame ) + { + foundServer = YES; + [gamePicker selectRow: index inComponent: 0 animated: YES ]; + break; + } + } + + // if server name is not found, popup error + if( foundServer == NO ) + { + MessageBox(@"is not hosting anymore. Select another player.", serverName); + [self enableShake:YES]; + joinButton.enabled = NO; + return; + } + + // else join match + + // get selected player from list + index = [gamePicker selectedRowInComponent: 0]; + ServerData* data = &servers[index]; + + // connect to host player + // if not connected or join canceled, send NN cancel to host server + if( [appDelegate joinGame: data->address port: data->port useNatNeg: data->useNatNeg] == false ) + { + [self enableShake:YES]; + joinButton.enabled = NO; + //SBSendMessageToServer(sbServer, data->address, data->port, "CANCEL_CONNECTION", <#int len#>); + } +} + + +// Called when Host button clicked +// - setup GUI +// - initiate registration of host with GameSpy server +// ===================================== +- (IBAction)hostButtonClicked: (id)sender +{ + // turn off the join button and accelerometer + joinButton.enabled = NO; + [ self enableShake: NO]; + + if ( ! [appDelegate hostGame] ) + { + [self hostingCanceled ]; + } +} + + +// Called when user cancels Hosting or timeout +// - release host countdwon timer +// - setup GUI logic +// ===================================== +- (void)hostingCanceled +{ + [appDelegate stopHostingCountdownTimer]; + + if( servers.size() > 0 ) + joinButton.enabled = YES; + + [ self enableShake: YES]; +} + + +// Enables/disables shake detection +// ===================================== +- (void)enableShake: (bool) enable +{ + if( enable ) + { + shakeLabel.hidden = NO; + [UIAccelerometer sharedAccelerometer].delegate = self; + } + else + { + shakeLabel.hidden = YES; + + UIAccelerometer* accelerometer = [UIAccelerometer sharedAccelerometer]; + if (accelerometer.delegate == self) + accelerometer.delegate = nil; + } +} + + +// Called when user cancels Hosting or timeout +// - reload server list in GUI +// - setup GUI logic +// ===================================== +- (void)reset +{ + [appDelegate stopHostingCountdownTimer]; + + // get new server list + [gamePicker reloadAllComponents]; + + [ self enableShake: YES]; + + // enable join if there are servers + joinButton.enabled = ( servers.size() > 0 ); +} + +// +// ===================================== +- (void)addServer: (NSString*)name address: (unsigned int)address port: (unsigned short)port useNatNeg: (bool)useNatNeg +{ + servers.push_back((ServerData){ [name copy], address, port, useNatNeg }); +} + + +// ===================================== +- (void)updateServer: (NSString*)name address: (unsigned int)address port: (unsigned short)port +{ + for (std::vector::iterator i = servers.begin(); i != servers.end(); ++i) { + if ((i->address == address) && (i->port == port)) { + [i->name release]; + i->name = [name copy]; + break; + } + } +} + + +// ===================================== +- (void)removeServer: (unsigned int)address port: (unsigned short)port +{ + for (std::vector::iterator i = servers.begin(); i != servers.end(); ++i) { + if ((i->address == address) && (i->port == port)) { + [i->name release]; + servers.erase(i); + break; + } + } +} + + +// ===================================== +- (void)updatePicker +{ + [gamePicker reloadAllComponents]; + + listCountLabel.text = [NSString stringWithFormat: @"Games available: %d", servers.size() ]; + + [self enableShake:YES]; + + // enable join & shake if there are servers + if( servers.size() > 0 ) + joinButton.enabled = YES; +} + + +// Called when the accelerometer detects motion; if shaken with enough force, makes a random selection from the choices. +// ===================================== +- (void) accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration +{ + UIAccelerationValue length, x, y, z; + + //Use a basic high-pass filter to remove the influence of the gravity + myAccelerometer[0] = acceleration.x * kFilteringFactor + myAccelerometer[0] * (1.0 - kFilteringFactor); + myAccelerometer[1] = acceleration.y * kFilteringFactor + myAccelerometer[1] * (1.0 - kFilteringFactor); + myAccelerometer[2] = acceleration.z * kFilteringFactor + myAccelerometer[2] * (1.0 - kFilteringFactor); + + // Compute values for the three axes of the acceleromater + x = acceleration.x - myAccelerometer[0]; + y = acceleration.y - myAccelerometer[0]; + z = acceleration.z - myAccelerometer[0]; + + //Compute the intensity of the current acceleration + length = sqrt(x * x + y * y + z * z); + + // If above a given threshold, pick a server at random and join + if( length >= kShakeAccelerationThreshold ) + { + //NSInteger n = [gamePicker numberOfRowsInComponent: 0]; + + // Update server list if none in it + if( servers.size() < 1 ) + { + // renew server list + [self refreshButtonClicked: self ]; + + // wait for server list to update + while ((ServerBrowserThink(appDelegate.serverBrowser) == sbe_noerror) && (sbUpdateFinished == gsi_false)) + msleep(10); + } + + // Host if no servers available + if( servers.size() < 1 ) + { + [self hostButtonClicked: self ]; + } + else + { + // select random player to join + int index = (rand() & INT_MAX) % servers.size(); + ServerData* data = &servers[index]; + + // turn off the join button and accelerometer + joinButton.enabled = NO; + [ self enableShake: NO]; + + // join + [appDelegate joinGame: data->address port: data->port useNatNeg: data->useNatNeg]; + } + + } +} + + +// ===================================== +- (IBAction)refreshButtonClicked: (id)sender +{ + joinButton.enabled = NO; + [self viewWillDisappear: NO]; // use the shutdown function to kill servers and picker list + [self viewWillAppear: NO]; // and pretend we're starting up again + [self updatePicker]; + +} + + +/* ===================================== +- (void)hostCountdown: (int) count +{ + if (hosting) + { + alertView.message = @"qwe qwe"; + } + + //[gameController countdown: countdown]; + +} +*/ +@end + + +@implementation MatchmakingController(Private) + +- (NSString*)pickerView: (UIPickerView*)pickerView titleForRow: (NSInteger)row forComponent: (NSInteger)component +{ + return servers[row].name; +} + +- (void)pickerView: (UIPickerView*)pickerView didSelectRow: (NSInteger)row inComponent: (NSInteger)component +{ +} + +- (NSInteger)numberOfComponentsInPickerView: (UIPickerView*)pickerView +{ + return 1; +} + +- (NSInteger)pickerView: (UIPickerView*)pickerView numberOfRowsInComponent: (NSInteger)component +{ + return servers.size(); +} + +@end + + +void SBCallback(ServerBrowser sb, SBCallbackReason reason, SBServer server, void* instance) +{ + MatchmakingController* controller = (MatchmakingController*)instance; + + switch (reason) { + case sbc_serveradded: + case sbc_serverupdated: + case sbc_serverupdatefailed: + case sbc_serverdeleted: + { + NSLog(@"SBCallback added/updated/deleted"); + unsigned int address = SBServerGetPublicInetAddress(server); + unsigned short port = SBServerGetPublicQueryPort(server); + bool useNatNeg = false; + + // Server is behind a NAT device; check and see if it's local. + if (SBServerHasPrivateAddress(server) && (address == ServerBrowserGetMyPublicIPAddr(sb))) { + // Use the private address. + address = SBServerGetPrivateInetAddress(server); + port = SBServerGetPrivateQueryPort(server); + } + else { + // We'll have to use NAT negotiation to connect. + useNatNeg = true; + } + + + NSString* name = [NSString stringWithCString: SBServerGetStringValueA(server, qr2_registered_key_list[HOSTNAME_KEY], "Unnamed Game") encoding: NSASCIIStringEncoding]; + + switch (reason) { + case sbc_serveradded: + if( [name compare: gPlayerData.uniquenick ] != NSOrderedSame ) + { + [controller addServer: name address: address port: port useNatNeg: useNatNeg]; + NSLog(@"SB adding"); + } + break; + + case sbc_serverupdated: + [controller updateServer: name address: address port: port]; + NSLog(@"SB updating"); + break; + + case sbc_serverupdatefailed: + case sbc_serverdeleted: + [controller removeServer: address port: port]; + NSLog(@"SB deleting"); + break; + + case sbc_updatecomplete: + case sbc_queryerror: + case sbc_serverchallengereceived: + NSLog(@"SB nothing to do code=%d", reason); + break; + } + break; + } + + case sbc_updatecomplete: + NSLog(@"SBCallback sbc_updatecomplete"); + sbUpdateFinished = gsi_true; + [controller updatePicker]; + break; + + case sbc_queryerror: + NSLog(@"SBCallback sbc_queryerror"); + sbUpdateFinished = gsi_true; + MessageBox([NSString stringWithCString: ServerBrowserListQueryErrorA(sb) encoding: NSASCIIStringEncoding]); + break; + + case sbc_serverchallengereceived: + NSLog(@"SBCallback sbc_serverchallengereceived"); + break; + } +} + diff --git a/Samples/TapRace/iphone/MenuController.h b/Samples/TapRace/iphone/MenuController.h new file mode 100644 index 00000000..f3e079e5 --- /dev/null +++ b/Samples/TapRace/iphone/MenuController.h @@ -0,0 +1,37 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import "MyCLController.h" +#import "FlipsideViewController.h" + +@interface MenuController : UIViewController +{ + IBOutlet UIButton* singlePlayerButton; + IBOutlet UIButton* multiPlayerButton; + IBOutlet UIButton* leaderboardsButton; + IBOutlet UILabel* leadersLabel; + IBOutlet UILabel* leadersPositionLabel; + IBOutlet UILabel* leadersTimeLabel; + + NSTimer* displayLeaderTimer; + + BOOL isCurrentlyUpdating; + UIAlertView* alertView; +} + +@property (nonatomic, retain) UILabel* leadersLabel; +@property (nonatomic, retain) UILabel* leadersPositionLabel; +@property (nonatomic, retain) UILabel* leadersTimeLabel; + +- (IBAction)onSinglePlayerClicked: (id)sender; +- (IBAction)onMultiPlayerClicked: (id)sender; +- (IBAction)onLeaderboardsClicked: (id)sender; +//- (IBAction)showAbout: (id) sender; +- (IBAction)showInfo; + +- (void)reset; +-(void)noTop5Leaders; + + +@end diff --git a/Samples/TapRace/iphone/MenuController.mm b/Samples/TapRace/iphone/MenuController.mm new file mode 100644 index 00000000..d56effe2 --- /dev/null +++ b/Samples/TapRace/iphone/MenuController.mm @@ -0,0 +1,230 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "MenuController.h" +#import "AboutController.h" +#import "AppDelegate.h" +#import + +#define ROTATE_LEADER_TIME 3 // seconds + +@interface MenuController(Private) + +- (void)updateLeaderLabel: (NSTimer*)timer; + +@end + + + +@implementation MenuController + + +@synthesize leadersLabel; +@synthesize leadersPositionLabel; +@synthesize leadersTimeLabel; + +- (void)dealloc +{ + [MyCLController sharedInstance].delegate = nil; // we won't handle this any more + [singlePlayerButton release]; + [multiPlayerButton release]; + [leaderboardsButton release]; + [super dealloc]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + + + +- (IBAction)onSinglePlayerClicked: (id)sender +{ + [appDelegate startSinglePlayerGame]; +} + +- (IBAction)onMultiPlayerClicked: (id)sender +{ + [appDelegate findGame]; +} + +- (IBAction)onLeaderboardsClicked: (id)sender +{ + [appDelegate showLeaderboards]; +} + +- (void)reset +{ + // Empty +} + +- (void)updatePosition: (BOOL)start +{ + if (!start) { + [[MyCLController sharedInstance].locationManager stopUpdatingLocation]; + isCurrentlyUpdating = NO; + } else { + [[MyCLController sharedInstance].locationManager startUpdatingLocation]; + isCurrentlyUpdating = YES; + } +} + + +- (void)viewWillDisappear: (BOOL)animated +{ + [self updatePosition: NO]; + + if( displayLeaderTimer != nil ) + { + [displayLeaderTimer invalidate]; + } +} + + +- (void)viewDidLoad +{ + isCurrentlyUpdating = NO; + + [MyCLController sharedInstance].delegate = self; + + // Check to see if the user has disabled location services + // In that case, we just print a message + NSString *modelType = [UIDevice currentDevice].model; + if( [modelType isEqualToString: (NSString *) @"iPhone" ] ) + { + isCurrentlyUpdating = YES; + + if ( ! [MyCLController sharedInstance].locationManager.locationServicesEnabled ) + { + alertView = [[UIAlertView alloc] initWithTitle: nil message: @"Location Services Disabled" delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + [alertView show]; + [self updatePosition: NO]; + } + else + { + [self updatePosition: YES]; + } + } +} + +- (void)viewWillAppear: (BOOL) animated +{ + self.title = @"Menu"; + [appDelegate showTop5LeadersByTime]; + + displayLeaderTimer = [NSTimer scheduledTimerWithTimeInterval: ROTATE_LEADER_TIME target: self selector: @selector(updateLeaderLabel:) userInfo: nil repeats: YES]; +} + + +-(void)noTop5Leaders +{ + leadersLabel.text = @"No leaders yet!"; + [displayLeaderTimer invalidate]; +} + + +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ +} + + + - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller + { + [self dismissModalViewControllerAnimated:YES]; + } + + + - (IBAction)showInfo + { + [ appDelegate showAboutInfo: self ]; + } + + + +#pragma mark ---- delegate methods for the MyCLController class ---- +-(void)newLocationUpdateLat:(float)lat Long: (float)lng +{ + NSLog(@"updated player location to %.5f %.5f", lat, lng); + isCurrentlyUpdating = NO; + if (gPlayerData.playerLocation == nil) + { + gPlayerData.playerLocation = [[CLLocation alloc] initWithLatitude: lat longitude: lng]; + } + else + { + [gPlayerData.playerLocation initWithLatitude: lat longitude: lng]; + } + + [self updatePosition: NO]; +} + +-(void)newError:(NSString *)text +{ + isCurrentlyUpdating = NO; + [self updatePosition: NO]; + alertView = [[UIAlertView alloc] initWithTitle: @"Location Services Failed" message: text delegate: self cancelButtonTitle: @"OK" otherButtonTitles: nil]; + [alertView show]; +} + + +@end + + +@implementation MenuController(Private) + +#define TOP_LEADERS_UPDATE_INTERVAL 10*60*1000 +//#define TOP_LEADER_COUNT 5 + +// rotate display of Top 5 in Menu page every 3 seconds +- (void)updateLeaderLabel: (NSTimer*)timer; +{ + static int displayedLeaderNumber = -1; + int maxLeaders = [ gLeaderboardData count ]; + + if( maxLeaders == 0 ) + return; + + ++displayedLeaderNumber; + if( displayedLeaderNumber >= maxLeaders ) + displayedLeaderNumber = 0; + + int secs = ((LeaderboardObject*)[ gLeaderboardData objectAtIndex:displayedLeaderNumber ]).bestRaceTime /1000; + int millis = ((LeaderboardObject*)[ gLeaderboardData objectAtIndex:displayedLeaderNumber ]).bestRaceTime - (secs * 1000); + NSString* leaderName = [((LeaderboardObject*)[gLeaderboardData objectAtIndex: displayedLeaderNumber]).name copy ]; + + leadersPositionLabel.text = [NSString stringWithFormat: @"%d", displayedLeaderNumber+1 ]; + leadersLabel.text = leaderName; //[NSString stringWithFormat: @"%d %s", displayedLeaderNumber+1, leaderName ]; + leadersTimeLabel.text = [NSString stringWithFormat: @"%2d.%03d", secs, millis ]; +} + + +/*- (void)updateLeaderLabel: (NSTimer*)timer; +{ +/* static int currentLeaderNumber = -1; + static uint64_t curTime, lastUpdateTime = nil; + + curTime = mach_absolute_time(); + + if( lastUpdateTime == nil || lastUpdateTime - curTime > TOP_LEADERS_UPDATE_INTERVAL ) + { + // update latest Top 5 leaders + [appDelegate showLeadersByTime ]; + } + + // set last update time + lastUpdateTime = curTime; + + // decide next leader to display + if( currentLeaderNumber == TOP_LEADER_COUNT ) + currentLeaderNumber = -1; + + ++currentLeaderNumber; + + leadersLabel.text = [NSString stringWithFormat: @" %d. %s", currentLeaderNumber, gLeaderboardData.]; +} +*/ + + +@end diff --git a/Samples/TapRace/iphone/MyCLController.h b/Samples/TapRace/iphone/MyCLController.h new file mode 100644 index 00000000..6a0c3a23 --- /dev/null +++ b/Samples/TapRace/iphone/MyCLController.h @@ -0,0 +1,82 @@ +/* + This source was taken from the LocateMe sample application released by APPLE +as part of the iPhone SDK. +*/ + +/* + +File: MyCLController.h +Abstract: Singleton class used to talk to CoreLocation and send results back to +the app's view controllers. + +Version: 1.1 + +Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. +("Apple") in consideration of your agreement to the following terms, and your +use, installation, modification or redistribution of this Apple software +constitutes acceptance of these terms. If you do not agree with these terms, +please do not use, install, modify or redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and subject +to these terms, Apple grants you a personal, non-exclusive license, under +Apple's copyrights in this original Apple software (the "Apple Software"), to +use, reproduce, modify and redistribute the Apple Software, with or without +modifications, in source and/or binary forms; provided that if you redistribute +the Apple Software in its entirety and without modifications, you must retain +this notice and the following text and disclaimers in all such redistributions +of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may be used +to endorse or promote products derived from the Apple Software without specific +prior written permission from Apple. Except as expressly stated in this notice, +no other rights or licenses, express or implied, are granted by Apple herein, +including but not limited to any patent rights that may be infringed by your +derivative works or by other works in which the Apple Software may be +incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO +WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED +WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN +COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR +DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF +CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF +APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2008 Apple Inc. All Rights Reserved. + +*/ + + +// This protocol is used to send the text for location updates back to another view controller +@protocol MyCLControllerDelegate +@required +-(void)newLocationUpdateLat:(float)lat Long: (float)lng; +-(void)newError:(NSString *)text; +@end + + +// Class definition +@interface MyCLController : NSObject { + CLLocationManager *locationManager; + id delegate; +} + +@property (nonatomic, retain) CLLocationManager *locationManager; +@property (nonatomic,assign) id delegate; + +- (void)locationManager:(CLLocationManager *)manager + didUpdateToLocation:(CLLocation *)newLocation + fromLocation:(CLLocation *)oldLocation; + +- (void)locationManager:(CLLocationManager *)manager + didFailWithError:(NSError *)error; + ++ (MyCLController *)sharedInstance; + +@end + diff --git a/Samples/TapRace/iphone/MyCLController.mm b/Samples/TapRace/iphone/MyCLController.mm new file mode 100644 index 00000000..9fe44846 --- /dev/null +++ b/Samples/TapRace/iphone/MyCLController.mm @@ -0,0 +1,254 @@ +/* + This source was taken (and modified) from the LocateMe sample application released by APPLE + as part of the iPhone SDK. + */ + +/* + +File: MyCLController.m +Abstract: Singleton class used to talk to CoreLocation and send results back to +the app's view controllers. + +Version: 1.1 + +Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. +("Apple") in consideration of your agreement to the following terms, and your +use, installation, modification or redistribution of this Apple software +constitutes acceptance of these terms. If you do not agree with these terms, +please do not use, install, modify or redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and subject +to these terms, Apple grants you a personal, non-exclusive license, under +Apple's copyrights in this original Apple software (the "Apple Software"), to +use, reproduce, modify and redistribute the Apple Software, with or without +modifications, in source and/or binary forms; provided that if you redistribute +the Apple Software in its entirety and without modifications, you must retain +this notice and the following text and disclaimers in all such redistributions +of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may be used +to endorse or promote products derived from the Apple Software without specific +prior written permission from Apple. Except as expressly stated in this notice, +no other rights or licenses, express or implied, are granted by Apple herein, +including but not limited to any patent rights that may be infringed by your +derivative works or by other works in which the Apple Software may be +incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO +WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED +WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN +COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR +DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF +CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF +APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2008 Apple Inc. All Rights Reserved. + +*/ + +#import "MyCLController.h" +#import "AppDelegate.h" + + +// Shorthand for getting localized strings, used in formats below for readability +//#define LocStr(key) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil] + + +// This is a singleton class, see below +static MyCLController *sharedCLDelegate = nil; + +@implementation MyCLController + +@synthesize delegate, locationManager; + +- (id) init { + self = [super init]; + if (self != nil) { + self.locationManager = [[[CLLocationManager alloc] init] autorelease]; + self.locationManager.delegate = self; // Tells the location manager to send updates to this object + [self.locationManager startUpdatingLocation]; + } + return self; +} + + +// Called when the location is updated +- (void)locationManager:(CLLocationManager *)manager + didUpdateToLocation:(CLLocation *)newLocation + fromLocation:(CLLocation *)oldLocation +{ + NSLog(@"got cl callback"); + if (delegate == nil) return; + if (signbit(newLocation.horizontalAccuracy)) { + // Negative accuracy means an invalid or unavailable measurement + + // TODO: pop-up message saying no coords available + } else { + // parse the string to extract the lat / long + // format looks like: <+37.33168900, -122.03073100> +/- 100.00m @ 2008-10-21 23:33:05 -0700 + [self.delegate newLocationUpdateLat: newLocation.coordinate.latitude Long: newLocation.coordinate.longitude ]; + } + +/*#if 0 // don't need to get all this data, so commented out. + NSMutableString *update = [[[NSMutableString alloc] init] autorelease]; + + // Timestamp + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setDateStyle:NSDateFormatterMediumStyle]; + [dateFormatter setTimeStyle:NSDateFormatterMediumStyle]; + [update appendFormat:@"%@\n\n", [dateFormatter stringFromDate:newLocation.timestamp]]; + + // Horizontal coordinates + if (signbit(newLocation.horizontalAccuracy)) { + // Negative accuracy means an invalid or unavailable measurement + [update appendString:LocStr(@"LatLongUnavailable")]; + } else { + // CoreLocation returns positive for North & East, negative for South & West + [update appendFormat:LocStr(@"LatLongFormat"), // This format takes 4 args: 2 pairs of the form coordinate + compass direction + fabs(newLocation.coordinate.latitude), signbit(newLocation.coordinate.latitude) ? LocStr(@"South") : LocStr(@"North"), + fabs(newLocation.coordinate.longitude), signbit(newLocation.coordinate.longitude) ? LocStr(@"West") : LocStr(@"East")]; + [update appendString:@"\n"]; + [update appendFormat:LocStr(@"MeterAccuracyFormat"), newLocation.horizontalAccuracy]; + } + [update appendString:@"\n\n"]; + + // Altitude + if (signbit(newLocation.verticalAccuracy)) { + // Negative accuracy means an invalid or unavailable measurement + [update appendString:LocStr(@"AltUnavailable")]; + } else { + // Positive and negative in altitude denote above & below sea level, respectively + [update appendFormat:LocStr(@"AltitudeFormat"), fabs(newLocation.altitude), (signbit(newLocation.altitude)) ? LocStr(@"BelowSeaLevel") : LocStr(@"AboveSeaLevel")]; + [update appendString:@"\n"]; + [update appendFormat:LocStr(@"MeterAccuracyFormat"), newLocation.verticalAccuracy]; + } + [update appendString:@"\n\n"]; + + // Calculate disatance moved and time elapsed, but only if we have an "old" location + // + // NOTE: Timestamps are based on when queries start, not when they return. CoreLocation will query your + // location based on several methods. Sometimes, queries can come back in a different order from which + // they were placed, which means the timestamp on the "old" location can sometimes be newer than on the + // "new" location. For the example, we will clamp the timeElapsed to zero to avoid showing negative times + // in the UI. + // + if (oldLocation != nil) { + CLLocationDistance distanceMoved = [newLocation getDistanceFrom:oldLocation]; + NSTimeInterval timeElapsed = [newLocation.timestamp timeIntervalSinceDate:oldLocation.timestamp]; + + [update appendFormat:LocStr(@"LocationChangedFormat"), distanceMoved]; + if (signbit(timeElapsed)) { + [update appendString:LocStr(@"FromPreviousMeasurement")]; + } else { + [update appendFormat:LocStr(@"TimeElapsedFormat"), timeElapsed]; + } + [update appendString:@"\n\n"]; + } + + // Send the update to our delegate + [self.delegate newLocationUpdate:update]; +#endif +*/ +} + + +// Called when there is an error getting the location +- (void)locationManager:(CLLocationManager *)manager + didFailWithError:(NSError *)error +{ + if (delegate == nil) return; + + NSMutableString *errorString = [[[NSMutableString alloc] init] autorelease]; + + if ([error domain] == kCLErrorDomain) { + + // We handle CoreLocation-related errors here + + switch ([error code]) { + // This error code is usually returned whenever user taps "Don't Allow" in response to + // being told your app wants to access the current location. Once this happens, you cannot + // attempt to get the location again until the app has quit and relaunched. + // + // "Don't Allow" on two successive app launches is the same as saying "never allow". The user + // can reset this for all apps by going to Settings > General > Reset > Reset Location Warnings. + // + case kCLErrorDenied: + [errorString appendFormat:@"%@\n", NSLocalizedString(@"LocationDenied", nil)]; + break; + + // This error code is usually returned whenever the device has no data or WiFi connectivity, + // or when the location cannot be determined for some other reason. + // + // CoreLocation will keep trying, so you can keep waiting, or prompt the user. + // + case kCLErrorLocationUnknown: + [errorString appendFormat:@"%@\n", NSLocalizedString(@"LocationUnknown", nil)]; + break; + + // We shouldn't ever get an unknown error code, but just in case... + // + default: + [errorString appendFormat:@"%@ %d\n", NSLocalizedString(@"GenericLocationError", nil), [error code]]; + break; + } + } else { + // We handle all non-CoreLocation errors here + // (we depend on localizedDescription for localization) + [errorString appendFormat:@"Error domain: \"%@\" Error code: %d\n", [error domain], [error code]]; + [errorString appendFormat:@"Description: \"%@\"\n", [error localizedDescription]]; + } + + // Send the update to our delegate + [self.delegate newError:errorString]; +} + +#pragma mark ---- singleton object methods ---- + +// See "Creating a Singleton Instance" in the Cocoa Fundamentals Guide for more info + ++ (MyCLController *)sharedInstance { + @synchronized(self) { + if (sharedCLDelegate == nil) { + [[self alloc] init]; // assignment not done here + } + } + return sharedCLDelegate; +} + ++ (id)allocWithZone:(NSZone *)zone { + @synchronized(self) { + if (sharedCLDelegate == nil) { + sharedCLDelegate = [super allocWithZone:zone]; + return sharedCLDelegate; // assignment and return on first allocation + } + } + return nil; // on subsequent allocation attempts return nil +} + +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + +- (id)retain { + return self; +} + +- (unsigned)retainCount { + return UINT_MAX; // denotes an object that cannot be released +} + +- (void)release { + //do nothing +} + +- (id)autorelease { + return self; +} + +@end \ No newline at end of file diff --git a/Samples/TapRace/iphone/Resources/AboutGamespy.png b/Samples/TapRace/iphone/Resources/AboutGamespy.png new file mode 100644 index 0000000000000000000000000000000000000000..1c3330a747b8b5ef62d8452a092c0c5e90bd4ce1 GIT binary patch literal 14763 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 literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/Default.png b/Samples/TapRace/iphone/Resources/Default.png new file mode 100644 index 0000000000000000000000000000000000000000..307f376a14e50ad4075e11e286a20e8a74c33b14 GIT binary patch literal 32137 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 QWB>pF07*qoM6N<$g0g7TB>(^b literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/FlipsideView.xib b/Samples/TapRace/iphone/Resources/FlipsideView.xib new file mode 100644 index 00000000..4353b6f4 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/FlipsideView.xib @@ -0,0 +1,416 @@ + + + + 768 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 274 + + YES + + + 290 + {320, 44} + + NO + NO + 1 + + YES + + + About + + 2 + 0 + + + Contact Us + 1 + + + + + + + 292 + {{52, 44}, {215, 29}} + + + 1 + MCAwIDAgMAA + + NO + YES + NO + TapRace v1.3 + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + + 1 + MSAxIDEAA + + 1 + + + + 1 + 1.800000e+01 + 1 + + + + 292 + {{19, 358}, {280, 82}} + + + 3 + MCAwAA + + NO + YES + 5 + NO + VGhpcyBhcHBsaWNhdGlvbiB3YXMgZGV2ZWxvcGVkIGJ5IApHYW1lU3B5IFRlY2hub2xvZ2llcyB0byBz +aG93Y2FzZSB0aGUgCnBvc3NpYmlsaXRpZXMgdGhyb3VnaCB0aGUgdXNlIG9mIEdhbWVTcHkgClRlY2hu +b2xvZ2llcycgb25saW5lIHNlcnZpY2VzIGFuZCBzb2x1dGlvbnMuA + + TrebuchetMS-Bold + 1.300000e+01 + 16 + + + 1 + MSAxIDEAA + + + 0 + 1.000000e+01 + 4 + 0 + + + + 292 + {{0, 73}, {320, 337}} + + NO + NO + 4 + NO + + NSImage + AboutGamespy.png + + + + + -2147483356 + {{124, 160}, {72, 37}} + + NO + NO + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 1 + Clear + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + {320, 460} + + + 1 + MCAwIDAAA + + NO + + 2 + + + + + + YES + + + view + + + + 41 + + + + done + + + + 46 + + + + contactUs: + + + + 51 + + + + clearBlockedImagesFile + + + 7 + + 54 + + + + + YES + + 0 + + YES + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 40 + + + YES + + + + + + + + + + 42 + + + YES + + + + + + 43 + + + YES + + + + + + + 44 + + + + + 47 + + + + + 48 + + + + + 49 + + + + + 52 + + + + + 53 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 40.CustomClassName + 40.IBEditorWindowLastContentRect + 40.IBPluginDependency + 42.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 47.IBPluginDependency + 48.IBPluginDependency + 49.IBPluginDependency + 52.IBPluginDependency + 53.IBPluginDependency + + + YES + FlipsideViewController + UIResponder + FlipsideView + {{663, 63}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 54 + + + + YES + + FlipsideView + UIView + + IBProjectSource + Samples/TapRace/iphone/FlipsideView.h + + + + FlipsideViewController + UIViewController + + YES + + YES + clearBlockedImagesFile + contactUs: + done + + + YES + id + id + id + + + + YES + + YES + clearBlockedImagesButton + delegate + + + YES + UIButton + id + + + + IBProjectSource + Samples/TapRace/iphone/FlipsideViewController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/Gamespy.png b/Samples/TapRace/iphone/Resources/Gamespy.png new file mode 100644 index 0000000000000000000000000000000000000000..c1c3e3d16a59dc2cd4f615e6eac4a8de74989a3b GIT binary patch literal 2236 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 FV1m{KJ}v+N literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/MainWindow.xib b/Samples/TapRace/iphone/Resources/MainWindow.xib new file mode 100644 index 00000000..c69338fe --- /dev/null +++ b/Samples/TapRace/iphone/Resources/MainWindow.xib @@ -0,0 +1,336 @@ + + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + + 1316 + + {320, 480} + + + 1 + MSAxIDEAA + + NO + NO + + + + + + + 256 + {0, 0} + NO + YES + YES + + + YES + + Login + + + Login + + Logout + 1 + + + + login + + + + + + + YES + + + window + + + + 4 + + + + navController + + + + 9 + + + + delegate + + + + 10 + + + + delegate + + + + 12 + + + + + YES + + 0 + + YES + + + + + + 2 + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 3 + + + + + 5 + + + YES + + + + + + + 6 + + + YES + + + + + + 7 + + + + + 8 + + + YES + + + + + + 13 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 2.IBAttributePlaceholdersKey + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + 2.UIWindow.visibleAtLaunch + 3.CustomClassName + 3.IBPluginDependency + 5.IBEditorWindowLastContentRect + 5.IBPluginDependency + 6.CustomClassName + 6.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + + + YES + UIApplication + UIResponder + + YES + + YES + + + YES + + + {{493, 376}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + AppDelegate + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + {{1400, 376}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + LoginController + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 13 + + + + YES + + AppDelegate + NSObject + + YES + + YES + navController + window + + + YES + UINavigationController + UIWindow + + + + IBProjectSource + Samples/TapRace/iphone/AppDelegate.h + + + + LoginController + UIViewController + + YES + + YES + autoLoginChanged: + createClicked: + forgotPasswordClicked: + loginClicked: + returnClicked: + + + YES + id + id + id + id + id + + + + YES + + YES + activityIndicator + autoLoginSwitch + forgotPassword + info + passwordField + userNameField + userNamePicker + userNamePickerView + + + YES + UIActivityIndicatorView + UISwitch + UIButton + UIButton + UITextField + UITextField + UIPickerView + UIView + + + + IBProjectSource + Samples/TapRace/iphone/LoginController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/Top100.jpg b/Samples/TapRace/iphone/Resources/Top100.jpg new file mode 100644 index 0000000000000000000000000000000000000000..800a127c2173146591ae2a4f447c9bd4cc333dda GIT binary patch literal 2868 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 QuO8^O5R!DV!19Ow2|afUD*ylh literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/UserStats.xib b/Samples/TapRace/iphone/Resources/UserStats.xib new file mode 100644 index 00000000..b163b1b3 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/UserStats.xib @@ -0,0 +1,913 @@ + + + + 528 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + User Stats + + + + 292 + + YES + + + 290 + + YES + + + 292 + {{20, 20}, {75, 93}} + + NO + NO + 4 + NO + + NSImage + singlePlayer.png + + + + + 292 + {{103, 14}, {198, 37}} + + NO + YES + NO + Leader + + TrebuchetMS-Bold + 3.000000e+01 + 16 + + + 1 + MSAxIDEAA + + + 1 + MCAwIDEgMAA + + + 1 + MCAwIDAAA + + 1 + 1.000000e+01 + 1 + + + + 292 + {{103, 51}, {198, 34}} + + NO + YES + NO + Stats + + + + 1 + MCAwIDEgMAA + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{40, 183}, {48, 19}} + + NO + YES + NO + Races: + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{93, 184}, {37, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{47, 210}, {41, 19}} + + NO + YES + NO + Wins: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{96, 211}, {35, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{187, 210}, {54, 19}} + + NO + YES + NO + Losses: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{249, 211}, {37, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{12, 238}, {76, 19}} + + NO + YES + NO + Win Ratio: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{93, 239}, {38, 19}} + + NO + YES + NO + 100% + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{165, 156}, {76, 19}} + + NO + YES + NO + Best Time: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{249, 156}, {53, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{162, 238}, {79, 19}} + + NO + YES + NO + Win Streak: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{249, 239}, {36, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{249, 183}, {47, 19}} + + NO + YES + NO + #5999 + + + + 1 + 1.000000e+01 + + + + 292 + {{197, 183}, {44, 19}} + + NO + YES + NO + Rank: + + + + 0 + 1.000000e+01 + 2 + + + + 265 + {{12, 121}, {90, 30}} + + NO + NO + 0 + 0 + + 1 + Report Image + Report Image + Report Image + Block Image + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + + {320, 274} + + + NO + NO + + + + 269 + {{0, 344}, {320, 76}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + -2147483380 + {{20, 282}, {120, 50}} + + NO + NO + 0 + 0 + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + 1 + Make Buddy + Make Buddy + Make Buddy + Make Buddy + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 265 + {{270, 372}, {18, 19}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + {320, 416} + + + + + NO + + + + + + YES + + + view + + + + 15 + + + + makeBuddyButton + + + + 17 + + + + reportImageButton + + + + 18 + + + + makeBuddyButtonClicked: + + + 7 + + 19 + + + + reportImageButtonClicked: + + + 7 + + 20 + + + + userName + + + + 21 + + + + thumbNail + + + + 22 + + + + userTotalRaces + + + + 27 + + + + userTotalWins + + + + 33 + + + + userTotalLosses + + + + 38 + + + + userBestTime + + + + 47 + + + + userWinStreak + + + + 52 + + + + userRatio + + + + 54 + + + + showInfo + + + 7 + + 60 + + + + myRank + + + + 63 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 3 + + + YES + + + + + + + + + + + + + + + + + + + + + + + 4 + + + + + 11 + + + + + 13 + + + + + 14 + + + + + 23 + + + + + 25 + + + userNbrRaces + + + 30 + + + + + 31 + + + userNbrWins + + + 35 + + + + + 36 + + + userNbrLosses + + + 40 + + + + + 41 + + + userRatio + + + 44 + + + + + 45 + + + userBestTime + + + 49 + + + + + 50 + + + userWinStreak + + + 7 + + + + + 59 + + + + + 61 + + + myWinStreak + + + 62 + + + + + 8 + + + + + 64 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 11.IBPluginDependency + 13.IBPluginDependency + 14.IBPluginDependency + 23.IBPluginDependency + 25.IBPluginDependency + 3.IBPluginDependency + 30.IBPluginDependency + 31.IBPluginDependency + 35.IBPluginDependency + 36.IBPluginDependency + 4.IBPluginDependency + 40.IBPluginDependency + 41.IBPluginDependency + 44.IBPluginDependency + 45.IBPluginDependency + 49.IBPluginDependency + 50.IBPluginDependency + 59.IBPluginDependency + 61.IBPluginDependency + 62.IBPluginDependency + 64.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + + + YES + UserStatsController + UIResponder + {{451, 185}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 64 + + + + YES + + UserStatsController + UIViewController + + YES + + YES + makeBuddyButtonClicked: + reportImageButtonClicked: + showInfo + + + YES + id + id + id + + + + YES + + YES + makeBuddyButton + myBestTime + myRank + myRatio + myTotalLosses + myTotalRaces + myTotalWins + myWinStreak + reportImageButton + thumbNail + userBestTime + userName + userRatio + userTotalLosses + userTotalRaces + userTotalWins + userWinStreak + + + YES + UIButton + UILabel + UILabel + UILabel + UILabel + UILabel + UILabel + UILabel + UIButton + UIImageView + UILabel + UILabel + UILabel + UILabel + UILabel + UILabel + UILabel + + + + IBProjectSource + Samples/TapRace/iphone/UserStatsController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/btn_down.png b/Samples/TapRace/iphone/Resources/btn_down.png new file mode 100644 index 0000000000000000000000000000000000000000..59a446a6b604bd0ef6fe2b73680da3f83fd94b1c GIT binary patch literal 7568 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 literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/buddies_rematch.png b/Samples/TapRace/iphone/Resources/buddies_rematch.png new file mode 100644 index 0000000000000000000000000000000000000000..7184c982a1ba614095b75312d769a1843f6c2c42 GIT binary patch literal 10783 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 literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/buddyListCell.xib b/Samples/TapRace/iphone/Resources/buddyListCell.xib new file mode 100644 index 00000000..91421ea5 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/buddyListCell.xib @@ -0,0 +1,359 @@ + + + + 512 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 296 + + YES + + + 300 + {{37, 8}, {50, 54}} + + NO + 1 + NO + + NSImage + singlePlayer.png + + + + + 292 + {{95, 8}, {165, 18}} + + + 1 + MSAxIDEAA + + YES + NO + Name + + TrebuchetMS + 1.600000e+01 + 16 + + + 1 + MCAwIDAAA + + + 1 + NO + 1.000000e+01 + + + + 292 + {{5, 20}, {24, 26}} + + + YES + NO + 99 + + TrebuchetMS-Bold + 2.400000e+01 + 16 + + + + 1 + 1.000000e+01 + + + + 292 + {{185, 42}, {75, 20}} + + + YES + NO + 12:34.56 + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + + + 1 + 1.000000e+01 + + + + 292 + {{162, 38}, {20, 20}} + + NO + NO + NO + YES + + + + 292 + {{52, 30}, {20, 20}} + + NO + NO + NO + NO + 2 + + + {320, 70} + + + NO + + + + + YES + + + indexLabel + + + + 33 + + + + nameLabel + + + + 34 + + + + thumbnail + + + + 35 + + + + timeLabel + + + + 36 + + + + cellView + + + + 37 + + + + busy + + + + 41 + + + + + YES + + 0 + + YES + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 18 + + + YES + + + + + + + + + + + 8 + + + Time + + + 7 + + + Index + + + 5 + + + Name + + + 3 + + + + + 38 + + + + + 40 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 18.IBEditorWindowLastContentRect + 18.IBPluginDependency + 18.showNotes + 3.IBPluginDependency + 38.IBPluginDependency + 40.IBPluginDependency + 5.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + + + YES + BuddyListCellView + UIResponder + {{559, 786}, {320, 70}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 41 + + + + YES + + BuddyListCellView + UIView + + YES + + YES + busy + cellView + indexLabel + nameLabel + thumbnail + timeLabel + + + YES + UIActivityIndicatorView + UIView + UILabel + UILabel + UIImageView + UILabel + + + + IBProjectSource + Samples/TapRace/iphone/BuddyListCellView.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/camera.png b/Samples/TapRace/iphone/Resources/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..6895aaf105a1a489e296e6c1edcfbee6a92ac912 GIT binary patch literal 8130 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 + + + 528 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 274 + + YES + + + 292 + {{20, 20}, {119, 21}} + + NO + YES + NO + E-mail Address + + TrebuchetMS-Bold + 1.700000e+01 + 16 + + + 2 + MC4yMTE3NjQ3MiAwLjQ0NzA1ODg2IDAuODU4ODIzNgA + + + 1 + 1.000000e+01 + + + + 290 + {{20, 43}, {280, 31}} + + NO + NO + 0 + + 3 + + 3 + MAA + + 2 + + + YES + 1.700000e+01 + + 1 + 7 + + 3 + + + + 292 + {{20, 89}, {151, 21}} + + NO + YES + NO + Desired User Name + + + 2 + MC4yMDAwMDAwMiAwLjQzNTI5NDE1IDAuODI3NDUxMDUAA + + + 1 + 1.000000e+01 + + + + 290 + {{20, 112}, {280, 31}} + + NO + NO + 0 + + 3 + + 3 + MAA + + + YES + 1.700000e+01 + + 1 + + 3 + + + + 292 + {{20, 159}, {75, 21}} + + NO + YES + NO + Password + + + 2 + MC4xODQzMTM3MyAwLjQwMDAwMDA0IDAuNzcyNTQ5MDkAA + + + 1 + 1.000000e+01 + + + + 290 + {{20, 183}, {280, 31}} + + NO + NO + 0 + + 3 + + 3 + MAA + + + YES + YES + 1.700000e+01 + + 1 + YES + + 3 + + + + 292 + {{20, 222}, {94, 27}} + + NO + YES + YES + 0 + 0 + YES + + + + 292 + {{122, 225}, {84, 21}} + + NO + YES + NO + Auto-login + + + 2 + MC4yMTE3NjQ3MiAwLjQ0NzA1ODg2IDAuODU4ODIzNgA + + + 1 + 1.000000e+01 + + + + 269 + {{0, 385}, {320, 75}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 269 + {{100, 327}, {119, 50}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 1 + Create + Create + Create + Create + + 1 + MSAxIDEAA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + + {320, 460} + + + 1 + MCAwIDAAA + + + + + + YES + + + view + + + + 13 + + + + emailField + + + + 14 + + + + passwordField + + + + 15 + + + + userNameField + + + + 16 + + + + returnClicked: + + + 20 + + 18 + + + + returnClicked: + + + 20 + + 19 + + + + returnClicked: + + + 20 + + 20 + + + + createButtonClicked: + + + 7 + + 21 + + + + delegate + + + + 22 + + + + delegate + + + + 23 + + + + delegate + + + + 24 + + + + autoLoginSwitch + + + + 26 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + Remember Switch + + + 11 + + + + + 12 + + + + + 17 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 10.IBPluginDependency + 11.IBPluginDependency + 12.IBPluginDependency + 17.IBPluginDependency + 4.IBPluginDependency + 5.IBPluginDependency + 6.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + 9.IBPluginDependency + + + YES + CreateUserController + UIResponder + {{409, 192}, {320, 460}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 26 + + + + YES + + CreateUserController + UIViewController + + YES + + YES + createButtonClicked: + returnClicked: + + + YES + id + id + + + + YES + + YES + autoLoginSwitch + emailField + passwordField + userNameField + + + YES + UISwitch + UITextField + UITextField + UITextField + + + + IBProjectSource + Samples/TapRace/iphone/CreateUserController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/forgotPassword.xib b/Samples/TapRace/iphone/Resources/forgotPassword.xib new file mode 100644 index 00000000..9a1c98a3 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/forgotPassword.xib @@ -0,0 +1,185 @@ + + + + 768 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 292 + + YES + + + 274 + {320, 480} + + + 3 + MQA + + YES + YES + YES + + + {320, 480} + + + 3 + MQA + + 2 + + + + + + + YES + + + webView + + + + 4 + + + + view + + + + 5 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 3 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 3.IBPluginDependency + + + YES + ForgotPassword + UIResponder + {{426, 160}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 5 + + + + YES + + ForgotPassword + UIViewController + + webView + UIWebView + + + IBProjectSource + Samples/TapRace/iphone/ForgotPassword.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/gamespyFooter.png b/Samples/TapRace/iphone/Resources/gamespyFooter.png new file mode 100644 index 0000000000000000000000000000000000000000..05a81ad6b05e7a891adb1bafc04a6add63e24544 GIT binary patch literal 7308 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 ZU;w}*zM%UtCgK19002ovPDHLkV1fmbPlf;h literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/icon_57x57.png b/Samples/TapRace/iphone/Resources/icon_57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..6a31c56de12035abcca8faed48f586166a5028fe GIT binary patch literal 2354 zcmV-23C;G2P)E5aG8C~qpOPV`?`ClXQyAYwj)JysWck*_H_U1tN-_?DvQ=1 zTJRQW65cxPI_q^-4erVw;F4~^S%;rHA1KHmLJBgANcx+~y3J=Cga89@A%6)|^xI_k z677T*_ys%=D#bk+>+UEG!}Y=ST=+~Fz8IQUjs1spTMp)vJ6GsK9Z~X-R5vN_6P^eJ z1VG&fLRXU!i`MU26Wem&paPG2a{vDbuaZeHp}`g*vSKlfF8p#2z6U z0%{<<32a=Ml|jjtNcoECJ=PoieE??wP5?AhOso8T4Pb`9%<>rKl7}n-TmUstwYzHS zMkS`1IA9xBz!k0M_f-N#bM_r{q5Ffa-cvnz0N``UL@$xqpp*Y|9LxgW9~mBR{x*2W zyxJXsXbI6I;@H5lnDx0bRvUxbH|*vko90Le_w(zKaxoTV)rMw1!_Xc2t^BnNz7(jV znut*qwGvzv+*2>Hs9^{CQ@HuH4SSEFm-PW21o$q`b!nSV-PU2F;_z+(I0^2c3%?V} z{ZclOl0Yd$K&}{cm8lP33OB#C;l9B>gm3z4){LVlPmNt);OqB}njgR43Gfj8)y=x@ zechS~^oO)0SRm`>Xjim;v#-dRaO(eUH@>oNzi9~+rFk}IV5fmZoY~eGZhUxWSr2Ux zUlzd^>iT_39FXL2em1TO?yl#_l)QcuHom@Lzi7xp1)$jO0yv!STQz{^$F484fc-iEE6w)J!yMS^z47E z>rhxkCmBzuQdr37cq`66#?fE($m=_-Q7DW`Gi6S8Cg=LYERqKxyd0XBLin$PbvPfXiRq0c8)e>6#N3kyN^4fS;jm-8Ff%$c}bOxSD*nyg|m4%jHPxdU!b2) z>cDx1Xr^Sk!vE}~{<&CTFw30G=E0l~-A6s>I>6#H6x+FVj5SKCe=%{wT+M&o7Lim|1OcBM5E zE#P!q8|dupI-5*NL8zK%6VCJZQ)99HTq55nrJYUgkA}BeGDYUHRflomN$D!zw@@Cg z!aSy9>ROKY@cQFfv7$y@%WX4XI*-l`FtU{hdR zQtjwiC?adSkQLis$1+(J({{>=U^1$tg_q*W#SOup-u<>Dp&Tpf)qGra53l+qJY(t0tfT(jc<8s15v-{bX`yQIQXY?~;No+wms z^;{Kg7c8VomZ&B?5i3U?kl^!c(kdA141SRES|S82!n{>5JI!egRUN_Bf*1a&}(Kv1WRBN1&- zR-4alOd6U~kpeb!gQI}*|xib!kk&YU0Ez!1|e4CPc zq^uDG!P+64jN@&(Q`(>nh1!X__?f7*5p?CPLnVYh>dU4SdnE0?MEgZ4uq;+fIzd)F zqy1w+$+p1NY7c|jtJMt(S!II+z>ZbQ>Ns>M5_s^by1uNhxe}H@{9gDs`EsumQ4i88O>IwhZw*U-=K$&Y~l;Q(s@In74(=;h-`s&U&?{^l%oj2-Ho zUiia-+waQbr}!1tKhhSKgPul;+kI?cgey>CR}Z2di{eSpOpRw z1GcD=`$+&4R`bseRO2&ES}10AvMWmmStZFxzuSj@f<14b3%fTk&ij3!)^+Uz0nR&t zwzQnabftu*S}=O=ct_T$^23_Qx|(F9-mcVs?VfS=`Yt$B>8``P|ar8JpDjNCefmAnWyJ^kqCVaYYe`;OVlJlh} z#HZ60F->DrpVnl&xlcpa8`Ou9Ka|(nIMW$_dXQhr?HPt=g;fyFAw; zNziVYanWKp5+3CgT-Xyd1tUo{6h|eWDOGCMvW#T%vty39^a83GoU04h2RU#KJA=CP z_Bq~+cWMzG98+@f_*|2uPiv*oTC|iECT5NH?js1WPShYq#Lu|;Nx@3SRSZX~sMu2r z&arY?g}tIcNiWt6;joFJ`dtbovaU!{k$@`dyc!zF&Bmlp+JmUzrn_s`|G&$%Y5x~s Y00c^+9dKhGrvLx|07*qoM6N<$g53^?CIA2c literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/leaderboard.png b/Samples/TapRace/iphone/Resources/leaderboard.png new file mode 100644 index 0000000000000000000000000000000000000000..a92ae080f672c6d39e5303bec464685a31f7c47e GIT binary patch literal 6865 zcmV;?8ZPCDP)dUfTvmt@D$*wmI6Elc&eoUPXV3+Jk?TwrvOj2 z6yPbqQ!NGfKO^9RAdLEVOY(l%ke8pfysA2Lv3)Z8C!l{o-O<_>ezUXlwm&<+YMs)b zSNlidGUfp%perZU<*d4FSw-W8GYbJxBqRl}+cr##+u`;htb6_F;mzAR?l}Jb?Qha) zrUQ7gr2zjRz!}($my~T^dE4YHOArY~0fxnlf$bRmV4I1}TY~ug3p;P_I>6a(B-uHG z{*S^%9Lst)b9T|Hn^#WGQVaocT|i7DW4$htNr)LqMn6xQl8o zlbz=j#=&|gp9H!z zRzR1>tT%VdRi%|#V1OGU$%r7qBw1m-!Fo%yrG*)(g+YR25T>$UKzVh3P4`x0B!T>) z^!%R!aQJ+%Bo}A3ArjbRhA8u60}=<<5ZFWz;KDHAx(#|7XLi6D$#itwbS%a2(>9Mw zh-t8yEHQv(Vz`U8Er`MAhp91`3 zz}*68H*6U(8M{f28R(WIaIx6i3$*ovM=zdC89j=O;1LaKT$COCPOy zvK4MUuA`tNjZ2A1@?sKh0)cH1Y=?%3-NaL04gpTQ)rrGul$i^P9#?1#cT$v+>@-Kk z@Z^y75}+ZwpkVp9+SP@7#FBu7L%uNY`yiF)0!e z$azv2j{SoKEZw;;o1UCtG)OG8I3X(8=jwf1I~;(e4-=sf(3Ms7^{eJxbyed97hPn} z7RBe5X>v1leev53H<M+WE#qBffuz|18|q|k0kZ9EkGxjZ(**>};_ z_3tEsYo4;iM^)5XO6;C8XHKIYiGZ0Ajl!hfatzGqGn{f})3M-4 zis*drcDc@)bjhQwNec^08Va)Wme-b6Eq3A}eq!HX|BeI4THn-P@BGN>(U}Y1S=kj@ zad&Rfp+HS$_=XH`C*D^A1hkg}Jra#1?D&j1%Cv-mn=8C!j{@k* zDbgA&p_&21G#0mYw%@;I`ntpK?%8}td~Mfx*P{coRzF-< zn}7f8=>=Iu`IQjWL#Q!cL}k%ZMEZx&AM=iY&18h>HoG6crQ>2(FYh* zE}3!lzUJvyAKbrWZK!Yi1M$ey2a>e?jQ2GRI0IL5z#cx9=$Q3`@4bh}{{37l7@ZRx z9q8@qf-0Up>5@JO?4`3Cw?Fv#FHb5hDuN{%yk4QvKiy0~vs7r*`+Q#1S6`5olbw0X zLHTgAdLVizDFpjKume+{YRDPC;?-N0*W7f);u3FRfgiC%0Dk$8@caAGwJU-PXUsu{ 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

JjER(JxroMTLIue}3uZ4Hz#sq6 zgP{=1zTKSV%H$BH-+qw%i-cEyRDz?MGBGj3jfnvdvRR;}DHKB5V4J$d-$pUO+4!<) zo!I?V`1!!Yld9|PU$J;)R*`QbM9)rCg@Z({rPyIaNT<6;LO;fKHwijhVzQZy3EE2d zGE3~T4Yh`vdAT!6$6dYq$iAzaTMn!pJUaM*`M$#I(UK&Z8hdkc9<5;fcMy>OieS$% zv>&DWAlZqL)YMM`YUgbOi}6FQH}{DOT(SV#-StqZg0{+j~Yv*`jXJLy7G%AA9>8-C=AwJE8`HBS)OiGCM@Z&8UUD@=LS7ei*sWXqWr?BN zvLY8=nbk0^xa5s5Ty=z#L zV#kvaOmT%t0d!=$?6jIWcLuI5d7-)eF#VallF31*d(;`=K2O@J8!!EA7Av>Lw1~6K zi{;cN!kL#s35jT^-G;)7Luh?>6}ozt!CPHMPRnK2fO+I%a^0*++XgTF9Fj{RkdMxh4Cp591K8jVBZsQ3Xn{4+;UV`vNqX0$+jh zth(6^x}_sZ8JOV2m;!lXk)_z_Ci;b$c>Q=buAI0LjW>pHc>OC_-*O4$@*;|nU3}{q z(BFqU5|DL_&#k-P$`;;aFJc@ zh!j=W?=6`l?G|MGYQjuB^3ZotS67EnD1?4Wf|yPH%ERQZe8_&|dH$w{RqfQ#qJ zZWK(Oi1m19q&2U(S*SuP55>FUFPvy7309cLkuAsL@A;9Whz#M!pc~aJzEjFbW;Zia7mZJz z;b(13LQc(SBasNAq{$5Yj0}pz^f=DtIkFn2Glmo3t1*iAJUC1N^|8C>}Gob4MWh! zIN($3C!)76jNwuQV>M$q1D$PAE-Ccjcz5XB(}++sM4nST0XVVSMa*P?yA0E=1L#Af zQBfom(tGl_RyW1qFPOON`wyUM@_Z=$Z6hB1TO~{2>-6FI=553V1;L>Kq-lQm%nXWD z1P(f5#aL%}Sy?{Zo^%9*z3A-hT1$g{6gJ@_DHZ7d^O ze$(w}i8mn5{#+x~PG`rw9Dp>(WxrEEHwLFwooe zcOJZc=>yo^YQJYpwbMw-hE1E#>n{~NVglv9E)?eap$;5^d;Bd3$AGpYM-h$1Q^=kP z@HgIWO8rl-#=3^5+J^z$NNH_~;s3uGo&tQ#hlQz@V)*|Zz>SoKrx>0BJk>rzNl5{o z0z3tHs(qAzn<*4NR=~{^!#`HQ%@o5wZic4-|9Bam0{mlb|0}=%S>tV;e^#9E00000 LNkvXXu0mjfaq}{n literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/leaderboardMap.png b/Samples/TapRace/iphone/Resources/leaderboardMap.png new file mode 100644 index 0000000000000000000000000000000000000000..e34b012d4dd55f72be93d434e89f3992ef7b6041 GIT binary patch literal 10977 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 P00000NkvXXu0mjfif&4> literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/leaderboards.xib b/Samples/TapRace/iphone/Resources/leaderboards.xib new file mode 100644 index 00000000..aad126e6 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/leaderboards.xib @@ -0,0 +1,1352 @@ + + + + 528 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Leaderboards + + + + 292 + + YES + + + 290 + + YES + + + 292 + {{9, 9}, {37, 19}} + + NO + YES + NO + Races: + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + 1 + MSAxIDEAA + + + 0 + 1.000000e+01 + + + + 292 + {{48, 9}, {68, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 32}, {61, 19}} + + NO + YES + NO + Best Time: + + + + 1 + 1.000000e+01 + + + + 292 + {{72, 32}, {57, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 55}, {61, 19}} + + NO + YES + NO + Avg Time: + + + + 1 + 1.000000e+01 + + + + 292 + {{72, 55}, {57, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + {{0, 154}, {129, 78}} + + + 1 + MCAwIDAAA + + NO + + + + 269 + {{0, 345}, {320, 71}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 293 + {{194, 248}, {92, 92}} + + NO + 1 + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + NSImage + leaderboard.png + + + + + -2147483356 + {{7, 314}, {92, 92}} + + NO + NO + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 293 + {{34, 248}, {92, 92}} + + NO + 1 + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + NSImage + leaderboardMap.png + + + + + + 292 + + YES + + + 292 + {{9, 4}, {45, 19}} + + NO + YES + NO + Races: + + + + 0 + 1.000000e+01 + + + + 292 + {{59, 4}, {41, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 27}, {34, 19}} + + NO + YES + NO + Wins: + + + + 1 + 1.000000e+01 + + + + 292 + {{45, 27}, {42, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{95, 27}, {43, 19}} + + NO + YES + NO + Losses: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{140, 27}, {42, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 50}, {62, 19}} + + NO + YES + NO + Win Ratio: + + TrebuchetMS-Bold + 1.600000e+01 + 16 + + + + 1 + 1.000000e+01 + + + + 292 + {{73, 50}, {46, 19}} + + NO + YES + NO + 100% + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 73}, {78, 19}} + + NO + YES + NO + LWin Streak: + + + + 1 + 1.000000e+01 + + + + 292 + {{89, 73}, {56, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 96}, {62, 19}} + + NO + YES + NO + Best Time: + + + + 1 + 1.000000e+01 + + + + 292 + {{73, 96}, {58, 19}} + + NO + YES + NO + 9999 + + + + 1 + 1.000000e+01 + + + + 292 + {{90, 119}, {49, 21}} + + NO + YES + NO + #5999 + + + + 1 + 1.000000e+01 + + + + 292 + {{9, 119}, {79, 21}} + + NO + YES + NO + Global Rank: + + + + 0 + 1.000000e+01 + + + {{131, 89}, {186, 143}} + + + NO + + + + 292 + {{59, 35}, {37, 37}} + + NO + YES + NO + NO + YES + 0 + + + + 289 + {{40, 2}, {75, 93}} + + NO + 4 + NO + + NSImage + singlePlayer.png + + + + + 289 + {{25, 102}, {21, 18}} + + + 3 + MCAwAA + + YES + NO + 1 + + + + 0 + 0 + + + + + + NSImage + camera.png + + + + + 292 + {{49, 103}, {72, 18}} + + NO + YES + NO + upload a photo + + TrebuchetMS-Bold + 1.000000e+01 + 16 + + + 2 + MC4wMzUyOTQxMTkgMC43Njg2Mjc1MiAwAA + + + 1 + 1.000000e+01 + 8 + + + + 292 + {{150, 7}, {122, 47}} + + NO + YES + NO + My Stats + + TrebuchetMS-Bold + 3.000000e+01 + 16 + + + + + 0 + 1.000000e+01 + + + + 292 + {{6, 129}, {25, 25}} + + NO + NO + 2 + NO + + + + + 292 + {{35, 131}, {86, 21}} + + + 1 + MCAwIDEgMAA + + NO + YES + NO + Single Races + + TrebuchetMS-Bold + 1.700000e+01 + 16 + + + + + 1 + MCAwIDEAA + + 1 + 1.000000e+01 + + + + 292 + {{169, 68}, {124, 21}} + + NO + YES + NO + Two Player Races + + + + + 1 + MSAwIDAAA + + 1 + 1.000000e+01 + + + + 292 + {{141, 68}, {25, 21}} + + NO + NO + 2 + NO + + NSImage + buddies.png + + + + + 292 + {{2, 235}, {316, 3}} + + + NO + YES + NO + + + 1 + MSAxIDEAA + + 1 + + + + 1 + 1.000000e+01 + + + + 265 + {{270, 371}, {18, 19}} + + NO + NO + 0 + 0 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + {320, 416} + + + + + NO + + + + + + YES + + + byLocationButton + + + + 18 + + + + mySpotButton + + + + 19 + + + + setPhotoButton + + + + 20 + + + + top100Button + + + + 21 + + + + view + + + + 22 + + + + byLocationButtonClicked: + + + 7 + + 23 + + + + setPhotoButtonClicked: + + + 7 + + 25 + + + + top100ButtonClicked: + + + 7 + + 26 + + + + myTotalRaces + + + + 45 + + + + myBestTime + + + + 46 + + + + myRatio + + + + 47 + + + + myTotalLosses + + + + 48 + + + + myTotalWins + + + + 49 + + + + thumbNail + + + + 51 + + + + busy + + + + 55 + + + + mySPTotalRaces + + + + 76 + + + + mySPBestTime + + + + 77 + + + + myStreakLabel + + + + 78 + + + + myStreak + + + + 79 + + + + myRank + + + + 82 + + + + mySPAvgTime + + + + 89 + + + + showInfo + + + 7 + + 92 + + + + myBuddiesButtonClicked: + + + 7 + + 93 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 6 + + + YES + + + + + + + + + + + 7 + + + + + 16 + + + YES + + + + + 13 + + + + + 15 + + + + + 14 + + + + + 75 + + + YES + + + + + + + + + + + + + + + + + + + 27 + + + + + 39 + + + myNbrRaces + + + 28 + + + + + 40 + + + myNbrWins + + + 29 + + + + + 41 + + + myNbrLosses + + + 30 + + + + + 42 + + + myRatio + + + 69 + + + + + 70 + + + myNbrRaces + + + 67 + + + + + 68 + + + myBestTime + + + 32 + + + + + 44 + + + myWinStreak + + + 31 + + + + + 43 + + + myBestTime + + + 83 + + + + + 84 + + + myBestTime + + + 54 + + + busy + + + 11 + + + + + 12 + + + + + 60 + + + + + 86 + + + + + 73 + + + + + 71 + + + + + 3 + + + + + 72 + + + + + 90 + + + + + 91 + + + + + 81 + + + myWinStreak + + + 85 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 1.IBViewEditorWindowController.showingBoundsRectangles + 1.IBViewEditorWindowController.showingLayoutRectangles + 11.IBPluginDependency + 12.IBPluginDependency + 13.IBPluginDependency + 14.IBPluginDependency + 15.IBPluginDependency + 16.IBPluginDependency + 27.IBPluginDependency + 28.IBPluginDependency + 29.IBPluginDependency + 3.IBPluginDependency + 30.IBPluginDependency + 31.IBPluginDependency + 32.IBPluginDependency + 39.IBPluginDependency + 40.IBPluginDependency + 41.IBPluginDependency + 42.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 54.IBPluginDependency + 6.IBPluginDependency + 60.IBPluginDependency + 67.IBPluginDependency + 68.IBPluginDependency + 69.IBPluginDependency + 7.IBPluginDependency + 70.IBPluginDependency + 71.IBPluginDependency + 72.IBPluginDependency + 73.IBPluginDependency + 75.IBPluginDependency + 81.IBPluginDependency + 83.IBPluginDependency + 84.IBPluginDependency + 85.IBPluginDependency + 86.IBPluginDependency + 90.IBPluginDependency + 91.IBPluginDependency + + + YES + LeaderboardsController + UIResponder + {{829, 220}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 93 + + + + YES + + LeaderboardsController + UIViewController + + YES + + YES + byLocationButtonClicked: + myBuddiesButtonClicked: + setPhotoButtonClicked: + showInfo + top100ButtonClicked: + + + YES + id + id + id + id + id + + + + YES + + YES + busy + byLocationButton + myBestTime + myRank + myRatio + mySPAvgTime + mySPBestTime + mySPTotalRaces + mySpotButton + myStreak + myStreakLabel + myTotalLosses + myTotalRaces + myTotalWins + setPhotoButton + thumbNail + top100Button + + + YES + UIActivityIndicatorView + UIButton + UILabel + UILabel + UILabel + UILabel + UILabel + UILabel + UIButton + UILabel + UILabel + UILabel + UILabel + UILabel + UIButton + UIImageView + UIButton + + + + IBProjectSource + Samples/TapRace/iphone/LeaderboardsController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/leadersbylocation.xib b/Samples/TapRace/iphone/Resources/leadersbylocation.xib new file mode 100644 index 00000000..897ccb40 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/leadersbylocation.xib @@ -0,0 +1,314 @@ + + + + 768 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Leaders By Location + + + + 292 + + YES + + + 269 + {{0, 343}, {320, 76}} + + NO + YES + NO + 4 + NO + + NSImage + gamespyFooter.png + + + + + 265 + {{271, 371}, {18, 19}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + + 292 + {{141, 165}, {37, 37}} + + NO + NO + NO + YES + 0 + + + + 268 + {{0, -1}, {320, 346}} + + NO + YES + 4 + YES + + + {320, 416} + + + 1 + MCAwIDAAA + + YES + + + NO + + + + + + YES + + + view + + + + 7 + + + + spinner + + + + 16 + + + + delegate + + + + 19 + + + + mapView + + + + 20 + + + + showInfo + + + 7 + + 22 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 6 + + + + + 15 + + + spinner + + + 18 + + + + + 21 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 15.IBPluginDependency + 18.IBPluginDependency + 21.IBPluginDependency + 4.IBPluginDependency + 6.IBPluginDependency + + + YES + LeadersByLocationController + UIResponder + {{432, 21}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 22 + + + + YES + + LeadersByLocationController + UIViewController + + showInfo + id + + + YES + + YES + mapView + spinner + webView + + + YES + MKMapView + UIActivityIndicatorView + UIWebView + + + + IBProjectSource + Samples/TapRace/iphone/LeadersByLocationController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/leadersbytime.xib b/Samples/TapRace/iphone/Resources/leadersbytime.xib new file mode 100644 index 00000000..4440bf05 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/leadersbytime.xib @@ -0,0 +1,274 @@ + + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Leaders by Time + + + + 292 + + YES + + + 269 + {{0, 345}, {320, 72}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 265 + {{270, 371}, {18, 19}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + + 292 + {{13, 12}, {293, 325}} + + NO + YES + NO + Loading... + + 1 + MCAwIDAAA + + + 1 + 1.000000e+01 + 1 + + + {320, 416} + + + 1 + MSAxIDEAA + + + + NO + + + + + + YES + + + view + + + + 8 + + + + tableContainer + + + + 18 + + + + showInfo + + + 7 + + 20 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 5 + + + + + 6 + + + + + 17 + + + tableContainer + + + 19 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 17.IBPluginDependency + 19.IBPluginDependency + 5.IBPluginDependency + 6.IBPluginDependency + + + YES + LeadersController + UIResponder + {{423, 91}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 20 + + + + YES + + LeadersController + UIViewController + + showInfo + id + + + tableContainer + UILabel + + + IBProjectSource + Samples/TapRace/iphone/LeadersController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/login.xib b/Samples/TapRace/iphone/Resources/login.xib new file mode 100644 index 00000000..6c94ce8d --- /dev/null +++ b/Samples/TapRace/iphone/Resources/login.xib @@ -0,0 +1,699 @@ + + + + 768 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 274 + + YES + + + 292 + {{20, 16}, {86, 21}} + + NO + YES + NO + User Name + + TrebuchetMS-Bold + 1.700000e+01 + 16 + + + 2 + MC4xNDUwOTgwNSAwLjQwMzkyMTYgMC43ODQzMTM4AA + + + 1 + 1.000000e+01 + + + + 292 + {{20, 84}, {75, 21}} + + NO + YES + NO + Password + + + 2 + MC4xMjE1Njg2NCAwLjM2MDc4NDMyIDAuNzEzNzI1NTEAA + + + 1 + 1.000000e+01 + + + + 292 + {{20, 147}, {94, 27}} + + NO + YES + YES + 0 + 0 + + + + 292 + {{122, 150}, {84, 21}} + + NO + YES + NO + Auto-login + + + 2 + MC4xMjE1Njg2NCAwLjM2MDc4NDMyIDAuNzEzNzI1NTEAA + + + 1 + 1.000000e+01 + + + + 290 + {{20, 45}, {280, 31}} + + NO + NO + 0 + + 3 + + 3 + MAA + + 2 + + + YES + 1.700000e+01 + + 1 + + 3 + + + + 290 + {{20, 108}, {280, 31}} + + NO + NO + 0 + + 3 + + 3 + MAA + + + YES + YES + 1.700000e+01 + + 1 + YES + + 3 + + + + 269 + {{0, 350}, {320, 66}} + + NO + NO + 4 + NO + + NSImage + gamespyFooter.png + + + + + 268 + {{20, 292}, {128, 50}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.000000e+01 + 16 + + 1 + Forgot Password + Forgot Password + Forgot Password + Forgot Password + + 1 + MSAxIDEAA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + + + 289 + {{95, 234}, {128, 50}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 1 + Login + Login + Login + Login + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + -2147483359 + {{141, 172}, {37, 37}} + + NO + NO + NO + 0 + + + + 268 + {{172, 292}, {128, 50}} + + NO + NO + 0 + 0 + + 1 + Create Account + Create Account + Create Account + Create Account + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + -2147483374 + + YES + + + -2147483356 + {{-141, -21}, {320, 216}} + + NO + YES + YES + YES + + + {{141, 20}, {156, 0}} + + + 3 + MCAwLjUAA + + NO + NO + + + {320, 416} + + + 1 + MCAwIDAAA + + YES + + + NO + + + + + + YES + + + view + + + + 37 + + + + passwordField + + + + 39 + + + + returnClicked: + + + 20 + + 41 + + + + returnClicked: + + + 20 + + 42 + + + + loginClicked: + + + 7 + + 43 + + + + activityIndicator + + + + 50 + + + + userNameField + + + + 51 + + + + createClicked: + + + 7 + + 53 + + + + delegate + + + + 54 + + + + delegate + + + + 55 + + + + forgotPasswordClicked: + + + 7 + + 56 + + + + userNamePicker + + + + 58 + + + + dataSource + + + + 59 + + + + delegate + + + + 60 + + + + userNamePickerView + + + + 63 + + + + autoLoginChanged: + + + 13 + + 73 + + + + autoLoginSwitch + + + + 74 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 13 + + + + + 17 + + + + + 49 + + + + + 52 + + + + + 61 + + + YES + + + + + + 57 + + + + + 16 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 10.IBPluginDependency + 13.IBPluginDependency + 16.IBPluginDependency + 17.IBPluginDependency + 4.IBPluginDependency + 49.IBPluginDependency + 5.IBPluginDependency + 52.IBPluginDependency + 57.IBPluginDependency + 61.IBEditorWindowLastContentRect + 61.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + 9.IBPluginDependency + + + YES + LoginController + UIResponder + {{356, 149}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + {{90, 195}, {320, 460}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 76 + + + + YES + + LoginController + UIViewController + + YES + + YES + autoLoginChanged: + createClicked: + forgotPasswordClicked: + loginClicked: + returnClicked: + + + YES + id + id + id + id + id + + + + YES + + YES + activityIndicator + autoLoginSwitch + forgotPassword + info + passwordField + userNameField + userNamePicker + userNamePickerView + + + YES + UIActivityIndicatorView + UISwitch + UIButton + UIButton + UITextField + UITextField + UIPickerView + UIView + + + + IBProjectSource + Samples/TapRace/iphone/LoginController.h + + + + LoginController + UIViewController + + myOutlet1 + id + + + IBUserSource + + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/matchmaking.xib b/Samples/TapRace/iphone/Resources/matchmaking.xib new file mode 100644 index 00000000..49d1fe83 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/matchmaking.xib @@ -0,0 +1,519 @@ + + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Matchmaking + + + + 274 + + YES + + + 292 + {{20, 20}, {110, 21}} + + NO + YES + NO + Choose Game + + 1 + MCAwIDAAA + + + 1 + 1.000000e+01 + + + + 293 + {{-1, 75}, {320, 216}} + + NO + YES + YES + YES + + + + 265 + {{20, 299}, {80, 40}} + + NO + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 1 + Join + Join + Join + Join + + 2 + MC4yOTQxMTc2NiAwLjM5NjA3ODQ3IDAuNTgwMzkyMTgAA + + + + + + 3 + MAA + + + + + -2147483355 + {{66, 0}, {189, 31}} + + NO + YES + NO + ( Shake to join any player ) + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + 1 + MSAxIDEAA + + + 1 + NO + 1.000000e+01 + 1 + + + + 269 + {{0, 347}, {320, 69}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 268 + {{220, 299}, {80, 40}} + + NO + NO + 0 + 0 + + 1 + Host + Host + Host + Host + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 269 + {{224, 29}, {90, 40}} + + NO + NO + 0 + 0 + + 1 + Refreshing + Refresh + Refreshing + Refresh + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 292 + {{15, 32}, {201, 35}} + + NO + YES + NO + Games available: 0 + + TrebuchetMS-Bold + 2.000000e+01 + 16 + + + 1 + MSAxIDEAA + + 1 + + + + 1 + 1.000000e+01 + + + {320, 416} + + + YES + + + NO + + + + + + YES + + + view + + + + 10 + + + + joinButton + + + + 12 + + + + gamePicker + + + + 13 + + + + joinButtonClicked: + + + 7 + + 14 + + + + dataSource + + + + 15 + + + + delegate + + + + 16 + + + + hostButton + + + + 18 + + + + hostButtonClicked: + + + 7 + + 19 + + + + RefreshButton + + + + 21 + + + + refreshButtonClicked: + + + 7 + + 22 + + + + shakeLabel + + + + 23 + + + + listCountLabel + + + + 25 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 17 + + + + + 20 + + + Refresh Button + + + 24 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 17.IBPluginDependency + 20.IBPluginDependency + 24.IBPluginDependency + 4.IBPluginDependency + 5.IBPluginDependency + 6.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + 9.IBPluginDependency + + + YES + MatchmakingController + UIResponder + {{451, 353}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 26 + + + + YES + + MatchmakingController + UIViewController + + YES + + YES + hostButtonClicked: + joinButtonClicked: + refreshButtonClicked: + + + YES + id + id + id + + + + YES + + YES + RefreshButton + gamePicker + hostButton + joinButton + listCountLabel + shakeLabel + + + YES + UIButton + UIPickerView + UIButton + UIButton + UILabel + UILabel + + + + IBProjectSource + Samples/TapRace/iphone/MatchmakingController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/menu.png b/Samples/TapRace/iphone/Resources/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c224f8f8d9ab2a843b40b318559bdfce8250a8 GIT binary patch literal 1733 zcma)-`#%#31IC%VohG-HjbbI2l3SX}A#z(2qFknt&E+t&xpNTlvSztVNm}l&*Qv$G zZ8b4dE(y_bC%2rTu(5`5>UjTy^TYFeKi_}gdC0EL4u|9+@?v6QhY*goZU?;e|Hw)o z^y0>_*8$X`?YyGh2|>|u{*i%V)&T@epe6$E9~|fw=pT>}(Hm$XCMF$>u(iGsKlzQe zMo_U+?`~&pPm@@Wnz`=)_>lTxu#_s@8t~*W8KSE6J;VN98}B%;ifmmu3fHNEM?@culxtawtm5`8F+pi;4A9g`-qNXNMmLUGQ zj!zwER;+dy@Cf@tn7Akva_=(#Pn++5DmGtwNOj@po@C3*iSLDgl6%np6Ea?ll}7K& zkZ->Z-2B9Lhk6A)w?^cGHcd6wBZrR7K&=(kW2xpdF$o?VhHnTr^G!dX&5Uur+_8>+ zYGmY>Qsbr$Ku%I`HbCmk2*vp^)UvD`V4i0x>(ulGuFtV4PXh&zks~*BV1z0_e2K&$ z75VH*bYaNn^Slb8o(j-IfyoJv*3(Y!wS7JeyJx_kmsV!et#DZdHNHI+dDn_Gc%y7j z<#RGK|p^8kc7t?pCXA3@}3hi;`+UFw3Sk3!-E* z#>(143SP8ZfY(3%xA!D*>*7#f)bhrg+_(i;R))o~TO;?D$1OyOoZ*X<>FU9*it&a@ z3C4Q-KsYu4mSLD%Mb%T`eQGOao=ZTl$H&%&<}X#&dncJ3CeDT7slvQ|^do z^^uZ1I*0ydU;|9DldT5Q^v*gYGf^v@#g0|-z}%c7pY_u;3#GTBfglhJ8=F@h7J3YK zA{?}5{FD;N>ws78hC zt8IiVx2v$n6R~+bR7~h4WX0JCc@3xL@}zN1~}=icAz=g;H+BmFc_=^aB@SplYSe zOdT^pvkdi?Bh7>0!}K;B#$?`~y(5~uhA(&zFAt14irJ)x(dxCqz6eYQGlC0c-kW8K7>A~Oeg$tW}V*NMl?A~hH{WFcPjw(R6 zWbIzB{V<`U1Nke8Y_(FSnlaMKZiu9f^l;SA&*MkB&!{7l;wP$aUb-nV?Q}JQz*{~v zhodN_SYWGD6FO^ZHr1WmFXiT7M8>wOcU4ii^?Mm4-u_}g<*PJngR&)I>hMcZ#e^>b z32`5q<##pA2sJN6w0of1bTi1h1FbLKV>f<#al0@Zy~uA4MKNI zw1Ah5KYL=l!$-{K^6~V~n>M{m^XZ;%k9T?t;z+y+mSNG@V`Fp{r)D;Fm5HPpQe|Xh ze(so!HsLS&Z1lYAw1#uE;9a%gvZX_UnoXLK{^o-2=BVy>D_2TRoo9(vj8-~b4-Gb` z@#)h+Uf&3HYcF@64Q=?$ZwO&&u##m2M0a)QlmBf`vU8oI!zsbb*OHqW>jNQBTCmXL zKN)2+Hr;W+mD6XqkA$4pF)xYLaE2@sd-~d|G1rPQ;lM7DG+Nhdo + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Menu + + Menu + 1 + + + + + 274 + + YES + + + 292 + {{58, 25}, {203, 60}} + + + 3 + MCAwAA + + NO + NO + 2 + 0 + + TrebuchetMS-Bold + 1.600000e+01 + 16 + + 1 + Single player + Single player + Single player + Single Player + + 1 + MSAxIDEAA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + + + 292 + {{58, 93}, {203, 60}} + + + NO + NO + 2 + 0 + + 1 + Two players + Two players + Two players + Two Players + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 292 + {{58, 161}, {203, 60}} + + + NO + NO + 2 + 2 + 0 + + 1 + Leaderboards + Leaderboards + Leaderboards + Leaderboards + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + 269 + {{0, 345}, {320, 75}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 265 + {{270, 372}, {18, 19}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + + 292 + {{264, 315}, {50, 25}} + + + NO + YES + NO + 3.33 + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + + 2 + MC4xOTYwNzg0NSAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{35, 315}, {229, 25}} + + + NO + YES + NO + sergsregt + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{78, 26}, {53, 57}} + + NO + 2 + NO + + NSImage + singlePlayer.png + + + + + 292 + {{70, 161}, {71, 55}} + + NO + 1 + NO + + NSImage + leaderboard.png + + + + + 292 + {{70, 96}, {70, 54}} + + NO + 1 + NO + + NSImage + buddies.png + + + + + 292 + {{5, 315}, {30, 25}} + + + NO + YES + NO + 5 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{107, 293}, {104, 15}} + + + 1 + MCAwIDAAA + + YES + NO + Top 5 Tappers + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{0, 341}, {320, 5}} + + + 2 + MC40MzEzNzI1OCAwLjQzOTIxNTcyIDAuNDc0NTA5ODQAA + + NO + YES + NO + + + Helvetica-Bold + 1.400000e+01 + 16 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{0, 309}, {320, 5}} + + + 2 + MC40MzEzNzI1OCAwLjQzOTIxNTcyIDAuNDc0NTA5ODQAA + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{0, 345}, {320, 1}} + + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{315, 314}, {4, 27}} + + + 2 + MC40MzEzNzI1OCAwLjQzOTIxNTcyIDAuNDc0NTA5ODQAA + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{319, 310}, {1, 35}} + + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{324, 297}, {1, 35}} + + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{0, 314}, {5, 27}} + + + 2 + MC40MzEzNzI1OCAwLjQzOTIxNTcyIDAuNDc0NTA5ODQAA + + NO + YES + NO + + + + + 1 + 1.000000e+01 + 1 + + + {320, 416} + + + + + NO + + + + + + YES + + + view + + + + 9 + + + + multiPlayerButton + + + + 10 + + + + leaderboardsButton + + + + 11 + + + + singlePlayerButton + + + + 12 + + + + onLeaderboardsClicked: + + + 7 + + 14 + + + + onMultiPlayerClicked: + + + 7 + + 15 + + + + onSinglePlayerClicked: + + + 7 + + 16 + + + + showInfo + + + 7 + + 26 + + + + leadersLabel + + + + 30 + + + + leadersTimeLabel + + + + 31 + + + + leadersPositionLabel + + + + 39 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 8 + + + + + 17 + + + YES + + + + + + 24 + + + + + 27 + + + + + 29 + + + + + 34 + + + + + 35 + + + + + 36 + + + + + 37 + + + + + 40 + + + + + 41 + + + + + 42 + + + + + 43 + + + + + 44 + + + + + 45 + + + + + 49 + + + + + 50 + + + + + 51 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 17.IBPluginDependency + 24.IBPluginDependency + 27.IBPluginDependency + 29.IBPluginDependency + 34.IBPluginDependency + 35.IBPluginDependency + 36.IBPluginDependency + 37.IBPluginDependency + 4.IBPluginDependency + 40.IBPluginDependency + 42.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 45.IBPluginDependency + 49.IBPluginDependency + 5.IBPluginDependency + 50.IBPluginDependency + 51.IBPluginDependency + 6.IBPluginDependency + 8.IBPluginDependency + + + YES + MenuController + UIResponder + {{745, 222}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 51 + + + + YES + + MenuController + UIViewController + + YES + + YES + onLeaderboardsClicked: + onMultiPlayerClicked: + onSinglePlayerClicked: + showAbout: + showInfo + + + YES + id + id + id + id + id + + + + YES + + YES + leaderboardsButton + leadersLabel + leadersPositionLabel + leadersTimeLabel + multiPlayerButton + singlePlayerButton + + + YES + UIButton + UILabel + UILabel + UILabel + UIButton + UIButton + + + + IBProjectSource + Samples/TapRace/iphone/MenuController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/nophoto.png b/Samples/TapRace/iphone/Resources/nophoto.png new file mode 100644 index 0000000000000000000000000000000000000000..a8512e47f3af3c6fffcc79d386163e77a55b1dfc GIT binary patch literal 3550 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 Y0G(PwDvSa3fB*mh07*qoM6N<$f(1LE2><{9 literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/people.png b/Samples/TapRace/iphone/Resources/people.png new file mode 100644 index 0000000000000000000000000000000000000000..a336399188aab2689dd8ba96de802d93fe47e5a1 GIT binary patch literal 8156 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 literal 0 HcmV?d00001 diff --git a/Samples/TapRace/iphone/Resources/singlePlayer.png b/Samples/TapRace/iphone/Resources/singlePlayer.png new file mode 100644 index 0000000000000000000000000000000000000000..95e9f7b562a4e28e2c24ae78a8f10693fa764914 GIT binary patch literal 5526 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 + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Single Player Game + + + + 274 + + YES + + + 290 + {{20, 86}, {280, 11}} + + + 1 + MSAwLjk0OTE4MDI1IDAuOTcxNjgwNzYAA + + NO + YES + NO + 1 + + + + 292 + {{85, 20}, {149, 45}} + + NO + YES + NO + 00:00.00 + + TrebuchetMS-Bold + 3.600000e+01 + 16 + + + 1 + MCAwIDAAA + + + 1 + 1.000000e+01 + 1 + + + + -2147483356 + {{35, 32}, {33, 21}} + + NO + YES + NO + Time: + + Helvetica + 3.600000e+01 + 16 + + + 3 + MQA + + 2 + + + + 1 + 1.000000e+01 + + + + 269 + {{60, 302}, {124, 23}} + + NO + YES + NO + Previous Best: + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + + + 1 + 1.000000e+01 + + + + 269 + {{187, 302}, {72, 25}} + + NO + YES + NO + 00:00.00 + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{20, 20}, {57, 16}} + + NO + YES + NO + START + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + + 1 + NO + 1.000000e+01 + + + + 289 + {{251, 20}, {49, 16}} + + NO + YES + NO + FINISH + + + + 1 + NO + 1.000000e+01 + 2 + + + + 300 + {{20, 153}, {114, 111}} + + + 1 + MSAxIDEAA + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + NO + NO + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + NSImage + btn_down.png + + + + + NSImage + btn_up.png + + + + + 300 + {{195, 153}, {105, 111}} + + + NO + NO + 0 + 0 + + NO + NO + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + + + + 269 + {{0, 343}, {320, 73}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 292 + {{40, 116}, {239, 21}} + + + 3 + MCAwAA + + NO + YES + NO + Start tapping the keys to begin! + + + + 1 + 1.000000e+01 + 1 + + + + 265 + {{270, 371}, {18, 19}} + + NO + NO + 0 + 0 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + {320, 416} + + + NO + + + NO + + + + + + YES + + + localProgress + + + + 15 + + + + tapButton1 + + + + 17 + + + + tapButton2 + + + + 18 + + + + view + + + + 19 + + + + tapButtonPressed: + + + 1 + + 21 + + + + startButtonClicked: + + + 1 + + 27 + + + + gameTimeLabel + + + + 29 + + + + previousBestLabel + + + + 30 + + + + previousBestTimeLabel + + + + 31 + + + + messageLabel + + + + 33 + + + + showInfo + + + 7 + + 35 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + 14 + + + + + 22 + + + YES + + + + + 6 + + + + + 32 + + + + + 34 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 10.IBPluginDependency + 11.IBPluginDependency + 12.IBPluginDependency + 14.IBPluginDependency + 22.IBPluginDependency + 32.IBPluginDependency + 34.IBPluginDependency + 4.IBPluginDependency + 5.IBPluginDependency + 6.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + 9.IBPluginDependency + + + YES + GameController + UIResponder + {{287, 295}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 35 + + + + YES + + GameController + UIViewController + + YES + + YES + showInfo + startButtonClicked: + tapButtonPressed: + + + YES + id + id + id + + + + YES + + YES + activityIndicator + gameTimeLabel + localPlayerLabel + localProgress + messageLabel + previousBestLabel + previousBestTimeLabel + remotePlayerLabel + remoteProgress + tapButton1 + tapButton2 + + + YES + UIActivityIndicatorView + UILabel + UILabel + UIProgressView + UILabel + UILabel + UILabel + UILabel + UIProgressView + UIButton + UIButton + + + + IBProjectSource + Samples/TapRace/iphone/GameController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/singleplayerresults.xib b/Samples/TapRace/iphone/Resources/singleplayerresults.xib new file mode 100644 index 00000000..359b3c42 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/singleplayerresults.xib @@ -0,0 +1,778 @@ + + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Results + + + + 274 + + YES + + + 292 + {{83, 20}, {153, 50}} + + NO + YES + NO + 00:00.00 + + TrebuchetMS-Bold + 3.600000e+01 + 16 + + + 1 + MSAxIDEAA + + + 1 + NO + 1.000000e+01 + 1 + + + + 269 + {{0, 343}, {320, 73}} + + NO + NO + 4 + NO + + NSImage + gamespyFooter.png + + + + + -2147483356 + {{73, 0}, {184, 27}} + + + 1 + MCAwIDAAA + + YES + NO + TmV3IEJlc3QgVGltZSF/A + + TrebuchetMS-Bold + 1.800000e+01 + 16 + + + + 1 + 1.600000e+01 + 2 + 1 + + + + 269 + {{210, 245}, {90, 90}} + + NO + 1 + 0 + 0 + + MarkerFelt-Thin + 1.800000e+01 + 16 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + NSImage + leaderboard.png + + + + + 269 + {{20, 245}, {90, 90}} + + NO + 1 + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + NSImage + singlePlayer.png + + + + + -2147483356 + {{20, 72}, {162, 21}} + + NO + YES + NO + Previous Best Time: + + + + 1 + 1.000000e+01 + + + + -2147483356 + {{190, 67}, {82, 30}} + + + NO + YES + NO + 00:00.00 + + + + 1 + 1.000000e+01 + + + + 265 + {{270, 370}, {18, 19}} + + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + + 292 + {{113, 113}, {48, 19}} + + NO + YES + NO + Races: + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{170, 113}, {46, 19}} + + NO + YES + NO + 99999 + + + + 1 + 1.000000e+01 + + + + 292 + {{62, 165}, {99, 19}} + + NO + YES + NO + Average Time: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{170, 165}, {64, 19}} + + NO + YES + NO + 00:00.00 + + + + 1 + 1.000000e+01 + + + + 292 + {{88, 138}, {73, 19}} + + NO + YES + NO + Best Time: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{170, 138}, {66, 19}} + + NO + YES + NO + 00:00.00 + + + + 1 + 1.000000e+01 + + + + 292 + {{40, 192}, {121, 19}} + + NO + YES + NO + Average Taps/Sec.: + + + + 1 + 1.000000e+01 + 2 + + + + 292 + {{170, 192}, {41, 19}} + + NO + YES + NO + 1.0 + + + + 1 + 1.000000e+01 + + + {320, 416} + + + 1 + + + NO + + + + + + YES + + + view + + + + 23 + + + + leaderboardsButton + + + + 25 + + + + playAgainButton + + + + 28 + + + + leaderboardsButtonClicked: + + + 7 + + 31 + + + + playAgainButtonClicked: + + + 7 + + 33 + + + + gameTimeLabel + + + + 34 + + + + bestTimeLabel + + + + 57 + + + + averageTimeLabel + + + + 58 + + + + gamesPlayedLabel + + + + 59 + + + + congratsLabel + + + + 63 + + + + newbestTimeLabel + + + + 66 + + + + oldbestLabel + + + + 68 + + + + showInfo + + + 7 + + 70 + + + + avgTapsPerSecLabel + + + + 73 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 5 + + + + + 13 + + + + + 22 + + + + + 60 + + + Congrats Label + + + 19 + + + + + 20 + + + + + 67 + + + Old Best Label + + + 62 + + + NewBestTime + + + 69 + + + + + 51 + + + + + 52 + + + + + 55 + + + + + 56 + + + + + 53 + + + + + 54 + + + + + 71 + + + + + 72 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 13.IBPluginDependency + 19.IBPluginDependency + 20.IBPluginDependency + 22.IBPluginDependency + 5.IBPluginDependency + 51.IBPluginDependency + 52.IBPluginDependency + 53.IBPluginDependency + 54.IBPluginDependency + 55.IBPluginDependency + 56.IBPluginDependency + 60.IBPluginDependency + 62.IBPluginDependency + 67.IBPluginDependency + 69.IBPluginDependency + 71.IBPluginDependency + 72.IBPluginDependency + + + YES + GameResultsController + UIResponder + {{919, 171}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 73 + + + + YES + + GameResultsController + UIViewController + + YES + + YES + gotoMatchMaking + gotoMenu + leaderboardsButtonClicked: + menuButtonClicked: + playAgainButtonClicked: + showInfo + viewMyStatsClicked: + viewOpponentClicked: + + + YES + id + id + id + id + id + id + id + id + + + + YES + + YES + averageTimeLabel + avgTapsPerSecLabel + bestTimeLabel + busyLocal + busyRemote + congratsLabel + gameTimeLabel + gamesPlayedLabel + leaderboardsButton + localPlayerLabel + localThumbnailButton + localTimeLabel + localWinnerLabel + menuButton + newbestLabel + newbestTimeLabel + oldbestLabel + playAgainButton + remotePlayerLabel + remoteThumbnailButton + remoteTimeLabel + remoteWinnerLabel + tieLabel + + + YES + UILabel + UILabel + UILabel + UIActivityIndicatorView + UIActivityIndicatorView + UILabel + UILabel + UILabel + UIButton + UILabel + UIButton + UILabel + UILabel + UIButton + UILabel + UILabel + UILabel + UIButton + UILabel + UIButton + UILabel + UILabel + UILabel + + + + IBProjectSource + Samples/TapRace/iphone/GameResultsController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/takephoto.xib b/Samples/TapRace/iphone/Resources/takephoto.xib new file mode 100644 index 00000000..d268c179 --- /dev/null +++ b/Samples/TapRace/iphone/Resources/takephoto.xib @@ -0,0 +1,167 @@ + + + + 528 + 9F33 + 672 + 949.34 + 352.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + IBFilesOwner + + + IBFirstResponder + + + + 292 + + YES + + + 274 + {{0, 62}, {320, 418}} + + NO + NO + 4 + NO + + NSImage + ScreenshotPictures.png + + + + + 292 + {{121, 20}, {77, 21}} + + NO + YES + NO + Get Photo + + 1 + MCAwIDAAA + + + 1 + 1.000000e+01 + + + {320, 480} + + + 3 + MQA + + 2 + + + + + + + YES + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 6 + + + + + 7 + + + + + + + YES + + YES + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 6.IBPluginDependency + 7.IBPluginDependency + + + YES + UIResponder + {{159, 384}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 7 + + + 0 + ../../Gamespy.xcodeproj + 3 + + diff --git a/Samples/TapRace/iphone/Resources/tapImage.png b/Samples/TapRace/iphone/Resources/tapImage.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa98f0c47c794c9fdb522ae716ccd848205ece8 GIT binary patch literal 10183 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 + + + 528 + 9J61 + 677 + 949.46 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Two Player Game + + + + 274 + + YES + + + 290 + {{20, 83}, {280, 11}} + + NO + YES + YES + 1 + + + + 292 + {{90, 11}, {147, 35}} + + NO + YES + NO + 00:00.00 + + Helvetica + 3.600000e+01 + 16 + + + 1 + MCAwIDAAA + + + 1 + 1.000000e+01 + 2 + + + + -2147483356 + {{49, 18}, {33, 21}} + + NO + YES + NO + Time: + + Helvetica + 1.300000e+01 + 16 + + + + 1 + 1.000000e+01 + + + + 292 + {{18, 101}, {36, 16}} + + NO + YES + NO + START + + Helvetica + 8.000000e+00 + 16 + + + + 1 + NO + 1.000000e+01 + + + + 289 + {{264, 101}, {36, 16}} + + NO + YES + NO + FINISH + + + + 1 + NO + 1.000000e+01 + 2 + + + + 293 + {{20, 175}, {105, 111}} + + + 1 + MSAxIDEAA + + NO + NO + NO + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + NSImage + btn_down.png + + + + + NSImage + btn_up.png + + + + + 293 + {{195, 175}, {105, 111}} + + NO + NO + NO + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + + + + + 269 + {{0, 345}, {320, 73}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 290 + {{18, 125}, {280, 11}} + + + NO + YES + YES + 1 + + + + 292 + {{145, 54}, {36, 21}} + + NO + YES + NO + You + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{130, 143}, {66, 21}} + + NO + YES + NO + Opponent + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{62, 294}, {203, 21}} + + NO + YES + NO + Waiting for other player... + + + 1 + 1.000000e+01 + + + + 292 + {{34, 295}, {20, 20}} + + NO + NO + NO + YES + 2 + + + {320, 416} + + + + + NO + + + + + + YES + + + view + + + + 19 + + + + localProgress + + + + 20 + + + + tapButton1 + + + + 22 + + + + tapButton2 + + + + 23 + + + + tapButtonPressed: + + + 1 + + 24 + + + + tapButtonPressed: + + + 1 + + 26 + + + + remoteProgress + + + + 27 + + + + activityIndicator + + + + 28 + + + + gameTimeLabel + + + + 31 + + + + messageLabel + + + + 32 + + + + localPlayerLabel + + + + 33 + + + + remotePlayerLabel + + + + 34 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + 13 + + + + + 14 + + + + + 15 + + + + + 16 + + + + + 17 + + + + + 18 + + + + + 29 + + + YES + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 10.IBPluginDependency + 11.IBPluginDependency + 12.IBPluginDependency + 13.IBPluginDependency + 14.IBPluginDependency + 15.IBPluginDependency + 16.IBPluginDependency + 17.IBPluginDependency + 18.IBPluginDependency + 29.IBPluginDependency + 4.IBPluginDependency + 5.IBPluginDependency + 6.IBPluginDependency + 9.IBPluginDependency + + + YES + GameController + UIResponder + {{968, 200}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 36 + + + + YES + + GameController + UIViewController + + YES + + YES + showInfo + startButtonClicked: + tapButtonPressed: + + + YES + id + id + id + + + + YES + + YES + activityIndicator + gameTimeLabel + localPlayerLabel + localProgress + messageLabel + previousBestLabel + previousBestTimeLabel + remotePlayerLabel + remoteProgress + tapButton1 + tapButton2 + + + YES + UIActivityIndicatorView + UILabel + UILabel + UIProgressView + UILabel + UILabel + UILabel + UILabel + UIProgressView + UIButton + UIButton + + + + IBProjectSource + Samples/TapRace/iphone/GameController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/Resources/twoplayerresults.xib b/Samples/TapRace/iphone/Resources/twoplayerresults.xib new file mode 100644 index 00000000..5251736c --- /dev/null +++ b/Samples/TapRace/iphone/Resources/twoplayerresults.xib @@ -0,0 +1,841 @@ + + + + 528 + 9L31a + 677 + 949.54 + 353.00 + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + + + IBFirstResponder + + + Results + + + + 274 + + YES + + + 269 + {{0, 343}, {320, 73}} + + NO + NO + 7 + NO + + NSImage + gamespyFooter.png + + + + + 290 + {{19, 145}, {120, 29}} + + NO + YES + NO + you + + TrebuchetMS-Bold + 1.400000e+01 + 16 + + + 1 + MSAxIDEAA + + + 1 + 1.000000e+01 + 1 + + + + -2147483358 + {{34, 17}, {90, 29}} + + NO + YES + NO + Winner + + TrebuchetMS-Bold + 2.400000e+01 + 16 + + + 2 + MC4yMDc4NDMxNSAwLjMxNzY0NzA3IDAuNTI1NDkwMjIAA + + + 1 + MSAwLjUgMCAwAA + + + 1 + MSAxIDEgMAA + + 1 + 1.000000e+01 + 1 + + + + -2147483358 + {{139, 16}, {42, 30}} + + NO + YES + NO + Tie + + + 2 + MC4yMDc4NDMxNSAwLjMxNzY0NzA3IDAuNTI1NDkwMjIAA + + + 1 + MSAwLjUgMCAwAA + + + 1 + MSAxIDEgMAA + + 1 + 1.000000e+01 + 1 + + + + -2147483358 + {{201, 17}, {90, 29}} + + NO + YES + NO + Winner + + + 2 + MC4yMDc4NDMxNSAwLjMxNzY0NzA3IDAuNTI1NDkwMjIAA + + + + 1 + MSAxIDEgMAA + + 1 + 1.000000e+01 + 1 + + + + 290 + {{187, 145}, {120, 29}} + + NO + YES + NO + opponent + + + + 1 + 1.000000e+01 + 1 + + + + 268 + {{49, 245}, {90, 90}} + + NO + 1 + 0 + 0 + + Helvetica-Bold + 1.500000e+01 + 16 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MAA + + + NSImage + buddies_rematch.png + + + + + 269 + {{187, 245}, {90, 90}} + + NO + 1 + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + NSImage + leaderboard.png + + + + + 265 + {{270, 370}, {18, 19}} + + NO + NO + 0 + 0 + + 3 + YES + + 3 + MQA + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + 3 + MC41AA + + + + + 292 + {{26, 182}, {105, 30}} + + NO + YES + NO + 00:00.00 + + TrebuchetMS-Bold + 2.500000e+01 + 16 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{195, 182}, {105, 30}} + + NO + YES + NO + 00:00.00 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{148, 88}, {23, 21}} + + NO + YES + NO + vs + + 1 + MSAxIDEAA + + 1 + + + + 1 + 1.000000e+01 + 1 + + + + 292 + {{209, 53}, {75, 93}} + + NO + 1 + NO + 0 + 0 + + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + NSImage + singlePlayer.png + + + + + 292 + {{41, 53}, {75, 93}} + + + 1 + MCAwIDAgMAA + + NO + 1 + 0 + 0 + + 1 + + + 1 + MC4xOTYwNzg0MyAwLjMwOTgwMzkzIDAuNTIxNTY4NjYAA + + + + + + + 292 + {{229, 93}, {37, 37}} + + NO + NO + NO + NO + 0 + + + + 292 + {{60, 93}, {37, 37}} + + NO + NO + NO + NO + 0 + + + {320, 416} + + + 1 + MCAwIDAAA + + + + NO + + + + + + YES + + + view + + + + 25 + + + + leaderboardsButton + + + + 27 + + + + playAgainButton + + + + 28 + + + + leaderboardsButtonClicked: + + + 7 + + 32 + + + + playAgainButtonClicked: + + + 7 + + 34 + + + + localTimeLabel + + + + 39 + + + + remoteTimeLabel + + + + 40 + + + + showInfo + + + 7 + + 45 + + + + localPlayerLabel + + + + 53 + + + + remotePlayerLabel + + + + 55 + + + + localWinnerLabel + + + + 61 + + + + remoteWinnerLabel + + + + 62 + + + + tieLabel + + + + 63 + + + + localThumbnailButton + + + + 68 + + + + remoteThumbnailButton + + + + 69 + + + + viewMyStatsClicked: + + + 7 + + 70 + + + + busyRemote + + + + 72 + + + + busyLocal + + + + 74 + + + + + YES + + 0 + + YES + + + + + + 1 + + + YES + + + + + + + + + + + + + + + + + + + + + -1 + + + RmlsZSdzIE93bmVyA + + + -2 + + + + + 11 + + + + + 24 + + + + + 35 + + + + + 14 + + + + + 13 + + + + + 44 + + + + + 16 + + + + + 22 + + + + + 46 + + + + + 47 + + + + + 48 + + + + + 54 + + + + + 56 + + + + + 66 + + + + + 67 + + + + + 71 + + + + + 73 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 1.IBEditorWindowLastContentRect + 1.IBPluginDependency + 11.IBPluginDependency + 13.IBPluginDependency + 14.IBPluginDependency + 16.IBPluginDependency + 22.IBPluginDependency + 24.IBPluginDependency + 35.IBPluginDependency + 44.IBPluginDependency + 46.IBPluginDependency + 47.IBPluginDependency + 48.IBPluginDependency + 54.IBPluginDependency + 56.IBPluginDependency + 66.IBPluginDependency + 67.IBPluginDependency + 71.IBPluginDependency + 73.IBPluginDependency + + + YES + GameResultsController + UIResponder + {{737, 100}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 74 + + + + YES + + GameResultsController + UIViewController + + YES + + YES + gotoMatchMaking + gotoMenu + leaderboardsButtonClicked: + menuButtonClicked: + playAgainButtonClicked: + showInfo + viewMyStatsClicked: + viewOpponentClicked: + + + YES + id + id + id + id + id + id + id + id + + + + YES + + YES + averageTimeLabel + avgTapsPerSecLabel + bestTimeLabel + busyLocal + busyRemote + congratsLabel + gameTimeLabel + gamesPlayedLabel + leaderboardsButton + localPlayerLabel + localThumbnailButton + localTimeLabel + localWinnerLabel + menuButton + newbestLabel + newbestTimeLabel + oldbestLabel + playAgainButton + remotePlayerLabel + remoteThumbnailButton + remoteTimeLabel + remoteWinnerLabel + tieLabel + + + YES + UILabel + UILabel + UILabel + UIActivityIndicatorView + UIActivityIndicatorView + UILabel + UILabel + UILabel + UIButton + UILabel + UIButton + UILabel + UILabel + UIButton + UILabel + UILabel + UILabel + UIButton + UILabel + UIButton + UILabel + UILabel + UILabel + + + + IBProjectSource + Samples/TapRace/iphone/GameResultsController.h + + + + + 0 + ../../../../Gamespy.xcodeproj + 3 + 3.0 + + diff --git a/Samples/TapRace/iphone/TapRace-Info.plist b/Samples/TapRace/iphone/TapRace-Info.plist new file mode 100644 index 00000000..8166aa95 --- /dev/null +++ b/Samples/TapRace/iphone/TapRace-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleIconFile + icon_57x57.png + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.fox.gamespy.${PRODUCT_NAME:identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 1.2 + NSMainNibFile + MainWindow + UIRequiresPersistentWiFi + + + diff --git a/Samples/TapRace/iphone/UserStatsController.h b/Samples/TapRace/iphone/UserStatsController.h new file mode 100644 index 00000000..575a3c16 --- /dev/null +++ b/Samples/TapRace/iphone/UserStatsController.h @@ -0,0 +1,55 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import + + +@interface UserStatsController : UIViewController +{ + IBOutlet UIImageView* thumbNail; + IBOutlet UILabel* userName; + IBOutlet UILabel* userTotalRaces; + IBOutlet UILabel* userTotalWins; + IBOutlet UILabel* userTotalLosses; + IBOutlet UILabel* userRatio; + IBOutlet UILabel* userBestTime; + IBOutlet UILabel* userWinStreak; + IBOutlet UILabel* myRank; + + IBOutlet UILabel* myTotalRaces; + IBOutlet UILabel* myTotalWins; + IBOutlet UILabel* myTotalLosses; + IBOutlet UILabel* myRatio; + IBOutlet UILabel* myBestTime; + IBOutlet UILabel* myWinStreak; + + IBOutlet UIButton* makeBuddyButton; + IBOutlet UIButton* reportImageButton; + + UIAlertView* alertView; + int currentAlert; + NSTimer* updateTimer; +} +@property (nonatomic, retain) UIImageView* thumbNail; +@property (nonatomic, retain) UILabel* userName; +@property (nonatomic, retain) UILabel* userTotalRaces; +@property (nonatomic, retain) UILabel* userTotalWins; +@property (nonatomic, retain) UILabel* userTotalLosses; +@property (nonatomic, retain) UILabel* userRatio; +@property (nonatomic, retain) UILabel* userBestTime; +@property (nonatomic, retain) UILabel* userWinStreak; +@property (nonatomic, retain) UILabel* myRank; + +@property (nonatomic, retain) UILabel* myTotalRaces; +@property (nonatomic, retain) UILabel* myTotalWins; +@property (nonatomic, retain) UILabel* myTotalLosses; +@property (nonatomic, retain) UILabel* myRatio; +@property (nonatomic, retain) UILabel* myBestTime; +@property (nonatomic, retain) UILabel* myWinStreak; + +- (IBAction)makeBuddyButtonClicked: (id)sender; +- (IBAction)reportImageButtonClicked: (id)sender; +- (IBAction)showInfo; + + +@end diff --git a/Samples/TapRace/iphone/UserStatsController.mm b/Samples/TapRace/iphone/UserStatsController.mm new file mode 100644 index 00000000..937b36d0 --- /dev/null +++ b/Samples/TapRace/iphone/UserStatsController.mm @@ -0,0 +1,284 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "UserStatsController.h" +#import "LeadersController.h" +#import "BuddyListCellView.h" +#import "AppDelegate.h" +#import "Utility.h" +#import "LoginController.h" + +enum AppAlert +{ + ALERT_NONE, + ALERT_MAKEBUDDY, + ALERT_REPORTIMAGE +}; + +@implementation UserStatsController + +@synthesize thumbNail; +@synthesize userName; +@synthesize userTotalRaces; +@synthesize userTotalWins; +@synthesize userTotalLosses; +@synthesize userRatio; +@synthesize userBestTime; +@synthesize userWinStreak; + +@synthesize myRank; +@synthesize myTotalRaces; +@synthesize myTotalWins; +@synthesize myTotalLosses; +@synthesize myRatio; +@synthesize myBestTime; +@synthesize myWinStreak; + +- (void)dealloc +{ + if(thumbNail != nil) [thumbNail release]; + [userName release]; + [userTotalRaces release]; + [userTotalWins release]; + [userTotalLosses release]; + [userRatio release]; + [userBestTime release]; + [userWinStreak release]; + [myRank release]; + [myTotalRaces release]; + [myTotalWins release]; + [myTotalLosses release]; + [myRatio release]; + [myBestTime release]; + [myWinStreak release]; + //if(updateTimer != nil) [updateTimer invalidate]; + //[updateTimer release]; + [super dealloc]; +} + +// display image if one is received +//************************************* +- (void)updateTime: (NSTimer*)timer; +{ + LeaderboardObject* leader = [gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]; + UIImage * img = leader.thumbNail; + + // Hide "Report image" button if no image or it is blocked or it is myself + if (img == nil || [appDelegate imageIsBlocked: leader.pictureFileId] || gPlayerData.profileId == leader.profileId ) + { + reportImageButton.hidden = YES; + [updateTimer invalidate]; + } + else + // if image found and it is not blocked, display it + // don't change if no image, will display the default No Photo + { + reportImageButton.hidden = NO; + [self.thumbNail setImage: img]; + [updateTimer invalidate]; + [self.thumbNail setNeedsDisplay]; + } + +} + + +// called after a delay to show rank +// gives the mainloop a chance for callback to set server value for rank +- (void)delayedRank +{ + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]; + + if( leader.rank != 0 ) + myRank.text = [NSString stringWithFormat: @"#%d", leader.rank ]; +} + + +//************************************* +- (void)viewWillAppear: (BOOL) animated +{ + int totalRaces; + int totalWins; + int bestTime; + + // V.1 hide buddy button + makeBuddyButton.hidden = YES; + + + + //[super viewWillAppear: animated]; + + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]; + + // if this is a buddy list, then hide the 'make buddy' button since he's already a buddy + if (gLeaderboardType == LEADERBOARD_BY_BUDDIES) + { + makeBuddyButton.hidden = YES; + } + + myRank.text = [NSString stringWithFormat: @"#%d", gLeaderboardProfileIndex + 1 ]; +/* else + { + // request rank from server + [appDelegate getMyLeaderPosition: &(leader.rank) ]; + + // go get it in 1 second + [self performSelector:@selector(delayedRank) withObject:nil afterDelay:1.0f]; + } +*/ + NSString* aTitle = [NSString stringWithFormat: @"%@'s Profile", leader.name]; + self.title = aTitle; //[leader.name copy]; // index of currently displayed profile + //self.title = [leader.name copy]; + // display image if not blocked and a picture exists + // if no image exists, will display the default No Photo + + UIImage * img = leader.thumbNail; + if( img == nil && leader.pictureFileId == 0 ) + { + // kick off a timer to poll for the image...just in case the leaderboard fills it in for us while we're sitting here + if (updateTimer != nil) { + [updateTimer invalidate]; + } + updateTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(updateTime:) userInfo: nil repeats: YES]; + } + else if ( ! [appDelegate imageIsBlocked: leader.pictureFileId] ) + { + reportImageButton.hidden = NO; + [self.thumbNail setImage: img]; + } + else // picture is blocked + { + reportImageButton.hidden = YES; + } + + if( gPlayerData.profileId == leader.profileId ) + { + reportImageButton.hidden = YES; + } + + self.userName.text = [leader.name copy]; + + totalRaces = leader.totalMatches; + self.userTotalRaces.text = [NSString stringWithFormat: @"%d", totalRaces]; + + totalWins = leader.careerWins; + self.userTotalWins.text = [NSString stringWithFormat: @"%d", totalWins]; + + self.userTotalLosses.text = [NSString stringWithFormat: @"%d", leader.careerLosses]; + if (totalWins > 0) { + self.userRatio.text = [NSString stringWithFormat: @"%d\%%", (totalWins*100)/totalRaces]; + } else { + self.userRatio.text = [NSString stringWithString: @"0%"]; + } + + bestTime = leader.bestRaceTime; + if ((bestTime <= 0) || (bestTime >= INITIAL_BEST_TIME)) + { + self.userBestTime.text = [NSString stringWithString: @"none"]; + } + else + { + SetTimeLabel(self.userBestTime, bestTime); + } + + self.userWinStreak.text = [NSString stringWithFormat: @"%d", leader.currentWinStreak]; + + self.myTotalRaces.text = [NSString stringWithFormat: @"%d", getLocalIntegerStat(matchTotalCompleteMatchesKey)]; + self.myTotalWins.text = [NSString stringWithFormat: @"%d", getLocalIntegerStat(matchWonKey)]; + self.myTotalLosses.text = [NSString stringWithFormat: @"%d", getLocalIntegerStat(matchLossKey)]; + + if (gPlayerData.totalMatches > 0) + { + self.myRatio.text = [NSString stringWithFormat: @"%d\%%", (getLocalIntegerStat(matchWonKey)*100) / getLocalIntegerStat(matchTotalCompleteMatchesKey)]; + } + else + { + self.myRatio.text = [NSString stringWithString: @"0%"]; + } + + int myBestRaceTime = getLocalIntegerStat(matchBestTimeKey); + if( (myBestRaceTime <= 0) || (myBestRaceTime >= INITIAL_BEST_TIME) ) + { + self.myBestTime.text = [NSString stringWithString: @"none"]; + } + else + { + SetTimeLabel(self.myBestTime, myBestRaceTime); + } + + self.myWinStreak.text = [NSString stringWithFormat: @"%d", gPlayerData.matchWinStreak]; + +} + +//************************************* +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return interfaceOrientation == UIInterfaceOrientationPortrait; +} + + +// Display the About page +- (IBAction)showInfo +{ + [ appDelegate showAboutInfo: self ]; +} + + +//************************************* +- (void)alertView: (UIAlertView*)alert didDismissWithButtonIndex: (NSInteger)buttonIndex +{ + switch (currentAlert) { + case ALERT_MAKEBUDDY: + if (buttonIndex != alertView.cancelButtonIndex) { + gsi_char reason[GP_REASON_LEN]; + reason[0] = 0; // null in any language + + LoginController* loginController = (LoginController*)[appDelegate findExistingControllerOfType: [LoginController class]]; + gpSendBuddyRequest( [loginController getGPConnectionPtr], + ((LeaderboardObject*)[gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]).profileId, + reason ); + // this could cause a gpRecvBuddyRequest callback indicating that it was accepted or rejected. + // for this simple app, we don't care + } + break; + case ALERT_REPORTIMAGE: + // Block image if cancel button ("No") was not pressed + if (buttonIndex != alertView.cancelButtonIndex) + { + // set & display default image in user stat page + NSString* defaultImage = [[NSBundle mainBundle] pathForResource:@"singlePlayer" ofType:@"png"]; + UIImage* imageObj = [[UIImage alloc] initWithContentsOfFile:defaultImage]; + [self.thumbNail setImage: imageObj]; + + // add to block images list + LeaderboardObject* leader = (LeaderboardObject*)[gLeaderboardData objectAtIndex: gLeaderboardProfileIndex]; + NSNumber* aFileId = [[ NSNumber alloc] initWithUnsignedInteger: leader.pictureFileId ]; + [ gPlayerData.blockedImagesData addObject:aFileId ]; + + // save to file + [ gPlayerData.blockedImagesData writeToFile: [gPlayerData.playerDataPath stringByAppendingPathComponent: blockedImagesDataFile] atomically: YES]; + } + break; + } + currentAlert = ALERT_NONE; +} + + +//************************************* +- (IBAction)makeBuddyButtonClicked: (id)sender +{ + alertView = [[UIAlertView alloc] initWithTitle: @"Make Buddy" message: @"Are you sure?" delegate: self cancelButtonTitle: @"No" otherButtonTitles: @"Yes", nil]; + [alertView show]; + currentAlert = ALERT_MAKEBUDDY; +} + + +//************************************* +- (IBAction)reportImageButtonClicked: (id)sender +{ + alertView = [[UIAlertView alloc] initWithTitle: @"Report Image" message: @"Are you sure?" delegate: self cancelButtonTitle: @"No" otherButtonTitles: @"Yes", nil]; + [alertView show]; + currentAlert = ALERT_REPORTIMAGE; +} + +@end diff --git a/Samples/TapRace/iphone/Utility.h b/Samples/TapRace/iphone/Utility.h new file mode 100644 index 00000000..7dedc0e3 --- /dev/null +++ b/Samples/TapRace/iphone/Utility.h @@ -0,0 +1,72 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#ifndef UTILITY_INCLUDED + +// Allows expanding macros into string constants. +#define STRINGIZE1(x) #x +#define STRINGIZE(x) STRINGIZE1(x) + +#define getLocalIntegerStat(key) [(NSNumber*)[gPlayerData.playerStatsData objectForKey: key ] intValue] +#define getLocalFloatStat(key) [(NSNumber*)[gPlayerData.playerStatsData objectForKey: key ] floatValue] +#define getLocalStringStat(key) [(NSString*)[gPlayerData.playerStatsData objectForKey: key ]] + + +// Multipliers for conversion between device time units and real time units. +extern double absolute_to_millis; +extern double millis_to_absolute; +extern double absolute_to_seconds; +extern double seconds_to_absolute; + +// printf-style format string for displaying game time. +extern NSString* timeFormatString; + +// Generic function for setting the text of a time label. +void SetTimeLabel(UILabel* label, int timeInMilliseconds); + +// File name for storing single-player data. +extern NSString* playerStatsDataFile; +extern NSString* blockedImagesDataFile; + +// Keys for accessing single-player data. +extern NSString* singlePlayerBestTimeKey; +extern NSString* singlePlayerWorstTimeKey; +extern NSString* singlePlayerAverageTimeKey; +extern NSString* singlePlayerGamesPlayedKey; +extern NSString* singlePlayerTotalPlaysKey; +extern NSString* singlePlayerTotalRaceTimeKey; + + + +// Keys for accessing two-player game stats data. +extern NSString* matchBestTimeKey; +extern NSString* matchWorstTimeKey; +extern NSString* matchAverageTimeKey; +extern NSString* matchGamesPlayedKey; +extern NSString* matchWonKey; +extern NSString* matchLossKey; +extern NSString* matchWinStreakKey; +extern NSString* matchLossStreakKey; +extern NSString* matchTotalRaceTimeKey; +extern NSString* matchCareerDisconnectsKey; +extern NSString* matchDisconnectRateKey; +extern NSString* matchDrawsKey; +extern NSString* matchDrawStreakKey; + +extern NSString* matchCareerLongestWinStreakKey; +extern NSString* matchCareerLongestLossStreakKey; +extern NSString* matchCareerLongestDrawStreakKey; +extern NSString* matchTotalCompleteMatchesKey; + +// Key for timestamp of stored stats data +extern NSString* lastTimePlayedKey; + +// Debugging and user notification convenience functions. +void MessageBox(NSString* text, NSString* caption = nil); +void MessageBoxNoBlock(NSString* text, NSString* caption = nil, NSString* cancelButtonString = @"OK"); +void OutputDebugString(NSString* text); + +// Get the bold version of an existing font. Returns the existing font if there is no bold verson. +UIFont* GetBoldVersion(UIFont* font); + +#endif \ No newline at end of file diff --git a/Samples/TapRace/iphone/Utility.mm b/Samples/TapRace/iphone/Utility.mm new file mode 100644 index 00000000..00e02570 --- /dev/null +++ b/Samples/TapRace/iphone/Utility.mm @@ -0,0 +1,165 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import "Utility.h" +#import + +static mach_timebase_info_data_t InitTimebaseInfo(); +static mach_timebase_info_data_t timebase_info = InitTimebaseInfo(); + +double absolute_to_millis; +double millis_to_absolute; +double absolute_to_seconds; +double seconds_to_absolute; + +NSString* timeFormatString = @"%02u:%05.2f"; +NSString* playerStatsDataFile = @"playerStats.plist"; +NSString* blockedImagesDataFile = @"blockedImages.plist"; + +NSString* singlePlayerBestTimeKey = @"spBestTime"; +NSString* singlePlayerWorstTimeKey = @"spWorstTime"; +NSString* singlePlayerAverageTimeKey = @"spAverageTime"; +NSString* singlePlayerGamesPlayedKey = @"spGamesPlayed"; +NSString* singlePlayerTotalPlaysKey = @"spTotalPlays"; +NSString* singlePlayerTotalRaceTimeKey = @"spTotalRaceTime"; + +NSString* matchBestTimeKey = @"BestTime"; +NSString* matchWorstTimeKey = @"WorstTime"; +NSString* matchAverageTimeKey = @"AverageTime"; +NSString* matchGamesPlayedKey = @"GamesPlayed"; + +NSString* matchWonKey = @"Won"; +NSString* matchLossKey = @"Loss"; +NSString* matchDrawsKey = @"careerDraws"; + +NSString* matchWinStreakKey = @"WinStreak"; +NSString* matchLossStreakKey = @"currentLossStreak"; +NSString* matchDrawStreakKey = @"currentDrawStreak"; + +NSString* matchCareerLongestWinStreakKey = @"careerLongestWinStreak"; +NSString* matchCareerLongestLossStreakKey = @"careerLongestLossStreak"; +NSString* matchCareerLongestDrawStreakKey = @"careerLongestDrawStreak"; + +NSString* matchTotalRaceTimeKey = @"totalRaceTimeKeyotalRaceTime"; +NSString* matchCareerDisconnectsKey = @"careerDisconnects"; +NSString* matchDisconnectRateKey = @"disconnectRate"; +NSString* matchTotalCompleteMatchesKey = @"totalCompleteMatches"; + +NSString* lastTimePlayedKey = @"LastTimePlayed"; + + +void SetTimeLabel(UILabel* label, int timeInMilliseconds) +{ + label.text = [NSString stringWithFormat: timeFormatString, timeInMilliseconds / (60 * 1000), (timeInMilliseconds % (60 * 1000)) / 1000.0f]; +} + + +@interface MessageBoxDelegate : NSObject +{ + NSInteger clickedButtonIndex; +} + +@property(nonatomic, readonly) NSInteger clickedButtonIndex; + +@end + + +@implementation MessageBoxDelegate + +@synthesize clickedButtonIndex; + +- (id)init +{ + self = [super init]; + + if (self != nil) { + clickedButtonIndex = -1; + } + + return self; +} + +- (void)alertView: (UIAlertView*)alertView willDismissWithButtonIndex: (NSInteger)buttonIndex +{ + clickedButtonIndex = buttonIndex; +} + +@end + + + +void MessageBox(NSString* text, NSString* title) +{ + MessageBoxDelegate* delegate = [[MessageBoxDelegate alloc] init]; + UIAlertView* alertView = [[UIAlertView alloc] initWithTitle: title message: text delegate: delegate cancelButtonTitle: @"OK" otherButtonTitles: nil]; + [alertView show]; + + while (delegate.clickedButtonIndex == -1) { + [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.10]]; + } + + [alertView release]; + [delegate release]; +} + + +void MessageBoxNoBlock(NSString* text, NSString* caption, NSString* cancelButtonString ) +{ + UIAlertView* alertView = [[UIAlertView alloc] initWithTitle: caption message: text delegate: nil cancelButtonTitle: cancelButtonString otherButtonTitles: nil]; + [alertView show]; + [alertView release]; +} + +void OutputDebugString(NSString* text) +{ +#ifdef GSI_COMMON_DEBUG + NSLog(@"%s\n", [text cStringUsingEncoding: NSASCIIStringEncoding]); +#endif +} + +UIFont* GetBoldVersion(UIFont* font) +{ + // Apple doesn't make this easy. There's no programmatic way to request the bold version of a given font, + // so we have to use the font names as a guide. + NSString* fontFamilyName = font.familyName; + NSArray* fontNames = [UIFont fontNamesForFamilyName: fontFamilyName]; + NSString* boldFontName = font.fontName; + NSUInteger shortestAttributeStringLength = NSUIntegerMax; + + // For each font name in the font's family, search for the string "bold." After this loop, boldFontName + // holds the best candidate. + for (NSString* fontName in fontNames) { + // Font names tend to follow the format "FamilyName-Attributes". We don't want to search the family + // name part (if there's a font family named "Kobold", don't match the "bold" part of that), so try + // to isolate just the attributes... + NSRange familyNameRange = [fontName rangeOfString: fontFamilyName options: NSCaseInsensitiveSearch]; + NSString* fontAttributes = [NSString stringWithFormat: @"%@ %@", [fontName substringToIndex: familyNameRange.location], [fontName substringFromIndex: familyNameRange.location + familyNameRange.length]]; + fontAttributes = [fontAttributes stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @" -"]]; + bool bold = [fontAttributes rangeOfString: @"bold" options: NSCaseInsensitiveSearch].length > 0; + + // A font can have more than one attribute, such as "ObliqueBold". Try to match the shortest + // attribute string -- this should be closest to pure "bold". + if (bold && (fontAttributes.length < shortestAttributeStringLength)) { + shortestAttributeStringLength = fontAttributes.length; + boldFontName = fontName; + } + } + + if ([boldFontName isEqualToString: font.fontName]) { + return font; + } + + return [UIFont fontWithName: boldFontName size: font.pointSize]; +} + +mach_timebase_info_data_t InitTimebaseInfo() +{ + mach_timebase_info_data_t timebase_info; + mach_timebase_info(&timebase_info); + absolute_to_millis = timebase_info.numer / (1000000.0 * timebase_info.denom); + millis_to_absolute = 1.0 / absolute_to_millis; + absolute_to_seconds = timebase_info.numer / (1000000000.0 * timebase_info.denom); + seconds_to_absolute = 1.0 / absolute_to_seconds; + + return timebase_info; +} diff --git a/Samples/TapRace/iphone/atlas_taprace_v2.c b/Samples/TapRace/iphone/atlas_taprace_v2.c new file mode 100644 index 00000000..dcad6de0 --- /dev/null +++ b/Samples/TapRace/iphone/atlas_taprace_v2.c @@ -0,0 +1,225 @@ +/////////////////////////////////////////////////////////////////////////////// +// GameSpy ATLAS Competition System Source File +// +// NOTE: This is an auto-generated file, do not edit this file directly. +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "atlas_taprace_v2.h" + +int atlas_rule_set_version = 2; + +int ATLAS_GET_KEY(char* keyName) +{ + if(!keyName) + return 0; + + if(!strcmp("RACE_TIME", keyName)) + return RACE_TIME; + if(!strcmp("KEY_NICK", keyName)) + return KEY_NICK; + if(!strcmp("KEY_LATITUDE", keyName)) + return KEY_LATITUDE; + if(!strcmp("KEY_LONGITUDE", keyName)) + return KEY_LONGITUDE; + if(!strcmp("KEY_MULTI_PLAY", keyName)) + return KEY_MULTI_PLAY; + if(!strcmp("KEY_SINGLE_PLAY", keyName)) + return KEY_SINGLE_PLAY; + if(!strcmp("SP_RACE_TIME", keyName)) + return SP_RACE_TIME; + if(!strcmp("KEY_RACE_DATE_TIME", keyName)) + return KEY_RACE_DATE_TIME; + + return 0; +} + +char* ATLAS_GET_KEY_NAME(int keyId) +{ + if(keyId <= 0) + return ""; + + if(keyId == RACE_TIME) + return "RACE_TIME"; + if(keyId == KEY_NICK) + return "KEY_NICK"; + if(keyId == KEY_LATITUDE) + return "KEY_LATITUDE"; + if(keyId == KEY_LONGITUDE) + return "KEY_LONGITUDE"; + if(keyId == KEY_MULTI_PLAY) + return "KEY_MULTI_PLAY"; + if(keyId == KEY_SINGLE_PLAY) + return "KEY_SINGLE_PLAY"; + if(keyId == SP_RACE_TIME) + return "SP_RACE_TIME"; + if(keyId == KEY_RACE_DATE_TIME) + return "KEY_RACE_DATE_TIME"; + + return ""; +} + +int ATLAS_GET_STAT(char* statName) +{ + if(!statName) + return 0; + + if(!strcmp("AVERAGE_RACE_TIME", statName)) + return AVERAGE_RACE_TIME; + if(!strcmp("BEST_RACE_TIME", statName)) + return BEST_RACE_TIME; + if(!strcmp("CAREER_DISCONNECTS", statName)) + return CAREER_DISCONNECTS; + if(!strcmp("CAREER_DRAWS", statName)) + return CAREER_DRAWS; + if(!strcmp("CAREER_LONGEST_DRAW_STREAK", statName)) + return CAREER_LONGEST_DRAW_STREAK; + if(!strcmp("CAREER_LONGEST_LOSS_STREAK", statName)) + return CAREER_LONGEST_LOSS_STREAK; + if(!strcmp("CAREER_LONGEST_WIN_STREAK", statName)) + return CAREER_LONGEST_WIN_STREAK; + if(!strcmp("CAREER_LOSSES", statName)) + return CAREER_LOSSES; + if(!strcmp("CAREER_WINS", statName)) + return CAREER_WINS; + if(!strcmp("CURRENT_DRAW_STREAK", statName)) + return CURRENT_DRAW_STREAK; + if(!strcmp("CURRENT_LOSS_STREAK", statName)) + return CURRENT_LOSS_STREAK; + if(!strcmp("CURRENT_WIN_STREAK", statName)) + return CURRENT_WIN_STREAK; + if(!strcmp("DISCONNECT_RATE", statName)) + return DISCONNECT_RATE; + if(!strcmp("LAST_TIME_PLAYED", statName)) + return LAST_TIME_PLAYED; + if(!strcmp("LATITUDE", statName)) + return LATITUDE; + if(!strcmp("LONGITUDE", statName)) + return LONGITUDE; + if(!strcmp("NICK", statName)) + return NICK; + if(!strcmp("SP_AVERAGE_RACE_TIME", statName)) + return SP_AVERAGE_RACE_TIME; + if(!strcmp("SP_BEST_RACE_TIME", statName)) + return SP_BEST_RACE_TIME; + if(!strcmp("SP_TOTAL_PLAYS", statName)) + return SP_TOTAL_PLAYS; + if(!strcmp("SP_TOTAL_RACE_TIME", statName)) + return SP_TOTAL_RACE_TIME; + if(!strcmp("SP_WORST_RACE_TIME", statName)) + return SP_WORST_RACE_TIME; + if(!strcmp("TOTAL_COMPLETE_MATCHES", statName)) + return TOTAL_COMPLETE_MATCHES; + if(!strcmp("TOTAL_MATCHES", statName)) + return TOTAL_MATCHES; + if(!strcmp("TOTAL_RACE_TIME", statName)) + return TOTAL_RACE_TIME; + if(!strcmp("WORST_RACE_TIME", statName)) + return WORST_RACE_TIME; + + return 0; +} +char* ATLAS_GET_STAT_NAME(int statId) +{ + if(statId <= 0) + return ""; + + if(statId == AVERAGE_RACE_TIME) + return "AVERAGE_RACE_TIME"; + if(statId == BEST_RACE_TIME) + return "BEST_RACE_TIME"; + if(statId == CAREER_DISCONNECTS) + return "CAREER_DISCONNECTS"; + if(statId == CAREER_DRAWS) + return "CAREER_DRAWS"; + if(statId == CAREER_LONGEST_DRAW_STREAK) + return "CAREER_LONGEST_DRAW_STREAK"; + if(statId == CAREER_LONGEST_LOSS_STREAK) + return "CAREER_LONGEST_LOSS_STREAK"; + if(statId == CAREER_LONGEST_WIN_STREAK) + return "CAREER_LONGEST_WIN_STREAK"; + if(statId == CAREER_LOSSES) + return "CAREER_LOSSES"; + if(statId == CAREER_WINS) + return "CAREER_WINS"; + if(statId == CURRENT_DRAW_STREAK) + return "CURRENT_DRAW_STREAK"; + if(statId == CURRENT_LOSS_STREAK) + return "CURRENT_LOSS_STREAK"; + if(statId == CURRENT_WIN_STREAK) + return "CURRENT_WIN_STREAK"; + if(statId == DISCONNECT_RATE) + return "DISCONNECT_RATE"; + if(statId == LAST_TIME_PLAYED) + return "LAST_TIME_PLAYED"; + if(statId == LATITUDE) + return "LATITUDE"; + if(statId == LONGITUDE) + return "LONGITUDE"; + if(statId == NICK) + return "NICK"; + if(statId == SP_AVERAGE_RACE_TIME) + return "SP_AVERAGE_RACE_TIME"; + if(statId == SP_BEST_RACE_TIME) + return "SP_BEST_RACE_TIME"; + if(statId == SP_TOTAL_PLAYS) + return "SP_TOTAL_PLAYS"; + if(statId == SP_TOTAL_RACE_TIME) + return "SP_TOTAL_RACE_TIME"; + if(statId == SP_WORST_RACE_TIME) + return "SP_WORST_RACE_TIME"; + if(statId == TOTAL_COMPLETE_MATCHES) + return "TOTAL_COMPLETE_MATCHES"; + if(statId == TOTAL_MATCHES) + return "TOTAL_MATCHES"; + if(statId == TOTAL_RACE_TIME) + return "TOTAL_RACE_TIME"; + if(statId == WORST_RACE_TIME) + return "WORST_RACE_TIME"; + + return ""; +} + +int ATLAS_GET_STAT_PAGE_BY_ID(int statId) +{ + if(statId <= 0) + return 0; + + + //PlayerStats + if(statId == 1 || statId == 2 || statId == 3 || statId == 4 || statId == 5 || statId == 6 || statId == 7 || + statId == 8 || statId == 9 || statId == 10 || statId == 11 || statId == 12 || statId == 13 || statId == 14 || + statId == 15 || statId == 16 || statId == 17 || statId == 18 || statId == 19 || statId == 20 || statId == 21 || + statId == 22 || statId == 23 || statId == 24 || statId == 25 || statId == 26) + return 1; + + + return 0; +} + +int ATLAS_GET_STAT_PAGE_BY_NAME(char* statName) +{ + if(!statName) + return 0; + + + //PlayerStats + if(!strcmp("CAREER_WINS", statName) || !strcmp("CAREER_LOSSES", statName) || + !strcmp("BEST_RACE_TIME", statName) || !strcmp("WORST_RACE_TIME", statName) || + !strcmp("TOTAL_MATCHES", statName) || !strcmp("AVERAGE_RACE_TIME", statName) || + !strcmp("CURRENT_WIN_STREAK", statName) || !strcmp("CURRENT_LOSS_STREAK", statName) || + !strcmp("TOTAL_RACE_TIME", statName) || !strcmp("CAREER_DISCONNECTS", statName) || + !strcmp("DISCONNECT_RATE", statName) || !strcmp("CAREER_DRAWS", statName) || + !strcmp("CURRENT_DRAW_STREAK", statName) || !strcmp("CAREER_LONGEST_WIN_STREAK", statName) || + !strcmp("CAREER_LONGEST_LOSS_STREAK", statName) || !strcmp("CAREER_LONGEST_DRAW_STREAK", statName) || + !strcmp("TOTAL_COMPLETE_MATCHES", statName) || !strcmp("NICK", statName) || + !strcmp("LATITUDE", statName) || !strcmp("LONGITUDE", statName) || + !strcmp("SP_BEST_RACE_TIME", statName) || !strcmp("SP_WORST_RACE_TIME", statName) || + !strcmp("SP_TOTAL_PLAYS", statName) || !strcmp("SP_AVERAGE_RACE_TIME", statName) || + !strcmp("SP_TOTAL_RACE_TIME", statName) || !strcmp("LAST_TIME_PLAYED", statName)) + return 1; + + + return 0; +} + diff --git a/Samples/TapRace/iphone/atlas_taprace_v2.h b/Samples/TapRace/iphone/atlas_taprace_v2.h new file mode 100644 index 00000000..6857589e --- /dev/null +++ b/Samples/TapRace/iphone/atlas_taprace_v2.h @@ -0,0 +1,72 @@ +/////////////////////////////////////////////////////////////////////////////// +// GameSpy ATLAS Competition System Header File +// +// NOTE: This is an auto-generated file, do not edit this file directly. +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _ATLAS_TAPRACE_V2_H_ +#define _ATLAS_TAPRACE_V2_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +extern int ATLAS_GET_KEY(char* keyName); +extern char* ATLAS_GET_KEY_NAME(int keyId); +extern int ATLAS_GET_STAT(char* statName); +extern char* ATLAS_GET_STAT_NAME(int statId); +extern int ATLAS_GET_STAT_PAGE_BY_ID(int statId); +extern int ATLAS_GET_STAT_PAGE_BY_NAME(char* statName); + +#define ATLAS_RULE_SET_VERSION 2 + +// KEYS +// Use these key ID's to report match data for your game. + +#define RACE_TIME 1 // [TYPE: int] [DESC: Player's race time for the match (milliseconds).] +#define KEY_NICK 2 // [TYPE: string] [DESC: Player's nickname to report] +#define KEY_LATITUDE 3 // [TYPE: float] +#define KEY_LONGITUDE 4 // [TYPE: float] +#define KEY_MULTI_PLAY 5 // [TYPE: int] [DESC: Key for setting multi-player match stats. Set to 1 if yes; otherwise zero] +#define KEY_SINGLE_PLAY 6 // [TYPE: int] [DESC: Key for setting single player game stats. Set to 1 if yes; otherwise zero] +#define SP_RACE_TIME 7 // [TYPE: int] [DESC: Player's race time for the match (milliseconds).] +#define KEY_RACE_DATE_TIME 8 // [TYPE: string] [DESC: Date/Time of last single play or match for a player] + +/////////////////////////////////////////////////////////////////////////////// + +// STATS +// Use these stat ID's to query aggregate statistics for your game. + +#define AVERAGE_RACE_TIME 6 // [TYPE: float] [DESC: Player's average race time per match (milliseconds/match).] +#define BEST_RACE_TIME 3 // [TYPE: int] [DESC: Player's career best match race time (milliseconds).] +#define CAREER_DISCONNECTS 10 // [TYPE: int] [DESC: Player's total number of times match disconnected.] +#define CAREER_DRAWS 12 // [TYPE: int] [DESC: Player's total number of draws (tied matches).] +#define CAREER_LONGEST_DRAW_STREAK 16 // [TYPE: int] [DESC: Player's longest streak of matches tied] +#define CAREER_LONGEST_LOSS_STREAK 15 // [TYPE: int] [DESC: Player's longest streak of matches loss] +#define CAREER_LONGEST_WIN_STREAK 14 // [TYPE: int] [DESC: Player's longest streak of matches won] +#define CAREER_LOSSES 2 // [TYPE: int] [DESC: Player's number of matches loss] +#define CAREER_WINS 1 // [TYPE: int] [DESC: Player's number of matches won] +#define CURRENT_DRAW_STREAK 13 // [TYPE: int] [DESC: Player's current streak of matches tied] +#define CURRENT_LOSS_STREAK 8 // [TYPE: int] [DESC: Player's current streak of matches loss] +#define CURRENT_WIN_STREAK 7 // [TYPE: int] [DESC: Player's current streak of matches won] +#define DISCONNECT_RATE 11 // [TYPE: float] [DESC: Player's disconnect rate (disconnects/matches).] +#define LAST_TIME_PLAYED 26 // [TYPE: string] [DESC: Date/Time of last single play or match for a player] +#define LATITUDE 19 // [TYPE: float] [DESC: Latitude for Player's location after a single or multiplayer game] +#define LONGITUDE 20 // [TYPE: float] [DESC: Longitude for a Player's location after a single or multiplayer game] +#define NICK 18 // [TYPE: string] [DESC: Player's nickname for leaderboard] +#define SP_AVERAGE_RACE_TIME 24 // [TYPE: float] [DESC: Player's average race time per single plays (milliseconds/play).] +#define SP_BEST_RACE_TIME 21 // [TYPE: int] [DESC: Player's career best match race time (milliseconds).] +#define SP_TOTAL_PLAYS 23 // [TYPE: int] [DESC: Total number of single plays that were completed] +#define SP_TOTAL_RACE_TIME 25 // [TYPE: int] [DESC: Player's total race time for all simgle plays (milliseconds).] +#define SP_WORST_RACE_TIME 22 // [TYPE: int] [DESC: Player's career worst single play time (milliseconds).] +#define TOTAL_COMPLETE_MATCHES 17 // [TYPE: int] [DESC: Total number of matches where the game went to completion (all win/loss/draw results).] +#define TOTAL_MATCHES 5 // [TYPE: int] [DESC: Total number of matches that were completed] +#define TOTAL_RACE_TIME 9 // [TYPE: int] [DESC: Player's total race time for all matches (milliseconds).] +#define WORST_RACE_TIME 4 // [TYPE: int] [DESC: Player's career worst match time (milliseconds).] + + +#ifdef __cplusplus +} +#endif + +#endif // _ATLAS_TAPRACE_V2_H_ diff --git a/Samples/TapRace/iphone/main.mm b/Samples/TapRace/iphone/main.mm new file mode 100644 index 00000000..39fd26f8 --- /dev/null +++ b/Samples/TapRace/iphone/main.mm @@ -0,0 +1,19 @@ +// Copyright Notice: This file is part of the GameSpy SDK designed and +// developed by GameSpy Industries. Copyright (c) 2009 GameSpy Industries, Inc. + +#import +#import "sake/sakeRequest.h" + +int main(int argc, char* argv[]) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + // code to wake up 3G + NSURL * url = [[NSURL alloc] initWithString:[NSString stringWithCString:"http://www.g1a2m3e4s5p6y.com"]]; + [NSData dataWithContentsOfURL:url]; + [url release]; + + int ret = UIApplicationMain(argc, argv, nil, nil); + [pool release]; + return ret; +} \ No newline at end of file