diff --git a/plugins/gamecenter/README.md b/plugins/gamecenter/README.md index 12648a9..5b3d69b 100644 --- a/plugins/gamecenter/README.md +++ b/plugins/gamecenter/README.md @@ -16,6 +16,10 @@ `request_achievement_descriptions()` - Downloads the achievement descriptions from iOS `GameCenter`. Generates new event with `achievement_descriptions` type. `show_game_center(Dictionary screen_dictionary)` - Displays Game Center information of your game. Generates new event with `show_game_center` type when information screen closes. `request_identity_verification_signature()` - Creates a signature for a third-party server to authenticate the local player. Generates new event with `identity_verification_signature` type. +`save_game_data(Dictionary save_dictionary)` - Save data to iCloud. Generates new event with `save_game_data` type. +`fetch_saved_games()` - Fetch saved games. Generates new event with `fetch_saved_games` type containing an array of `saved_games`. To load one of the games' data, call `load_data` and wait for the event with `saved_game_loaded` type. +`delete_saved_games(String name)` - Delete saved games that match the specified name. Generates new event with `delete_saved_games` type. +`resolve_conflicting_saved_games(Dictionary resolve_conflict_dictionary)` - Resolve conflicting saved games by passing an array of `saved_games` and the `data` that resolves the conflict. Call this if you receive an event of type `conflicting_saved_games`. Generates new event with `resolve_conflicting_saved_games` type. ## Properties diff --git a/plugins/gamecenter/game_center.h b/plugins/gamecenter/game_center.h index 9d1ac97..2db4690 100644 --- a/plugins/gamecenter/game_center.h +++ b/plugins/gamecenter/game_center.h @@ -35,10 +35,14 @@ #if VERSION_MAJOR == 4 #include "core/object/class_db.h" +typedef PackedByteArray GodotByteArray; #else #include "core/object.h" +typedef PoolByteArray GodotByteArray; #endif +class GameCenterSavedGame; + class GameCenter : public Object { GDCLASS(GameCenter, Object); @@ -64,7 +68,15 @@ class GameCenter : public Object { Error show_game_center(Dictionary p_params); Error request_identity_verification_signature(); + Error save_game_data(Dictionary p_params); + Error fetch_saved_games(); + Error delete_saved_games(String p_name); + Error resolve_conflicting_saved_games(Dictionary p_params); + void game_center_closed(); + void game_center_saved_game_loaded(GameCenterSavedGame *saved_game, const GodotByteArray& data, int64_t error_code, const char *error_description); + void player_has_conflicting_saved_games(const Array& saved_games); + void player_did_modify_saved_game(GameCenterSavedGame *saved_game); int get_pending_event_count(); Variant pop_pending_event(); diff --git a/plugins/gamecenter/game_center.mm b/plugins/gamecenter/game_center.mm index f027ab7..5de1c74 100644 --- a/plugins/gamecenter/game_center.mm +++ b/plugins/gamecenter/game_center.mm @@ -31,6 +31,7 @@ #include "game_center.h" #import "game_center_delegate.h" +#import "game_center_saved_game.h" #if VERSION_MAJOR == 4 #import "platform/ios/app_delegate.h" @@ -42,14 +43,22 @@ #import +static void *_get_ptr(const GodotByteArray& arr); + #if VERSION_MAJOR == 4 typedef PackedStringArray GodotStringArray; typedef PackedInt32Array GodotIntArray; typedef PackedFloat32Array GodotFloatArray; +static void *_get_ptr(const GodotByteArray& arr) { + return (void *) arr.ptr(); +} #else typedef PoolStringArray GodotStringArray; typedef PoolIntArray GodotIntArray; typedef PoolRealArray GodotFloatArray; +static void *_get_ptr(const GodotByteArray& arr) { + return (void *) arr.read().ptr(); +} #endif GameCenter *GameCenter::instance = NULL; @@ -66,6 +75,11 @@ ClassDB::bind_method(D_METHOD("request_achievement_descriptions"), &GameCenter::request_achievement_descriptions); ClassDB::bind_method(D_METHOD("show_game_center"), &GameCenter::show_game_center); ClassDB::bind_method(D_METHOD("request_identity_verification_signature"), &GameCenter::request_identity_verification_signature); + + ClassDB::bind_method(D_METHOD("save_game_data"), &GameCenter::save_game_data); + ClassDB::bind_method(D_METHOD("fetch_saved_games"), &GameCenter::fetch_saved_games); + ClassDB::bind_method(D_METHOD("delete_saved_games", "name"), &GameCenter::delete_saved_games); + ClassDB::bind_method(D_METHOD("resolve_conflicting_saved_games"), &GameCenter::resolve_conflicting_saved_games); ClassDB::bind_method(D_METHOD("get_pending_event_count"), &GameCenter::get_pending_event_count); ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); @@ -367,6 +381,118 @@ return OK; }; +Error GameCenter::save_game_data(Dictionary p_params) { + ERR_FAIL_COND_V(![GKLocalPlayer instancesRespondToSelector:@selector(saveGameData:withName:completionHandler:)], ERR_UNAVAILABLE); + ERR_FAIL_COND_V(!p_params.has("name") || !p_params.has("data"), ERR_INVALID_PARAMETER); + + String name = p_params["name"]; + GodotByteArray data = p_params["data"]; + + NSString *nsname = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; + NSData *nsdata = [[NSData alloc] initWithBytes:_get_ptr(data) length:data.size()]; + [GKLocalPlayer.localPlayer saveGameData:nsdata withName:nsname completionHandler:^(GKSavedGame * _Nullable savedGame, NSError * _Nullable error) { + Dictionary ret; + ret["type"] = "save_game_data"; + ret["name"] = name; + if (savedGame) { + ret["result"] = "ok"; + ret["saved_game"] = memnew(GameCenterSavedGame(savedGame)); + } + else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + ret["error_description"] = [error.localizedDescription UTF8String]; + } + pending_events.push_back(ret); + }]; + + return OK; +} + +Error GameCenter::fetch_saved_games() { + ERR_FAIL_COND_V(![GKLocalPlayer instancesRespondToSelector:@selector(fetchSavedGamesWithCompletionHandler:)], ERR_UNAVAILABLE); + + [GKLocalPlayer.localPlayer fetchSavedGamesWithCompletionHandler:^(NSArray * _Nullable savedGames, NSError * _Nullable error) { + Dictionary ret; + ret["type"] = "fetch_saved_games"; + if (savedGames) { + ret["result"] = "ok"; + Array array; + for (GKSavedGame *savedGame in savedGames) { + array.append(memnew(GameCenterSavedGame(savedGame))); + } + ret["saved_games"] = array; + } + else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + ret["error_description"] = [error.localizedDescription UTF8String]; + } + pending_events.push_back(ret); + }]; + + return OK; +} + +Error GameCenter::delete_saved_games(String p_name) { + ERR_FAIL_COND_V(![GKLocalPlayer instancesRespondToSelector:@selector(deleteSavedGamesWithName:completionHandler:)], ERR_UNAVAILABLE); + + NSString *nsname = [[NSString alloc] initWithUTF8String:p_name.utf8().get_data()]; + [GKLocalPlayer.localPlayer deleteSavedGamesWithName:nsname completionHandler:^(NSError * _Nullable error) { + Dictionary ret; + ret["type"] = "delete_saved_games"; + ret["name"] = p_name; + if (!error) { + ret["result"] = "ok"; + } + else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + ret["error_description"] = [error.localizedDescription UTF8String]; + } + pending_events.push_back(ret); + }]; + + return OK; +} + +Error GameCenter::resolve_conflicting_saved_games(Dictionary p_params) { + ERR_FAIL_COND_V(![GKLocalPlayer instancesRespondToSelector:@selector(resolveConflictingSavedGames:withData:completionHandler:)], ERR_UNAVAILABLE); + ERR_FAIL_COND_V(!p_params.has("saved_games") || !p_params.has("data"), ERR_INVALID_PARAMETER); + + Array saved_games = p_params["saved_games"]; + GodotByteArray data = p_params["data"]; + + NSMutableArray *nssaved_games = [[NSMutableArray alloc] init]; + for (int i = 0; i < saved_games.size(); i++) { + if (GameCenterSavedGame *saved_game = Object::cast_to(saved_games[i])) { + [nssaved_games addObject:saved_game->get_saved_game()]; + } + } + + NSData *nsdata = [[NSData alloc] initWithBytes:_get_ptr(data) length:data.size()]; + [GKLocalPlayer.localPlayer resolveConflictingSavedGames:nssaved_games withData:nsdata completionHandler:^(NSArray * _Nullable savedGames, NSError * _Nullable error) { + Dictionary ret; + ret["type"] = "resolve_conflicting_saved_games"; + if (savedGames) { + ret["result"] = "ok"; + Array array; + for (GKSavedGame *savedGame in savedGames) { + array.append(memnew(GameCenterSavedGame(savedGame))); + } + ret["saved_games"] = array; + } + else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + ret["error_description"] = [error.localizedDescription UTF8String]; + } + pending_events.push_back(ret); + }]; + + return OK; +} + void GameCenter::game_center_closed() { Dictionary ret; ret["type"] = "show_game_center"; @@ -374,6 +500,39 @@ pending_events.push_back(ret); } +void GameCenter::game_center_saved_game_loaded(GameCenterSavedGame *saved_game, const GodotByteArray& data, int64_t error_code, const char *error_description) { + Dictionary ret; + ret["type"] = "saved_game_loaded"; + ret["name"] = saved_game->get_name(); + ret["saved_game"] = saved_game; + if (error_code == 0) { + ret["result"] = "ok"; + ret["data"] = data; + } + else { + ret["result"] = "error"; + ret["error_code"] = error_code; + ret["error_description"] = error_description; + } + pending_events.push_back(ret); +} + +void GameCenter::player_has_conflicting_saved_games(const Array& saved_games) { + Dictionary ret; + ret["type"] = "conflicting_saved_games"; + ret["result"] = "ok"; + ret["saved_games"] = saved_games; + pending_events.push_back(ret); +} + +void GameCenter::player_did_modify_saved_game(GameCenterSavedGame *saved_game) { + Dictionary ret; + ret["type"] = "player_did_modify_saved_game"; + ret["result"] = "ok"; + ret["saved_game"] = saved_game; + pending_events.push_back(ret); +} + int GameCenter::get_pending_event_count() { return pending_events.size(); }; @@ -395,10 +554,12 @@ authenticated = false; gameCenterDelegate = [[GodotGameCenterDelegate alloc] init]; + [GKLocalPlayer.localPlayer registerListener:gameCenterDelegate]; }; GameCenter::~GameCenter() { if (gameCenterDelegate) { + [GKLocalPlayer.localPlayer unregisterListener:gameCenterDelegate]; gameCenterDelegate = nil; } } diff --git a/plugins/gamecenter/game_center_delegate.h b/plugins/gamecenter/game_center_delegate.h index ef1d2ae..c3752de 100644 --- a/plugins/gamecenter/game_center_delegate.h +++ b/plugins/gamecenter/game_center_delegate.h @@ -30,6 +30,6 @@ #import -@interface GodotGameCenterDelegate : NSObject +@interface GodotGameCenterDelegate : NSObject @end diff --git a/plugins/gamecenter/game_center_delegate.mm b/plugins/gamecenter/game_center_delegate.mm index 6e20db5..9841df4 100644 --- a/plugins/gamecenter/game_center_delegate.mm +++ b/plugins/gamecenter/game_center_delegate.mm @@ -31,6 +31,7 @@ #import "game_center_delegate.h" #include "game_center.h" +#include "game_center_saved_game.h" @implementation GodotGameCenterDelegate @@ -42,4 +43,20 @@ - (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCent [gameCenterViewController dismissViewControllerAnimated:YES completion:nil]; } +- (void)player:(GKPlayer *)player hasConflictingSavedGames:(NSArray *)savedGames { + if (GameCenter::get_singleton()) { + Array gsaved_games; + for (GKSavedGame *savedGame in savedGames) { + gsaved_games.append(memnew(GameCenterSavedGame(savedGame))); + } + GameCenter::get_singleton()->player_has_conflicting_saved_games(gsaved_games); + } +} + +- (void)player:(GKPlayer *)player didModifySavedGame:(GKSavedGame *)savedGame { + if (GameCenter::get_singleton()) { + GameCenter::get_singleton()->player_did_modify_saved_game(memnew(GameCenterSavedGame(savedGame))); + } +} + @end diff --git a/plugins/gamecenter/game_center_saved_game.h b/plugins/gamecenter/game_center_saved_game.h new file mode 100644 index 0000000..bd19d7f --- /dev/null +++ b/plugins/gamecenter/game_center_saved_game.h @@ -0,0 +1,69 @@ +/*************************************************************************/ +/* game_center_saved_game.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GAME_CENTER_SAVED_GAME_H +#define GAME_CENTER_SAVED_GAME_H + +#include "core/version.h" + +#if VERSION_MAJOR == 4 +#include "core/object/ref_counted.h" +#else +#include "core/reference.h" +typedef Reference RefCounted; +#endif + +@class GKSavedGame; + +class GameCenterSavedGame : public RefCounted { + + GDCLASS(GameCenterSavedGame, RefCounted); + + static void _bind_methods(); + + GKSavedGame *saved_game; + +public: + String get_name() const; + int64_t get_modification_date() const; + String get_device_name() const; + bool is_current_device() const; + + GKSavedGame *get_saved_game() const; + + void load_data(); + + virtual String to_string() override; + + GameCenterSavedGame(GKSavedGame *saved_game); + ~GameCenterSavedGame(); +}; + +#endif // GAME_CENTER_SAVED_GAME_H \ No newline at end of file diff --git a/plugins/gamecenter/game_center_saved_game.mm b/plugins/gamecenter/game_center_saved_game.mm new file mode 100644 index 0000000..e31adfd --- /dev/null +++ b/plugins/gamecenter/game_center_saved_game.mm @@ -0,0 +1,122 @@ +/*************************************************************************/ +/* game_center_saved_game.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "game_center_saved_game.h" + +#include "game_center.h" + +#import +#import + +static void *_get_ptrw(GodotByteArray& arr); + +#if VERSION_MAJOR == 4 +typedef PackedByteArray GodotByteArray; +static void *_get_ptrw(GodotByteArray& arr) { + return (void *) arr.ptrw(); +} +#else +typedef PoolByteArray GodotByteArray; +static void *_get_ptrw(GodotByteArray& arr) { + return (void *) arr.write().ptr(); +} +#endif + +void GameCenterSavedGame::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_name"), &GameCenterSavedGame::get_name); + ClassDB::bind_method(D_METHOD("get_modification_date"), &GameCenterSavedGame::get_modification_date); + ClassDB::bind_method(D_METHOD("get_device_name"), &GameCenterSavedGame::get_device_name); + ClassDB::bind_method(D_METHOD("is_current_device"), &GameCenterSavedGame::is_current_device); + ClassDB::bind_method(D_METHOD("load_data"), &GameCenterSavedGame::load_data); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "name"), "", "get_name"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "modification_date"), "", "get_modification_date"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "device_name"), "", "get_device_name"); +}; + +String GameCenterSavedGame::get_name() const { + return [saved_game.name UTF8String]; +} + +int64_t GameCenterSavedGame::get_modification_date() const { + return saved_game.modificationDate.timeIntervalSince1970; +} + +String GameCenterSavedGame::get_device_name() const { + return [saved_game.deviceName UTF8String]; +} + +bool GameCenterSavedGame::is_current_device() const { + if ([saved_game.deviceName isEqualToString:UIDevice.currentDevice.name]) { + return true; + } + + // Fallback to checking device model, in case running on iOS 16+ and app doesn't have com.apple.developer.device-information.user-assigned-device-name entitlement. + // Note that running iPad apps on macOS via Catalyst will return "iPad..." here and could be a false negative. I don't really know how to handle that case properly. + struct utsname systemInfo; + uname(&systemInfo); + NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + return [saved_game.deviceName isEqualToString:deviceModel]; +} + +GKSavedGame *GameCenterSavedGame::get_saved_game() const { + return saved_game; +} + +void GameCenterSavedGame::load_data() { + // make sure a reference is held while the async operation is in progress + reference(); + + [saved_game loadDataWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) { + if (GameCenter::get_singleton()) { + GodotByteArray gdata; + if (data.bytes) { + gdata.resize(data.length); + memcpy(_get_ptrw(gdata), data.bytes, data.length); + } + GameCenter::get_singleton()->game_center_saved_game_loaded(this, gdata, error.code, [error.localizedDescription UTF8String]); + } + + // release the reference held for the async operation + unreference(); + }]; +} + +String GameCenterSavedGame::to_string() { + return vformat("", get_name(), get_modification_date(), get_device_name()); +} + +GameCenterSavedGame::GameCenterSavedGame(GKSavedGame *saved_game) : saved_game(saved_game) {} + +GameCenterSavedGame::~GameCenterSavedGame() { + if (saved_game) { + saved_game = nil; + } +}