diff options
author | RĂ©mi Verschelde <rverschelde@gmail.com> | 2020-11-10 15:43:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-10 15:43:50 +0100 |
commit | 1626cfd9372925df9fb2216e6612481eb3a91886 (patch) | |
tree | f13863a32849e737862c97b74d767a2fe24df437 /modules | |
parent | 05fe063ea966406f0ab13393f77f76515491c408 (diff) | |
parent | 1f2f477e1ee28f9fac8b7fc17bbd6d4707c4e5e8 (diff) |
Merge pull request #41230 from naithar/feature/pluggable-ios-modules
[iOS] [4.0] iOS Plugins
Diffstat (limited to 'modules')
37 files changed, 1920 insertions, 22 deletions
diff --git a/modules/SCsub b/modules/SCsub index edfc4ed9c6..24598f4b28 100644 --- a/modules/SCsub +++ b/modules/SCsub @@ -47,7 +47,14 @@ for name, path in env.module_list.items(): # Some modules are not linked automatically but can be enabled optionally # on iOS, so we handle those specially. - if env["platform"] == "iphone" and name in ["arkit", "camera"]: + if env["platform"] == "iphone" and name in [ + "arkit", + "camera", + "camera_iphone", + "gamecenter", + "inappstore", + "icloud", + ]: continue lib = env_modules.add_library("module_%s" % name, env.modules_sources) diff --git a/modules/arkit/arkit.gdip b/modules/arkit/arkit.gdip new file mode 100644 index 0000000000..22c0a07e26 --- /dev/null +++ b/modules/arkit/arkit.gdip @@ -0,0 +1,18 @@ +[config] +name="ARKit" +binary="arkit_lib.a" + +initialization="register_arkit_types" +deinitialization="unregister_arkit_types" + +[dependencies] +linked=[] +embedded=[] +system=["AVFoundation.framework", "ARKit.framework"] + +capabilities=["arkit"] + +files=[] + +[plist] +NSCameraUsageDescription="Device camera is used for some functionality" diff --git a/modules/arkit/register_types.cpp b/modules/arkit/arkit_module.cpp index 91069ab364..87ee3b87a5 100644 --- a/modules/arkit/register_types.cpp +++ b/modules/arkit/arkit_module.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* register_types.cpp */ +/* arkit_module.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "register_types.h" +#include "arkit_module.h" #include "arkit_interface.h" diff --git a/modules/arkit/register_types.h b/modules/arkit/arkit_module.h index f8939a1e3f..8aa8175ed5 100644 --- a/modules/arkit/register_types.h +++ b/modules/arkit/arkit_module.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* register_types.h */ +/* arkit_module.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ diff --git a/modules/camera/SCsub b/modules/camera/SCsub index 631a65bde2..de97724d09 100644 --- a/modules/camera/SCsub +++ b/modules/camera/SCsub @@ -5,17 +5,7 @@ Import("env_modules") env_camera = env_modules.Clone() -if env["platform"] == "iphone": - # (iOS) Enable module support - env_camera.Append(CCFLAGS=["-fmodules", "-fcxx-modules"]) - - # (iOS) Build as separate static library - modules_sources = [] - env_camera.add_source_files(modules_sources, "register_types.cpp") - env_camera.add_source_files(modules_sources, "camera_ios.mm") - mod_lib = env_modules.add_library("#bin/libgodot_camera_module" + env["LIBSUFFIX"], modules_sources) - -elif env["platform"] == "windows": +if env["platform"] == "windows": env_camera.add_source_files(env.modules_sources, "register_types.cpp") env_camera.add_source_files(env.modules_sources, "camera_win.cpp") diff --git a/modules/camera/config.py b/modules/camera/config.py index 87d7542741..8a22751aa7 100644 --- a/modules/camera/config.py +++ b/modules/camera/config.py @@ -1,5 +1,5 @@ def can_build(env, platform): - return platform == "iphone" or platform == "osx" or platform == "windows" + return platform == "osx" or platform == "windows" def configure(env): diff --git a/modules/camera/register_types.cpp b/modules/camera/register_types.cpp index 3b6c916914..9479310a13 100644 --- a/modules/camera/register_types.cpp +++ b/modules/camera/register_types.cpp @@ -33,9 +33,6 @@ #if defined(WINDOWS_ENABLED) #include "camera_win.h" #endif -#if defined(IPHONE_ENABLED) -#include "camera_ios.h" -#endif #if defined(OSX_ENABLED) #include "camera_osx.h" #endif @@ -44,9 +41,6 @@ void register_camera_types() { #if defined(WINDOWS_ENABLED) CameraServer::make_default<CameraWindows>(); #endif -#if defined(IPHONE_ENABLED) - CameraServer::make_default<CameraIOS>(); -#endif #if defined(OSX_ENABLED) CameraServer::make_default<CameraOSX>(); #endif diff --git a/modules/camera_iphone/SCsub b/modules/camera_iphone/SCsub new file mode 100644 index 0000000000..0a37d9a6f5 --- /dev/null +++ b/modules/camera_iphone/SCsub @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_camera = env_modules.Clone() + +# (iOS) Enable module support +env_camera.Append(CCFLAGS=["-fmodules", "-fcxx-modules"]) + +# (iOS) Build as separate static library +modules_sources = [] +env_camera.add_source_files(modules_sources, "*.cpp") +env_camera.add_source_files(modules_sources, "*.mm") +mod_lib = env_modules.add_library("#bin/libgodot_camera_module" + env["LIBSUFFIX"], modules_sources) diff --git a/modules/camera_iphone/camera.gdip b/modules/camera_iphone/camera.gdip new file mode 100644 index 0000000000..09017b8d27 --- /dev/null +++ b/modules/camera_iphone/camera.gdip @@ -0,0 +1,18 @@ +[config] +name="Camera" +binary="camera_lib.a" + +initialization="register_camera_types" +deinitialization="unregister_camera_types" + +[dependencies] +linked=[] +embedded=[] +system=["AVFoundation.framework"] + +capabilities=[] + +files=[] + +[plist] +NSCameraUsageDescription="Device camera is used for some functionality" diff --git a/modules/camera/camera_ios.h b/modules/camera_iphone/camera_ios.h index 7da43e4851..7da43e4851 100644 --- a/modules/camera/camera_ios.h +++ b/modules/camera_iphone/camera_ios.h diff --git a/modules/camera/camera_ios.mm b/modules/camera_iphone/camera_ios.mm index e4cb928805..e4cb928805 100644 --- a/modules/camera/camera_ios.mm +++ b/modules/camera_iphone/camera_ios.mm diff --git a/modules/camera_iphone/camera_module.cpp b/modules/camera_iphone/camera_module.cpp new file mode 100644 index 0000000000..f3d00be204 --- /dev/null +++ b/modules/camera_iphone/camera_module.cpp @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* camera_module.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "camera_module.h" + +#include "camera_ios.h" + +void register_camera_types() { + CameraServer::make_default<CameraIOS>(); +} + +void unregister_camera_types() { +} diff --git a/modules/camera_iphone/camera_module.h b/modules/camera_iphone/camera_module.h new file mode 100644 index 0000000000..d123071a70 --- /dev/null +++ b/modules/camera_iphone/camera_module.h @@ -0,0 +1,32 @@ +/*************************************************************************/ +/* camera_module.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +void register_camera_types(); +void unregister_camera_types(); diff --git a/modules/camera_iphone/config.py b/modules/camera_iphone/config.py new file mode 100644 index 0000000000..e68603fc93 --- /dev/null +++ b/modules/camera_iphone/config.py @@ -0,0 +1,6 @@ +def can_build(env, platform): + return platform == "iphone" + + +def configure(env): + pass diff --git a/modules/gamecenter/SCsub b/modules/gamecenter/SCsub new file mode 100644 index 0000000000..72fbf7ab0e --- /dev/null +++ b/modules/gamecenter/SCsub @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_gamecenter = env_modules.Clone() + +# (iOS) Enable module support +env_gamecenter.Append(CCFLAGS=["-fmodules", "-fcxx-modules"]) + +# (iOS) Build as separate static library +modules_sources = [] +env_gamecenter.add_source_files(modules_sources, "*.cpp") +env_gamecenter.add_source_files(modules_sources, "*.mm") +mod_lib = env_modules.add_library("#bin/libgodot_gamecenter_module" + env["LIBSUFFIX"], modules_sources) diff --git a/modules/gamecenter/config.py b/modules/gamecenter/config.py new file mode 100644 index 0000000000..e68603fc93 --- /dev/null +++ b/modules/gamecenter/config.py @@ -0,0 +1,6 @@ +def can_build(env, platform): + return platform == "iphone" + + +def configure(env): + pass diff --git a/modules/gamecenter/game_center.h b/modules/gamecenter/game_center.h new file mode 100644 index 0000000000..76fd295460 --- /dev/null +++ b/modules/gamecenter/game_center.h @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* game_center.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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_H +#define GAME_CENTER_H + +#include "core/object/class_db.h" + +class GameCenter : public Object { + GDCLASS(GameCenter, Object); + + static GameCenter *instance; + static void _bind_methods(); + + List<Variant> pending_events; + + bool authenticated; + + void return_connect_error(const char *p_error_description); + +public: + Error authenticate(); + bool is_authenticated(); + + Error post_score(Dictionary p_score); + Error award_achievement(Dictionary p_params); + void reset_achievements(); + void request_achievements(); + void request_achievement_descriptions(); + Error show_game_center(Dictionary p_params); + Error request_identity_verification_signature(); + + void game_center_closed(); + + int get_pending_event_count(); + Variant pop_pending_event(); + + static GameCenter *get_singleton(); + + GameCenter(); + ~GameCenter(); +}; + +#endif diff --git a/modules/gamecenter/game_center.mm b/modules/gamecenter/game_center.mm new file mode 100644 index 0000000000..114f639a32 --- /dev/null +++ b/modules/gamecenter/game_center.mm @@ -0,0 +1,380 @@ +/*************************************************************************/ +/* game_center.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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.h" +#import "platform/iphone/app_delegate.h" + +#import "game_center_delegate.h" +#import "platform/iphone/view_controller.h" +#import <GameKit/GameKit.h> + +GameCenter *GameCenter::instance = NULL; +GodotGameCenterDelegate *gameCenterDelegate = nil; + +void GameCenter::_bind_methods() { + ClassDB::bind_method(D_METHOD("authenticate"), &GameCenter::authenticate); + ClassDB::bind_method(D_METHOD("is_authenticated"), &GameCenter::is_authenticated); + + ClassDB::bind_method(D_METHOD("post_score"), &GameCenter::post_score); + ClassDB::bind_method(D_METHOD("award_achievement", "achievement"), &GameCenter::award_achievement); + ClassDB::bind_method(D_METHOD("reset_achievements"), &GameCenter::reset_achievements); + ClassDB::bind_method(D_METHOD("request_achievements"), &GameCenter::request_achievements); + 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("get_pending_event_count"), &GameCenter::get_pending_event_count); + ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); +}; + +Error GameCenter::authenticate() { + //if this class isn't available, game center isn't implemented + if ((NSClassFromString(@"GKLocalPlayer")) == nil) { + return ERR_UNAVAILABLE; + } + + GKLocalPlayer *player = [GKLocalPlayer localPlayer]; + ERR_FAIL_COND_V(![player respondsToSelector:@selector(authenticateHandler)], ERR_UNAVAILABLE); + + UIViewController *root_controller = [[UIApplication sharedApplication] delegate].window.rootViewController; + ERR_FAIL_COND_V(!root_controller, FAILED); + + // This handler is called several times. First when the view needs to be shown, then again + // after the view is cancelled or the user logs in. Or if the user's already logged in, it's + // called just once to confirm they're authenticated. This is why no result needs to be specified + // in the presentViewController phase. In this case, more calls to this function will follow. + _weakify(root_controller); + _weakify(player); + player.authenticateHandler = (^(UIViewController *controller, NSError *error) { + _strongify(root_controller); + _strongify(player); + + if (controller) { + [root_controller presentViewController:controller animated:YES completion:nil]; + } else { + Dictionary ret; + ret["type"] = "authentication"; + if (player.isAuthenticated) { + ret["result"] = "ok"; + if (@available(iOS 13, *)) { + ret["player_id"] = [player.teamPlayerID UTF8String]; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + ret["player_id"] = [player.playerID UTF8String]; +#endif + } + + GameCenter::get_singleton()->authenticated = true; + } else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + ret["error_description"] = [error.localizedDescription UTF8String]; + GameCenter::get_singleton()->authenticated = false; + }; + + pending_events.push_back(ret); + }; + }); + + return OK; +}; + +bool GameCenter::is_authenticated() { + return authenticated; +}; + +Error GameCenter::post_score(Dictionary p_score) { + ERR_FAIL_COND_V(!p_score.has("score") || !p_score.has("category"), ERR_INVALID_PARAMETER); + float score = p_score["score"]; + String category = p_score["category"]; + + NSString *cat_str = [[NSString alloc] initWithUTF8String:category.utf8().get_data()]; + GKScore *reporter = [[GKScore alloc] initWithLeaderboardIdentifier:cat_str]; + reporter.value = score; + + ERR_FAIL_COND_V([GKScore respondsToSelector:@selector(reportScores)], ERR_UNAVAILABLE); + + [GKScore reportScores:@[ reporter ] + withCompletionHandler:^(NSError *error) { + Dictionary ret; + ret["type"] = "post_score"; + if (error == nil) { + 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::award_achievement(Dictionary p_params) { + ERR_FAIL_COND_V(!p_params.has("name") || !p_params.has("progress"), ERR_INVALID_PARAMETER); + String name = p_params["name"]; + float progress = p_params["progress"]; + + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; + GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:name_str]; + ERR_FAIL_COND_V(!achievement, FAILED); + + ERR_FAIL_COND_V([GKAchievement respondsToSelector:@selector(reportAchievements)], ERR_UNAVAILABLE); + + achievement.percentComplete = progress; + achievement.showsCompletionBanner = NO; + if (p_params.has("show_completion_banner")) { + achievement.showsCompletionBanner = p_params["show_completion_banner"] ? YES : NO; + } + + [GKAchievement reportAchievements:@[ achievement ] + withCompletionHandler:^(NSError *error) { + Dictionary ret; + ret["type"] = "award_achievement"; + if (error == nil) { + ret["result"] = "ok"; + } else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + }; + + pending_events.push_back(ret); + }]; + + return OK; +}; + +void GameCenter::request_achievement_descriptions() { + [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray *descriptions, NSError *error) { + Dictionary ret; + ret["type"] = "achievement_descriptions"; + if (error == nil) { + ret["result"] = "ok"; + PackedStringArray names; + PackedStringArray titles; + PackedStringArray unachieved_descriptions; + PackedStringArray achieved_descriptions; + PackedInt32Array maximum_points; + Array hidden; + Array replayable; + + for (NSUInteger i = 0; i < [descriptions count]; i++) { + GKAchievementDescription *description = [descriptions objectAtIndex:i]; + + const char *str = [description.identifier UTF8String]; + names.push_back(String::utf8(str != NULL ? str : "")); + + str = [description.title UTF8String]; + titles.push_back(String::utf8(str != NULL ? str : "")); + + str = [description.unachievedDescription UTF8String]; + unachieved_descriptions.push_back(String::utf8(str != NULL ? str : "")); + + str = [description.achievedDescription UTF8String]; + achieved_descriptions.push_back(String::utf8(str != NULL ? str : "")); + + maximum_points.push_back(description.maximumPoints); + + hidden.push_back(description.hidden == YES); + + replayable.push_back(description.replayable == YES); + } + + ret["names"] = names; + ret["titles"] = titles; + ret["unachieved_descriptions"] = unachieved_descriptions; + ret["achieved_descriptions"] = achieved_descriptions; + ret["maximum_points"] = maximum_points; + ret["hidden"] = hidden; + ret["replayable"] = replayable; + + } else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + }; + + pending_events.push_back(ret); + }]; +}; + +void GameCenter::request_achievements() { + [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error) { + Dictionary ret; + ret["type"] = "achievements"; + if (error == nil) { + ret["result"] = "ok"; + PackedStringArray names; + PackedFloat32Array percentages; + + for (NSUInteger i = 0; i < [achievements count]; i++) { + GKAchievement *achievement = [achievements objectAtIndex:i]; + const char *str = [achievement.identifier UTF8String]; + names.push_back(String::utf8(str != NULL ? str : "")); + + percentages.push_back(achievement.percentComplete); + } + + ret["names"] = names; + ret["progress"] = percentages; + + } else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + }; + + pending_events.push_back(ret); + }]; +}; + +void GameCenter::reset_achievements() { + [GKAchievement resetAchievementsWithCompletionHandler:^(NSError *error) { + Dictionary ret; + ret["type"] = "reset_achievements"; + if (error == nil) { + ret["result"] = "ok"; + } else { + ret["result"] = "error"; + ret["error_code"] = (int64_t)error.code; + }; + + pending_events.push_back(ret); + }]; +}; + +Error GameCenter::show_game_center(Dictionary p_params) { + ERR_FAIL_COND_V(!NSProtocolFromString(@"GKGameCenterControllerDelegate"), FAILED); + + GKGameCenterViewControllerState view_state = GKGameCenterViewControllerStateDefault; + if (p_params.has("view")) { + String view_name = p_params["view"]; + if (view_name == "default") { + view_state = GKGameCenterViewControllerStateDefault; + } else if (view_name == "leaderboards") { + view_state = GKGameCenterViewControllerStateLeaderboards; + } else if (view_name == "achievements") { + view_state = GKGameCenterViewControllerStateAchievements; + } else if (view_name == "challenges") { + view_state = GKGameCenterViewControllerStateChallenges; + } else { + return ERR_INVALID_PARAMETER; + } + } + + GKGameCenterViewController *controller = [[GKGameCenterViewController alloc] init]; + ERR_FAIL_COND_V(!controller, FAILED); + + UIViewController *root_controller = [[UIApplication sharedApplication] delegate].window.rootViewController; + ERR_FAIL_COND_V(!root_controller, FAILED); + + controller.gameCenterDelegate = gameCenterDelegate; + controller.viewState = view_state; + if (view_state == GKGameCenterViewControllerStateLeaderboards) { + controller.leaderboardIdentifier = nil; + if (p_params.has("leaderboard_name")) { + String name = p_params["leaderboard_name"]; + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; + controller.leaderboardIdentifier = name_str; + } + } + + [root_controller presentViewController:controller animated:YES completion:nil]; + + return OK; +}; + +Error GameCenter::request_identity_verification_signature() { + ERR_FAIL_COND_V(!is_authenticated(), ERR_UNAUTHORIZED); + + GKLocalPlayer *player = [GKLocalPlayer localPlayer]; + [player generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) { + Dictionary ret; + ret["type"] = "identity_verification_signature"; + if (error == nil) { + ret["result"] = "ok"; + ret["public_key_url"] = [publicKeyUrl.absoluteString UTF8String]; + ret["signature"] = [[signature base64EncodedStringWithOptions:0] UTF8String]; + ret["salt"] = [[salt base64EncodedStringWithOptions:0] UTF8String]; + ret["timestamp"] = timestamp; + if (@available(iOS 13, *)) { + ret["player_id"] = [player.teamPlayerID UTF8String]; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + ret["player_id"] = [player.playerID UTF8String]; +#endif + } + } 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"; + ret["result"] = "ok"; + pending_events.push_back(ret); +} + +int GameCenter::get_pending_event_count() { + return pending_events.size(); +}; + +Variant GameCenter::pop_pending_event() { + Variant front = pending_events.front()->get(); + pending_events.pop_front(); + + return front; +}; + +GameCenter *GameCenter::get_singleton() { + return instance; +}; + +GameCenter::GameCenter() { + ERR_FAIL_COND(instance != NULL); + instance = this; + authenticated = false; + + gameCenterDelegate = [[GodotGameCenterDelegate alloc] init]; +}; + +GameCenter::~GameCenter() { + if (gameCenterDelegate) { + gameCenterDelegate = nil; + } +} diff --git a/modules/gamecenter/game_center_delegate.h b/modules/gamecenter/game_center_delegate.h new file mode 100644 index 0000000000..1b7025f915 --- /dev/null +++ b/modules/gamecenter/game_center_delegate.h @@ -0,0 +1,35 @@ +/*************************************************************************/ +/* game_center_delegate.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#import <GameKit/GameKit.h> + +@interface GodotGameCenterDelegate : NSObject <GKGameCenterControllerDelegate> + +@end diff --git a/modules/gamecenter/game_center_delegate.mm b/modules/gamecenter/game_center_delegate.mm new file mode 100644 index 0000000000..9a10c439c6 --- /dev/null +++ b/modules/gamecenter/game_center_delegate.mm @@ -0,0 +1,45 @@ +/*************************************************************************/ +/* game_center_delegate.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +#import "game_center_delegate.h" + +#include "game_center.h" + +@implementation GodotGameCenterDelegate + +- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController { + //[gameCenterViewController dismissViewControllerAnimated:YES completion:^{GameCenter::get_singleton()->game_center_closed();}];//version for signaling when overlay is completely gone + if (GameCenter::get_singleton()) { + GameCenter::get_singleton()->game_center_closed(); + } + [gameCenterViewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/modules/gamecenter/game_center_module.cpp b/modules/gamecenter/game_center_module.cpp new file mode 100644 index 0000000000..6c5157345f --- /dev/null +++ b/modules/gamecenter/game_center_module.cpp @@ -0,0 +1,48 @@ +/*************************************************************************/ +/* game_center_module.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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_module.h" + +#include "core/config/engine.h" + +#include "game_center.h" + +GameCenter *game_center; + +void register_gamecenter_types() { + game_center = memnew(GameCenter); + Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); +} + +void unregister_gamecenter_types() { + if (game_center) { + memdelete(game_center); + } +} diff --git a/modules/gamecenter/game_center_module.h b/modules/gamecenter/game_center_module.h new file mode 100644 index 0000000000..8da3ae02ee --- /dev/null +++ b/modules/gamecenter/game_center_module.h @@ -0,0 +1,32 @@ +/*************************************************************************/ +/* game_center_module.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +void register_gamecenter_types(); +void unregister_gamecenter_types(); diff --git a/modules/gamecenter/gamecenter.gdip b/modules/gamecenter/gamecenter.gdip new file mode 100644 index 0000000000..eb44effbdd --- /dev/null +++ b/modules/gamecenter/gamecenter.gdip @@ -0,0 +1,17 @@ +[config] +name="GameCenter" +binary="gamecenter_lib.a" + +initialization="register_gamecenter_types" +deinitialization="unregister_gamecenter_types" + +[dependencies] +linked=[] +embedded=[] +system=["GameKit.framework"] + +capabilities=["gamekit"] + +files=[] + +[plist] diff --git a/modules/icloud/SCsub b/modules/icloud/SCsub new file mode 100644 index 0000000000..805a484600 --- /dev/null +++ b/modules/icloud/SCsub @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_icloud = env_modules.Clone() + +# (iOS) Enable module support +env_icloud.Append(CCFLAGS=["-fmodules", "-fcxx-modules"]) + +# (iOS) Build as separate static library +modules_sources = [] +env_icloud.add_source_files(modules_sources, "*.cpp") +env_icloud.add_source_files(modules_sources, "*.mm") +mod_lib = env_modules.add_library("#bin/libgodot_icloud_module" + env["LIBSUFFIX"], modules_sources) diff --git a/modules/icloud/config.py b/modules/icloud/config.py new file mode 100644 index 0000000000..e68603fc93 --- /dev/null +++ b/modules/icloud/config.py @@ -0,0 +1,6 @@ +def can_build(env, platform): + return platform == "iphone" + + +def configure(env): + pass diff --git a/modules/icloud/icloud.gdip b/modules/icloud/icloud.gdip new file mode 100644 index 0000000000..9f81be8a34 --- /dev/null +++ b/modules/icloud/icloud.gdip @@ -0,0 +1,17 @@ +[config] +name="iCloud" +binary="icloud_lib.a" + +initialization="register_icloud_types" +deinitialization="unregister_icloud_types" + +[dependencies] +linked=[] +embedded=[] +system=[] + +capabilities=[] + +files=[] + +[plist] diff --git a/modules/icloud/icloud.h b/modules/icloud/icloud.h new file mode 100644 index 0000000000..35eede0bf9 --- /dev/null +++ b/modules/icloud/icloud.h @@ -0,0 +1,60 @@ +/*************************************************************************/ +/* icloud.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 ICLOUD_H +#define ICLOUD_H + +#include "core/object/class_db.h" + +class ICloud : public Object { + GDCLASS(ICloud, Object); + + static ICloud *instance; + static void _bind_methods(); + + List<Variant> pending_events; + +public: + Error remove_key(String p_param); + Array set_key_values(Dictionary p_params); + Variant get_key_value(String p_param); + Error synchronize_key_values(); + Variant get_all_key_values(); + + int get_pending_event_count(); + Variant pop_pending_event(); + + static ICloud *get_singleton(); + + ICloud(); + ~ICloud(); +}; + +#endif diff --git a/modules/icloud/icloud.mm b/modules/icloud/icloud.mm new file mode 100644 index 0000000000..8a8ddbefe9 --- /dev/null +++ b/modules/icloud/icloud.mm @@ -0,0 +1,345 @@ +/*************************************************************************/ +/* icloud.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "icloud.h" + +#import "platform/iphone/app_delegate.h" + +#import <Foundation/Foundation.h> + +ICloud *ICloud::instance = NULL; + +void ICloud::_bind_methods() { + ClassDB::bind_method(D_METHOD("remove_key"), &ICloud::remove_key); + + ClassDB::bind_method(D_METHOD("set_key_values"), &ICloud::set_key_values); + ClassDB::bind_method(D_METHOD("get_key_value"), &ICloud::get_key_value); + + ClassDB::bind_method(D_METHOD("synchronize_key_values"), &ICloud::synchronize_key_values); + ClassDB::bind_method(D_METHOD("get_all_key_values"), &ICloud::get_all_key_values); + + ClassDB::bind_method(D_METHOD("get_pending_event_count"), &ICloud::get_pending_event_count); + ClassDB::bind_method(D_METHOD("pop_pending_event"), &ICloud::pop_pending_event); +}; + +int ICloud::get_pending_event_count() { + return pending_events.size(); +}; + +Variant ICloud::pop_pending_event() { + Variant front = pending_events.front()->get(); + pending_events.pop_front(); + + return front; +}; + +ICloud *ICloud::get_singleton() { + return instance; +}; + +//convert from apple's abstract type to godot's abstract type.... +Variant nsobject_to_variant(NSObject *object) { + if ([object isKindOfClass:[NSString class]]) { + const char *str = [(NSString *)object UTF8String]; + return String::utf8(str != NULL ? str : ""); + } else if ([object isKindOfClass:[NSData class]]) { + PackedByteArray ret; + NSData *data = (NSData *)object; + if ([data length] > 0) { + ret.resize([data length]); + { + // PackedByteArray::Write w = ret.write(); + copymem((void *)ret.ptr(), [data bytes], [data length]); + } + } + return ret; + } else if ([object isKindOfClass:[NSArray class]]) { + Array result; + NSArray *array = (NSArray *)object; + for (NSUInteger i = 0; i < [array count]; ++i) { + NSObject *value = [array objectAtIndex:i]; + result.push_back(nsobject_to_variant(value)); + } + return result; + } else if ([object isKindOfClass:[NSDictionary class]]) { + Dictionary result; + NSDictionary *dic = (NSDictionary *)object; + + NSArray *keys = [dic allKeys]; + int count = [keys count]; + for (int i = 0; i < count; ++i) { + NSObject *k = [keys objectAtIndex:i]; + NSObject *v = [dic objectForKey:k]; + + result[nsobject_to_variant(k)] = nsobject_to_variant(v); + } + return result; + } else if ([object isKindOfClass:[NSNumber class]]) { + //Every type except numbers can reliably identify its type. The following is comparing to the *internal* representation, which isn't guaranteed to match the type that was used to create it, and is not advised, particularly when dealing with potential platform differences (ie, 32/64 bit) + //To avoid errors, we'll cast as broadly as possible, and only return int or float. + //bool, char, int, uint, longlong -> int + //float, double -> float + NSNumber *num = (NSNumber *)object; + if (strcmp([num objCType], @encode(BOOL)) == 0) { + return Variant((int)[num boolValue]); + } else if (strcmp([num objCType], @encode(char)) == 0) { + return Variant((int)[num charValue]); + } else if (strcmp([num objCType], @encode(int)) == 0) { + return Variant([num intValue]); + } else if (strcmp([num objCType], @encode(unsigned int)) == 0) { + return Variant((int)[num unsignedIntValue]); + } else if (strcmp([num objCType], @encode(long long)) == 0) { + return Variant((int)[num longValue]); + } else if (strcmp([num objCType], @encode(float)) == 0) { + return Variant([num floatValue]); + } else if (strcmp([num objCType], @encode(double)) == 0) { + return Variant((float)[num doubleValue]); + } else { + return Variant(); + } + } else if ([object isKindOfClass:[NSDate class]]) { + //this is a type that icloud supports...but how did you submit it in the first place? + //I guess this is a type that *might* show up, if you were, say, trying to make your game + //compatible with existing cloud data written by another engine's version of your game + WARN_PRINT("NSDate unsupported, returning null Variant"); + return Variant(); + } else if ([object isKindOfClass:[NSNull class]] or object == nil) { + return Variant(); + } else { + WARN_PRINT("Trying to convert unknown NSObject type to Variant"); + return Variant(); + } +} + +NSObject *variant_to_nsobject(Variant v) { + if (v.get_type() == Variant::STRING) { + return [[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()]; + } else if (v.get_type() == Variant::FLOAT) { + return [NSNumber numberWithDouble:(double)v]; + } else if (v.get_type() == Variant::INT) { + return [NSNumber numberWithLongLong:(long)(int)v]; + } else if (v.get_type() == Variant::BOOL) { + return [NSNumber numberWithBool:BOOL((bool)v)]; + } else if (v.get_type() == Variant::DICTIONARY) { + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + Dictionary dic = v; + Array keys = dic.keys(); + for (int i = 0; i < keys.size(); ++i) { + NSString *key = [[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()]; + NSObject *value = variant_to_nsobject(dic[keys[i]]); + + if (key == NULL || value == NULL) { + return NULL; + } + + [result setObject:value forKey:key]; + } + return result; + } else if (v.get_type() == Variant::ARRAY) { + NSMutableArray *result = [[NSMutableArray alloc] init]; + Array arr = v; + for (int i = 0; i < arr.size(); ++i) { + NSObject *value = variant_to_nsobject(arr[i]); + if (value == NULL) { + //trying to add something unsupported to the array. cancel the whole array + return NULL; + } + [result addObject:value]; + } + return result; + } else if (v.get_type() == Variant::PACKED_BYTE_ARRAY) { + PackedByteArray arr = v; + // PackedByteArray::Read r = arr.read(); + NSData *result = [NSData dataWithBytes:arr.ptr() length:arr.size()]; + return result; + } + WARN_PRINT(String("Could not add unsupported type to iCloud: '" + Variant::get_type_name(v.get_type()) + "'").utf8().get_data()); + return NULL; +} + +Error ICloud::remove_key(String p_param) { + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; + + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + + if (![[store dictionaryRepresentation] objectForKey:key]) { + return ERR_INVALID_PARAMETER; + } + + [store removeObjectForKey:key]; + return OK; +} + +//return an array of the keys that could not be set +Array ICloud::set_key_values(Dictionary p_params) { + Array keys = p_params.keys(); + + Array error_keys; + + for (int i = 0; i < keys.size(); ++i) { + String variant_key = keys[i]; + Variant variant_value = p_params[variant_key]; + + NSString *key = [[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()]; + if (key == NULL) { + error_keys.push_back(variant_key); + continue; + } + + NSObject *value = variant_to_nsobject(variant_value); + + if (value == NULL) { + error_keys.push_back(variant_key); + continue; + } + + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + [store setObject:value forKey:key]; + } + + return error_keys; +} + +Variant ICloud::get_key_value(String p_param) { + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + + if (![[store dictionaryRepresentation] objectForKey:key]) { + return Variant(); + } + + Variant result = nsobject_to_variant([[store dictionaryRepresentation] objectForKey:key]); + + return result; +} + +Variant ICloud::get_all_key_values() { + Dictionary result; + + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + NSDictionary *store_dictionary = [store dictionaryRepresentation]; + + NSArray *keys = [store_dictionary allKeys]; + int count = [keys count]; + for (int i = 0; i < count; ++i) { + NSString *k = [keys objectAtIndex:i]; + NSObject *v = [store_dictionary objectForKey:k]; + + const char *str = [k UTF8String]; + if (str != NULL) { + result[String::utf8(str)] = nsobject_to_variant(v); + } + } + + return result; +} + +Error ICloud::synchronize_key_values() { + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + BOOL result = [store synchronize]; + if (result == YES) { + return OK; + } else { + return FAILED; + } +} + +/* +Error ICloud::initial_sync() { + //you sometimes have to write something to the store to get it to download new data. go apple! + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + if ([store boolForKey:@"isb"]) + { + [store setBool:NO forKey:@"isb"]; + } + else + { + [store setBool:YES forKey:@"isb"]; + } + return synchronize(); +} + +*/ +ICloud::ICloud() { + ERR_FAIL_COND(instance != NULL); + instance = this; + //connected = false; + + [[NSNotificationCenter defaultCenter] + addObserverForName:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:[NSUbiquitousKeyValueStore defaultStore] + queue:nil + usingBlock:^(NSNotification *notification) { + NSDictionary *userInfo = [notification userInfo]; + NSInteger change = [[userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey] integerValue]; + + Dictionary ret; + ret["type"] = "key_value_changed"; + + //PackedStringArray result_keys; + //Array result_values; + Dictionary keyValues; + String reason = ""; + + if (change == NSUbiquitousKeyValueStoreServerChange) { + reason = "server"; + } else if (change == NSUbiquitousKeyValueStoreInitialSyncChange) { + reason = "initial_sync"; + } else if (change == NSUbiquitousKeyValueStoreQuotaViolationChange) { + reason = "quota_violation"; + } else if (change == NSUbiquitousKeyValueStoreAccountChange) { + reason = "account"; + } + + ret["reason"] = reason; + + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + + NSArray *keys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; + for (NSString *key in keys) { + const char *str = [key UTF8String]; + if (str == NULL) { + continue; + } + + NSObject *object = [store objectForKey:key]; + + //figure out what kind of object it is + Variant value = nsobject_to_variant(object); + + keyValues[String::utf8(str)] = value; + } + + ret["changed_values"] = keyValues; + pending_events.push_back(ret); + }]; +} + +ICloud::~ICloud() {} diff --git a/modules/icloud/icloud_module.cpp b/modules/icloud/icloud_module.cpp new file mode 100644 index 0000000000..43fdc7d45e --- /dev/null +++ b/modules/icloud/icloud_module.cpp @@ -0,0 +1,48 @@ +/*************************************************************************/ +/* icloud_module.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "icloud_module.h" + +#include "core/config/engine.h" + +#include "icloud.h" + +ICloud *icloud; + +void register_icloud_types() { + icloud = memnew(ICloud); + Engine::get_singleton()->add_singleton(Engine::Singleton("ICloud", icloud)); +} + +void unregister_icloud_types() { + if (icloud) { + memdelete(icloud); + } +} diff --git a/modules/icloud/icloud_module.h b/modules/icloud/icloud_module.h new file mode 100644 index 0000000000..7fd057525e --- /dev/null +++ b/modules/icloud/icloud_module.h @@ -0,0 +1,32 @@ +/*************************************************************************/ +/* icloud_module.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +void register_icloud_types(); +void unregister_icloud_types(); diff --git a/modules/inappstore/SCsub b/modules/inappstore/SCsub new file mode 100644 index 0000000000..cee6a256d5 --- /dev/null +++ b/modules/inappstore/SCsub @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_inappstore = env_modules.Clone() + +# (iOS) Enable module support +env_inappstore.Append(CCFLAGS=["-fmodules", "-fcxx-modules"]) + +# (iOS) Build as separate static library +modules_sources = [] +env_inappstore.add_source_files(modules_sources, "*.cpp") +env_inappstore.add_source_files(modules_sources, "*.mm") +mod_lib = env_modules.add_library("#bin/libgodot_inappstore_module" + env["LIBSUFFIX"], modules_sources) diff --git a/modules/inappstore/config.py b/modules/inappstore/config.py new file mode 100644 index 0000000000..e68603fc93 --- /dev/null +++ b/modules/inappstore/config.py @@ -0,0 +1,6 @@ +def can_build(env, platform): + return platform == "iphone" + + +def configure(env): + pass diff --git a/modules/inappstore/in_app_store.h b/modules/inappstore/in_app_store.h new file mode 100644 index 0000000000..c8e5d17cec --- /dev/null +++ b/modules/inappstore/in_app_store.h @@ -0,0 +1,77 @@ +/*************************************************************************/ +/* in_app_store.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 IN_APP_STORE_H +#define IN_APP_STORE_H + +#include "core/object/class_db.h" + +#ifdef __OBJC__ +@class GodotProductsDelegate; +@class GodotTransactionsObserver; + +typedef GodotProductsDelegate InAppStoreProductDelegate; +typedef GodotTransactionsObserver InAppStoreTransactionObserver; +#else +typedef void InAppStoreProductDelegate; +typedef void InAppStoreTransactionObserver; +#endif + +class InAppStore : public Object { + GDCLASS(InAppStore, Object); + + static InAppStore *instance; + static void _bind_methods(); + + List<Variant> pending_events; + + InAppStoreProductDelegate *products_request_delegate; + InAppStoreTransactionObserver *transactions_observer; + +public: + Error request_product_info(Dictionary p_params); + Error restore_purchases(); + Error purchase(Dictionary p_params); + + int get_pending_event_count(); + Variant pop_pending_event(); + void finish_transaction(String product_id); + void set_auto_finish_transaction(bool b); + + void _post_event(Variant p_event); + void _record_purchase(String product_id); + + static InAppStore *get_singleton(); + + InAppStore(); + ~InAppStore(); +}; + +#endif diff --git a/modules/inappstore/in_app_store.mm b/modules/inappstore/in_app_store.mm new file mode 100644 index 0000000000..62977318c1 --- /dev/null +++ b/modules/inappstore/in_app_store.mm @@ -0,0 +1,411 @@ +/*************************************************************************/ +/* in_app_store.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "in_app_store.h" + +#import <Foundation/Foundation.h> +#import <StoreKit/StoreKit.h> + +InAppStore *InAppStore::instance = NULL; + +@interface SKProduct (LocalizedPrice) + +@property(nonatomic, readonly) NSString *localizedPrice; + +@end + +//----------------------------------// +// SKProduct extension +//----------------------------------// +@implementation SKProduct (LocalizedPrice) + +- (NSString *)localizedPrice { + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + [numberFormatter setLocale:self.priceLocale]; + NSString *formattedString = [numberFormatter stringFromNumber:self.price]; + return formattedString; +} + +@end + +@interface GodotProductsDelegate : NSObject <SKProductsRequestDelegate> + +@property(nonatomic, strong) NSMutableArray *loadedProducts; +@property(nonatomic, strong) NSMutableArray *pendingRequests; + +- (void)performRequestWithProductIDs:(NSSet *)productIDs; +- (Error)purchaseProductWithProductID:(NSString *)productID; +- (void)reset; + +@end + +@implementation GodotProductsDelegate + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.loadedProducts = [NSMutableArray new]; + self.pendingRequests = [NSMutableArray new]; +} + +- (void)performRequestWithProductIDs:(NSSet *)productIDs { + SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs]; + + request.delegate = self; + [self.pendingRequests addObject:request]; + [request start]; +} + +- (Error)purchaseProductWithProductID:(NSString *)productID { + SKProduct *product = nil; + + NSLog(@"searching for product!"); + + if (self.loadedProducts) { + for (SKProduct *storedProduct in self.loadedProducts) { + if ([storedProduct.productIdentifier isEqualToString:productID]) { + product = storedProduct; + break; + } + } + } + + if (!product) { + return ERR_INVALID_PARAMETER; + } + + NSLog(@"product found!"); + + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + + NSLog(@"purchase sent!"); + + return OK; +} + +- (void)reset { + [self.loadedProducts removeAllObjects]; + [self.pendingRequests removeAllObjects]; +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + [self.pendingRequests removeObject:request]; + + Dictionary ret; + ret["type"] = "product_info"; + ret["result"] = "error"; + ret["error"] = String::utf8([error.localizedDescription UTF8String]); + + InAppStore::get_singleton()->_post_event(ret); +} + +- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { + [self.pendingRequests removeObject:request]; + + NSArray *products = response.products; + [self.loadedProducts addObjectsFromArray:products]; + + Dictionary ret; + ret["type"] = "product_info"; + ret["result"] = "ok"; + PackedStringArray titles; + PackedStringArray descriptions; + PackedFloat32Array prices; + PackedStringArray ids; + PackedStringArray localized_prices; + PackedStringArray currency_codes; + + for (NSUInteger i = 0; i < [products count]; i++) { + SKProduct *product = [products objectAtIndex:i]; + + const char *str = [product.localizedTitle UTF8String]; + titles.push_back(String::utf8(str != NULL ? str : "")); + + str = [product.localizedDescription UTF8String]; + descriptions.push_back(String::utf8(str != NULL ? str : "")); + prices.push_back([product.price doubleValue]); + ids.push_back(String::utf8([product.productIdentifier UTF8String])); + localized_prices.push_back(String::utf8([product.localizedPrice UTF8String])); + currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String])); + } + + ret["titles"] = titles; + ret["descriptions"] = descriptions; + ret["prices"] = prices; + ret["ids"] = ids; + ret["localized_prices"] = localized_prices; + ret["currency_codes"] = currency_codes; + + PackedStringArray invalid_ids; + + for (NSString *ipid in response.invalidProductIdentifiers) { + invalid_ids.push_back(String::utf8([ipid UTF8String])); + } + + ret["invalid_ids"] = invalid_ids; + + InAppStore::get_singleton()->_post_event(ret); +} + +@end + +@interface GodotTransactionsObserver : NSObject <SKPaymentTransactionObserver> + +@property(nonatomic, assign) BOOL shouldAutoFinishTransactions; +@property(nonatomic, strong) NSMutableDictionary *pendingTransactions; + +- (void)finishTransactionWithProductID:(NSString *)productID; +- (void)reset; + +@end + +@implementation GodotTransactionsObserver + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.pendingTransactions = [NSMutableDictionary new]; +} + +- (void)finishTransactionWithProductID:(NSString *)productID { + SKPaymentTransaction *transaction = self.pendingTransactions[productID]; + + if (transaction) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } + + self.pendingTransactions[productID] = nil; +} + +- (void)reset { + [self.pendingTransactions removeAllObjects]; +} + +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { + printf("transactions updated!\n"); + for (SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + case SKPaymentTransactionStatePurchased: { + printf("status purchased!\n"); + String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); + String transactionId = String::utf8([transaction.transactionIdentifier UTF8String]); + InAppStore::get_singleton()->_record_purchase(pid); + Dictionary ret; + ret["type"] = "purchase"; + ret["result"] = "ok"; + ret["product_id"] = pid; + ret["transaction_id"] = transactionId; + + NSData *receipt = nil; + int sdk_version = [[[UIDevice currentDevice] systemVersion] intValue]; + + NSBundle *bundle = [NSBundle mainBundle]; + // Get the transaction receipt file path location in the app bundle. + NSURL *receiptFileURL = [bundle appStoreReceiptURL]; + + // Read in the contents of the transaction file. + receipt = [NSData dataWithContentsOfURL:receiptFileURL]; + + NSString *receipt_to_send = nil; + + if (receipt != nil) { + receipt_to_send = [receipt base64EncodedStringWithOptions:0]; + } + Dictionary receipt_ret; + receipt_ret["receipt"] = String::utf8(receipt_to_send != nil ? [receipt_to_send UTF8String] : ""); + receipt_ret["sdk"] = sdk_version; + ret["receipt"] = receipt_ret; + + InAppStore::get_singleton()->_post_event(ret); + + if (self.shouldAutoFinishTransactions) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } else { + self.pendingTransactions[transaction.payment.productIdentifier] = transaction; + } + + } break; + case SKPaymentTransactionStateFailed: { + printf("status transaction failed!\n"); + String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); + Dictionary ret; + ret["type"] = "purchase"; + ret["result"] = "error"; + ret["product_id"] = pid; + ret["error"] = String::utf8([transaction.error.localizedDescription UTF8String]); + InAppStore::get_singleton()->_post_event(ret); + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } break; + case SKPaymentTransactionStateRestored: { + printf("status transaction restored!\n"); + String pid = String::utf8([transaction.originalTransaction.payment.productIdentifier UTF8String]); + InAppStore::get_singleton()->_record_purchase(pid); + Dictionary ret; + ret["type"] = "restore"; + ret["result"] = "ok"; + ret["product_id"] = pid; + InAppStore::get_singleton()->_post_event(ret); + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } break; + default: { + printf("status default %i!\n", (int)transaction.transactionState); + } break; + } + } +} + +@end + +void InAppStore::_bind_methods() { + ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); + ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); + ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); + + ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); + ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); + ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); + ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); +} + +Error InAppStore::request_product_info(Dictionary p_params) { + ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER); + + PackedStringArray pids = p_params["product_ids"]; + printf("************ request product info! %i\n", pids.size()); + + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:pids.size()]; + for (int i = 0; i < pids.size(); i++) { + printf("******** adding %s to product list\n", pids[i].utf8().get_data()); + NSString *pid = [[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()]; + [array addObject:pid]; + }; + + NSSet *products = [[NSSet alloc] initWithArray:array]; + + [products_request_delegate performRequestWithProductIDs:products]; + + return OK; +} + +Error InAppStore::restore_purchases() { + printf("restoring purchases!\n"); + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + + return OK; +} + +Error InAppStore::purchase(Dictionary p_params) { + ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); + if (![SKPaymentQueue canMakePayments]) { + return ERR_UNAVAILABLE; + } + + printf("purchasing!\n"); + Dictionary params = p_params; + ERR_FAIL_COND_V(!params.has("product_id"), ERR_INVALID_PARAMETER); + + NSString *pid = [[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()]; + + return [products_request_delegate purchaseProductWithProductID:pid]; +} + +int InAppStore::get_pending_event_count() { + return pending_events.size(); +} + +Variant InAppStore::pop_pending_event() { + Variant front = pending_events.front()->get(); + pending_events.pop_front(); + + return front; +} + +void InAppStore::_post_event(Variant p_event) { + pending_events.push_back(p_event); +} + +void InAppStore::_record_purchase(String product_id) { + String skey = "purchased/" + product_id; + NSString *key = [[NSString alloc] initWithUTF8String:skey.utf8().get_data()]; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +InAppStore *InAppStore::get_singleton() { + return instance; +} + +InAppStore::InAppStore() { + ERR_FAIL_COND(instance != NULL); + instance = this; + + products_request_delegate = [[GodotProductsDelegate alloc] init]; + transactions_observer = [[GodotTransactionsObserver alloc] init]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:transactions_observer]; +} + +void InAppStore::finish_transaction(String product_id) { + NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding]; + + [transactions_observer finishTransactionWithProductID:prod_id]; +} + +void InAppStore::set_auto_finish_transaction(bool b) { + transactions_observer.shouldAutoFinishTransactions = b; +} + +InAppStore::~InAppStore() { + [products_request_delegate reset]; + [transactions_observer reset]; + + products_request_delegate = nil; + [[SKPaymentQueue defaultQueue] removeTransactionObserver:transactions_observer]; + transactions_observer = nil; +} diff --git a/modules/inappstore/in_app_store_module.cpp b/modules/inappstore/in_app_store_module.cpp new file mode 100644 index 0000000000..039bdd4f83 --- /dev/null +++ b/modules/inappstore/in_app_store_module.cpp @@ -0,0 +1,48 @@ +/*************************************************************************/ +/* in_app_store_module.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "in_app_store_module.h" + +#include "core/config/engine.h" + +#include "in_app_store.h" + +InAppStore *store_kit; + +void register_inappstore_types() { + store_kit = memnew(InAppStore); + Engine::get_singleton()->add_singleton(Engine::Singleton("InAppStore", store_kit)); +} + +void unregister_inappstore_types() { + if (store_kit) { + memdelete(store_kit); + } +} diff --git a/modules/inappstore/in_app_store_module.h b/modules/inappstore/in_app_store_module.h new file mode 100644 index 0000000000..44673e58bc --- /dev/null +++ b/modules/inappstore/in_app_store_module.h @@ -0,0 +1,32 @@ +/*************************************************************************/ +/* in_app_store_module.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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. */ +/*************************************************************************/ + +void register_inappstore_types(); +void unregister_inappstore_types(); diff --git a/modules/inappstore/inappstore.gdip b/modules/inappstore/inappstore.gdip new file mode 100644 index 0000000000..7a5efb8ad3 --- /dev/null +++ b/modules/inappstore/inappstore.gdip @@ -0,0 +1,17 @@ +[config] +name="InAppStore" +binary="inappstore_lib.a" + +initialization="register_inappstore_types" +deinitialization="unregister_inappstore_types" + +[dependencies] +linked=[] +embedded=[] +system=["StoreKit.framework"] + +capabilities=[] + +files=[] + +[plist] |