summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/SCsub9
-rw-r--r--modules/arkit/arkit.gdip18
-rw-r--r--modules/arkit/arkit_module.cpp (renamed from modules/arkit/register_types.cpp)4
-rw-r--r--modules/arkit/arkit_module.h (renamed from modules/arkit/register_types.h)2
-rw-r--r--modules/camera/SCsub12
-rw-r--r--modules/camera/config.py2
-rw-r--r--modules/camera/register_types.cpp6
-rw-r--r--modules/camera_iphone/SCsub15
-rw-r--r--modules/camera_iphone/camera.gdip18
-rw-r--r--modules/camera_iphone/camera_ios.h (renamed from modules/camera/camera_ios.h)0
-rw-r--r--modules/camera_iphone/camera_ios.mm (renamed from modules/camera/camera_ios.mm)0
-rw-r--r--modules/camera_iphone/camera_module.cpp40
-rw-r--r--modules/camera_iphone/camera_module.h32
-rw-r--r--modules/camera_iphone/config.py6
-rw-r--r--modules/gamecenter/SCsub15
-rw-r--r--modules/gamecenter/config.py6
-rw-r--r--modules/gamecenter/game_center.h71
-rw-r--r--modules/gamecenter/game_center.mm380
-rw-r--r--modules/gamecenter/game_center_delegate.h35
-rw-r--r--modules/gamecenter/game_center_delegate.mm45
-rw-r--r--modules/gamecenter/game_center_module.cpp48
-rw-r--r--modules/gamecenter/game_center_module.h32
-rw-r--r--modules/gamecenter/gamecenter.gdip17
-rw-r--r--modules/icloud/SCsub15
-rw-r--r--modules/icloud/config.py6
-rw-r--r--modules/icloud/icloud.gdip17
-rw-r--r--modules/icloud/icloud.h60
-rw-r--r--modules/icloud/icloud.mm345
-rw-r--r--modules/icloud/icloud_module.cpp48
-rw-r--r--modules/icloud/icloud_module.h32
-rw-r--r--modules/inappstore/SCsub15
-rw-r--r--modules/inappstore/config.py6
-rw-r--r--modules/inappstore/in_app_store.h77
-rw-r--r--modules/inappstore/in_app_store.mm411
-rw-r--r--modules/inappstore/in_app_store_module.cpp48
-rw-r--r--modules/inappstore/in_app_store_module.h32
-rw-r--r--modules/inappstore/inappstore.gdip17
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]