diff --git a/.github/workflows/tvos_builds.yml b/.github/workflows/tvos_builds.yml
new file mode 100644
index 000000000000..631305178f86
--- /dev/null
+++ b/.github/workflows/tvos_builds.yml
@@ -0,0 +1,58 @@
+name: 📺 tvOS Builds
+on: [push, pull_request]
+
+# Global Settings
+env:
+ GODOT_BASE_BRANCH: 3.2
+ SCONSFLAGS: platform=tvos verbose=yes warnings=all werror=yes --jobs=2
+ SCONS_CACHE_LIMIT: 4096
+
+jobs:
+ tvos-template:
+ runs-on: "macos-latest"
+ name: Template (target=release, tools=no)
+
+ steps:
+ - uses: actions/checkout@v2
+
+ # Upload cache on completion and check it out now
+ - name: Load .scons_cache directory
+ id: tvos-template-cache
+ uses: actions/cache@v2
+ with:
+ path: ${{github.workspace}}/.scons_cache/
+ key: ${{github.job}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}-${{github.sha}}
+ restore-keys: |
+ ${{github.job}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}-${{github.sha}}
+ ${{github.job}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}
+ ${{github.job}}-${{env.GODOT_BASE_BRANCH}}
+
+ # Use python 3.x release (works cross platform)
+ - name: Set up Python 3.x
+ uses: actions/setup-python@v2
+ with:
+ # Semantic version range syntax or exact version of a Python version
+ python-version: '3.x'
+ # Optional - x64 or x86 architecture, defaults to x64
+ architecture: 'x64'
+
+ # You can test your matrix by printing the current Python version
+ - name: Configuring Python packages
+ run: |
+ python -c "import sys; print(sys.version)"
+ python -m pip install scons
+ python --version
+ scons --version
+
+ - name: Compilation
+ env:
+ SCONS_CACHE: ${{github.workspace}}/.scons_cache/
+ run: |
+ scons target=release tools=no
+ ls -l bin/
+
+ - uses: actions/upload-artifact@v2
+ with:
+ name: ${{ github.job }}
+ path: bin/*
+ retention-days: 14
diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml
index 4a5507d3e91d..8452c3243393 100644
--- a/doc/classes/EditorExportPlugin.xml
+++ b/doc/classes/EditorExportPlugin.xml
@@ -123,6 +123,71 @@
Adds a static lib from the given [code]path[/code] to the iOS project.
+
+
+
+
+
+
+ Adds an tvOS bundle file from the given [code]path[/code] to the exported project.
+
+
+
+
+
+
+
+
+ Adds a C++ code to the tvOS export. The final code is created from the code appended by each active export plugin.
+
+
+
+
+
+
+
+
+ Adds a dynamic library (*.dylib, *.framework) to Linking Phase in tvOS's Xcode project and embeds it into resulting binary.
+ [b]Note:[/b] For static libraries (*.a) works in same way as [method add_ios_framework].
+ This method should not be used for System libraries as they are already present on the device.
+
+
+
+
+
+
+
+
+ Adds a static library (*.a) or dynamic library (*.dylib, *.framework) to Linking Phase in tvOS's Xcode project.
+
+
+
+
+
+
+
+
+ Adds linker flags for the tvOS export.
+
+
+
+
+
+
+
+
+ Adds content for tvOS Property List files.
+
+
+
+
+
+
+
+
+ Adds a static lib from the given [code]path[/code] to the tvOS project.
+
+
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 901a57883230..5796a59f7098 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -606,6 +606,9 @@
Default delay for touch events. This only affects iOS devices.
+
+ Default delay for end press events. This only affects tvOS devices.
+
Optional name for the 2D physics layer 1.
diff --git a/drivers/gles2/rasterizer_gles2.cpp b/drivers/gles2/rasterizer_gles2.cpp
index e6a237134f23..4a1b9625cf65 100644
--- a/drivers/gles2/rasterizer_gles2.cpp
+++ b/drivers/gles2/rasterizer_gles2.cpp
@@ -65,9 +65,9 @@
#endif
#endif
-#ifndef IPHONE_ENABLED
+#if !defined(IPHONE_ENABLED) && !defined(TVOS_ENABLED)
// We include EGL below to get debug callback on GLES2 platforms,
-// but EGL is not available on iOS.
+// but EGL is not available on tvOS/iOS.
#define CAN_DEBUG
#endif
diff --git a/drivers/gles2/rasterizer_scene_gles2.cpp b/drivers/gles2/rasterizer_scene_gles2.cpp
index f2cbb88d5f20..3a78c8e67b70 100644
--- a/drivers/gles2/rasterizer_scene_gles2.cpp
+++ b/drivers/gles2/rasterizer_scene_gles2.cpp
@@ -44,7 +44,7 @@
#endif
#ifndef GLES_OVER_GL
-#ifdef IPHONE_ENABLED
+#if defined(IPHONE_ENABLED) || defined(TVOS_ENABLED)
#include
//void *glResolveMultisampleFramebufferAPPLE;
@@ -2778,7 +2778,7 @@ void RasterizerSceneGLES2::_post_process(Environment *env, const CameraMatrix &p
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
-#elif IPHONE_ENABLED
+#elif IPHONE_ENABLED || TVOS_ENABLED
glBindFramebuffer(GL_READ_FRAMEBUFFER, storage->frame.current_rt->multisample_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, next_buffer);
@@ -3518,7 +3518,7 @@ void RasterizerSceneGLES2::render_scene(const Transform &p_cam_transform, const
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
-#elif IPHONE_ENABLED
+#elif IPHONE_ENABLED || TVOS_ENABLED
glBindFramebuffer(GL_READ_FRAMEBUFFER, storage->frame.current_rt->multisample_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, storage->frame.current_rt->fbo);
diff --git a/drivers/gles2/rasterizer_storage_gles2.cpp b/drivers/gles2/rasterizer_storage_gles2.cpp
index 57ac5199ba6b..25efcebe9e27 100644
--- a/drivers/gles2/rasterizer_storage_gles2.cpp
+++ b/drivers/gles2/rasterizer_storage_gles2.cpp
@@ -94,7 +94,7 @@ GLuint RasterizerStorageGLES2::system_fbo = 0;
#include // needed to load extensions
#endif
-#ifdef IPHONE_ENABLED
+#if defined(IPHONE_ENABLED) || defined(TVOS_ENABLED)
#include
//void *glRenderbufferStorageMultisampleAPPLE;
@@ -4931,7 +4931,7 @@ void RasterizerStorageGLES2::_render_target_allocate(RenderTarget *rt) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rt->multisample_depth);
-#if defined(GLES_OVER_GL) || defined(IPHONE_ENABLED)
+#if defined(GLES_OVER_GL) || defined(IPHONE_ENABLED) || defined(TVOS_ENABLED)
glGenRenderbuffers(1, &rt->multisample_color);
glBindRenderbuffer(GL_RENDERBUFFER, rt->multisample_color);
@@ -6115,7 +6115,7 @@ void RasterizerStorageGLES2::initialize() {
#ifndef GLES_OVER_GL
//Manually load extensions for android and ios
-#ifdef IPHONE_ENABLED
+#if defined(IPHONE_ENABLED) || defined(TVOS_ENABLED)
// appears that IPhone doesn't need to dlopen TODO: test this rigorously before removing
//void *gles2_lib = dlopen(NULL, RTLD_LAZY);
//glRenderbufferStorageMultisampleAPPLE = dlsym(gles2_lib, "glRenderbufferStorageMultisampleAPPLE");
diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp
index 44ce14b32de8..a8c5a1176464 100644
--- a/drivers/unix/os_unix.cpp
+++ b/drivers/unix/os_unix.cpp
@@ -274,6 +274,8 @@ Error OS_Unix::execute(const String &p_path, const List &p_arguments, bo
// Don't compile this code at all to avoid undefined references.
// Actual virtual call goes to OS_JavaScript.
ERR_FAIL_V(ERR_BUG);
+#elif defined(TVOS_ENABLED)
+ ERR_FAIL_V(ERR_CANT_FORK);
#else
if (p_blocking && r_pipe) {
diff --git a/editor/editor_export.cpp b/editor/editor_export.cpp
index d437d79cd0b6..68923db3c7fc 100644
--- a/editor/editor_export.cpp
+++ b/editor/editor_export.cpp
@@ -587,6 +587,65 @@ Vector EditorExportPlugin::get_ios_project_static_libs() const {
return ios_project_static_libs;
}
+void EditorExportPlugin::add_tvos_framework(const String &p_path) {
+ tvos_frameworks.push_back(p_path);
+}
+
+void EditorExportPlugin::add_tvos_embedded_framework(const String &p_path) {
+ tvos_embedded_frameworks.push_back(p_path);
+}
+
+Vector EditorExportPlugin::get_tvos_frameworks() const {
+ return tvos_frameworks;
+}
+
+Vector EditorExportPlugin::get_tvos_embedded_frameworks() const {
+ return tvos_embedded_frameworks;
+}
+
+void EditorExportPlugin::add_tvos_plist_content(const String &p_plist_content) {
+ tvos_plist_content += p_plist_content + "\n";
+}
+
+String EditorExportPlugin::get_tvos_plist_content() const {
+ return tvos_plist_content;
+}
+
+void EditorExportPlugin::add_tvos_linker_flags(const String &p_flags) {
+ if (tvos_linker_flags.length() > 0) {
+ tvos_linker_flags += ' ';
+ }
+ tvos_linker_flags += p_flags;
+}
+
+String EditorExportPlugin::get_tvos_linker_flags() const {
+ return tvos_linker_flags;
+}
+
+void EditorExportPlugin::add_tvos_bundle_file(const String &p_path) {
+ tvos_bundle_files.push_back(p_path);
+}
+
+Vector EditorExportPlugin::get_tvos_bundle_files() const {
+ return tvos_bundle_files;
+}
+
+void EditorExportPlugin::add_tvos_cpp_code(const String &p_code) {
+ tvos_cpp_code += p_code;
+}
+
+String EditorExportPlugin::get_tvos_cpp_code() const {
+ return tvos_cpp_code;
+}
+
+void EditorExportPlugin::add_tvos_project_static_lib(const String &p_path) {
+ tvos_project_static_libs.push_back(p_path);
+}
+
+Vector EditorExportPlugin::get_tvos_project_static_libs() const {
+ return tvos_project_static_libs;
+}
+
void EditorExportPlugin::_export_file_script(const String &p_path, const String &p_type, const PoolVector &p_features) {
if (get_script_instance()) {
@@ -622,14 +681,24 @@ void EditorExportPlugin::skip() {
void EditorExportPlugin::_bind_methods() {
ClassDB::bind_method(D_METHOD("add_shared_object", "path", "tags"), &EditorExportPlugin::add_shared_object);
- ClassDB::bind_method(D_METHOD("add_ios_project_static_lib", "path"), &EditorExportPlugin::add_ios_project_static_lib);
ClassDB::bind_method(D_METHOD("add_file", "path", "file", "remap"), &EditorExportPlugin::add_file);
+ // iOS specific methods
+ ClassDB::bind_method(D_METHOD("add_ios_project_static_lib", "path"), &EditorExportPlugin::add_ios_project_static_lib);
ClassDB::bind_method(D_METHOD("add_ios_framework", "path"), &EditorExportPlugin::add_ios_framework);
ClassDB::bind_method(D_METHOD("add_ios_embedded_framework", "path"), &EditorExportPlugin::add_ios_embedded_framework);
ClassDB::bind_method(D_METHOD("add_ios_plist_content", "plist_content"), &EditorExportPlugin::add_ios_plist_content);
ClassDB::bind_method(D_METHOD("add_ios_linker_flags", "flags"), &EditorExportPlugin::add_ios_linker_flags);
ClassDB::bind_method(D_METHOD("add_ios_bundle_file", "path"), &EditorExportPlugin::add_ios_bundle_file);
ClassDB::bind_method(D_METHOD("add_ios_cpp_code", "code"), &EditorExportPlugin::add_ios_cpp_code);
+ // tvOS specific methods
+ ClassDB::bind_method(D_METHOD("add_tvos_project_static_lib", "path"), &EditorExportPlugin::add_tvos_project_static_lib);
+ ClassDB::bind_method(D_METHOD("add_tvos_framework", "path"), &EditorExportPlugin::add_tvos_framework);
+ ClassDB::bind_method(D_METHOD("add_tvos_embedded_framework", "path"), &EditorExportPlugin::add_tvos_embedded_framework);
+ ClassDB::bind_method(D_METHOD("add_tvos_plist_content", "plist_content"), &EditorExportPlugin::add_tvos_plist_content);
+ ClassDB::bind_method(D_METHOD("add_tvos_linker_flags", "flags"), &EditorExportPlugin::add_tvos_linker_flags);
+ ClassDB::bind_method(D_METHOD("add_tvos_bundle_file", "path"), &EditorExportPlugin::add_tvos_bundle_file);
+ ClassDB::bind_method(D_METHOD("add_tvos_cpp_code", "code"), &EditorExportPlugin::add_tvos_cpp_code);
+
ClassDB::bind_method(D_METHOD("skip"), &EditorExportPlugin::skip);
BIND_VMETHOD(MethodInfo("_export_file", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::STRING, "type"), PropertyInfo(Variant::POOL_STRING_ARRAY, "features")));
diff --git a/editor/editor_export.h b/editor/editor_export.h
index 00239349a5f8..4b2293e239d4 100644
--- a/editor/editor_export.h
+++ b/editor/editor_export.h
@@ -294,6 +294,14 @@ class EditorExportPlugin : public Reference {
Vector ios_bundle_files;
String ios_cpp_code;
+ Vector tvos_frameworks;
+ Vector tvos_embedded_frameworks;
+ Vector tvos_project_static_libs;
+ String tvos_plist_content;
+ String tvos_linker_flags;
+ Vector tvos_bundle_files;
+ String tvos_cpp_code;
+
_FORCE_INLINE_ void _clear() {
shared_objects.clear();
extra_files.clear();
@@ -307,6 +315,13 @@ class EditorExportPlugin : public Reference {
ios_plist_content = "";
ios_linker_flags = "";
ios_cpp_code = "";
+
+ tvos_frameworks.clear();
+ tvos_embedded_frameworks.clear();
+ tvos_bundle_files.clear();
+ tvos_plist_content = "";
+ tvos_linker_flags = "";
+ tvos_cpp_code = "";
}
void _export_file_script(const String &p_path, const String &p_type, const PoolVector &p_features);
@@ -328,6 +343,14 @@ class EditorExportPlugin : public Reference {
void add_ios_bundle_file(const String &p_path);
void add_ios_cpp_code(const String &p_code);
+ void add_tvos_framework(const String &p_path);
+ void add_tvos_embedded_framework(const String &p_path);
+ void add_tvos_project_static_lib(const String &p_path);
+ void add_tvos_plist_content(const String &p_plist_content);
+ void add_tvos_linker_flags(const String &p_flags);
+ void add_tvos_bundle_file(const String &p_path);
+ void add_tvos_cpp_code(const String &p_code);
+
void skip();
virtual void _export_file(const String &p_path, const String &p_type, const Set &p_features);
@@ -344,6 +367,14 @@ class EditorExportPlugin : public Reference {
Vector get_ios_bundle_files() const;
String get_ios_cpp_code() const;
+ Vector get_tvos_frameworks() const;
+ Vector get_tvos_embedded_frameworks() const;
+ Vector get_tvos_project_static_libs() const;
+ String get_tvos_plist_content() const;
+ String get_tvos_linker_flags() const;
+ Vector get_tvos_bundle_files() const;
+ String get_tvos_cpp_code() const;
+
EditorExportPlugin();
};
diff --git a/main/main.cpp b/main/main.cpp
index 7e310fc091c6..62d2c89fc004 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -1220,6 +1220,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
GLOBAL_DEF("display/window/ios/hide_home_indicator", true);
GLOBAL_DEF("input_devices/pointing/ios/touch_delay", 0.150);
+ GLOBAL_DEF("input_devices/pointing/tvos/press_end_delay", 0.150);
Engine::get_singleton()->set_frame_delay(frame_delay);
diff --git a/main/main_builders.py b/main/main_builders.py
index 6e5a5ceede4f..f3846cb71d85 100644
--- a/main/main_builders.py
+++ b/main/main_builders.py
@@ -110,6 +110,7 @@ def make_default_controller_mappings(target, source, env):
"Mac OS X": "#ifdef OSX_ENABLED",
"Android": "#if defined(__ANDROID__)",
"iOS": "#ifdef IPHONE_ENABLED",
+ "tvOS": "#ifdef TVOS_ENABLED",
"Javascript": "#ifdef JAVASCRIPT_ENABLED",
"UWP": "#ifdef UWP_ENABLED",
}
diff --git a/methods.py b/methods.py
index 8e3b525cb745..f5de5c68821d 100644
--- a/methods.py
+++ b/methods.py
@@ -773,6 +773,10 @@ def get_darwin_sdk_version(platform):
sdk_name = "iphoneos"
elif platform == "iphonesimulator":
sdk_name = "iphonesimulator"
+ elif platform == "tvos":
+ sdk_name = "appletvos"
+ elif platform == "tvossimulator":
+ sdk_name = "appletvsimulator"
else:
raise Exception("Invalid platform argument passed to get_darwin_sdk_version")
@@ -794,6 +798,12 @@ def detect_darwin_sdk_path(platform, env):
elif platform == "iphonesimulator":
sdk_name = "iphonesimulator"
var_name = "IPHONESDK"
+ elif platform == "tvos":
+ sdk_name = "appletvos"
+ var_name = "TVOSSDK"
+ elif platform == "tvossimulator":
+ sdk_name = "appletvsimulator"
+ var_name = "TVOSSDK"
else:
raise Exception("Invalid platform argument passed to detect_darwin_sdk_path")
diff --git a/misc/dist/tvos_xcode/data.pck b/misc/dist/tvos_xcode/data.pck
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.pbxproj b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.pbxproj
new file mode 100644
index 000000000000..f267bdfc7152
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.pbxproj
@@ -0,0 +1,374 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1F1575721F582BE20003B888 /* dylibs in Resources */ = {isa = PBXBuildFile; fileRef = 1F1575711F582BE20003B888 /* dylibs */; };
+ 9088C79E25C98AF800FCAE9A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9088C79D25C98AF800FCAE9A /* Assets.xcassets */; };
+ 9088C7A125C98AF800FCAE9A /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9088C79F25C98AF800FCAE9A /* Launch Screen.storyboard */; };
+ 9088C7A425C98AF800FCAE9A /* dummy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9088C7A325C98AF800FCAE9A /* dummy.cpp */; };
+ 90C4BA0B25C9A59C00CD5FD1 /* $binary.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 90C4BA0A25C9A59C00CD5FD1 /* $binary.a */; };
+ 90C4BA1125C9A60500CD5FD1 /* $binary.pck in Resources */ = {isa = PBXBuildFile; fileRef = 90C4BA1025C9A5FC00CD5FD1 /* $binary.pck */; };
+/* End PBXBuildFile section */
+
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 90A13CD024AA68E500E8464F /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ $pbx_embeded_frameworks
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1F1575711F582BE20003B888 /* dylibs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dylibs; path = "$binary/dylibs"; sourceTree = ""; };
+ 9088C79125C98AF700FCAE9A /* $binary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "$binary.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9088C79D25C98AF800FCAE9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 9088C7A025C98AF800FCAE9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Launch Screen.storyboard"; sourceTree = ""; };
+ 9088C7A225C98AF800FCAE9A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9088C7A325C98AF800FCAE9A /* dummy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dummy.cpp; sourceTree = ""; };
+ 90C4BA0A25C9A59C00CD5FD1 /* $binary.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "$binary.a"; name = godot; sourceTree = SOURCE_ROOT; };
+ 90C4BA1025C9A5FC00CD5FD1 /* $binary.pck */ = {isa = PBXFileReference; lastKnownFileType = file; path = "$binary.pck"; sourceTree = SOURCE_ROOT; };
+ $additional_pbx_files
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 9088C78E25C98AF700FCAE9A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 90C4BA0B25C9A59C00CD5FD1 /* $binary.a */,
+ $additional_pbx_frameworks_build
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9088C78825C98AF700FCAE9A = {
+ isa = PBXGroup;
+ children = (
+ 1F1575711F582BE20003B888 /* dylibs */,
+ 90C4BA1025C9A5FC00CD5FD1 /* $binary.pck */,
+ 9088C79325C98AF700FCAE9A /* $binary */,
+ D0BCFE3618AEBDA2004A7AAE /* Frameworks */,
+ 9088C79225C98AF700FCAE9A /* Products */,
+ $additional_pbx_resources_refs
+ );
+ sourceTree = "";
+ };
+ 9088C79225C98AF700FCAE9A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 9088C79125C98AF700FCAE9A /* $binary.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ D0BCFE3618AEBDA2004A7AAE /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 90C4BA0A25C9A59C00CD5FD1 /* $binary.a */,
+ $additional_pbx_frameworks_refs
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 9088C79325C98AF700FCAE9A /* $binary */ = {
+ isa = PBXGroup;
+ children = (
+ 9088C79D25C98AF800FCAE9A /* Assets.xcassets */,
+ 9088C79F25C98AF800FCAE9A /* Launch Screen.storyboard */,
+ 90C4BA1025C9A5FC00CD5FD1 /* $binary.pck */,
+ 9088C7A225C98AF800FCAE9A /* Info.plist */,
+ 9088C7A325C98AF800FCAE9A /* dummy.cpp */,
+ );
+ path = "$binary";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 9088C79025C98AF700FCAE9A /* $binary */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 9088C7A725C98AF800FCAE9A /* Build configuration list for PBXNativeTarget "$binary" */;
+ buildPhases = (
+ 9088C78D25C98AF700FCAE9A /* Sources */,
+ 9088C78E25C98AF700FCAE9A /* Frameworks */,
+ 9088C78F25C98AF700FCAE9A /* Resources */,
+ 90A13CD024AA68E500E8464F /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "$binary";
+ productName = "$name";
+ productReference = 9088C79125C98AF700FCAE9A /* $binary.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 9088C78925C98AF700FCAE9A /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1240;
+ TargetAttributes = {
+ 9088C79025C98AF700FCAE9A = {
+ CreatedOnToolsVersion = 12.4;
+ };
+ };
+ };
+ buildConfigurationList = 9088C78C25C98AF700FCAE9A /* Build configuration list for PBXProject "$binary" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 9088C78825C98AF700FCAE9A;
+ productRefGroup = 9088C79225C98AF700FCAE9A /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 9088C79025C98AF700FCAE9A /* $binary */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 9088C78F25C98AF700FCAE9A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9088C7A125C98AF800FCAE9A /* Launch Screen.storyboard in Resources */,
+ 9088C79E25C98AF800FCAE9A /* Assets.xcassets in Resources */,
+ 90C4BA1125C9A60500CD5FD1 /* $binary.pck in Resources */,
+ $additional_pbx_resources_build
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 9088C78D25C98AF700FCAE9A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9088C7A425C98AF800FCAE9A /* dummy.cpp in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 9088C79F25C98AF800FCAE9A /* Launch Screen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 9088C7A025C98AF800FCAE9A /* Base */,
+ );
+ name = "Launch Screen.storyboard";
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 9088C7A525C98AF800FCAE9A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ OTHER_LDFLAGS = "$linker_flags";
+ SDKROOT = appletvos;
+ TVOS_DEPLOYMENT_TARGET = 6;
+ };
+ name = Debug;
+ };
+ 9088C7A625C98AF800FCAE9A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ OTHER_LDFLAGS = "$linker_flags";
+ SDKROOT = appletvos;
+ TVOS_DEPLOYMENT_TARGET = 6;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 9088C7A825C98AF800FCAE9A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = "$team_id";
+ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**";
+ INFOPLIST_FILE = "$binary/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/**",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "$bundle_identifier";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = 3;
+ TVOS_DEPLOYMENT_TARGET = 10.0;
+ };
+ name = Debug;
+ };
+ 9088C7A925C98AF800FCAE9A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = "$team_id";
+ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/**";
+ INFOPLIST_FILE = "$binary/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/**",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "$bundle_identifier";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = 3;
+ TVOS_DEPLOYMENT_TARGET = 10.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 9088C78C25C98AF700FCAE9A /* Build configuration list for PBXProject "$binary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9088C7A525C98AF800FCAE9A /* Debug */,
+ 9088C7A625C98AF800FCAE9A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 9088C7A725C98AF800FCAE9A /* Build configuration list for PBXNativeTarget "$binary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9088C7A825C98AF800FCAE9A /* Debug */,
+ 9088C7A925C98AF800FCAE9A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 9088C78925C98AF700FCAE9A /* Project object */;
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000000..c9c19829f4ae
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/misc/dist/tvos_xcode/godot_tvos.xcodeproj/xcshareddata/xcschemes/godot_tvos.xcscheme b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/xcshareddata/xcschemes/godot_tvos.xcscheme
new file mode 100644
index 000000000000..b6beeb012fea
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos.xcodeproj/xcshareddata/xcschemes/godot_tvos.xcscheme
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/AccentColor.colorset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000000..eb8789700816
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..2e003356c750
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json
new file mode 100644
index 000000000000..de59d885ae8d
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json
@@ -0,0 +1,17 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "layers" : [
+ {
+ "filename" : "Front.imagestacklayer"
+ },
+ {
+ "filename" : "Middle.imagestacklayer"
+ },
+ {
+ "filename" : "Back.imagestacklayer"
+ }
+ ]
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..2e003356c750
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..2e003356c750
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..795cce17243c
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json
new file mode 100644
index 000000000000..de59d885ae8d
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json
@@ -0,0 +1,17 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "layers" : [
+ {
+ "filename" : "Front.imagestacklayer"
+ },
+ {
+ "filename" : "Middle.imagestacklayer"
+ },
+ {
+ "filename" : "Back.imagestacklayer"
+ }
+ ]
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..795cce17243c
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 000000000000..795cce17243c
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json
new file mode 100644
index 000000000000..f47ba43daac4
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json
@@ -0,0 +1,32 @@
+{
+ "assets" : [
+ {
+ "filename" : "App Icon - App Store.imagestack",
+ "idiom" : "tv",
+ "role" : "primary-app-icon",
+ "size" : "1280x768"
+ },
+ {
+ "filename" : "App Icon.imagestack",
+ "idiom" : "tv",
+ "role" : "primary-app-icon",
+ "size" : "400x240"
+ },
+ {
+ "filename" : "Top Shelf Image Wide.imageset",
+ "idiom" : "tv",
+ "role" : "top-shelf-image-wide",
+ "size" : "2320x720"
+ },
+ {
+ "filename" : "Top Shelf Image.imageset",
+ "idiom" : "tv",
+ "role" : "top-shelf-image",
+ "size" : "1920x720"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json
new file mode 100644
index 000000000000..b65f0cddcfcf
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "tv-marketing",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv-marketing",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json
new file mode 100644
index 000000000000..b65f0cddcfcf
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "tv-marketing",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv-marketing",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/Contents.json b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/misc/dist/tvos_xcode/godot_tvos/Base.lproj/Launch Screen.storyboard b/misc/dist/tvos_xcode/godot_tvos/Base.lproj/Launch Screen.storyboard
new file mode 100644
index 000000000000..660ba53de4f7
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Base.lproj/Launch Screen.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/misc/dist/tvos_xcode/godot_tvos/Info.plist b/misc/dist/tvos_xcode/godot_tvos/Info.plist
new file mode 100644
index 000000000000..daa9782324e2
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ ITSAppUsesNonExemptEncryption
+
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $short_version
+ CFBundleVersion
+ $build_version
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ Launch Screen
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UIUserInterfaceStyle
+ Automatic
+
+
diff --git a/misc/dist/tvos_xcode/godot_tvos/dummy.cpp b/misc/dist/tvos_xcode/godot_tvos/dummy.cpp
new file mode 100644
index 000000000000..acbf7f03d1df
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/dummy.cpp
@@ -0,0 +1,31 @@
+/*************************************************************************/
+/* dummy.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+$cpp_code
diff --git a/misc/dist/tvos_xcode/godot_tvos/dylibs/empty b/misc/dist/tvos_xcode/godot_tvos/dylibs/empty
new file mode 100644
index 000000000000..bd3e89433361
--- /dev/null
+++ b/misc/dist/tvos_xcode/godot_tvos/dylibs/empty
@@ -0,0 +1 @@
+Dummy file to make dylibs folder exported
diff --git a/misc/dist/tvos_xcode/libgodot.tvos.debug.fat.a b/misc/dist/tvos_xcode/libgodot.tvos.debug.fat.a
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/misc/dist/tvos_xcode/libgodot.tvos.release.fat.a b/misc/dist/tvos_xcode/libgodot.tvos.release.fat.a
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/modules/gdnative/doc_classes/GDNativeLibrary.xml b/modules/gdnative/doc_classes/GDNativeLibrary.xml
index 53d24ef0e6c6..63a76646e64a 100644
--- a/modules/gdnative/doc_classes/GDNativeLibrary.xml
+++ b/modules/gdnative/doc_classes/GDNativeLibrary.xml
@@ -44,7 +44,7 @@
The prefix this library's entry point functions begin with. For example, a GDNativeLibrary would declare its [code]gdnative_init[/code] function as [code]godot_gdnative_init[/code] by default.
- On platforms that require statically linking libraries (currently only iOS), each library must have a different [code]symbol_prefix[/code].
+ On platforms that require statically linking libraries (currently tvOS and iOS), each library must have a different [code]symbol_prefix[/code].
diff --git a/modules/gdnative/gdnative.cpp b/modules/gdnative/gdnative.cpp
index bc0d6e3c957f..0e9b5c8d5424 100644
--- a/modules/gdnative/gdnative.cpp
+++ b/modules/gdnative/gdnative.cpp
@@ -291,11 +291,11 @@ bool GDNative::initialize() {
ERR_PRINT("No library set for this platform");
return false;
}
-#ifdef IPHONE_ENABLED
- // On iOS we use static linking by default.
+#if defined(IPHONE_ENABLED) || defined(TVOS_ENABLED)
+ // On tvOS/iOS we use static linking by default.
String path = "";
- // On iOS dylibs is not allowed, but can be replaced with .framework or .xcframework.
+ // On tvOS/iOS dylibs is not allowed, but can be replaced with .framework or .xcframework.
// If they are used, we can run dlopen on them.
// They should be located under Frameworks directory, so we need to replace library path.
if (!lib_path.ends_with(".a")) {
diff --git a/modules/gdnative/gdnative_library_editor_plugin.cpp b/modules/gdnative/gdnative_library_editor_plugin.cpp
index 1eafabc32aa2..16f1e9c42a35 100644
--- a/modules/gdnative/gdnative_library_editor_plugin.cpp
+++ b/modules/gdnative/gdnative_library_editor_plugin.cpp
@@ -146,7 +146,7 @@ void GDNativeLibraryEditor::_on_item_button(Object *item, int column, int id) {
if (id == BUTTON_SELECT_DEPENDENCES) {
mode = EditorFileDialog::MODE_OPEN_FILES;
- } else if (treeItem->get_text(0) == "iOS") {
+ } else if (treeItem->get_text(0) == "iOS" || treeItem->get_text(0) == "tvOS") {
mode = EditorFileDialog::MODE_OPEN_ANY;
}
@@ -348,6 +348,15 @@ GDNativeLibraryEditor::GDNativeLibraryEditor() {
// Frameworks is actually a folder with files.
platform_ios.library_extension = "*.framework; Framework, *.xcframework; Binary Framework, *.a; Static Library, *.dylib; Dynamic Library";
platforms["iOS"] = platform_ios;
+
+ NativePlatformConfig platform_tvos;
+ platform_tvos.name = "tvOS";
+ platform_tvos.entries.push_back("arm64");
+ platform_tvos.entries.push_back("x86_64");
+ // tvOS can use both Static and Dynamic libraries.
+ // Frameworks is actually a folder with files.
+ platform_tvos.library_extension = "*.framework; Framework, *.xcframework; Binary Framework, *.a; Static Library, *.dylib; Dynamic Library";
+ platforms["tvOS"] = platform_tvos;
}
VBoxContainer *container = memnew(VBoxContainer);
diff --git a/modules/gdnative/include/gdnative/gdnative.h b/modules/gdnative/include/gdnative/gdnative.h
index b5316244fb5d..f9a661c10a0d 100644
--- a/modules/gdnative/include/gdnative/gdnative.h
+++ b/modules/gdnative/include/gdnative/gdnative.h
@@ -43,6 +43,9 @@ extern "C" {
#if TARGET_OS_IPHONE
#define GDCALLINGCONV __attribute__((visibility("default")))
#define GDAPI GDCALLINGCONV
+#elif TARGET_OS_TV
+#define GDCALLINGCONV __attribute__((visibility("default")))
+#define GDAPI GDCALLINGCONV
#elif TARGET_OS_MAC
#define GDCALLINGCONV __attribute__((sysv_abi))
#define GDAPI GDCALLINGCONV
diff --git a/modules/gdnative/nativescript/SCsub b/modules/gdnative/nativescript/SCsub
index b1ddb2489c6e..144836acf3cc 100644
--- a/modules/gdnative/nativescript/SCsub
+++ b/modules/gdnative/nativescript/SCsub
@@ -5,5 +5,5 @@ Import("env_gdnative")
env_gdnative.add_source_files(env.modules_sources, "*.cpp")
-if "platform" in env and env["platform"] in ["x11", "iphone"]:
+if "platform" in env and env["platform"] in ["x11", "iphone", "tvos"]:
env.Append(LINKFLAGS=["-rdynamic"])
diff --git a/modules/gdnative/register_types.cpp b/modules/gdnative/register_types.cpp
index 3778a98c79a4..c07e55d5a58e 100644
--- a/modules/gdnative/register_types.cpp
+++ b/modules/gdnative/register_types.cpp
@@ -144,8 +144,8 @@ void GDNativeExportPlugin::_export_file(const String &p_path, const String &p_ty
}
}
- // Add symbols for staticaly linked libraries on iOS
- if (p_features.has("iOS")) {
+ // Add symbols for staticaly linked libraries on tvOS/iOS
+ if (p_features.has("iOS") || p_features.has("tvOS")) {
bool should_fake_dynamic = false;
@@ -183,7 +183,7 @@ void GDNativeExportPlugin::_export_file(const String &p_path, const String &p_ty
}
if (should_fake_dynamic) {
- // Register symbols in the "fake" dynamic lookup table, because dlsym does not work well on iOS.
+ // Register symbols in the "fake" dynamic lookup table, because dlsym does not work well on tvOS/iOS.
LibrarySymbol expected_symbols[] = {
{ "gdnative_init", true },
{ "gdnative_terminate", false },
@@ -193,9 +193,10 @@ void GDNativeExportPlugin::_export_file(const String &p_path, const String &p_ty
{ "nativescript_thread_exit", false },
{ "gdnative_singleton", false }
};
+
String declare_pattern = "extern \"C\" void $name(void)$weak;\n";
String additional_code = "extern void register_dynamic_symbol(char *name, void *address);\n"
- "extern void add_ios_init_callback(void (*cb)());\n";
+ "extern void $callback_registration(void (*cb)());\n";
String linker_flags = "";
for (unsigned long i = 0; i < sizeof(expected_symbols) / sizeof(expected_symbols[0]); ++i) {
String full_name = lib->get_symbol_prefix() + expected_symbols[i].name;
@@ -218,11 +219,22 @@ void GDNativeExportPlugin::_export_file(const String &p_path, const String &p_ty
additional_code += register_pattern.replace("$name", full_name);
}
additional_code += "}\n";
- additional_code += String("struct $prefixstruct {$prefixstruct() {add_ios_init_callback($prefixinit);}};\n").replace("$prefix", lib->get_symbol_prefix());
+ additional_code += String("struct $prefixstruct {$prefixstruct() {$callback_registration($prefixinit);}};\n").replace("$prefix", lib->get_symbol_prefix());
additional_code += String("$prefixstruct $prefixstruct_instance;\n").replace("$prefix", lib->get_symbol_prefix());
- add_ios_cpp_code(additional_code);
- add_ios_linker_flags(linker_flags);
+ if (p_features.has("iOS")) {
+ const String callback_registration_string = "add_ios_init_callback";
+ additional_code = additional_code.replace("$callback_registration", callback_registration_string);
+
+ add_ios_cpp_code(additional_code);
+ add_ios_linker_flags(linker_flags);
+ } else if (p_features.has("tvOS")) {
+ const String callback_registration_string = "add_tvos_init_callback";
+ additional_code = additional_code.replace("$callback_registration", callback_registration_string);
+
+ add_tvos_cpp_code(additional_code);
+ add_tvos_linker_flags(linker_flags);
+ }
}
}
}
diff --git a/modules/webm/config.py b/modules/webm/config.py
index 93b49d177ad4..3562591b0df1 100644
--- a/modules/webm/config.py
+++ b/modules/webm/config.py
@@ -1,5 +1,5 @@
def can_build(env, platform):
- return platform not in ["iphone"]
+ return platform not in ["iphone", "tvos"]
def configure(env):
diff --git a/platform/tvos/SCsub b/platform/tvos/SCsub
new file mode 100644
index 000000000000..206dfee7f51d
--- /dev/null
+++ b/platform/tvos/SCsub
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+
+Import("env")
+
+tvos_lib = [
+ "godot_appletv.mm",
+ "os_appletv.mm",
+ "main.m",
+ "app_delegate.mm",
+ "view_controller.mm",
+ "tvos.mm",
+ "joypad_appletv.mm",
+ "godot_view.mm",
+ "display_layer.mm",
+ "godot_app_delegate.m",
+ "godot_view_renderer.mm",
+ "godot_view_gesture_recognizer.mm",
+ "keyboard_input_view.mm",
+ "native_video_view.m",
+]
+
+env_tvos = env.Clone()
+tvos_lib = env_tvos.add_library("tvos", tvos_lib)
+
+# (tvOS) Enable module support
+env_tvos.Append(CCFLAGS=["-fmodules", "-fcxx-modules"])
+
+
+def combine_libs(target=None, source=None, env=None):
+ lib_path = target[0].srcnode().abspath
+ if "osxcross" in env:
+ libtool = "$TVOSPATH/usr/bin/${tvos_triple}libtool"
+ else:
+ libtool = "$TVOSPATH/usr/bin/libtool"
+ env.Execute(
+ libtool + ' -static -o "' + lib_path + '" ' + " ".join([('"' + lib.srcnode().abspath + '"') for lib in source])
+ )
+
+
+combine_command = env_tvos.Command("#bin/libgodot" + env_tvos["LIBSUFFIX"], [tvos_lib] + env_tvos["LIBS"], combine_libs)
diff --git a/platform/tvos/app_delegate.h b/platform/tvos/app_delegate.h
new file mode 100644
index 000000000000..c5ab8bad1805
--- /dev/null
+++ b/platform/tvos/app_delegate.h
@@ -0,0 +1,40 @@
+/*************************************************************************/
+/* app_delegate.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+@class ViewController;
+
+@interface AppDelegate : NSObject
+
+@property(strong, nonatomic) UIWindow *window;
+@property(strong, class, readonly, nonatomic) ViewController *viewController;
+
+@end
diff --git a/platform/tvos/app_delegate.mm b/platform/tvos/app_delegate.mm
new file mode 100644
index 000000000000..95ac4510ac15
--- /dev/null
+++ b/platform/tvos/app_delegate.mm
@@ -0,0 +1,131 @@
+/*************************************************************************/
+/* app_delegate.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "app_delegate.h"
+
+#include "core/project_settings.h"
+#include "drivers/coreaudio/audio_driver_coreaudio.h"
+#import "godot_view.h"
+#include "main/main.h"
+#include "os_appletv.h"
+#import "view_controller.h"
+
+#import
+
+#define kRenderingFrequency 60
+
+extern int gargc;
+extern char **gargv;
+
+extern int appletv_main(int, char **, String);
+extern void appletv_finish();
+
+@implementation AppDelegate
+
+static ViewController *mainViewController = nil;
+
++ (ViewController *)viewController {
+ return mainViewController;
+}
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // Create a full-screen window
+ CGRect windowBounds = [[UIScreen mainScreen] bounds];
+ self.window = [[UIWindow alloc] initWithFrame:windowBounds];
+
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
+ NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+
+ int err = appletv_main(gargc, gargv, String::utf8([documentsDirectory UTF8String]));
+ if (err != 0) {
+ // bail, things did not go very well for us, should probably output a message on screen with our error code...
+ exit(0);
+ return FALSE;
+ }
+
+ // WARNING: We must *always* create the GodotView after we have constructed the
+ // OS with appletv_main. This allows the GodotView to access project settings so
+ // it can properly initialize the OpenGL context
+
+ ViewController *viewController = [[ViewController alloc] init];
+ viewController.godotView.useCADisplayLink = bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO;
+ viewController.godotView.renderingInterval = 1.0 / kRenderingFrequency;
+
+ self.window.rootViewController = viewController;
+
+ // Show the window
+ [self.window makeKeyAndVisible];
+
+ mainViewController = viewController;
+
+ // prevent to stop music in another background app
+ [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
+
+ bool keep_screen_on = bool(GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true));
+ OSAppleTV::get_singleton()->set_keep_screen_on(keep_screen_on);
+
+ return TRUE;
+}
+
+- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
+ if (OS::get_singleton()->get_main_loop()) {
+ OS::get_singleton()->get_main_loop()->notification(
+ MainLoop::NOTIFICATION_OS_MEMORY_WARNING);
+ }
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+ appletv_finish();
+}
+
+// When application goes to background (e.g. user switches to another app or presses Home),
+// then applicationWillResignActive -> applicationDidEnterBackground are called.
+// When user opens the inactive app again,
+// applicationWillEnterForeground -> applicationDidBecomeActive are called.
+
+// There are cases when applicationWillResignActive -> applicationDidBecomeActive
+// sequence is called without the app going to background. For example, that happens
+// if you open the app list without switching to another app or open/close the
+// notification panel by swiping from the upper part of the screen.
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+ OSAppleTV::get_singleton()->on_focus_out();
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+ OSAppleTV::get_singleton()->on_focus_in();
+}
+
+- (void)dealloc {
+ self.window = nil;
+}
+
+@end
diff --git a/platform/tvos/detect.py b/platform/tvos/detect.py
new file mode 100644
index 000000000000..b77307e36559
--- /dev/null
+++ b/platform/tvos/detect.py
@@ -0,0 +1,176 @@
+import os
+import sys
+from methods import detect_darwin_sdk_path, get_darwin_sdk_version
+
+
+def is_active():
+ return True
+
+
+def get_name():
+ return "tvOS"
+
+
+def can_build():
+ if sys.platform == "darwin":
+ if get_darwin_sdk_version("tvos") < 13.0:
+ print("Detected tvOS SDK version older than 13")
+ return False
+ return True
+ elif "OSXCROSS_TVOS" in os.environ:
+ return True
+
+ return False
+
+
+def get_opts():
+ from SCons.Variables import BoolVariable
+
+ return [
+ (
+ "TVOSPATH",
+ "Path to tvOS toolchain",
+ "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain",
+ ),
+ ("TVOSSDK", "Path to the tvOS SDK", ""),
+ BoolVariable("simulator", "Build for simulator", False),
+ ("tvos_triple", "Triple for tvOS toolchain", ""),
+ ]
+
+
+def get_flags():
+ return [
+ ("tools", False),
+ ]
+
+
+def configure(env):
+ ## Build type
+
+ if env["target"].startswith("release"):
+ env.Append(CPPDEFINES=["NDEBUG", ("NS_BLOCK_ASSERTIONS", 1)])
+ if env["optimize"] == "speed": # optimize for speed (default)
+ env.Append(CCFLAGS=["-O2", "-ftree-vectorize", "-fomit-frame-pointer"])
+ env.Append(LINKFLAGS=["-O2"])
+ else: # optimize for size
+ env.Append(CCFLAGS=["-Os", "-ftree-vectorize"])
+ env.Append(LINKFLAGS=["-Os"])
+
+ if env["target"] == "release_debug":
+ env.Append(CPPDEFINES=["DEBUG_ENABLED"])
+
+ elif env["target"] == "debug":
+ env.Append(CCFLAGS=["-gdwarf-2", "-O0"])
+ env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1), "DEBUG_ENABLED"])
+
+ if env["use_lto"]:
+ env.Append(CCFLAGS=["-flto=thin"])
+ env.Append(LINKFLAGS=["-flto=thin"])
+
+ ## Architecture
+ if env["arch"] == "x86": # i386
+ env["bits"] = "32"
+ elif env["arch"] == "x86_64":
+ env["bits"] = "64"
+ else: # armv64
+ env["arch"] = "arm64"
+ env["bits"] = "64"
+
+ ## Compiler configuration
+
+ # Save this in environment for use by other modules
+ if "OSXCROSS_TVOS" in os.environ:
+ env["osxcross"] = True
+
+ env["ENV"]["PATH"] = env["TVOSSDK"] + "/Developer/usr/bin/:" + env["ENV"]["PATH"]
+
+ compiler_path = "$TVOSPATH/usr/bin/${tvos_triple}"
+ s_compiler_path = "$TVOSPATH/Developer/usr/bin/"
+
+ ccache_path = os.environ.get("CCACHE")
+ if ccache_path is None:
+ env["CC"] = compiler_path + "clang"
+ env["CXX"] = compiler_path + "clang++"
+ env["S_compiler"] = s_compiler_path + "gcc"
+ else:
+ # there aren't any ccache wrappers available for iOS,
+ # to enable caching we need to prepend the path to the ccache binary
+ env["CC"] = ccache_path + " " + compiler_path + "clang"
+ env["CXX"] = ccache_path + " " + compiler_path + "clang++"
+ env["S_compiler"] = ccache_path + " " + s_compiler_path + "gcc"
+ env["AR"] = compiler_path + "ar"
+ env["RANLIB"] = compiler_path + "ranlib"
+
+ ## Compile flags
+
+ if env["simulator"]:
+ detect_darwin_sdk_path("tvossimulator", env)
+ env.Append(CCFLAGS=("-isysroot $TVOSSDK -mappletvsimulator-version-min=10.0").split())
+ env.Append(LINKFLAGS=["-mappletvsimulator-version-min=10.0"])
+ env["LIBSUFFIX"] = ".simulator" + env["LIBSUFFIX"]
+ else:
+ detect_darwin_sdk_path("tvos", env)
+ env.Append(CCFLAGS=("-isysroot $TVOSSDK -mappletvos-version-min=10.0").split())
+ env.Append(LINKFLAGS=["-mappletvos-version-min=10.0"])
+
+ if env["arch"] == "x86" or env["arch"] == "x86_64":
+ env["ENV"]["MACOSX_DEPLOYMENT_TARGET"] = "10.9"
+ arch_flag = "i386" if env["arch"] == "x86" else env["arch"]
+ env.Append(
+ CCFLAGS=(
+ "-arch "
+ + arch_flag
+ + " -fobjc-arc -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks -fasm-blocks"
+ ).split()
+ )
+ elif env["arch"] == "arm64":
+ env.Append(
+ CCFLAGS="-fobjc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits -fpascal-strings -fblocks -fvisibility=hidden -MMD -MT dependencies".split()
+ )
+ env.Append(CPPDEFINES=["NEED_LONG_INT"])
+ env.Append(CPPDEFINES=["LIBYUV_DISABLE_NEON"])
+
+ # Temp fix for ABS/MAX/MIN macros in tvOS/iOS SDK blocking compilation
+ env.Append(CCFLAGS=["-Wno-ambiguous-macro"])
+
+ # tvOS requires Bitcode.
+ env.Append(CCFLAGS=["-fembed-bitcode"])
+ env.Append(LINKFLAGS=["-bitcode_bundle"])
+
+ ## Link flags
+
+ if env["arch"] == "x86" or env["arch"] == "x86_64":
+ arch_flag = "i386" if env["arch"] == "x86" else env["arch"]
+ env.Append(
+ LINKFLAGS=[
+ "-arch",
+ arch_flag,
+ "-isysroot",
+ "$TVOSSDK",
+ "-Xlinker",
+ "-objc_abi_version",
+ "-Xlinker",
+ "2",
+ "-F$TVOSSDK",
+ ]
+ )
+ if env["arch"] == "arm64":
+ env.Append(LINKFLAGS=["-arch", "arm64", "-Wl,-dead_strip"])
+
+ env.Append(
+ LINKFLAGS=[
+ "-isysroot",
+ "$TVOSSDK",
+ ]
+ )
+
+ env.Prepend(
+ CPPPATH=[
+ "$TVOSSDK/usr/include",
+ "$TVOSSDK/System/Library/Frameworks/OpenGLES.framework/Headers",
+ "$TVOSSDK/System/Library/Frameworks/AudioUnit.framework/Headers",
+ ]
+ )
+
+ env.Prepend(CPPPATH=["#platform/tvos"])
+ env.Append(CPPDEFINES=["TVOS_ENABLED", "UNIX_ENABLED", "GLES_ENABLED", "COREAUDIO_ENABLED"])
diff --git a/platform/tvos/display_layer.h b/platform/tvos/display_layer.h
new file mode 100644
index 000000000000..cac5e13b52f4
--- /dev/null
+++ b/platform/tvos/display_layer.h
@@ -0,0 +1,45 @@
+/*************************************************************************/
+/* display_layer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+#import
+
+@protocol DisplayLayer
+
+- (void)startRenderDisplayLayer;
+- (void)stopRenderDisplayLayer;
+- (void)initializeDisplayLayer;
+- (void)layoutDisplayLayer;
+
+@end
+
+@interface GodotOpenGLLayer : CAEAGLLayer
+
+@end
diff --git a/platform/tvos/display_layer.mm b/platform/tvos/display_layer.mm
new file mode 100644
index 000000000000..7b560f718413
--- /dev/null
+++ b/platform/tvos/display_layer.mm
@@ -0,0 +1,189 @@
+/*************************************************************************/
+/* display_layer.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "display_layer.h"
+
+#include "core/os/keyboard.h"
+#include "core/project_settings.h"
+#include "main/main.h"
+#include "os_appletv.h"
+#include "servers/audio_server.h"
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+int gl_view_base_fb;
+bool gles3_available = true;
+
+@implementation GodotOpenGLLayer {
+ // The pixel dimensions of the backbuffer
+ GLint backingWidth;
+ GLint backingHeight;
+
+ EAGLContext *context;
+ GLuint viewRenderbuffer, viewFramebuffer;
+ GLuint depthRenderbuffer;
+}
+
+- (void)initializeDisplayLayer {
+ // Configure it so that it is opaque, does not retain the contents of the backbuffer when displayed, and uses RGBA8888 color.
+ self.opaque = YES;
+ self.drawableProperties = [NSDictionary
+ dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:FALSE],
+ kEAGLDrawablePropertyRetainedBacking,
+ kEAGLColorFormatRGBA8,
+ kEAGLDrawablePropertyColorFormat,
+ nil];
+ bool fallback_gl2 = false;
+ // Create a GL ES 3 context based on the gl driver from project settings
+ if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES3") {
+ context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+ NSLog(@"Setting up an OpenGL ES 3.0 context. Based on Project Settings \"rendering/quality/driver/driver_name\"");
+ if (!context && GLOBAL_GET("rendering/quality/driver/fallback_to_gles2")) {
+ gles3_available = false;
+ fallback_gl2 = true;
+ NSLog(@"Failed to create OpenGL ES 3.0 context. Falling back to OpenGL ES 2.0");
+ }
+ }
+
+ // Create GL ES 2 context
+ if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES2" || fallback_gl2) {
+ context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+ NSLog(@"Setting up an OpenGL ES 2.0 context.");
+ if (!context) {
+ NSLog(@"Failed to create OpenGL ES 2.0 context!");
+ return;
+ }
+ }
+
+ if (![EAGLContext setCurrentContext:context]) {
+ NSLog(@"Failed to set EAGLContext!");
+ return;
+ }
+ if (![self createFramebuffer]) {
+ NSLog(@"Failed to create frame buffer!");
+ return;
+ }
+}
+
+- (void)layoutDisplayLayer {
+ [EAGLContext setCurrentContext:context];
+ [self destroyFramebuffer];
+ [self createFramebuffer];
+}
+
+- (void)startRenderDisplayLayer {
+ [EAGLContext setCurrentContext:context];
+
+ glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
+}
+
+- (void)stopRenderDisplayLayer {
+ glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
+ [context presentRenderbuffer:GL_RENDERBUFFER_OES];
+
+#ifdef DEBUG_ENABLED
+ GLenum err = glGetError();
+ if (err) {
+ NSLog(@"DrawView: %x error", err);
+ }
+#endif
+}
+
+- (void)dealloc {
+ if ([EAGLContext currentContext] == context) {
+ [EAGLContext setCurrentContext:nil];
+ }
+
+ if (context) {
+ context = nil;
+ }
+}
+
+- (BOOL)createFramebuffer {
+ // Generate IDs for a framebuffer object and a color renderbuffer
+ glGenFramebuffersOES(1, &viewFramebuffer);
+ glGenRenderbuffersOES(1, &viewRenderbuffer);
+
+ glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
+ glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
+ // This call associates the storage for the current render buffer with the EAGLDrawable (our CAEAGLLayer)
+ // allowing us to draw into a buffer that will later be rendered to screen wherever the layer is (which corresponds with our view).
+ [context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(id)self];
+ glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, viewRenderbuffer);
+
+ glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &backingWidth);
+ glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &backingHeight);
+
+ // For this sample, we also need a depth buffer, so we'll create and attach one via another renderbuffer.
+ glGenRenderbuffersOES(1, &depthRenderbuffer);
+ glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthRenderbuffer);
+ glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, backingWidth, backingHeight);
+ glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthRenderbuffer);
+
+ if (glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES) {
+ NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES));
+ return NO;
+ }
+
+ if (OS::get_singleton()) {
+ OS::VideoMode vm;
+ vm.fullscreen = true;
+ vm.width = backingWidth;
+ vm.height = backingHeight;
+ vm.resizable = false;
+ OS::get_singleton()->set_video_mode(vm);
+ OSAppleTV::get_singleton()->set_base_framebuffer(viewFramebuffer);
+ }
+
+ gl_view_base_fb = viewFramebuffer;
+
+ return YES;
+}
+
+// Clean up any buffers we have allocated.
+- (void)destroyFramebuffer {
+ glDeleteFramebuffersOES(1, &viewFramebuffer);
+ viewFramebuffer = 0;
+ glDeleteRenderbuffersOES(1, &viewRenderbuffer);
+ viewRenderbuffer = 0;
+
+ if (depthRenderbuffer) {
+ glDeleteRenderbuffersOES(1, &depthRenderbuffer);
+ depthRenderbuffer = 0;
+ }
+}
+
+@end
diff --git a/platform/tvos/export/export.cpp b/platform/tvos/export/export.cpp
new file mode 100644
index 000000000000..4ac21f3c3759
--- /dev/null
+++ b/platform/tvos/export/export.cpp
@@ -0,0 +1,1268 @@
+/*************************************************************************/
+/* export.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "export.h"
+#include "core/io/marshalls.h"
+#include "core/io/resource_saver.h"
+#include "core/io/zip_io.h"
+#include "core/os/file_access.h"
+#include "core/os/os.h"
+#include "core/project_settings.h"
+#include "core/version.h"
+#include "editor/editor_export.h"
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+#include "main/splash.gen.h"
+#include "platform/tvos/logo.gen.h"
+#include "platform/tvos/plugin/godot_plugin_config.h"
+#include "string.h"
+
+#include
+
+class EditorExportPlatformTVOS : public EditorExportPlatform {
+
+ GDCLASS(EditorExportPlatformTVOS, EditorExportPlatform);
+
+ int version_code;
+
+ Ref logo;
+
+ // Plugins
+ SafeFlag plugins_changed;
+ Thread check_for_changes_thread;
+ SafeFlag quit_request;
+ Mutex plugins_lock;
+ Vector plugins;
+
+ typedef Error (*FileHandler)(String p_file, void *p_userdata);
+ static Error _walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata);
+
+ struct TVOSConfigData {
+ String pkg_name;
+ String binary_name;
+ String plist_content;
+ String linker_flags;
+ String cpp_code;
+ String modules_buildfile;
+ String modules_fileref;
+ String modules_buildphase;
+ String modules_buildgrp;
+ Vector capabilities;
+ };
+ struct ExportArchitecture {
+
+ String name;
+ bool is_default;
+
+ ExportArchitecture() :
+ name(""),
+ is_default(false) {
+ }
+
+ ExportArchitecture(String p_name, bool p_is_default) {
+ name = p_name;
+ is_default = p_is_default;
+ }
+ };
+
+ struct TVOSExportAsset {
+ String exported_path;
+ bool is_framework; // framework is anything linked to the binary, otherwise it's a resource
+ bool should_embed;
+ };
+
+ String _get_additional_plist_content();
+ String _get_linker_flags();
+ String _get_cpp_code();
+ void _fix_config_file(const Ref &p_preset, Vector &pfile, const TVOSConfigData &p_config, bool p_debug);
+
+ void _add_assets_to_project(const Ref &p_preset, Vector &p_project_data, const Vector &p_additional_assets);
+ Error _copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector &r_exported_assets);
+ Error _export_additional_assets(const String &p_out_dir, const Vector &p_assets, bool p_is_framework, bool p_should_embed, Vector &r_exported_assets);
+ Error _export_additional_assets(const String &p_out_dir, const Vector &p_libraries, Vector &r_exported_assets);
+ Error _export_tvos_plugins(const Ref &p_preset, TVOSConfigData &p_config_data, const String &dest_dir, Vector &r_exported_assets, bool p_debug);
+
+ bool is_package_name_valid(const String &p_package, String *r_error = NULL) const {
+
+ String pname = p_package;
+
+ if (pname.length() == 0) {
+ if (r_error) {
+ *r_error = TTR("Identifier is missing.");
+ }
+ return false;
+ }
+
+ for (int i = 0; i < pname.length(); i++) {
+ CharType c = pname[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) {
+ if (r_error) {
+ *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c));
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ static void _check_for_changes_poll_thread(void *ud) {
+ EditorExportPlatformTVOS *ea = (EditorExportPlatformTVOS *)ud;
+
+ while (!ea->quit_request.is_set()) {
+ // Nothing to do if we already know the plugins have changed.
+ if (!ea->plugins_changed.is_set()) {
+
+ ea->plugins_lock.lock();
+
+ Vector loaded_plugins = get_plugins();
+
+ if (ea->plugins.size() != loaded_plugins.size()) {
+ ea->plugins_changed.set();
+ } else {
+ for (int i = 0; i < ea->plugins.size(); i++) {
+ if (ea->plugins[i].name != loaded_plugins[i].name || ea->plugins[i].last_updated != loaded_plugins[i].last_updated) {
+ ea->plugins_changed.set();
+ break;
+ }
+ }
+ }
+
+ ea->plugins_lock.unlock();
+ }
+
+ uint64_t wait = 3000000;
+ uint64_t time = OS::get_singleton()->get_ticks_usec();
+ while (OS::get_singleton()->get_ticks_usec() - time < wait) {
+ OS::get_singleton()->delay_usec(300000);
+
+ if (ea->quit_request.is_set()) {
+ break;
+ }
+ }
+ }
+ }
+
+protected:
+ virtual void get_preset_features(const Ref &p_preset, List *r_features);
+ virtual void get_export_options(List *r_options);
+
+public:
+ virtual String get_name() const { return "tvOS"; }
+ virtual String get_os_name() const { return "tvOS"; }
+ virtual Ref get_logo() const { return logo; }
+
+ virtual bool should_update_export_options() {
+ bool export_options_changed = plugins_changed.is_set();
+ if (export_options_changed) {
+ // don't clear unless we're reporting true, to avoid race
+ plugins_changed.clear();
+ }
+ return export_options_changed;
+ }
+
+ virtual List get_binary_extensions(const Ref &p_preset) const {
+ List list;
+ list.push_back("ipa");
+ return list;
+ }
+
+ virtual Error export_project(const Ref &p_preset, bool p_debug, const String &p_path, int p_flags = 0);
+
+ virtual bool can_export(const Ref &p_preset, String &r_error, bool &r_missing_templates) const;
+
+ virtual void get_platform_features(List *r_features) {
+
+ r_features->push_back("mobile");
+ r_features->push_back("tvOS");
+ }
+
+ virtual void resolve_platform_feature_priorities(const Ref &p_preset, Set &p_features) {
+ }
+
+ EditorExportPlatformTVOS();
+ ~EditorExportPlatformTVOS();
+
+ /// List the gdip files in the directory specified by the p_path parameter.
+ static Vector list_plugin_config_files(const String &p_path, bool p_check_directories) {
+ Vector dir_files;
+ DirAccessRef da = DirAccess::open(p_path);
+ if (da) {
+ da->list_dir_begin();
+ while (true) {
+ String file = da->get_next();
+ if (file.empty()) {
+ break;
+ }
+
+ if (file == "." || file == "..") {
+ continue;
+ }
+
+ if (da->current_is_hidden()) {
+ continue;
+ }
+
+ if (da->current_is_dir()) {
+ if (p_check_directories) {
+ Vector directory_files = list_plugin_config_files(p_path.plus_file(file), false);
+ for (int i = 0; i < directory_files.size(); ++i) {
+ dir_files.push_back(file.plus_file(directory_files[i]));
+ }
+ }
+
+ continue;
+ }
+
+ if (file.ends_with(PluginConfigTVOS::PLUGIN_CONFIG_EXT)) {
+ dir_files.push_back(file);
+ }
+ }
+ da->list_dir_end();
+ }
+
+ return dir_files;
+ }
+
+ static Vector get_plugins() {
+ Vector loaded_plugins;
+
+ String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().plus_file("tvos/plugins");
+
+ if (DirAccess::exists(plugins_dir)) {
+ Vector plugins_filenames = list_plugin_config_files(plugins_dir, true);
+
+ if (!plugins_filenames.empty()) {
+ Ref config_file = memnew(ConfigFile);
+ for (int i = 0; i < plugins_filenames.size(); i++) {
+ PluginConfigTVOS config = load_plugin_config(config_file, plugins_dir.plus_file(plugins_filenames[i]));
+ if (config.valid_config) {
+ loaded_plugins.push_back(config);
+ } else {
+ print_error("Invalid plugin config file " + plugins_filenames[i]);
+ }
+ }
+ }
+ }
+
+ return loaded_plugins;
+ }
+
+ static Vector get_enabled_plugins(const Ref &p_presets) {
+ Vector enabled_plugins;
+ Vector all_plugins = get_plugins();
+ for (int i = 0; i < all_plugins.size(); i++) {
+ PluginConfigTVOS plugin = all_plugins[i];
+ bool enabled = p_presets->get("plugins/" + plugin.name);
+ if (enabled) {
+ enabled_plugins.push_back(plugin);
+ }
+ }
+
+ return enabled_plugins;
+ }
+};
+
+void EditorExportPlatformTVOS::get_preset_features(const Ref &p_preset, List *r_features) {
+
+ String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name");
+ r_features->push_back("pvrtc");
+ if (driver == "GLES3") {
+ r_features->push_back("etc2");
+ }
+
+ r_features->push_back("arm64");
+}
+
+void EditorExportPlatformTVOS::get_export_options(List *r_options) {
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_store_team_id"), ""));
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/build_version"), "1"));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
+
+ Vector found_plugins = get_plugins();
+ for (int i = 0; i < found_plugins.size(); i++) {
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "plugins/" + found_plugins[i].name), false));
+ }
+
+ plugins_changed.clear();
+ plugins = found_plugins;
+}
+
+void EditorExportPlatformTVOS::_fix_config_file(const Ref &p_preset, Vector &pfile, const TVOSConfigData &p_config, bool p_debug) {
+ String str;
+ String strnew;
+ str.parse_utf8((const char *)pfile.ptr(), pfile.size());
+ Vector lines = str.split("\n");
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines[i].find("$binary") != -1) {
+ strnew += lines[i].replace("$binary", p_config.binary_name) + "\n";
+ } else if (lines[i].find("$modules_buildfile") != -1) {
+ strnew += lines[i].replace("$modules_buildfile", p_config.modules_buildfile) + "\n";
+ } else if (lines[i].find("$modules_fileref") != -1) {
+ strnew += lines[i].replace("$modules_fileref", p_config.modules_fileref) + "\n";
+ } else if (lines[i].find("$modules_buildphase") != -1) {
+ strnew += lines[i].replace("$modules_buildphase", p_config.modules_buildphase) + "\n";
+ } else if (lines[i].find("$modules_buildgrp") != -1) {
+ strnew += lines[i].replace("$modules_buildgrp", p_config.modules_buildgrp) + "\n";
+ } else if (lines[i].find("$name") != -1) {
+ strnew += lines[i].replace("$name", p_config.pkg_name) + "\n";
+ } else if (lines[i].find("$info") != -1) {
+ strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n";
+ } else if (lines[i].find("$bundle_identifier") != -1) {
+ strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
+ } else if (lines[i].find("$short_version") != -1) {
+ strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
+ } else if (lines[i].find("$build_version") != -1) {
+ strnew += lines[i].replace("$build_version", p_preset->get("application/build_version")) + "\n";
+ } else if (lines[i].find("$copyright") != -1) {
+ strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
+ } else if (lines[i].find("$team_id") != -1) {
+ strnew += lines[i].replace("$team_id", p_preset->get("application/app_store_team_id")) + "\n";
+ } else if (lines[i].find("$additional_plist_content") != -1) {
+ strnew += lines[i].replace("$additional_plist_content", p_config.plist_content) + "\n";
+ } else if (lines[i].find("$linker_flags") != -1) {
+ strnew += lines[i].replace("$linker_flags", p_config.linker_flags) + "\n";
+ } else if (lines[i].find("$cpp_code") != -1) {
+ strnew += lines[i].replace("$cpp_code", p_config.cpp_code) + "\n";
+ } else {
+ strnew += lines[i] + "\n";
+ }
+ }
+
+ // !BAS! I'm assuming the 9 in the original code was a typo. I've added -1 or else it seems to also be adding our terminating zero...
+ // should apply the same fix in our OSX export.
+ CharString cs = strnew.utf8();
+ pfile.resize(cs.size() - 1);
+ for (int i = 0; i < cs.size() - 1; i++) {
+ pfile.write[i] = cs[i];
+ }
+}
+
+String EditorExportPlatformTVOS::_get_additional_plist_content() {
+ Vector[ > export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ String result;
+ for (int i = 0; i < export_plugins.size(); ++i) {
+ result += export_plugins[i]->get_tvos_plist_content();
+ }
+ return result;
+}
+
+String EditorExportPlatformTVOS::_get_linker_flags() {
+ Vector][ > export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ String result;
+ for (int i = 0; i < export_plugins.size(); ++i) {
+ String flags = export_plugins[i]->get_tvos_linker_flags();
+ if (flags.length() == 0) continue;
+ if (result.length() > 0) {
+ result += ' ';
+ }
+ result += flags;
+ }
+ // the flags will be enclosed in quotes, so need to escape them
+ return result.replace("\"", "\\\"");
+}
+
+String EditorExportPlatformTVOS::_get_cpp_code() {
+ Vector][ > export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ String result;
+ for (int i = 0; i < export_plugins.size(); ++i) {
+ result += export_plugins[i]->get_tvos_cpp_code();
+ }
+ return result;
+}
+
+Error EditorExportPlatformTVOS::_walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata) {
+ Vector dirs;
+ String path;
+ String current_dir = p_da->get_current_dir();
+ p_da->list_dir_begin();
+ while ((path = p_da->get_next()).length() != 0) {
+ if (p_da->current_is_dir()) {
+ if (path != "." && path != "..") {
+ dirs.push_back(path);
+ }
+ } else {
+ Error err = p_handler(current_dir.plus_file(path), p_userdata);
+ if (err) {
+ p_da->list_dir_end();
+ return err;
+ }
+ }
+ }
+ p_da->list_dir_end();
+
+ for (int i = 0; i < dirs.size(); ++i) {
+ String dir = dirs[i];
+ p_da->change_dir(dir);
+ Error err = _walk_dir_recursive(p_da, p_handler, p_userdata);
+ p_da->change_dir("..");
+ if (err) {
+ return err;
+ }
+ }
+
+ return OK;
+}
+
+struct PbxId {
+private:
+ static char _hex_char(uint8_t four_bits) {
+ if (four_bits < 10) {
+ return ('0' + four_bits);
+ }
+ return 'A' + (four_bits - 10);
+ }
+
+ static String _hex_pad(uint32_t num) {
+ Vector ret;
+ ret.resize(sizeof(num) * 2);
+ for (uint64_t i = 0; i < sizeof(num) * 2; ++i) {
+ uint8_t four_bits = (num >> (sizeof(num) * 8 - (i + 1) * 4)) & 0xF;
+ ret.write[i] = _hex_char(four_bits);
+ }
+ return String::utf8(ret.ptr(), ret.size());
+ }
+
+public:
+ uint32_t high_bits;
+ uint32_t mid_bits;
+ uint32_t low_bits;
+
+ String str() const {
+ return _hex_pad(high_bits) + _hex_pad(mid_bits) + _hex_pad(low_bits);
+ }
+
+ PbxId &operator++() {
+ low_bits++;
+ if (!low_bits) {
+ mid_bits++;
+ if (!mid_bits) {
+ high_bits++;
+ }
+ }
+
+ return *this;
+ }
+};
+
+struct ExportLibsData {
+ Vector lib_paths;
+ String dest_dir;
+};
+
+void EditorExportPlatformTVOS::_add_assets_to_project(const Ref &p_preset, Vector &p_project_data, const Vector &p_additional_assets) {
+ // that is just a random number, we just need Godot IDs not to clash with
+ // existing IDs in the project.
+ PbxId current_id = { 0x58938401, 0, 0 };
+ String pbx_files;
+ String pbx_frameworks_build;
+ String pbx_frameworks_refs;
+ String pbx_resources_build;
+ String pbx_resources_refs;
+ String pbx_embeded_frameworks;
+
+ const String file_info_format = String("$build_id = {isa = PBXBuildFile; fileRef = $ref_id; };\n") +
+ "$ref_id = {isa = PBXFileReference; lastKnownFileType = $file_type; name = \"$name\"; path = \"$file_path\"; sourceTree = \"\"; };\n";
+
+ for (int i = 0; i < p_additional_assets.size(); ++i) {
+ String additional_asset_info_format = file_info_format;
+
+ String build_id = (++current_id).str();
+ String ref_id = (++current_id).str();
+ String framework_id = "";
+
+ const TVOSExportAsset &asset = p_additional_assets[i];
+
+ String type;
+ if (asset.exported_path.ends_with(".framework")) {
+ if (asset.should_embed) {
+ additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n";
+ framework_id = (++current_id).str();
+ pbx_embeded_frameworks += framework_id + ",\n";
+ }
+
+ type = "wrapper.framework";
+ } else if (asset.exported_path.ends_with(".xcframework")) {
+ if (asset.should_embed) {
+ additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n";
+ framework_id = (++current_id).str();
+ pbx_embeded_frameworks += framework_id + ",\n";
+ }
+
+ type = "wrapper.xcframework";
+ } else if (asset.exported_path.ends_with(".dylib")) {
+ type = "compiled.mach-o.dylib";
+ } else if (asset.exported_path.ends_with(".a")) {
+ type = "archive.ar";
+ } else {
+ type = "file";
+ }
+
+ String &pbx_build = asset.is_framework ? pbx_frameworks_build : pbx_resources_build;
+ String &pbx_refs = asset.is_framework ? pbx_frameworks_refs : pbx_resources_refs;
+
+ if (pbx_build.length() > 0) {
+ pbx_build += ",\n";
+ pbx_refs += ",\n";
+ }
+ pbx_build += build_id;
+ pbx_refs += ref_id;
+
+ Dictionary format_dict;
+ format_dict["build_id"] = build_id;
+ format_dict["ref_id"] = ref_id;
+ format_dict["name"] = asset.exported_path.get_file();
+ format_dict["file_path"] = asset.exported_path;
+ format_dict["file_type"] = type;
+ if (framework_id.length() > 0) {
+ format_dict["framework_id"] = framework_id;
+ }
+ pbx_files += additional_asset_info_format.format(format_dict, "$_");
+ }
+
+ // Note, frameworks like gamekit are always included in our project.pbxprof file
+ // even if turned off in capabilities.
+
+ String str = String::utf8((const char *)p_project_data.ptr(), p_project_data.size());
+ str = str.replace("$additional_pbx_files", pbx_files);
+ str = str.replace("$additional_pbx_frameworks_build", pbx_frameworks_build);
+ str = str.replace("$additional_pbx_frameworks_refs", pbx_frameworks_refs);
+ str = str.replace("$additional_pbx_resources_build", pbx_resources_build);
+ str = str.replace("$additional_pbx_resources_refs", pbx_resources_refs);
+ str = str.replace("$pbx_embeded_frameworks", pbx_embeded_frameworks);
+
+ CharString cs = str.utf8();
+ p_project_data.resize(cs.size() - 1);
+ for (int i = 0; i < cs.size() - 1; i++) {
+ p_project_data.write[i] = cs[i];
+ }
+}
+
+Error EditorExportPlatformTVOS::_copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector &r_exported_assets) {
+ DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'.");
+
+ String binary_name = p_out_dir.get_file().get_basename();
+
+ DirAccess *da = DirAccess::create_for_path(p_asset);
+ if (!da) {
+ memdelete(filesystem_da);
+ ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + p_asset + ".");
+ }
+ bool file_exists = da->file_exists(p_asset);
+ bool dir_exists = da->dir_exists(p_asset);
+ if (!file_exists && !dir_exists) {
+ memdelete(da);
+ memdelete(filesystem_da);
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ String base_dir = p_asset.get_base_dir().replace("res://", "");
+ String destination_dir;
+ String destination;
+ String asset_path;
+
+ bool create_framework = false;
+
+ if (p_is_framework && p_asset.ends_with(".dylib")) {
+ // For tvOS we need to turn .dylib into .framework
+ // to be able to send application to AppStore
+ asset_path = String("dylibs").plus_file(base_dir);
+
+ String file_name;
+
+ if (!p_custom_file_name) {
+ file_name = p_asset.get_basename().get_file();
+ } else {
+ file_name = *p_custom_file_name;
+ }
+
+ String framework_name = file_name + ".framework";
+
+ asset_path = asset_path.plus_file(framework_name);
+ destination_dir = p_out_dir.plus_file(asset_path);
+ destination = destination_dir.plus_file(file_name);
+ create_framework = true;
+ } else if (p_is_framework && (p_asset.ends_with(".framework") || p_asset.ends_with(".xcframework"))) {
+ asset_path = String("dylibs").plus_file(base_dir);
+
+ String file_name;
+
+ if (!p_custom_file_name) {
+ file_name = p_asset.get_file();
+ } else {
+ file_name = *p_custom_file_name;
+ }
+
+ asset_path = asset_path.plus_file(file_name);
+ destination_dir = p_out_dir.plus_file(asset_path);
+ destination = destination_dir;
+ } else {
+ asset_path = base_dir;
+
+ String file_name;
+
+ if (!p_custom_file_name) {
+ file_name = p_asset.get_file();
+ } else {
+ file_name = *p_custom_file_name;
+ }
+
+ destination_dir = p_out_dir.plus_file(asset_path);
+ asset_path = asset_path.plus_file(file_name);
+ destination = p_out_dir.plus_file(asset_path);
+ }
+
+ if (!filesystem_da->dir_exists(destination_dir)) {
+ Error make_dir_err = filesystem_da->make_dir_recursive(destination_dir);
+ if (make_dir_err) {
+ memdelete(da);
+ memdelete(filesystem_da);
+ return make_dir_err;
+ }
+ }
+
+ Error err = dir_exists ? da->copy_dir(p_asset, destination) : da->copy(p_asset, destination);
+ memdelete(da);
+ if (err) {
+ memdelete(filesystem_da);
+ return err;
+ }
+ TVOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed };
+ r_exported_assets.push_back(exported_asset);
+
+ if (create_framework) {
+ String file_name;
+
+ if (!p_custom_file_name) {
+ file_name = p_asset.get_basename().get_file();
+ } else {
+ file_name = *p_custom_file_name;
+ }
+
+ String framework_name = file_name + ".framework";
+
+ // Performing `install_name_tool -id @rpath/{name}.framework/{name} ./{name}` on dylib
+ {
+ List install_name_args;
+ install_name_args.push_back("-id");
+ install_name_args.push_back(String("@rpath").plus_file(framework_name).plus_file(file_name));
+ install_name_args.push_back(destination);
+
+ OS::get_singleton()->execute("install_name_tool", install_name_args, true);
+ }
+
+ // Creating Info.plist
+ {
+ String info_plist_format = "\n"
+ "\n"
+ "\n"
+ "\n"
+ "CFBundleShortVersionString\n"
+ "1.0\n"
+ "CFBundleIdentifier\n"
+ "com.gdnative.framework.$name\n"
+ "CFBundleName\n"
+ "$name\n"
+ "CFBundleExecutable\n"
+ "$name\n"
+ "DTPlatformName\n"
+ "appletvos\n"
+ "CFBundleInfoDictionaryVersion\n"
+ "6.0\n"
+ "CFBundleVersion\n"
+ "1\n"
+ "CFBundlePackageType\n"
+ "FMWK\n"
+ "MinimumOSVersion\n"
+ "10.0\n"
+ "\n"
+ "";
+
+ String info_plist = info_plist_format.replace("$name", file_name);
+
+ FileAccess *f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE);
+ if (f) {
+ f->store_string(info_plist);
+ f->close();
+ memdelete(f);
+ }
+ }
+ }
+
+ memdelete(filesystem_da);
+
+ return OK;
+}
+
+Error EditorExportPlatformTVOS::_export_additional_assets(const String &p_out_dir, const Vector &p_assets, bool p_is_framework, bool p_should_embed, Vector &r_exported_assets) {
+ for (int f_idx = 0; f_idx < p_assets.size(); ++f_idx) {
+ String asset = p_assets[f_idx];
+ if (!asset.begins_with("res://")) {
+ // either SDK-builtin or already a part of the export template
+ TVOSExportAsset exported_asset = { asset, p_is_framework, p_should_embed };
+ r_exported_assets.push_back(exported_asset);
+ } else {
+ Error err = _copy_asset(p_out_dir, asset, nullptr, p_is_framework, p_should_embed, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+ }
+ }
+
+ return OK;
+}
+
+Error EditorExportPlatformTVOS::_export_additional_assets(const String &p_out_dir, const Vector &p_libraries, Vector &r_exported_assets) {
+ Vector][ > export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ for (int i = 0; i < export_plugins.size(); i++) {
+ Vector linked_frameworks = export_plugins[i]->get_tvos_frameworks();
+ Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ Vector embedded_frameworks = export_plugins[i]->get_tvos_embedded_frameworks();
+ err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ Vector project_static_libs = export_plugins[i]->get_tvos_project_static_libs();
+ for (int j = 0; j < project_static_libs.size(); j++)
+ project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project
+ err = _export_additional_assets(p_out_dir, project_static_libs, true, true, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ Vector tvos_bundle_files = export_plugins[i]->get_tvos_bundle_files();
+ err = _export_additional_assets(p_out_dir, tvos_bundle_files, false, false, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+ }
+
+ Vector library_paths;
+ for (int i = 0; i < p_libraries.size(); ++i) {
+ library_paths.push_back(p_libraries[i].path);
+ }
+ Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ return OK;
+}
+
+Error EditorExportPlatformTVOS::_export_tvos_plugins(const Ref &p_preset, TVOSConfigData &p_config_data, const String &dest_dir, Vector &r_exported_assets, bool p_debug) {
+ String plugin_definition_cpp_code;
+ String plugin_initialization_cpp_code;
+ String plugin_deinitialization_cpp_code;
+
+ Vector plugin_linked_dependencies;
+ Vector plugin_embedded_dependencies;
+ Vector plugin_files;
+
+ Vector enabled_plugins = get_enabled_plugins(p_preset);
+
+ Vector added_linked_dependenciy_names;
+ Vector added_embedded_dependenciy_names;
+ HashMap plist_values;
+
+ Error err;
+
+ for (int i = 0; i < enabled_plugins.size(); i++) {
+ PluginConfigTVOS plugin = enabled_plugins[i];
+
+ // Export plugin binary.
+ String plugin_main_binary = get_plugin_main_binary(plugin, p_debug);
+ String plugin_binary_result_file = plugin.binary.get_file();
+ // We shouldn't embed .xcframework that contains static libraries.
+ // Static libraries are not embedded anyway.
+ err = _copy_asset(dest_dir, plugin_main_binary, &plugin_binary_result_file, true, false, r_exported_assets);
+
+ ERR_FAIL_COND_V(err, err);
+
+ // Adding dependencies.
+ // Use separate container for names to check for duplicates.
+ for (int j = 0; j < plugin.linked_dependencies.size(); j++) {
+ String dependency = plugin.linked_dependencies[j];
+ String name = dependency.get_file();
+
+ if (added_linked_dependenciy_names.find(name) != -1) {
+ continue;
+ }
+
+ added_linked_dependenciy_names.push_back(name);
+ plugin_linked_dependencies.push_back(dependency);
+ }
+
+ for (int j = 0; j < plugin.system_dependencies.size(); j++) {
+ String dependency = plugin.system_dependencies[j];
+ String name = dependency.get_file();
+
+ if (added_linked_dependenciy_names.find(name) != -1) {
+ continue;
+ }
+
+ added_linked_dependenciy_names.push_back(name);
+ plugin_linked_dependencies.push_back(dependency);
+ }
+
+ for (int j = 0; j < plugin.embedded_dependencies.size(); j++) {
+ String dependency = plugin.embedded_dependencies[j];
+ String name = dependency.get_file();
+
+ if (added_embedded_dependenciy_names.find(name) != -1) {
+ continue;
+ }
+
+ added_embedded_dependenciy_names.push_back(name);
+ plugin_embedded_dependencies.push_back(dependency);
+ }
+
+ plugin_files.append_array(plugin.files_to_copy);
+
+ // Capabilities
+ // Also checking for duplicates.
+ for (int j = 0; j < plugin.capabilities.size(); j++) {
+ String capability = plugin.capabilities[j];
+
+ if (p_config_data.capabilities.find(capability) != -1) {
+ continue;
+ }
+
+ p_config_data.capabilities.push_back(capability);
+ }
+
+ // Plist
+ // Using hash map container to remove duplicates
+ const String *K = nullptr;
+
+ while ((K = plugin.plist.next(K))) {
+ String key = *K;
+ String value = plugin.plist[key];
+
+ if (key.empty() || value.empty()) {
+ continue;
+ }
+
+ plist_values[key] = value;
+ }
+
+ // CPP Code
+ String definition_comment = "// Plugin: " + plugin.name + "\n";
+ String initialization_method = plugin.initialization_method + "();\n";
+ String deinitialization_method = plugin.deinitialization_method + "();\n";
+
+ plugin_definition_cpp_code += definition_comment +
+ "extern void " + initialization_method +
+ "extern void " + deinitialization_method + "\n";
+
+ plugin_initialization_cpp_code += "\t" + initialization_method;
+ plugin_deinitialization_cpp_code += "\t" + deinitialization_method;
+ }
+
+ // Updating `Info.plist`
+ {
+ const String *K = nullptr;
+ while ((K = plist_values.next(K))) {
+ String key = *K;
+ String value = plist_values[key];
+
+ if (key.empty() || value.empty()) {
+ continue;
+ }
+
+ p_config_data.plist_content += "" + key + "" + value + "\n";
+ }
+ }
+
+ // Export files
+ {
+ // Export linked plugin dependency
+ err = _export_additional_assets(dest_dir, plugin_linked_dependencies, true, false, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ // Export embedded plugin dependency
+ err = _export_additional_assets(dest_dir, plugin_embedded_dependencies, true, true, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+
+ // Export plugin files
+ err = _export_additional_assets(dest_dir, plugin_files, false, false, r_exported_assets);
+ ERR_FAIL_COND_V(err, err);
+ }
+
+ // Update CPP
+ {
+ Dictionary plugin_format;
+ plugin_format["definition"] = plugin_definition_cpp_code;
+ plugin_format["initialization"] = plugin_initialization_cpp_code;
+ plugin_format["deinitialization"] = plugin_deinitialization_cpp_code;
+
+ String plugin_cpp_code = "\n// Godot Plugins\n"
+ "void godot_tvos_plugins_initialize();\n"
+ "void godot_tvos_plugins_deinitialize();\n"
+ "// Exported Plugins\n\n"
+ "$definition"
+ "// Use Plugins\n"
+ "void godot_tvos_plugins_initialize() {\n"
+ "$initialization"
+ "}\n\n"
+ "void godot_tvos_plugins_deinitialize() {\n"
+ "$deinitialization"
+ "}\n";
+
+ p_config_data.cpp_code += plugin_cpp_code.format(plugin_format, "$_");
+ }
+ return OK;
+}
+
+Error EditorExportPlatformTVOS::export_project(const Ref &p_preset, bool p_debug, const String &p_path, int p_flags) {
+ ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
+
+ String src_pkg_name;
+ String dest_dir = p_path.get_base_dir() + "/";
+ String binary_name = p_path.get_file().get_basename();
+
+ EditorProgress ep("export", "Exporting for tvOS", 5, true);
+
+ String team_id = p_preset->get("application/app_store_team_id");
+
+ if (p_debug)
+ src_pkg_name = p_preset->get("custom_template/debug");
+ else
+ src_pkg_name = p_preset->get("custom_template/release");
+
+ if (src_pkg_name == "") {
+ String err;
+ src_pkg_name = find_export_template("tvos.zip", &err);
+ if (src_pkg_name == "") {
+ EditorNode::add_io_error(err);
+ return ERR_FILE_NOT_FOUND;
+ }
+ }
+
+ if (!DirAccess::exists(dest_dir)) {
+ return ERR_FILE_BAD_PATH;
+ }
+
+ DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (da) {
+ String current_dir = da->get_current_dir();
+
+ // remove leftovers from last export so they don't interfere
+ // in case some files are no longer needed
+ if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) {
+ da->erase_contents_recursive();
+ }
+ if (da->change_dir(dest_dir + binary_name) == OK) {
+ da->erase_contents_recursive();
+ }
+
+ da->change_dir(current_dir);
+
+ if (!da->dir_exists(dest_dir + binary_name)) {
+ Error err = da->make_dir(dest_dir + binary_name);
+ if (err) {
+ memdelete(da);
+ return err;
+ }
+ }
+ memdelete(da);
+ }
+
+ if (ep.step("Making .pck", 0)) {
+ return ERR_SKIP;
+ }
+ String pack_path = dest_dir + binary_name + ".pck";
+ Vector libraries;
+ Error err = save_pack(p_preset, pack_path, &libraries);
+ if (err)
+ return err;
+
+ if (ep.step("Extracting and configuring Xcode project", 1)) {
+ return ERR_SKIP;
+ }
+
+ String library_to_use = "libgodot.tvos." + String(p_debug ? "debug" : "release") + ".fat.a";
+
+ print_line("Static library: " + library_to_use);
+ String pkg_name;
+ if (p_preset->get("application/name") != "")
+ pkg_name = p_preset->get("application/name"); // app_name
+ else if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "")
+ pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name"));
+ else
+ pkg_name = "Unnamed";
+
+ bool found_library = false;
+ int total_size = 0;
+
+ const String project_file = "godot_tvos.xcodeproj/project.pbxproj";
+ Set files_to_parse;
+ files_to_parse.insert("godot_tvos/Info.plist");
+ files_to_parse.insert(project_file);
+ files_to_parse.insert("godot_tvos/dummy.cpp");
+ files_to_parse.insert("godot_tvos.xcodeproj/project.xcworkspace/contents.xcworkspacedata");
+ files_to_parse.insert("godot_tvos.xcodeproj/xcshareddata/xcschemes/godot_tvos.xcscheme");
+ files_to_parse.insert("godot_tvos/Launch Screen.storyboard");
+
+ TVOSConfigData config_data = {
+ pkg_name,
+ binary_name,
+ _get_additional_plist_content(),
+ _get_linker_flags(),
+ _get_cpp_code(),
+ "",
+ "",
+ "",
+ "",
+ Vector()
+ };
+
+ Vector assets;
+
+ DirAccess *tmp_app_path = DirAccess::create_for_path(dest_dir);
+ ERR_FAIL_COND_V(!tmp_app_path, ERR_CANT_CREATE);
+
+ print_line("Unzipping...");
+ FileAccess *src_f = NULL;
+ zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
+ unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
+ if (!src_pkg_zip) {
+ EditorNode::add_io_error("Could not open export template (not a zip file?):\n" + src_pkg_name);
+ return ERR_CANT_OPEN;
+ }
+
+ err = _export_tvos_plugins(p_preset, config_data, dest_dir + binary_name, assets, p_debug);
+ ERR_FAIL_COND_V(err, err);
+
+ //export rest of the files
+ int ret = unzGoToFirstFile(src_pkg_zip);
+ Vector project_file_data;
+ while (ret == UNZ_OK) {
+#if defined(OSX_ENABLED) || defined(X11_ENABLED)
+ bool is_execute = false;
+#endif
+
+ //get filename
+ unz_file_info info;
+ char fname[16384];
+ ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, NULL, 0, NULL, 0);
+
+ String file = fname;
+
+ print_line("READ: " + file);
+ Vector data;
+ data.resize(info.uncompressed_size);
+
+ //read
+ unzOpenCurrentFile(src_pkg_zip);
+ unzReadCurrentFile(src_pkg_zip, data.ptrw(), data.size());
+ unzCloseCurrentFile(src_pkg_zip);
+
+ //write
+
+ file = file.replace_first("tvos/", "");
+
+ if (files_to_parse.has(file)) {
+ _fix_config_file(p_preset, data, config_data, p_debug);
+ } else if (file.begins_with("libgodot.tvos")) {
+ if (file != library_to_use) {
+ ret = unzGoToNextFile(src_pkg_zip);
+ continue; //ignore!
+ }
+ found_library = true;
+#if defined(OSX_ENABLED) || defined(X11_ENABLED)
+ is_execute = true;
+#endif
+ file = "godot_tvos.a";
+ }
+
+ if (file == project_file) {
+ project_file_data = data;
+ }
+
+ ///@TODO need to parse logo files
+
+ if (data.size() > 0) {
+ file = file.replace("godot_tvos", binary_name);
+
+ print_line("ADDING: " + file + " size: " + itos(data.size()));
+ total_size += data.size();
+
+ /* write it into our folder structure */
+ file = dest_dir + file;
+
+ /* make sure this folder exists */
+ String dir_name = file.get_base_dir();
+ if (!tmp_app_path->dir_exists(dir_name)) {
+ print_line("Creating " + dir_name);
+ Error dir_err = tmp_app_path->make_dir_recursive(dir_name);
+ if (dir_err) {
+ ERR_PRINTS("Can't create '" + dir_name + "'.");
+ unzClose(src_pkg_zip);
+ memdelete(tmp_app_path);
+ return ERR_CANT_CREATE;
+ }
+ }
+
+ /* write the file */
+ FileAccess *f = FileAccess::open(file, FileAccess::WRITE);
+ if (!f) {
+ ERR_PRINTS("Can't write '" + file + "'.");
+ unzClose(src_pkg_zip);
+ memdelete(tmp_app_path);
+ return ERR_CANT_CREATE;
+ };
+ f->store_buffer(data.ptr(), data.size());
+ f->close();
+ memdelete(f);
+
+#if defined(OSX_ENABLED) || defined(X11_ENABLED)
+ if (is_execute) {
+ // we need execute rights on this file
+ chmod(file.utf8().get_data(), 0755);
+ }
+#endif
+ }
+
+ ret = unzGoToNextFile(src_pkg_zip);
+ }
+
+ /* we're done with our source zip */
+ unzClose(src_pkg_zip);
+
+ if (!found_library) {
+ ERR_PRINTS("Requested template library '" + library_to_use + "' not found. It might be missing from your template archive.");
+ memdelete(tmp_app_path);
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ // Copy project static libs to the project
+ Vector][ > export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ for (int i = 0; i < export_plugins.size(); i++) {
+ Vector project_static_libs = export_plugins[i]->get_tvos_project_static_libs();
+ for (int j = 0; j < project_static_libs.size(); j++) {
+ const String &static_lib_path = project_static_libs[j];
+ String dest_lib_file_path = dest_dir + static_lib_path.get_file();
+ Error lib_copy_err = tmp_app_path->copy(static_lib_path, dest_lib_file_path);
+ if (lib_copy_err != OK) {
+ ERR_PRINTS("Can't copy '" + static_lib_path + "'.");
+ memdelete(tmp_app_path);
+ return lib_copy_err;
+ }
+ }
+ }
+
+ print_line("Exporting additional assets");
+ _export_additional_assets(dest_dir + binary_name, libraries, assets);
+ _add_assets_to_project(p_preset, project_file_data, assets);
+ String project_file_name = dest_dir + binary_name + ".xcodeproj/project.pbxproj";
+ FileAccess *f = FileAccess::open(project_file_name, FileAccess::WRITE);
+ if (!f) {
+ ERR_PRINTS("Can't write '" + project_file_name + "'.");
+ return ERR_CANT_CREATE;
+ };
+ f->store_buffer(project_file_data.ptr(), project_file_data.size());
+ f->close();
+ memdelete(f);
+
+ return OK;
+}
+
+bool EditorExportPlatformTVOS::can_export(const Ref &p_preset, String &r_error, bool &r_missing_templates) const {
+
+ String err;
+ bool valid = false;
+
+ // Look for export templates (first official, and if defined custom templates).
+
+ bool dvalid = exists_export_template("tvos.zip", &err);
+ bool rvalid = dvalid; // Both in the same ZIP.
+
+ if (p_preset->get("custom_template/debug") != "") {
+ dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
+ if (!dvalid) {
+ err += TTR("Custom debug template not found.") + "\n";
+ }
+ }
+ if (p_preset->get("custom_template/release") != "") {
+ rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
+ if (!rvalid) {
+ err += TTR("Custom release template not found.") + "\n";
+ }
+ }
+
+ valid = dvalid || rvalid;
+ r_missing_templates = !valid;
+
+ // Validate the rest of the configuration.
+
+ String identifier = p_preset->get("application/bundle_identifier");
+ String pn_err;
+ if (!is_package_name_valid(identifier, &pn_err)) {
+ err += TTR("Invalid Identifier:") + " " + pn_err + "\n";
+ valid = false;
+ }
+
+ String etc_error = test_etc2_or_pvrtc();
+ if (etc_error != String()) {
+ valid = false;
+ err += etc_error;
+ }
+
+ if (!err.empty())
+ r_error = err;
+
+ return valid;
+}
+
+EditorExportPlatformTVOS::EditorExportPlatformTVOS() {
+
+ Ref img = memnew(Image(_tvos_logo));
+ logo.instance();
+ logo->create_from_image(img);
+
+ plugins_changed.set();
+
+ check_for_changes_thread.start(_check_for_changes_poll_thread, this);
+}
+
+EditorExportPlatformTVOS::~EditorExportPlatformTVOS() {
+ quit_request.set();
+ check_for_changes_thread.wait_to_finish();
+}
+
+void register_tvos_exporter() {
+
+ Ref platform;
+ platform.instance();
+
+ EditorExport::get_singleton()->add_export_platform(platform);
+}
diff --git a/platform/tvos/export/export.h b/platform/tvos/export/export.h
new file mode 100644
index 000000000000..d86e67125a0c
--- /dev/null
+++ b/platform/tvos/export/export.h
@@ -0,0 +1,36 @@
+/*************************************************************************/
+/* export.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TVOS_EXPORT_H
+#define TVOS_EXPORT_H
+
+void register_tvos_exporter();
+
+#endif // TVOS_EXPORT_H
diff --git a/platform/tvos/godot_app_delegate.h b/platform/tvos/godot_app_delegate.h
new file mode 100644
index 000000000000..6335ada50ebe
--- /dev/null
+++ b/platform/tvos/godot_app_delegate.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* godot_app_delegate.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+typedef NSObject ApplicationDelegateService;
+
+@interface GodotApplicalitionDelegate : NSObject
+
+@property(class, readonly, strong) NSArray *services;
+
++ (void)addService:(ApplicationDelegateService *)service;
+
+@end
diff --git a/platform/tvos/godot_app_delegate.m b/platform/tvos/godot_app_delegate.m
new file mode 100644
index 000000000000..d6c429bb3790
--- /dev/null
+++ b/platform/tvos/godot_app_delegate.m
@@ -0,0 +1,453 @@
+/*************************************************************************/
+/* godot_app_delegate.m */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "godot_app_delegate.h"
+
+#import "app_delegate.h"
+
+@interface GodotApplicalitionDelegate ()
+
+@end
+
+@implementation GodotApplicalitionDelegate
+
+static NSMutableArray *services = nil;
+
++ (NSArray *)services {
+ return services;
+}
+
++ (void)load {
+ services = [NSMutableArray new];
+ [services addObject:[AppDelegate new]];
+}
+
++ (void)addService:(ApplicationDelegateService *)service {
+ if (!services || !service) {
+ return;
+ }
+ [services addObject:service];
+}
+
+// UIApplicationDelegate documantation can be found here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate
+
+// MARK: Window
+
+- (UIWindow *)window {
+ UIWindow *result = nil;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ UIWindow *value = [service window];
+
+ if (value) {
+ result = value;
+ }
+ }
+
+ return result;
+}
+
+// MARK: Initializing
+
+- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application willFinishLaunchingWithOptions:launchOptions]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application didFinishLaunchingWithOptions:launchOptions]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+/* Can be handled by Info.plist. Not yet supported by Godot.
+
+// MARK: Scene
+
+- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {}
+
+- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions {}
+
+*/
+
+// MARK: Life-Cycle
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationDidBecomeActive:application];
+ }
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationWillResignActive:application];
+ }
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationDidEnterBackground:application];
+ }
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationWillEnterForeground:application];
+ }
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationWillTerminate:application];
+ }
+}
+
+// MARK: Environment Changes
+
+- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationProtectedDataDidBecomeAvailable:application];
+ }
+}
+
+- (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationProtectedDataWillBecomeUnavailable:application];
+ }
+}
+
+- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationDidReceiveMemoryWarning:application];
+ }
+}
+
+- (void)applicationSignificantTimeChange:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationSignificantTimeChange:application];
+ }
+}
+
+// MARK: App State Restoration
+
+- (BOOL)application:(UIApplication *)application shouldSaveSecureApplicationState:(NSCoder *)coder API_AVAILABLE(ios(13.2)) {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application shouldSaveSecureApplicationState:coder]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+- (BOOL)application:(UIApplication *)application shouldRestoreSecureApplicationState:(NSCoder *)coder API_AVAILABLE(ios(13.2)) {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application shouldRestoreSecureApplicationState:coder]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ UIViewController *controller = [service application:application viewControllerWithRestorationIdentifierPath:identifierComponents coder:coder];
+
+ if (controller) {
+ return controller;
+ }
+ }
+
+ return nil;
+}
+
+- (void)application:(UIApplication *)application willEncodeRestorableStateWithCoder:(NSCoder *)coder {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application willEncodeRestorableStateWithCoder:coder];
+ }
+}
+
+- (void)application:(UIApplication *)application didDecodeRestorableStateWithCoder:(NSCoder *)coder {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application didDecodeRestorableStateWithCoder:coder];
+ }
+}
+
+// MARK: Download Data in Background
+
+- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application handleEventsForBackgroundURLSession:identifier completionHandler:completionHandler];
+ }
+
+ completionHandler();
+}
+
+// MARK: User Activity and Handling Quick Actions
+
+- (BOOL)application:(UIApplication *)application willContinueUserActivityWithType:(NSString *)userActivityType {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application willContinueUserActivityWithType:userActivityType]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray > *restorableObjects))restorationHandler {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application continueUserActivity:userActivity restorationHandler:restorationHandler]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+- (void)application:(UIApplication *)application didUpdateUserActivity:(NSUserActivity *)userActivity {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application didUpdateUserActivity:userActivity];
+ }
+}
+
+- (void)application:(UIApplication *)application didFailToContinueUserActivityWithType:(NSString *)userActivityType error:(NSError *)error {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application didFailToContinueUserActivityWithType:userActivityType error:error];
+ }
+}
+
+// MARK: WatchKit
+
+- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application handleWatchKitExtensionRequest:userInfo reply:reply];
+ }
+}
+
+// MARK: HealthKit
+
+- (void)applicationShouldRequestHealthAuthorization:(UIApplication *)application {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service applicationShouldRequestHealthAuthorization:application];
+ }
+}
+
+// MARK: Opening an URL
+
+- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:app openURL:url options:options]) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+// MARK: Disallowing Specified App Extension Types
+
+- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier {
+ BOOL result = NO;
+
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ if ([service application:application shouldAllowExtensionPointIdentifier:extensionPointIdentifier]) {
+ result = YES;
+ }
+ }
+
+ return result;
+}
+
+// MARK: SiriKit
+
+- (id)application:(UIApplication *)application handlerForIntent:(INIntent *)intent API_AVAILABLE(ios(14.0)) {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ id result = [service application:application handlerForIntent:intent];
+
+ if (result) {
+ return result;
+ }
+ }
+
+ return nil;
+}
+
+// MARK: CloudKit
+
+- (void)application:(UIApplication *)application userDidAcceptCloudKitShareWithMetadata:(CKShareMetadata *)cloudKitShareMetadata {
+ for (ApplicationDelegateService *service in services) {
+ if (![service respondsToSelector:_cmd]) {
+ continue;
+ }
+
+ [service application:application userDidAcceptCloudKitShareWithMetadata:cloudKitShareMetadata];
+ }
+}
+
+/* Handled By Info.plist file for now
+
+// MARK: Interface Geometry
+
+- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {}
+
+*/
+
+@end
diff --git a/platform/tvos/godot_appletv.mm b/platform/tvos/godot_appletv.mm
new file mode 100644
index 000000000000..4750f786eb5f
--- /dev/null
+++ b/platform/tvos/godot_appletv.mm
@@ -0,0 +1,116 @@
+/*************************************************************************/
+/* godot_appletv.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "core/ustring.h"
+#include "main/main.h"
+#include "os_appletv.h"
+
+#include
+#include
+#include
+
+static OSAppleTV *os = NULL;
+
+int add_path(int p_argc, char **p_args) {
+ NSString *str = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_path"];
+ if (!str) {
+ return p_argc;
+ }
+
+ p_args[p_argc++] = (char *)"--path";
+ p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding];
+ p_args[p_argc] = NULL;
+
+ return p_argc;
+}
+
+int add_cmdline(int p_argc, char **p_args) {
+ NSArray *arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"];
+ if (!arr) {
+ return p_argc;
+ }
+
+ for (NSUInteger i = 0; i < [arr count]; i++) {
+ NSString *str = [arr objectAtIndex:i];
+ if (!str) {
+ continue;
+ }
+ p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding];
+ }
+
+ p_args[p_argc] = NULL;
+
+ return p_argc;
+}
+
+int appletv_main(int argc, char **argv, String data_dir) {
+ size_t len = strlen(argv[0]);
+
+ while (len--) {
+ if (argv[0][len] == '/') break;
+ }
+
+ if (len >= 0) {
+ char path[512];
+ memcpy(path, argv[0], len > sizeof(path) ? sizeof(path) : len);
+ path[len] = 0;
+ printf("Path: %s\n", path);
+ chdir(path);
+ }
+
+ printf("godot_appletv %s\n", argv[0]);
+ char cwd[512];
+ getcwd(cwd, sizeof(cwd));
+ printf("cwd %s\n", cwd);
+ os = new OSAppleTV(data_dir);
+
+ char *fargv[64];
+ for (int i = 0; i < argc; i++) {
+ fargv[i] = argv[i];
+ }
+ fargv[argc] = NULL;
+ argc = add_path(argc, fargv);
+ argc = add_cmdline(argc, fargv);
+
+ printf("os created\n");
+ Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false);
+ printf("setup %i\n", err);
+ if (err != OK) {
+ return 255;
+ }
+
+ return 0;
+}
+
+void appletv_finish() {
+ printf("appletv_finish\n");
+ Main::cleanup();
+ delete os;
+}
diff --git a/platform/tvos/godot_view.h b/platform/tvos/godot_view.h
new file mode 100644
index 000000000000..759e21fe55b5
--- /dev/null
+++ b/platform/tvos/godot_view.h
@@ -0,0 +1,68 @@
+/*************************************************************************/
+/* godot_view.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+#import
+#import
+#import
+#import
+#import
+
+class String;
+
+@class GodotView;
+@protocol DisplayLayer;
+@protocol GodotViewRendererProtocol;
+
+@protocol GodotViewDelegate
+
+- (BOOL)godotViewFinishedSetup:(GodotView *)view;
+
+@end
+
+@interface GodotView : UIView
+
+@property(assign, nonatomic) id renderer;
+@property(assign, nonatomic) id delegate;
+
+@property(assign, readonly, nonatomic) BOOL isActive;
+
+@property(strong, readonly, nonatomic) CALayer *renderingLayer;
+@property(assign, readonly, nonatomic) BOOL canRender;
+
+@property(assign, nonatomic) NSTimeInterval renderingInterval;
+
+- (CALayer *)initializeRendering;
+- (void)stopRendering;
+- (void)startRendering;
+
+@property(nonatomic, assign) BOOL useCADisplayLink;
+
+@end
diff --git a/platform/tvos/godot_view.mm b/platform/tvos/godot_view.mm
new file mode 100644
index 000000000000..dd554f48e28d
--- /dev/null
+++ b/platform/tvos/godot_view.mm
@@ -0,0 +1,308 @@
+/*************************************************************************/
+/* godot_view.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "godot_view.h"
+
+#include "core/project_settings.h"
+#include "os_appletv.h"
+#include "servers/audio_server.h"
+
+#import
+#import
+
+#import "display_layer.h"
+#import "godot_view_gesture_recognizer.h"
+#import "godot_view_renderer.h"
+
+@interface GodotView ()
+
+@property(assign, nonatomic) BOOL isActive;
+
+// CADisplayLink available on 3.1+ synchronizes the animation timer & drawing with the refresh rate of the display, only supports animation intervals of 1/60 1/30 & 1/15
+@property(strong, nonatomic) CADisplayLink *displayLink;
+
+// An animation timer that, when animation is started, will periodically call -drawView at the given rate.
+// Only used if CADisplayLink is not
+@property(strong, nonatomic) NSTimer *animationTimer;
+
+@property(strong, nonatomic) CALayer *renderingLayer;
+
+@property(strong, nonatomic) GodotViewGestureRecognizer *delayGestureRecognizer;
+@end
+
+@implementation GodotView
+
+// Implement this to override the default layer class (which is [CALayer class]).
+// We do this so that our view will be backed by a layer that is capable of OpenGL ES rendering.
+- (CALayer *)initializeRendering {
+ if (self.renderingLayer) {
+ return self.renderingLayer;
+ }
+
+ CALayer *layer = [GodotOpenGLLayer layer];
+
+ layer.frame = self.bounds;
+ layer.contentsScale = self.contentScaleFactor;
+
+ [self.layer addSublayer:layer];
+ self.renderingLayer = layer;
+
+ [layer initializeDisplayLayer];
+
+ return self.renderingLayer;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+ self = [super initWithCoder:coder];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+// Stop animating and release resources when they are no longer needed.
+- (void)dealloc {
+ [self stopRendering];
+
+ self.renderer = nil;
+ self.delegate = nil;
+
+ if (self.renderingLayer) {
+ [self.renderingLayer removeFromSuperlayer];
+ self.renderingLayer = nil;
+ }
+
+ if (self.displayLink) {
+ [self.displayLink invalidate];
+ self.displayLink = nil;
+ }
+
+ if (self.animationTimer) {
+ [self.animationTimer invalidate];
+ self.animationTimer = nil;
+ }
+
+ if (self.delayGestureRecognizer) {
+ self.delayGestureRecognizer = nil;
+ }
+}
+
+- (void)godot_commonInit {
+ self.contentScaleFactor = [UIScreen mainScreen].nativeScale;
+
+ // Initialize delay gesture recognizer
+ GodotViewGestureRecognizer *gestureRecognizer = [[GodotViewGestureRecognizer alloc] init];
+ self.delayGestureRecognizer = gestureRecognizer;
+ [self addGestureRecognizer:self.delayGestureRecognizer];
+}
+
+- (void)startRendering {
+ if (self.isActive) {
+ return;
+ }
+
+ self.isActive = YES;
+
+ printf("start animation!\n");
+
+ if (self.useCADisplayLink) {
+ self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)];
+
+ // Approximate frame rate
+ // assumes device refreshes at 60 fps
+ int displayFPS = (NSInteger)(1.0 / self.renderingInterval);
+
+ self.displayLink.preferredFramesPerSecond = displayFPS;
+
+ // Setup DisplayLink in main thread
+ [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
+ } else {
+ self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:self.renderingInterval target:self selector:@selector(drawView) userInfo:nil repeats:YES];
+ }
+}
+
+- (void)stopRendering {
+ if (!self.isActive) {
+ return;
+ }
+
+ self.isActive = NO;
+
+ printf("******** stop animation!\n");
+
+ if (self.useCADisplayLink) {
+ [self.displayLink invalidate];
+ self.displayLink = nil;
+ } else {
+ [self.animationTimer invalidate];
+ self.animationTimer = nil;
+ }
+}
+
+// Updates the OpenGL view when the timer fires
+- (void)drawView {
+ if (!self.isActive) {
+ printf("draw view not active!\n");
+ return;
+ }
+
+ if (self.useCADisplayLink) {
+ // Pause the CADisplayLink to avoid recursion
+ [self.displayLink setPaused:YES];
+
+ // Process all input events
+ while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource)
+ ;
+
+ // We are good to go, resume the CADisplayLink
+ [self.displayLink setPaused:NO];
+ }
+
+ [self.renderingLayer startRenderDisplayLayer];
+
+ if (!self.renderer) {
+ return;
+ }
+
+ if ([self.renderer setupView:self]) {
+ return;
+ }
+
+ if (self.delegate) {
+ BOOL delegateFinishedSetup = [self.delegate godotViewFinishedSetup:self];
+
+ if (!delegateFinishedSetup) {
+ return;
+ }
+ }
+
+ [self.renderer renderOnView:self];
+
+ [self.renderingLayer stopRenderDisplayLayer];
+}
+
+- (BOOL)canRender {
+ if (self.useCADisplayLink) {
+ return self.displayLink != nil;
+ } else {
+ return self.animationTimer != nil;
+ }
+}
+
+- (void)setRenderingInterval:(NSTimeInterval)renderingInterval {
+ _renderingInterval = renderingInterval;
+
+ if (self.canRender) {
+ [self stopRendering];
+ [self startRendering];
+ }
+}
+
+// If our view is resized, we'll be asked to layout subviews.
+// This is the perfect opportunity to also update the framebuffer so that it is
+// the same size as our display area.
+
+- (void)layoutSubviews {
+ if (self.renderingLayer) {
+ self.renderingLayer.frame = self.bounds;
+ [self.renderingLayer layoutDisplayLayer];
+ }
+
+ [super layoutSubviews];
+}
+
+// MARK: - Input
+
+// MARK: Menu Button
+
+- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ if (!self.delayGestureRecognizer.overridesRemoteButtons) {
+ return [super pressesEnded:presses withEvent:event];
+ }
+
+ NSArray *tlist = [event.allPresses allObjects];
+
+ for (UIPress *press in tlist) {
+ if ([presses containsObject:press] && press.type == UIPressTypeMenu) {
+ int joy_id = OSAppleTV::get_singleton()->joy_id_for_name("Remote");
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_START, true);
+ } else {
+ [super pressesBegan:presses withEvent:event];
+ }
+ }
+}
+
+- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ if (!self.delayGestureRecognizer.overridesRemoteButtons) {
+ return [super pressesEnded:presses withEvent:event];
+ }
+
+ NSArray *tlist = [presses allObjects];
+
+ for (UIPress *press in tlist) {
+ if ([presses containsObject:press] && press.type == UIPressTypeMenu) {
+ int joy_id = OSAppleTV::get_singleton()->joy_id_for_name("Remote");
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_START, false);
+ } else {
+ [super pressesEnded:presses withEvent:event];
+ }
+ }
+}
+
+- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ if (!self.delayGestureRecognizer.overridesRemoteButtons) {
+ return [super pressesEnded:presses withEvent:event];
+ }
+
+ NSArray *tlist = [event.allPresses allObjects];
+
+ for (UIPress *press in tlist) {
+ if ([presses containsObject:press] && press.type == UIPressTypeMenu) {
+ int joy_id = OSAppleTV::get_singleton()->joy_id_for_name("Remote");
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_START, false);
+ } else {
+ [super pressesCancelled:presses withEvent:event];
+ }
+ }
+}
+
+@end
diff --git a/platform/tvos/godot_view_gesture_recognizer.h b/platform/tvos/godot_view_gesture_recognizer.h
new file mode 100644
index 000000000000..85a6d2a5bbe3
--- /dev/null
+++ b/platform/tvos/godot_view_gesture_recognizer.h
@@ -0,0 +1,47 @@
+/*************************************************************************/
+/* godot_view_gesture_recognizer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+// GodotViewGestureRecognizer allows iOS gestures to work currectly by
+// emulating UIScrollView's UIScrollViewDelayedTouchesBeganGestureRecognizer.
+// It catches all gestures incoming to UIView and delays them for 150ms
+// (the same value used by UIScrollViewDelayedTouchesBeganGestureRecognizer)
+// If touch cancellation or end message is fired it fires delayed
+// begin touch immediately as well as last touch signal
+
+#import
+
+@interface GodotViewGestureRecognizer : UIGestureRecognizer
+
+@property(nonatomic, readonly, assign) NSTimeInterval delayTimeInterval;
+@property(nonatomic, readonly, assign) BOOL overridesRemoteButtons;
+
+- (instancetype)init;
+
+@end
diff --git a/platform/tvos/godot_view_gesture_recognizer.mm b/platform/tvos/godot_view_gesture_recognizer.mm
new file mode 100644
index 000000000000..5f6558322141
--- /dev/null
+++ b/platform/tvos/godot_view_gesture_recognizer.mm
@@ -0,0 +1,123 @@
+/*************************************************************************/
+/* godot_view_gesture_recognizer.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "godot_view_gesture_recognizer.h"
+
+#include "core/project_settings.h"
+#include "os_appletv.h"
+
+@interface GodotViewGestureRecognizer ()
+
+// Timer used to delay end press message.
+@property(nonatomic, readwrite, strong) NSTimer *delayTimer;
+
+// Delayed touch parameters
+@property(nonatomic, readwrite, copy) NSSet *delayedPresses;
+@property(nonatomic, readwrite, strong) UIPressesEvent *delayedEvent;
+
+@property(nonatomic, readwrite, assign) NSTimeInterval delayTimeInterval;
+
+@end
+
+@implementation GodotViewGestureRecognizer
+
+- (instancetype)init {
+ self = [super init];
+
+ self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/tvos/press_end_delay");
+ self.allowedPressTypes = @[ @(UIPressTypeMenu) ];
+
+ return self;
+}
+
+- (void)delayPresses:(NSSet *)presses andEvent:(UIPressesEvent *)event {
+ [self.delayTimer fire];
+
+ self.delayedPresses = presses;
+ self.delayedEvent = event;
+
+ self.delayTimer = [NSTimer scheduledTimerWithTimeInterval:self.delayTimeInterval target:self selector:@selector(fireDelayedPress:) userInfo:nil repeats:NO];
+}
+
+- (void)fireDelayedPress:(id)timer {
+ [self.delayTimer invalidate];
+ self.delayTimer = nil;
+
+ if (self.delayedPresses) {
+ [self.view pressesEnded:self.delayedPresses withEvent:self.delayedEvent];
+ }
+
+ self.delayedPresses = nil;
+ self.delayedEvent = nil;
+}
+
+- (BOOL)overridesRemoteButtons {
+ return OSAppleTV::get_singleton()->get_overrides_menu_button();
+}
+
+- (BOOL)shouldReceiveEvent:(UIEvent *)event {
+ return self.overridesRemoteButtons;
+}
+
+- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ [self.delayTimer fire];
+ [self.view pressesBegan:presses withEvent:event];
+}
+
+- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ NSSet *cleared = [self copyClearedPresses:presses type:UIPressTypeMenu phase:UIPressPhaseEnded];
+ [self delayPresses:cleared andEvent:event];
+}
+
+- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event {
+ [self cancelDelayTimer];
+ [self.view pressesCancelled:presses withEvent:event];
+};
+
+- (void)cancelDelayTimer {
+ [self.delayTimer invalidate];
+ self.delayTimer = nil;
+ self.delayedPresses = nil;
+ self.delayedEvent = nil;
+}
+
+- (NSSet *)copyClearedPresses:(NSSet *)presses type:(UIPressType)phaseToSave phase:(UIPressPhase)phase {
+ NSMutableSet *cleared = [NSMutableSet new];
+
+ for (UIPress *press in presses) {
+ if (press.type == phaseToSave && press.phase == phase) {
+ [cleared addObject:press];
+ }
+ }
+
+ return cleared;
+}
+
+@end
diff --git a/platform/tvos/godot_view_renderer.h b/platform/tvos/godot_view_renderer.h
new file mode 100644
index 000000000000..c6b0a05a4eee
--- /dev/null
+++ b/platform/tvos/godot_view_renderer.h
@@ -0,0 +1,44 @@
+/*************************************************************************/
+/* godot_view_renderer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+@protocol GodotViewRendererProtocol
+
+@property(assign, readonly, nonatomic) BOOL hasFinishedSetup;
+
+- (BOOL)setupView:(UIView *)view;
+- (void)renderOnView:(UIView *)view;
+
+@end
+
+@interface GodotViewRenderer : NSObject
+
+@end
diff --git a/platform/tvos/godot_view_renderer.mm b/platform/tvos/godot_view_renderer.mm
new file mode 100644
index 000000000000..353fdeafe13e
--- /dev/null
+++ b/platform/tvos/godot_view_renderer.mm
@@ -0,0 +1,117 @@
+/*************************************************************************/
+/* godot_view_renderer.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "godot_view_renderer.h"
+
+#include "core/os/keyboard.h"
+#include "core/project_settings.h"
+#include "main/main.h"
+#include "os_appletv.h"
+#include "servers/audio_server.h"
+
+#import
+//#import
+#import
+#import
+#import
+
+@interface GodotViewRenderer ()
+
+@property(assign, nonatomic) BOOL hasFinishedProjectDataSetup;
+@property(assign, nonatomic) BOOL hasStartedMain;
+@property(assign, nonatomic) BOOL hasFinishedSetup;
+
+@end
+
+@implementation GodotViewRenderer
+
+- (BOOL)setupView:(UIView *)view {
+ if (self.hasFinishedSetup) {
+ return NO;
+ }
+
+ if (!OS::get_singleton()) {
+ exit(0);
+ }
+
+ if (!self.hasFinishedProjectDataSetup) {
+ [self setupProjectData];
+ return YES;
+ }
+
+ if (!self.hasStartedMain) {
+ self.hasStartedMain = YES;
+ OSAppleTV::get_singleton()->start();
+ return YES;
+ }
+
+ self.hasFinishedSetup = YES;
+
+ return NO;
+}
+
+- (void)setupProjectData {
+ self.hasFinishedProjectDataSetup = YES;
+
+ Main::setup2();
+
+ // this might be necessary before here
+ NSDictionary *dict = [[NSBundle mainBundle] infoDictionary];
+ for (NSString *key in dict) {
+ NSObject *value = [dict objectForKey:key];
+ String ukey = String::utf8([key UTF8String]);
+
+ // we need a NSObject to Variant conversor
+
+ if ([value isKindOfClass:[NSString class]]) {
+ NSString *str = (NSString *)value;
+ String uval = String::utf8([str UTF8String]);
+
+ ProjectSettings::get_singleton()->set("Info.plist/" + ukey, uval);
+
+ } else if ([value isKindOfClass:[NSNumber class]]) {
+ NSNumber *n = (NSNumber *)value;
+ double dval = [n doubleValue];
+
+ ProjectSettings::get_singleton()->set("Info.plist/" + ukey, dval);
+ };
+ // do stuff
+ }
+}
+
+- (void)renderOnView:(UIView *)view {
+ if (!OSAppleTV::get_singleton()) {
+ return;
+ }
+
+ OSAppleTV::get_singleton()->iterate();
+}
+
+@end
diff --git a/platform/tvos/joypad_appletv.h b/platform/tvos/joypad_appletv.h
new file mode 100644
index 000000000000..787159632496
--- /dev/null
+++ b/platform/tvos/joypad_appletv.h
@@ -0,0 +1,54 @@
+/*************************************************************************/
+/* joypad_appletv.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+class String;
+
+@interface JoypadAppleTVObserver : NSObject
+
+- (void)startObserving;
+- (void)startProcessing;
+- (void)finishObserving;
+
+@end
+
+class JoypadAppleTV {
+private:
+ JoypadAppleTVObserver *observer;
+
+public:
+ JoypadAppleTV();
+ ~JoypadAppleTV();
+
+ void start_processing();
+
+ int joy_id_for_name(const String &p_name);
+};
diff --git a/platform/tvos/joypad_appletv.mm b/platform/tvos/joypad_appletv.mm
new file mode 100644
index 000000000000..f362ea2616a6
--- /dev/null
+++ b/platform/tvos/joypad_appletv.mm
@@ -0,0 +1,391 @@
+/*************************************************************************/
+/* joypad_appletv.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "joypad_appletv.h"
+
+#include "core/project_settings.h"
+#include "drivers/coreaudio/audio_driver_coreaudio.h"
+#import "godot_view.h"
+#include "main/main.h"
+#include "os_appletv.h"
+
+@interface JoypadAppleTVObserver (JoypadSearch)
+
+- (int)getJoyIdForControllerName:(NSString *)controllerName;
+
+@end
+
+JoypadAppleTV::JoypadAppleTV() {
+ observer = [[JoypadAppleTVObserver alloc] init];
+ [observer startObserving];
+}
+
+JoypadAppleTV::~JoypadAppleTV() {
+ if (observer) {
+ [observer finishObserving];
+ observer = nil;
+ }
+}
+
+void JoypadAppleTV::start_processing() {
+ if (observer) {
+ [observer startProcessing];
+ }
+}
+
+int JoypadAppleTV::joy_id_for_name(const String &p_name) {
+ if (!observer) {
+ return -1;
+ }
+
+ @autoreleasepool {
+ NSString *controllerName = [[NSString alloc] initWithUTF8String:p_name.utf8().get_data()];
+
+ return [observer getJoyIdForControllerName:controllerName];
+ }
+}
+
+@interface JoypadAppleTVObserver ()
+
+@property(assign, nonatomic) BOOL isObserving;
+@property(assign, nonatomic) BOOL isProcessing;
+@property(strong, nonatomic) NSMutableDictionary *connectedJoypads;
+@property(strong, nonatomic) NSMutableArray *joypadsQueue;
+
+@end
+
+@implementation JoypadAppleTVObserver
+
+- (instancetype)init {
+ self = [super init];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (void)godot_commonInit {
+ self.isObserving = NO;
+ self.isProcessing = NO;
+}
+
+- (void)startProcessing {
+ self.isProcessing = YES;
+
+ for (GCController *controller in self.joypadsQueue) {
+ [self addiOSJoypad:controller];
+ }
+
+ [self.joypadsQueue removeAllObjects];
+}
+
+- (void)startObserving {
+ if (self.isObserving) {
+ return;
+ }
+
+ self.isObserving = YES;
+
+ self.connectedJoypads = [NSMutableDictionary dictionary];
+ self.joypadsQueue = [NSMutableArray array];
+
+ [self.joypadsQueue addObjectsFromArray:[GCController controllers]];
+
+ // get told when controllers connect, this will be called right away for
+ // already connected controllers
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(controllerWasConnected:)
+ name:GCControllerDidConnectNotification
+ object:nil];
+
+ // get told when controllers disconnect
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(controllerWasDisconnected:)
+ name:GCControllerDidDisconnectNotification
+ object:nil];
+}
+
+- (void)finishObserving {
+ if (self.isObserving) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ }
+
+ self.isObserving = NO;
+ self.isProcessing = NO;
+
+ self.connectedJoypads = nil;
+ self.joypadsQueue = nil;
+}
+
+- (void)dealloc {
+ [self finishObserving];
+}
+
+- (int)getJoyIdForController:(GCController *)controller {
+ NSArray *keys = [self.connectedJoypads allKeysForObject:controller];
+
+ for (NSNumber *key in keys) {
+ int joy_id = [key intValue];
+ return joy_id;
+ };
+
+ return -1;
+};
+
+- (int)getJoyIdForControllerName:(NSString *)controllerName {
+ NSArray *keys = [self.connectedJoypads allKeys];
+
+ for (NSNumber *key in keys) {
+ int joy_id = [key intValue];
+ GCController *controller = self.connectedJoypads[key];
+
+ if ([controller.vendorName containsString:controllerName]) {
+ return joy_id;
+ }
+ };
+
+ return -1;
+};
+
+- (void)addiOSJoypad:(GCController *)controller {
+ // get a new id for our controller
+ int joy_id = OSAppleTV::get_singleton()->get_unused_joy_id();
+
+ if (joy_id == -1) {
+ printf("Couldn't retrieve new joy id\n");
+ return;
+ }
+
+ // assign our player index
+ if (controller.playerIndex == GCControllerPlayerIndexUnset) {
+ controller.playerIndex = [self getFreePlayerIndex];
+ };
+
+ // tell Godot about our new controller
+ OSAppleTV::get_singleton()->joy_connection_changed(joy_id, true, [controller.vendorName UTF8String]);
+
+ // add it to our dictionary, this will retain our controllers
+ [self.connectedJoypads setObject:controller forKey:[NSNumber numberWithInt:joy_id]];
+
+ // set our input handler
+ [self setControllerInputHandler:controller];
+}
+
+- (void)controllerWasConnected:(NSNotification *)notification {
+ // get our controller
+ GCController *controller = (GCController *)notification.object;
+
+ [self connectController:controller];
+}
+
+- (void)connectController:(GCController *)controller {
+ if (!controller) {
+ printf("Couldn't retrieve new controller\n");
+ return;
+ }
+
+ if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) {
+ printf("Controller is already registered\n");
+ } else if (!self.isProcessing) {
+ [self.joypadsQueue addObject:controller];
+ } else {
+ [self addiOSJoypad:controller];
+ }
+}
+
+- (void)controllerWasDisconnected:(NSNotification *)notification {
+ // find our joystick, there should be only one in our dictionary
+ GCController *controller = (GCController *)notification.object;
+
+ if (!controller) {
+ return;
+ }
+
+ NSArray *keys = [self.connectedJoypads allKeysForObject:controller];
+ for (NSNumber *key in keys) {
+ // tell Godot this joystick is no longer there
+ int joy_id = [key intValue];
+ OSAppleTV::get_singleton()->joy_connection_changed(joy_id, false, "");
+
+ // and remove it from our dictionary
+ [self.connectedJoypads removeObjectForKey:key];
+ };
+};
+
+- (GCControllerPlayerIndex)getFreePlayerIndex {
+ bool have_player_1 = false;
+ bool have_player_2 = false;
+ bool have_player_3 = false;
+ bool have_player_4 = false;
+
+ if (self.connectedJoypads == nil) {
+ NSArray *keys = [self.connectedJoypads allKeys];
+ for (NSNumber *key in keys) {
+ GCController *controller = [self.connectedJoypads objectForKey:key];
+ if (controller.playerIndex == GCControllerPlayerIndex1) {
+ have_player_1 = true;
+ } else if (controller.playerIndex == GCControllerPlayerIndex2) {
+ have_player_2 = true;
+ } else if (controller.playerIndex == GCControllerPlayerIndex3) {
+ have_player_3 = true;
+ } else if (controller.playerIndex == GCControllerPlayerIndex4) {
+ have_player_4 = true;
+ };
+ };
+ };
+
+ if (!have_player_1) {
+ return GCControllerPlayerIndex1;
+ } else if (!have_player_2) {
+ return GCControllerPlayerIndex2;
+ } else if (!have_player_3) {
+ return GCControllerPlayerIndex3;
+ } else if (!have_player_4) {
+ return GCControllerPlayerIndex4;
+ } else {
+ return GCControllerPlayerIndexUnset;
+ };
+}
+
+- (void)setControllerInputHandler:(GCController *)controller {
+ // Hook in the callback handler for the correct gamepad profile.
+ // This is a bit of a weird design choice on Apples part.
+ // You need to select the most capable gamepad profile for the
+ // gamepad attached.
+ if (controller.extendedGamepad != nil) {
+ // The extended gamepad profile has all the input you could possibly find on
+ // a gamepad but will only be active if your gamepad actually has all of
+ // these...
+ _weakify(self);
+ _weakify(controller);
+
+ controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
+ _strongify(self);
+ _strongify(controller);
+
+ int joy_id = [self getJoyIdForController:controller];
+
+ if (element == gamepad.buttonA) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_0,
+ gamepad.buttonA.isPressed);
+ } else if (element == gamepad.buttonB) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_1,
+ gamepad.buttonB.isPressed);
+ } else if (element == gamepad.buttonX) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_2,
+ gamepad.buttonX.isPressed);
+ } else if (element == gamepad.buttonY) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_3,
+ gamepad.buttonY.isPressed);
+ } else if (element == gamepad.leftShoulder) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_L,
+ gamepad.leftShoulder.isPressed);
+ } else if (element == gamepad.rightShoulder) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_R,
+ gamepad.rightShoulder.isPressed);
+ } else if (element == gamepad.leftTrigger) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_L2,
+ gamepad.leftTrigger.isPressed);
+ } else if (element == gamepad.rightTrigger) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_R2,
+ gamepad.rightTrigger.isPressed);
+ } else if (element == gamepad.dpad) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_UP,
+ gamepad.dpad.up.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_DOWN,
+ gamepad.dpad.down.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_LEFT,
+ gamepad.dpad.left.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_RIGHT,
+ gamepad.dpad.right.isPressed);
+ };
+
+ InputDefault::JoyAxis jx;
+ jx.min = -1;
+ if (element == gamepad.leftThumbstick) {
+ jx.value = gamepad.leftThumbstick.xAxis.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_LX, jx);
+ jx.value = -gamepad.leftThumbstick.yAxis.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_LY, jx);
+ } else if (element == gamepad.rightThumbstick) {
+ jx.value = gamepad.rightThumbstick.xAxis.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_RX, jx);
+ jx.value = -gamepad.rightThumbstick.yAxis.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_RY, jx);
+ } else if (element == gamepad.leftTrigger) {
+ jx.value = gamepad.leftTrigger.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_L2, jx);
+ } else if (element == gamepad.rightTrigger) {
+ jx.value = gamepad.rightTrigger.value;
+ OSAppleTV::get_singleton()->joy_axis(joy_id, JOY_ANALOG_R2, jx);
+ }
+ };
+ } else if (controller.microGamepad != nil) {
+ // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad
+ _weakify(self);
+ _weakify(controller);
+
+ controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) {
+ _strongify(self);
+ _strongify(controller);
+
+ int joy_id = [self getJoyIdForController:controller];
+
+ if (element == gamepad.buttonA) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_0,
+ gamepad.buttonA.isPressed);
+ } else if (element == gamepad.buttonX) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_BUTTON_2,
+ gamepad.buttonX.isPressed);
+ } else if (element == gamepad.dpad) {
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_UP,
+ gamepad.dpad.up.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_DOWN,
+ gamepad.dpad.down.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_LEFT,
+ gamepad.dpad.left.isPressed);
+ OSAppleTV::get_singleton()->joy_button(joy_id, JOY_DPAD_RIGHT,
+ gamepad.dpad.right.isPressed);
+ }
+ };
+ }
+
+ ///@TODO need to add support for controller.motion which gives us access to
+ /// the orientation of the device (if supported)
+
+ ///@TODO need to add support for controllerPausedHandler which should be a
+ /// toggle
+};
+
+@end
diff --git a/platform/tvos/keyboard_input_view.h b/platform/tvos/keyboard_input_view.h
new file mode 100644
index 000000000000..a9eebc59103c
--- /dev/null
+++ b/platform/tvos/keyboard_input_view.h
@@ -0,0 +1,37 @@
+/*************************************************************************/
+/* keyboard_input_view.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+@interface GodotKeyboardInputView : UITextField
+
+- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end;
+
+@end
diff --git a/platform/tvos/keyboard_input_view.mm b/platform/tvos/keyboard_input_view.mm
new file mode 100644
index 000000000000..5acadc666bfb
--- /dev/null
+++ b/platform/tvos/keyboard_input_view.mm
@@ -0,0 +1,216 @@
+/*************************************************************************/
+/* keyboard_input_view.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "keyboard_input_view.h"
+
+#include "core/os/keyboard.h"
+#include "os_appletv.h"
+
+@interface GodotKeyboardInputView ()
+
+@property(nonatomic, copy) NSString *previousText;
+@property(nonatomic, assign) NSRange previousSelectedRange;
+
+@end
+
+@implementation GodotKeyboardInputView
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+ self = [super initWithCoder:coder];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (void)godot_commonInit {
+ self.hidden = YES;
+ self.delegate = self;
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(observeTextChange:)
+ name:UITextFieldTextDidChangeNotification
+ object:self];
+}
+
+- (void)setSelectedRange:(NSRange)range {
+ UITextPosition *beginning = self.beginningOfDocument;
+ UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
+ UITextPosition *end = [self positionFromPosition:start offset:range.length];
+ UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
+
+ self.selectedTextRange = textRange;
+}
+
+- (NSRange)selectedRange {
+ UITextPosition *beginning = self.beginningOfDocument;
+
+ UITextRange *selectedRange = self.selectedTextRange;
+ UITextPosition *selectionStart = selectedRange.start;
+ UITextPosition *selectionEnd = selectedRange.end;
+
+ const NSInteger location = [self offsetFromPosition:beginning toPosition:selectionStart];
+ const NSInteger length = [self offsetFromPosition:selectionStart toPosition:selectionEnd];
+
+ return NSMakeRange(location, length);
+}
+
+- (void)dealloc {
+ self.delegate = nil;
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+// MARK: Keyboard
+
+- (BOOL)canBecomeFirstResponder {
+ return YES;
+}
+
+- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end {
+ self.text = existingString;
+ self.previousText = existingString;
+
+ NSRange textRange;
+
+ // Either a simple cursor or a selection.
+ if (end > 0) {
+ textRange = NSMakeRange(start, end - start);
+ } else {
+ textRange = NSMakeRange(start, 0);
+ }
+
+ self.selectedRange = textRange;
+ self.previousSelectedRange = textRange;
+
+ return [self becomeFirstResponder];
+}
+
+- (BOOL)resignFirstResponder {
+ self.text = nil;
+ self.previousText = nil;
+ return [super resignFirstResponder];
+}
+
+// MARK: OS Messages
+
+- (void)deleteText:(NSInteger)charactersToDelete {
+ for (int i = 0; i < charactersToDelete; i++) {
+ OSAppleTV::get_singleton()->key(KEY_BACKSPACE, true);
+ OSAppleTV::get_singleton()->key(KEY_BACKSPACE, false);
+ }
+}
+
+- (void)enterText:(NSString *)substring {
+ String characters;
+ characters.parse_utf8([substring UTF8String]);
+
+ for (int i = 0; i < characters.size(); i++) {
+ int character = characters[i];
+
+ switch (character) {
+ case 10:
+ character = KEY_ENTER;
+ break;
+ case 8198:
+ character = KEY_SPACE;
+ break;
+ default:
+ break;
+ }
+
+ OSAppleTV::get_singleton()->key(character, true);
+ OSAppleTV::get_singleton()->key(character, false);
+ }
+}
+
+// MARK: Observer
+
+- (void)observeTextChange:(NSNotification *)notification {
+ if (notification.object != self) {
+ return;
+ }
+
+ if (self.previousSelectedRange.length == 0) {
+ // We are deleting all text before cursor if no range was selected.
+ // This way any inserted or changed text will be updated.
+ NSString *substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location];
+ [self deleteText:substringToDelete.length];
+ } else {
+ // If text was previously selected
+ // we are sending only one `backspace`.
+ // It will remove all text from text input.
+ [self deleteText:1];
+ }
+
+ NSString *substringToEnter;
+
+ if (self.selectedRange.length == 0) {
+ // If previous cursor had a selection
+ // we have to calculate an inserted text.
+ if (self.previousSelectedRange.length != 0) {
+ NSInteger rangeEnd = self.selectedRange.location + self.selectedRange.length;
+ NSInteger rangeStart = MIN(self.previousSelectedRange.location, self.selectedRange.location);
+ NSInteger rangeLength = MAX(0, rangeEnd - rangeStart);
+
+ NSRange calculatedRange;
+
+ if (rangeLength >= 0) {
+ calculatedRange = NSMakeRange(rangeStart, rangeLength);
+ } else {
+ calculatedRange = NSMakeRange(rangeStart, 0);
+ }
+
+ substringToEnter = [self.text substringWithRange:calculatedRange];
+ } else {
+ substringToEnter = [self.text substringToIndex:self.selectedRange.location];
+ }
+ } else {
+ substringToEnter = [self.text substringWithRange:self.selectedRange];
+ }
+
+ [self enterText:substringToEnter];
+
+ self.previousText = self.text;
+ self.previousSelectedRange = self.selectedRange;
+}
+
+@end
diff --git a/platform/tvos/logo.png b/platform/tvos/logo.png
new file mode 100644
index 000000000000..012e75dbe7f4
Binary files /dev/null and b/platform/tvos/logo.png differ
diff --git a/platform/tvos/main.m b/platform/tvos/main.m
new file mode 100644
index 000000000000..a6efc7aef26c
--- /dev/null
+++ b/platform/tvos/main.m
@@ -0,0 +1,51 @@
+/*************************************************************************/
+/* main.m */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "godot_app_delegate.h"
+
+#import
+#include
+
+int gargc;
+char **gargv;
+
+int main(int argc, char *argv[]) {
+ printf("*********** main.m\n");
+ gargc = argc;
+ gargv = argv;
+
+ printf("running app main\n");
+ @autoreleasepool {
+ NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]);
+ UIApplicationMain(argc, argv, nil, className);
+ }
+ printf("main done\n");
+ return 0;
+}
diff --git a/platform/tvos/native_video_view.h b/platform/tvos/native_video_view.h
new file mode 100644
index 000000000000..203b49392aa2
--- /dev/null
+++ b/platform/tvos/native_video_view.h
@@ -0,0 +1,42 @@
+/*************************************************************************/
+/* native_video_view.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+@interface GodotNativeVideoView : UIView
+
+- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack;
+- (BOOL)isVideoPlaying;
+- (void)pauseVideo;
+- (void)unfocusVideo;
+- (void)unpauseVideo;
+- (void)stopVideo;
+
+@end
diff --git a/platform/tvos/native_video_view.m b/platform/tvos/native_video_view.m
new file mode 100644
index 000000000000..6c6b7b7b3c23
--- /dev/null
+++ b/platform/tvos/native_video_view.m
@@ -0,0 +1,271 @@
+/*************************************************************************/
+/* native_video_view.m */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "native_video_view.h"
+
+#import
+
+@interface GodotNativeVideoView ()
+
+@property(strong, nonatomic) AVAsset *avAsset;
+@property(strong, nonatomic) AVPlayerItem *avPlayerItem;
+@property(strong, nonatomic) AVPlayer *avPlayer;
+@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer;
+@property(assign, nonatomic) CMTime videoCurrentTime;
+@property(assign, nonatomic) BOOL isVideoCurrentlyPlaying;
+
+@end
+
+@implementation GodotNativeVideoView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+ self = [super initWithCoder:coder];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (void)godot_commonInit {
+ self.isVideoCurrentlyPlaying = NO;
+ self.videoCurrentTime = kCMTimeZero;
+
+ [self observeVideoAudio];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+
+ self.avPlayerLayer.frame = self.bounds;
+}
+
+- (void)observeVideoAudio {
+ printf("******** adding observer for sound routing changes\n");
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(audioRouteChangeListenerCallback:)
+ name:AVAudioSessionRouteChangeNotification
+ object:nil];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+ if (object == self.avPlayerItem && [keyPath isEqualToString:@"status"]) {
+ [self handleVideoOrPlayerStatus];
+ }
+
+ if (object == self.avPlayer && [keyPath isEqualToString:@"rate"]) {
+ [self handleVideoPlayRate];
+ }
+}
+
+// MARK: Video Audio
+
+- (void)audioRouteChangeListenerCallback:(NSNotification *)notification {
+ printf("*********** route changed!\n");
+ NSDictionary *interuptionDict = notification.userInfo;
+
+ NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
+
+ switch (routeChangeReason) {
+ case AVAudioSessionRouteChangeReasonNewDeviceAvailable: {
+ NSLog(@"AVAudioSessionRouteChangeReasonNewDeviceAvailable");
+ NSLog(@"Headphone/Line plugged in");
+ } break;
+ case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: {
+ NSLog(@"AVAudioSessionRouteChangeReasonOldDeviceUnavailable");
+ NSLog(@"Headphone/Line was pulled. Resuming video play....");
+ if ([self isVideoPlaying]) {
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [self.avPlayer play]; // NOTE: change this line according your current player implementation
+ NSLog(@"resumed play");
+ });
+ }
+ } break;
+ case AVAudioSessionRouteChangeReasonCategoryChange: {
+ // called at start - also when other audio wants to play
+ NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
+ } break;
+ }
+}
+
+// MARK: Native Video Player
+
+- (void)handleVideoOrPlayerStatus {
+ if (self.avPlayerItem.status == AVPlayerItemStatusFailed || self.avPlayer.status == AVPlayerStatusFailed) {
+ [self stopVideo];
+ }
+
+ if (self.avPlayer.status == AVPlayerStatusReadyToPlay && self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && CMTimeCompare(self.videoCurrentTime, kCMTimeZero) == 0) {
+ // NSLog(@"time: %@", self.video_current_time);
+ [self.avPlayer seekToTime:self.videoCurrentTime];
+ self.videoCurrentTime = kCMTimeZero;
+ }
+}
+
+- (void)handleVideoPlayRate {
+ NSLog(@"Player playback rate changed: %.5f", self.avPlayer.rate);
+ if ([self isVideoPlaying] && self.avPlayer.rate == 0.0 && !self.avPlayer.error) {
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [self.avPlayer play]; // NOTE: change this line according your current player implementation
+ NSLog(@"resumed play");
+ });
+
+ NSLog(@" . . . PAUSED (or just started)");
+ }
+}
+
+- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack {
+ self.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]];
+
+ self.avPlayerItem = [AVPlayerItem playerItemWithAsset:self.avAsset];
+ [self.avPlayerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
+
+ self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem];
+ self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer];
+
+ [self.avPlayer addObserver:self forKeyPath:@"status" options:0 context:nil];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(playerItemDidReachEnd:)
+ name:AVPlayerItemDidPlayToEndTimeNotification
+ object:[self.avPlayer currentItem]];
+
+ [self.avPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0];
+
+ [self.avPlayerLayer setFrame:self.bounds];
+ [self.layer addSublayer:self.avPlayerLayer];
+ [self.avPlayer play];
+
+ AVMediaSelectionGroup *audioGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];
+
+ NSMutableArray *allAudioParams = [NSMutableArray array];
+ for (id track in audioGroup.options) {
+ NSString *language = [[track locale] localeIdentifier];
+ NSLog(@"subtitle lang: %@", language);
+
+ if ([language isEqualToString:audioTrack]) {
+ AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
+ [audioInputParams setVolume:videoVolume atTime:kCMTimeZero];
+ [audioInputParams setTrackID:[track trackID]];
+ [allAudioParams addObject:audioInputParams];
+
+ AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
+ [audioMix setInputParameters:allAudioParams];
+
+ [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup];
+ [self.avPlayer.currentItem setAudioMix:audioMix];
+
+ break;
+ }
+ }
+
+ AVMediaSelectionGroup *subtitlesGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
+ NSArray *useableTracks = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitlesGroup.options withoutMediaCharacteristics:[NSArray arrayWithObject:AVMediaCharacteristicContainsOnlyForcedSubtitles]];
+
+ for (id track in useableTracks) {
+ NSString *language = [[track locale] localeIdentifier];
+ NSLog(@"subtitle lang: %@", language);
+
+ if ([language isEqualToString:subtitleTrack]) {
+ [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup];
+ break;
+ }
+ }
+
+ self.isVideoCurrentlyPlaying = YES;
+
+ return true;
+}
+
+- (BOOL)isVideoPlaying {
+ if (self.avPlayer.error) {
+ printf("Error during playback\n");
+ }
+ return (self.avPlayer.rate > 0 && !self.avPlayer.error);
+}
+
+- (void)pauseVideo {
+ self.videoCurrentTime = self.avPlayer.currentTime;
+ [self.avPlayer pause];
+ self.isVideoCurrentlyPlaying = NO;
+}
+
+- (void)unfocusVideo {
+ [self.avPlayer pause];
+}
+
+- (void)unpauseVideo {
+ [self.avPlayer play];
+ self.isVideoCurrentlyPlaying = YES;
+}
+
+- (void)playerItemDidReachEnd:(NSNotification *)notification {
+ [self stopVideo];
+}
+
+- (void)finishPlayingVideo {
+ [self.avPlayer pause];
+ [self.avPlayerLayer removeFromSuperlayer];
+ self.avPlayerLayer = nil;
+
+ if (self.avPlayerItem) {
+ [self.avPlayerItem removeObserver:self forKeyPath:@"status"];
+ self.avPlayerItem = nil;
+ }
+
+ if (self.avPlayer) {
+ [self.avPlayer removeObserver:self forKeyPath:@"status"];
+ self.avPlayer = nil;
+ }
+
+ self.avAsset = nil;
+
+ self.isVideoCurrentlyPlaying = NO;
+}
+
+- (void)stopVideo {
+ [self finishPlayingVideo];
+
+ [self removeFromSuperview];
+}
+
+@end
diff --git a/platform/tvos/os_appletv.h b/platform/tvos/os_appletv.h
new file mode 100644
index 000000000000..f4830e3c4ba3
--- /dev/null
+++ b/platform/tvos/os_appletv.h
@@ -0,0 +1,182 @@
+/*************************************************************************/
+/* os_appletv.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifdef TVOS_ENABLED
+
+#ifndef OS_APPLETV_H
+#define OS_APPLETV_H
+
+#include "core/os/input.h"
+#include "drivers/coreaudio/audio_driver_coreaudio.h"
+#include "drivers/unix/os_unix.h"
+#include "joypad_appletv.h"
+
+#include "main/input_default.h"
+#include "servers/audio_server.h"
+#include "servers/visual/rasterizer.h"
+#include "servers/visual_server.h"
+#include "tvos.h"
+
+extern void godot_tvos_plugins_initialize();
+extern void godot_tvos_plugins_deinitialize();
+
+class OSAppleTV : public OS_Unix {
+
+private:
+ static HashMap dynamic_symbol_lookup_table;
+ friend void register_dynamic_symbol(char *name, void *address);
+
+ VisualServer *visual_server;
+
+ AudioDriverCoreAudio audio_driver;
+
+ tvOS *tvos;
+
+ JoypadAppleTV *joypad_appletv;
+
+ MainLoop *main_loop;
+
+ VideoMode video_mode;
+
+ virtual int get_video_driver_count() const;
+ virtual const char *get_video_driver_name(int p_driver) const;
+
+ virtual int get_current_video_driver() const;
+
+ virtual void initialize_core();
+ virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver);
+
+ virtual void set_main_loop(MainLoop *p_main_loop);
+ virtual MainLoop *get_main_loop() const;
+
+ virtual void delete_main_loop();
+
+ virtual void finalize();
+
+ void perform_event(const Ref &p_event);
+
+ void set_data_dir(String p_dir);
+
+ String data_dir;
+
+ InputDefault *input;
+
+ int video_driver_index;
+
+ bool is_focused = false;
+ bool overrides_menu_button = true;
+
+public:
+ static OSAppleTV *get_singleton();
+
+ OSAppleTV(String p_data_dir);
+ ~OSAppleTV();
+
+ bool iterate();
+
+ void start();
+
+ virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false);
+ virtual Error close_dynamic_library(void *p_library_handle);
+ virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false);
+
+ virtual void alert(const String &p_alert, const String &p_title = "ALERT!");
+
+ virtual String get_name() const;
+ virtual String get_model_name() const;
+
+ Error shell_open(String p_uri);
+
+ String get_user_data_dir() const;
+
+ String get_locale() const;
+
+ String get_unique_id() const;
+
+ virtual void vibrate_handheld(int p_duration_ms = 500);
+
+ virtual bool _check_internal_feature_support(const String &p_feature);
+
+ virtual int get_screen_dpi(int p_screen = -1) const;
+
+ void key(uint32_t p_key, bool p_pressed);
+
+ int set_base_framebuffer(int p_fb);
+
+ int get_unused_joy_id();
+ int joy_id_for_name(const String &p_name);
+ void joy_connection_changed(int p_idx, bool p_connected, String p_name);
+ void joy_button(int p_device, int p_button, bool p_pressed);
+ void joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value);
+
+ virtual void set_mouse_show(bool p_show);
+ virtual void set_mouse_grab(bool p_grab);
+ virtual bool is_mouse_grab_enabled() const;
+ virtual Point2 get_mouse_position() const;
+ virtual int get_mouse_button_state() const;
+
+ virtual void set_window_title(const String &p_title);
+
+ virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);
+ virtual VideoMode get_video_mode(int p_screen = 0) const;
+
+ virtual void get_fullscreen_mode_list(List *p_list, int p_screen = 0) const;
+
+ virtual void set_keep_screen_on(bool p_enabled);
+
+ virtual bool can_draw() const;
+
+ virtual bool has_virtual_keyboard() const;
+ virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1);
+ virtual void hide_virtual_keyboard();
+ virtual int get_virtual_keyboard_height() const;
+
+ virtual Size2 get_window_size() const;
+ virtual Rect2 get_window_safe_area() const;
+
+ virtual bool has_touchscreen_ui_hint() const;
+
+ virtual Error native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track);
+ virtual bool native_video_is_playing() const;
+ virtual void native_video_pause();
+ virtual void native_video_unpause();
+ virtual void native_video_focus_out();
+ virtual void native_video_stop();
+
+ void on_focus_out();
+ void on_focus_in();
+
+ bool get_overrides_menu_button() const;
+ void set_overrides_menu_button(bool p_flag);
+};
+
+#endif // OS_APPLETV_H
+
+#endif
diff --git a/platform/tvos/os_appletv.mm b/platform/tvos/os_appletv.mm
new file mode 100644
index 000000000000..ea265a251753
--- /dev/null
+++ b/platform/tvos/os_appletv.mm
@@ -0,0 +1,637 @@
+/*************************************************************************/
+/* os_appletv.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifdef TVOS_ENABLED
+
+#include "os_appletv.h"
+
+#include "drivers/gles2/rasterizer_gles2.h"
+#include "drivers/gles3/rasterizer_gles3.h"
+#include "servers/visual/visual_server_raster.h"
+#include "servers/visual/visual_server_wrap_mt.h"
+
+#include "main/main.h"
+
+#include "core/io/file_access_pack.h"
+#include "core/os/dir_access.h"
+#include "core/os/file_access.h"
+#include "core/project_settings.h"
+#include "drivers/unix/syslog_logger.h"
+
+#import "app_delegate.h"
+
+#import "godot_view.h"
+#import "keyboard_input_view.h"
+#import "native_video_view.h"
+#import "view_controller.h"
+
+#import
+#include
+#import
+
+extern int gl_view_base_fb; // from gl_view.mm
+extern bool gles3_available; // from gl_view.mm
+
+// Initialization order between compilation units is not guaranteed,
+// so we use this as a hack to ensure certain code is called before
+// everything else, but after all units are initialized.
+typedef void (*init_callback)();
+static init_callback *tvos_init_callbacks = NULL;
+static int tvos_init_callbacks_count = 0;
+static int tvos_init_callbacks_capacity = 0;
+HashMap OSAppleTV::dynamic_symbol_lookup_table;
+
+int OSAppleTV::get_video_driver_count() const {
+ return 2;
+};
+
+const char *OSAppleTV::get_video_driver_name(int p_driver) const {
+ switch (p_driver) {
+ case VIDEO_DRIVER_GLES3:
+ return "GLES3";
+ case VIDEO_DRIVER_GLES2:
+ return "GLES2";
+ }
+ ERR_FAIL_V_MSG(NULL, "Invalid video driver index: " + itos(p_driver) + ".");
+};
+
+OSAppleTV *OSAppleTV::get_singleton() {
+ return (OSAppleTV *)OS::get_singleton();
+};
+
+void OSAppleTV::set_data_dir(String p_dir) {
+ DirAccess *da = DirAccess::open(p_dir);
+
+ data_dir = da->get_current_dir();
+ printf("setting data dir to %ls from %ls\n", data_dir.c_str(), p_dir.c_str());
+ memdelete(da);
+};
+
+String OSAppleTV::get_unique_id() const {
+ NSString *uuid = [UIDevice currentDevice].identifierForVendor.UUIDString;
+ return String::utf8([uuid UTF8String]);
+};
+
+void OSAppleTV::initialize_core() {
+
+ OS_Unix::initialize_core();
+
+ set_data_dir(data_dir);
+};
+
+int OSAppleTV::get_current_video_driver() const {
+ return video_driver_index;
+}
+
+void OSAppleTV::start() {
+ godot_tvos_plugins_initialize();
+
+ Main::start();
+
+ if (joypad_appletv) {
+ joypad_appletv->start_processing();
+ }
+}
+
+Error OSAppleTV::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {
+
+ bool use_gl3 = GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES3";
+ bool gl_initialization_error = false;
+
+ while (true) {
+ if (use_gl3) {
+ if (RasterizerGLES3::is_viable() == OK && gles3_available) {
+ RasterizerGLES3::register_config();
+ RasterizerGLES3::make_current();
+ break;
+ } else {
+ if (GLOBAL_GET("rendering/quality/driver/fallback_to_gles2")) {
+ p_video_driver = VIDEO_DRIVER_GLES2;
+ use_gl3 = false;
+ continue;
+ } else {
+ gl_initialization_error = true;
+ break;
+ }
+ }
+ } else {
+ if (RasterizerGLES2::is_viable() == OK) {
+ RasterizerGLES2::register_config();
+ RasterizerGLES2::make_current();
+ break;
+ } else {
+ gl_initialization_error = true;
+ break;
+ }
+ }
+ }
+
+ if (gl_initialization_error) {
+ OS::get_singleton()->alert("Your device does not support any of the supported OpenGL versions.",
+ "Unable to initialize Video driver");
+ return ERR_UNAVAILABLE;
+ }
+
+ video_driver_index = p_video_driver;
+ visual_server = memnew(VisualServerRaster);
+ // FIXME: Reimplement threaded rendering
+ if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) {
+ visual_server = memnew(VisualServerWrapMT(visual_server, false));
+ }
+
+ visual_server->init();
+ //visual_server->cursor_set_visible(false, 0);
+
+ // reset this to what it should be, it will have been set to 0 after visual_server->init() is called
+ if (use_gl3) {
+ RasterizerStorageGLES3::system_fbo = gl_view_base_fb;
+ } else {
+ RasterizerStorageGLES2::system_fbo = gl_view_base_fb;
+ }
+
+ AudioDriverManager::initialize(p_audio_driver);
+
+ input = memnew(InputDefault);
+
+ tvos = memnew(tvOS);
+ Engine::get_singleton()->add_singleton(Engine::Singleton("tvOS", tvos));
+
+ joypad_appletv = memnew(JoypadAppleTV);
+
+ return OK;
+};
+
+MainLoop *OSAppleTV::get_main_loop() const {
+
+ return main_loop;
+};
+
+void OSAppleTV::set_main_loop(MainLoop *p_main_loop) {
+ main_loop = p_main_loop;
+
+ if (main_loop) {
+ input->set_main_loop(p_main_loop);
+ main_loop->init();
+ }
+};
+
+bool OSAppleTV::iterate() {
+ if (!main_loop) {
+ return true;
+ }
+
+ return Main::iteration();
+};
+
+void OSAppleTV::key(uint32_t p_key, bool p_pressed) {
+ Ref ev;
+ ev.instance();
+ ev->set_echo(false);
+ ev->set_pressed(p_pressed);
+ ev->set_scancode(p_key);
+ ev->set_unicode(p_key);
+ perform_event(ev);
+};
+
+void OSAppleTV::perform_event(const Ref &p_event) {
+ input->parse_input_event(p_event);
+}
+
+int OSAppleTV::get_unused_joy_id() {
+ return input->get_unused_joy_id();
+};
+
+int OSAppleTV::joy_id_for_name(const String &p_name) {
+ return joypad_appletv->joy_id_for_name(p_name);
+}
+
+void OSAppleTV::joy_connection_changed(int p_idx, bool p_connected, String p_name) {
+ input->joy_connection_changed(p_idx, p_connected, p_name);
+};
+
+void OSAppleTV::joy_button(int p_device, int p_button, bool p_pressed) {
+ input->joy_button(p_device, p_button, p_pressed);
+};
+
+void OSAppleTV::joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value) {
+ input->joy_axis(p_device, p_axis, p_value);
+};
+
+void OSAppleTV::delete_main_loop() {
+
+ if (main_loop) {
+ main_loop->finish();
+ memdelete(main_loop);
+ };
+
+ main_loop = NULL;
+};
+
+void OSAppleTV::finalize() {
+
+ delete_main_loop();
+
+ if (joypad_appletv) {
+ memdelete(joypad_appletv);
+ }
+
+ if (input) {
+ memdelete(input);
+ }
+
+ if (tvos) {
+ memdelete(tvos);
+ }
+
+ godot_tvos_plugins_deinitialize();
+
+ visual_server->finish();
+ memdelete(visual_server);
+ // memdelete(rasterizer);
+}
+
+void OSAppleTV::set_mouse_show(bool p_show) {
+ // Not supported for tvOS
+}
+
+void OSAppleTV::set_mouse_grab(bool p_grab) {
+ // Not supported for tvOS
+}
+
+bool OSAppleTV::is_mouse_grab_enabled() const {
+ // Not supported for tvOS
+ return true;
+}
+
+Point2 OSAppleTV::get_mouse_position() const {
+ // Not supported for tvOS
+ return Point2();
+}
+
+int OSAppleTV::get_mouse_button_state() const {
+ // Not supported for tvOS
+ return 0;
+}
+
+void OSAppleTV::set_window_title(const String &p_title) {
+ // Not supported for tvOS
+}
+
+void OSAppleTV::alert(const String &p_alert, const String &p_title) {
+ const CharString utf8_alert = p_alert.utf8();
+ const CharString utf8_title = p_title.utf8();
+ tvOS::alert(utf8_alert.get_data(), utf8_title.get_data());
+}
+
+// MARK: Dynamic Libraries
+
+Error OSAppleTV::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) {
+ if (p_path.length() == 0) {
+ p_library_handle = RTLD_SELF;
+ return OK;
+ }
+ return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path);
+}
+
+Error OSAppleTV::close_dynamic_library(void *p_library_handle) {
+ if (p_library_handle == RTLD_SELF) {
+ return OK;
+ }
+ return OS_Unix::close_dynamic_library(p_library_handle);
+}
+
+void register_dynamic_symbol(char *name, void *address) {
+ OSAppleTV::dynamic_symbol_lookup_table[String(name)] = address;
+}
+
+Error OSAppleTV::get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional) {
+ if (p_library_handle == RTLD_SELF) {
+ void **ptr = OSAppleTV::dynamic_symbol_lookup_table.getptr(p_name);
+ if (ptr) {
+ p_symbol_handle = *ptr;
+ return OK;
+ }
+ }
+ return OS_Unix::get_dynamic_library_symbol_handle(p_library_handle, p_name, p_symbol_handle, p_optional);
+}
+
+void OSAppleTV::set_video_mode(const VideoMode &p_video_mode, int p_screen) {
+ video_mode = p_video_mode;
+}
+
+OS::VideoMode OSAppleTV::get_video_mode(int p_screen) const {
+
+ return video_mode;
+}
+
+void OSAppleTV::get_fullscreen_mode_list(List *p_list, int p_screen) const {
+ p_list->push_back(video_mode);
+}
+
+bool OSAppleTV::can_draw() const {
+
+ if (native_video_is_playing())
+ return false;
+ return true;
+}
+
+int OSAppleTV::set_base_framebuffer(int p_fb) {
+ // gl_view_base_fb has not been updated yet
+ RasterizerStorageGLES3::system_fbo = p_fb;
+
+ return 0;
+}
+
+bool OSAppleTV::has_virtual_keyboard() const {
+ return true;
+};
+
+void OSAppleTV::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
+ NSString *existingString = [[NSString alloc] initWithUTF8String:p_existing_text.utf8().get_data()];
+
+ [AppDelegate.viewController.keyboardView
+ becomeFirstResponderWithString:existingString
+ multiline:p_multiline
+ cursorStart:p_cursor_start
+ cursorEnd:p_cursor_end];
+};
+
+void OSAppleTV::hide_virtual_keyboard() {
+ [AppDelegate.viewController.keyboardView resignFirstResponder];
+}
+
+int OSAppleTV::get_virtual_keyboard_height() const {
+ return 0;
+}
+
+Error OSAppleTV::shell_open(String p_uri) {
+ NSString *urlPath = [[NSString alloc] initWithUTF8String:p_uri.utf8().get_data()];
+ NSURL *url = [NSURL URLWithString:urlPath];
+
+ if (![[UIApplication sharedApplication] canOpenURL:url]) {
+ return ERR_CANT_OPEN;
+ }
+
+ printf("opening url %s\n", p_uri.utf8().get_data());
+
+ [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
+
+ return OK;
+}
+
+void OSAppleTV::set_keep_screen_on(bool p_enabled) {
+ OS::set_keep_screen_on(p_enabled);
+ [UIApplication sharedApplication].idleTimerDisabled = p_enabled;
+};
+
+String OSAppleTV::get_user_data_dir() const {
+ return data_dir;
+}
+
+String OSAppleTV::get_name() const {
+ return "tvOS";
+}
+
+String OSAppleTV::get_model_name() const {
+ String model = tvos->get_model();
+ if (model != "") {
+ return model;
+ }
+
+ return OS_Unix::get_model_name();
+}
+
+Size2 OSAppleTV::get_window_size() const {
+ return Vector2(video_mode.width, video_mode.height);
+}
+
+int OSAppleTV::get_screen_dpi(int p_screen) const {
+ return 96;
+}
+
+Rect2 OSAppleTV::get_window_safe_area() const {
+ if (@available(tvOS 11, *)) {
+ UIEdgeInsets insets = UIEdgeInsetsZero;
+ UIView *view = AppDelegate.viewController.godotView;
+
+ if ([view respondsToSelector:@selector(safeAreaInsets)]) {
+ insets = [view safeAreaInsets];
+ }
+
+ float scale = [UIScreen mainScreen].nativeScale;
+ Size2i insets_position = Size2i(insets.left, insets.top) * scale;
+ Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale;
+
+ return Rect2i(insets_position, get_window_size() - insets_size);
+ } else {
+ return Rect2i(Size2i(0, 0), get_window_size());
+ }
+}
+
+bool OSAppleTV::has_touchscreen_ui_hint() const {
+ return true;
+}
+
+String OSAppleTV::get_locale() const {
+ NSString *preferedLanguage = [NSLocale preferredLanguages].firstObject;
+
+ if (preferedLanguage) {
+ return String::utf8([preferedLanguage UTF8String]).replace("-", "_");
+ }
+
+ NSString *localeIdentifier = [[NSLocale currentLocale] localeIdentifier];
+ return String::utf8([localeIdentifier UTF8String]).replace("-", "_");
+}
+
+Error OSAppleTV::native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track) {
+ FileAccess *f = FileAccess::open(p_path, FileAccess::READ);
+ bool exists = f && f->is_open();
+
+ String user_data_dir = OSAppleTV::get_singleton()->get_user_data_dir();
+
+ if (!exists) {
+ return FAILED;
+ }
+
+ String tempFile = OSAppleTV::get_singleton()->get_user_data_dir();
+
+ if (p_path.begins_with("res://")) {
+ if (PackedData::get_singleton()->has_path(p_path)) {
+ printf("Unable to play %s using the native player as it resides in a .pck file\n", p_path.utf8().get_data());
+ return ERR_INVALID_PARAMETER;
+ } else {
+ p_path = p_path.replace("res:/", ProjectSettings::get_singleton()->get_resource_path());
+ }
+ } else if (p_path.begins_with("user://")) {
+ p_path = p_path.replace("user:/", user_data_dir);
+ }
+
+ memdelete(f);
+
+ printf("Playing video: %s\n", p_path.utf8().get_data());
+
+ String file_path = ProjectSettings::get_singleton()->globalize_path(p_path);
+
+ NSString *filePath = [[NSString alloc] initWithUTF8String:file_path.utf8().get_data()];
+ NSString *audioTrack = [NSString stringWithUTF8String:p_audio_track.utf8()];
+ NSString *subtitleTrack = [NSString stringWithUTF8String:p_subtitle_track.utf8()];
+
+ if (![AppDelegate.viewController playVideoAtPath:filePath
+ volume:p_volume
+ audio:audioTrack
+ subtitle:subtitleTrack]) {
+ return OK;
+ }
+
+ return FAILED;
+}
+
+bool OSAppleTV::native_video_is_playing() const {
+ return [AppDelegate.viewController.videoView isVideoPlaying];
+}
+
+void OSAppleTV::native_video_pause() {
+ if (native_video_is_playing()) {
+ [AppDelegate.viewController.videoView pauseVideo];
+ }
+}
+
+void OSAppleTV::native_video_unpause() {
+ [AppDelegate.viewController.videoView unpauseVideo];
+}
+
+void OSAppleTV::native_video_focus_out() {
+ [AppDelegate.viewController.videoView unfocusVideo];
+}
+
+void OSAppleTV::native_video_stop() {
+ if (native_video_is_playing()) {
+ [AppDelegate.viewController.videoView stopVideo];
+ }
+}
+
+void OSAppleTV::vibrate_handheld(int p_duration_ms) {
+ // iOS does not support duration for vibration
+ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
+}
+
+bool OSAppleTV::_check_internal_feature_support(const String &p_feature) {
+ return p_feature == "mobile";
+}
+
+void add_tvos_init_callback(init_callback cb) {
+ if (tvos_init_callbacks_count == tvos_init_callbacks_capacity) {
+ void *new_ptr = realloc(tvos_init_callbacks, sizeof(cb) * 32);
+ if (new_ptr) {
+ tvos_init_callbacks = (init_callback *)(new_ptr);
+ tvos_init_callbacks_capacity += 32;
+ }
+ }
+ if (tvos_init_callbacks_capacity > tvos_init_callbacks_count) {
+ tvos_init_callbacks[tvos_init_callbacks_count] = cb;
+ ++tvos_init_callbacks_count;
+ }
+}
+
+OSAppleTV::OSAppleTV(String p_data_dir) {
+ for (int i = 0; i < tvos_init_callbacks_count; ++i) {
+ tvos_init_callbacks[i]();
+ }
+ free(tvos_init_callbacks);
+ tvos_init_callbacks = NULL;
+ tvos_init_callbacks_count = 0;
+ tvos_init_callbacks_capacity = 0;
+
+ main_loop = NULL;
+ visual_server = NULL;
+
+ // can't call set_data_dir from here, since it requires DirAccess
+ // which is initialized in initialize_core
+ data_dir = p_data_dir;
+
+ Vector loggers;
+ loggers.push_back(memnew(SyslogLogger));
+#ifdef DEBUG_ENABLED
+ // it seems tvOS/iOS app's stdout/stderr is only obtainable if you launch it from Xcode
+ loggers.push_back(memnew(StdLogger));
+#endif
+ _set_logger(memnew(CompositeLogger(loggers)));
+
+ AudioDriverManager::add_driver(&audio_driver);
+};
+
+OSAppleTV::~OSAppleTV() {
+}
+
+void OSAppleTV::on_focus_out() {
+ if (is_focused) {
+ is_focused = false;
+
+ if (get_main_loop()) {
+ get_main_loop()->notification(MainLoop::NOTIFICATION_WM_FOCUS_OUT);
+ }
+
+ [AppDelegate.viewController.godotView stopRendering];
+
+ if (native_video_is_playing()) {
+ native_video_focus_out();
+ }
+
+ audio_driver.stop();
+ }
+}
+
+void OSAppleTV::on_focus_in() {
+ if (!is_focused) {
+ is_focused = true;
+
+ if (get_main_loop()) {
+ get_main_loop()->notification(MainLoop::NOTIFICATION_WM_FOCUS_IN);
+ }
+
+ [AppDelegate.viewController.godotView startRendering];
+
+ if (native_video_is_playing()) {
+ native_video_unpause();
+ }
+
+ audio_driver.start();
+ }
+}
+
+bool OSAppleTV::get_overrides_menu_button() const {
+ return overrides_menu_button;
+}
+
+void OSAppleTV::set_overrides_menu_button(bool p_flag) {
+ overrides_menu_button = p_flag;
+}
+
+#endif
diff --git a/platform/tvos/platform_config.h b/platform/tvos/platform_config.h
new file mode 100644
index 000000000000..f8a8eb15d2c8
--- /dev/null
+++ b/platform/tvos/platform_config.h
@@ -0,0 +1,45 @@
+/*************************************************************************/
+/* platform_config.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include
+
+#define GLES2_INCLUDE_H
+#define GLES3_INCLUDE_H
+
+#define PLATFORM_REFCOUNT
+
+#define PTHREAD_RENAME_SELF
+
+#define _weakify(var) __weak typeof(var) GDWeak_##var = var;
+#define _strongify(var) \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wshadow\"") \
+ __strong typeof(var) var = GDWeak_##var; \
+ _Pragma("clang diagnostic pop")
diff --git a/platform/tvos/plugin/godot_plugin_config.h b/platform/tvos/plugin/godot_plugin_config.h
new file mode 100644
index 000000000000..d0557ad6fe7b
--- /dev/null
+++ b/platform/tvos/plugin/godot_plugin_config.h
@@ -0,0 +1,305 @@
+/*************************************************************************/
+/* godot_plugin_config.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef GODOT_PLUGIN_CONFIG_H
+#define GODOT_PLUGIN_CONFIG_H
+
+#include "core/error_list.h"
+#include "core/io/config_file.h"
+#include "core/ustring.h"
+
+/*
+ The `config` section and fields are required and defined as follow:
+- **name**: name of the plugin
+- **binary**: path to static `.a` library
+
+The `dependencies` and fields are optional.
+- **linked**: dependencies that should only be linked.
+- **embedded**: dependencies that should be linked and embedded into application.
+- **system**: system dependencies that should be linked.
+- **capabilities**: capabilities that would be used for `UIRequiredDeviceCapabilities` options in Info.plist file.
+- **files**: files that would be copied into application
+
+The `plist` section are optional.
+- **key**: key and value that would be added in Info.plist file.
+ */
+
+struct PluginConfigTVOS {
+
+ static const char *PLUGIN_CONFIG_EXT;
+
+ static const char *CONFIG_SECTION;
+ static const char *CONFIG_NAME_KEY;
+ static const char *CONFIG_BINARY_KEY;
+ static const char *CONFIG_INITIALIZE_KEY;
+ static const char *CONFIG_DEINITIALIZE_KEY;
+
+ static const char *DEPENDENCIES_SECTION;
+ static const char *DEPENDENCIES_LINKED_KEY;
+ static const char *DEPENDENCIES_EMBEDDED_KEY;
+ static const char *DEPENDENCIES_SYSTEM_KEY;
+ static const char *DEPENDENCIES_CAPABILITIES_KEY;
+ static const char *DEPENDENCIES_FILES_KEY;
+
+ static const char *PLIST_SECTION;
+
+ // Set to true when the config file is properly loaded.
+ bool valid_config = false;
+ bool supports_targets = false;
+ // Unix timestamp of last change to this plugin.
+ uint64_t last_updated = 0;
+
+ // Required config section
+ String name;
+ String binary;
+ String initialization_method;
+ String deinitialization_method;
+
+ // Optional dependencies section
+ Vector linked_dependencies;
+ Vector embedded_dependencies;
+ Vector system_dependencies;
+
+ Vector files_to_copy;
+ Vector capabilities;
+
+ // Optional plist section
+ // Supports only string types for now
+ HashMap plist;
+};
+
+const char *PluginConfigTVOS::PLUGIN_CONFIG_EXT = ".gdatvp";
+
+const char *PluginConfigTVOS::CONFIG_SECTION = "config";
+const char *PluginConfigTVOS::CONFIG_NAME_KEY = "name";
+const char *PluginConfigTVOS::CONFIG_BINARY_KEY = "binary";
+const char *PluginConfigTVOS::CONFIG_INITIALIZE_KEY = "initialization";
+const char *PluginConfigTVOS::CONFIG_DEINITIALIZE_KEY = "deinitialization";
+
+const char *PluginConfigTVOS::DEPENDENCIES_SECTION = "dependencies";
+const char *PluginConfigTVOS::DEPENDENCIES_LINKED_KEY = "linked";
+const char *PluginConfigTVOS::DEPENDENCIES_EMBEDDED_KEY = "embedded";
+const char *PluginConfigTVOS::DEPENDENCIES_SYSTEM_KEY = "system";
+const char *PluginConfigTVOS::DEPENDENCIES_CAPABILITIES_KEY = "capabilities";
+const char *PluginConfigTVOS::DEPENDENCIES_FILES_KEY = "files";
+
+const char *PluginConfigTVOS::PLIST_SECTION = "plist";
+
+static inline String resolve_local_dependency_path(String plugin_config_dir, String dependency_path) {
+ String absolute_path;
+
+ if (dependency_path.empty()) {
+ return absolute_path;
+ }
+
+ if (dependency_path.is_abs_path()) {
+ return dependency_path;
+ }
+
+ String res_path = ProjectSettings::get_singleton()->globalize_path("res://");
+ absolute_path = plugin_config_dir.plus_file(dependency_path);
+
+ return absolute_path.replace(res_path, "res://");
+}
+
+static inline String resolve_system_dependency_path(String dependency_path) {
+ String absolute_path;
+
+ if (dependency_path.empty()) {
+ return absolute_path;
+ }
+
+ if (dependency_path.is_abs_path()) {
+ return dependency_path;
+ }
+
+ String system_path = "/System/Library/Frameworks";
+
+ return system_path.plus_file(dependency_path);
+}
+
+static inline Vector resolve_local_dependencies(String plugin_config_dir, Vector p_paths) {
+ Vector paths;
+
+ for (int i = 0; i < p_paths.size(); i++) {
+ String path = resolve_local_dependency_path(plugin_config_dir, p_paths[i]);
+
+ if (path.empty()) {
+ continue;
+ }
+
+ paths.push_back(path);
+ }
+
+ return paths;
+}
+
+static inline Vector resolve_system_dependencies(Vector p_paths) {
+ Vector paths;
+
+ for (int i = 0; i < p_paths.size(); i++) {
+ String path = resolve_system_dependency_path(p_paths[i]);
+
+ if (path.empty()) {
+ continue;
+ }
+
+ paths.push_back(path);
+ }
+
+ return paths;
+}
+
+static inline bool validate_plugin(PluginConfigTVOS &plugin_config) {
+ bool valid_name = !plugin_config.name.empty();
+ bool valid_binary_name = !plugin_config.binary.empty();
+ bool valid_initialize = !plugin_config.initialization_method.empty();
+ bool valid_deinitialize = !plugin_config.deinitialization_method.empty();
+
+ bool fields_value = valid_name && valid_binary_name && valid_initialize && valid_deinitialize;
+
+ if (!fields_value) {
+ return false;
+ }
+
+ String plugin_extension = plugin_config.binary.get_extension().to_lower();
+
+ if ((plugin_extension == "a" && FileAccess::exists(plugin_config.binary)) ||
+ (plugin_extension == "xcframework" && DirAccess::exists(plugin_config.binary))) {
+ plugin_config.valid_config = true;
+ plugin_config.supports_targets = false;
+ } else {
+ String file_path = plugin_config.binary.get_base_dir();
+ String file_name = plugin_config.binary.get_basename().get_file();
+ String file_extension = plugin_config.binary.get_extension();
+ String release_file_name = file_path.plus_file(file_name + ".release." + file_extension);
+ String debug_file_name = file_path.plus_file(file_name + ".debug." + file_extension);
+
+ if ((plugin_extension == "a" && FileAccess::exists(release_file_name) && FileAccess::exists(debug_file_name)) ||
+ (plugin_extension == "xcframework" && DirAccess::exists(release_file_name) && DirAccess::exists(debug_file_name))) {
+ plugin_config.valid_config = true;
+ plugin_config.supports_targets = true;
+ }
+ }
+
+ return plugin_config.valid_config;
+}
+
+static inline String get_plugin_main_binary(PluginConfigTVOS &plugin_config, bool p_debug) {
+ if (!plugin_config.supports_targets) {
+ return plugin_config.binary;
+ }
+
+ String plugin_binary_dir = plugin_config.binary.get_base_dir();
+ String plugin_name_prefix = plugin_config.binary.get_basename().get_file();
+ String plugin_extension = plugin_config.binary.get_extension();
+ String plugin_file = plugin_name_prefix + "." + (p_debug ? "debug" : "release") + "." + plugin_extension;
+
+ return plugin_binary_dir.plus_file(plugin_file);
+}
+
+static inline uint64_t get_plugin_modification_time(const PluginConfigTVOS &plugin_config, const String &config_path) {
+ uint64_t last_updated = FileAccess::get_modified_time(config_path);
+
+ if (!plugin_config.supports_targets) {
+ last_updated = MAX(last_updated, FileAccess::get_modified_time(plugin_config.binary));
+ } else {
+ String file_path = plugin_config.binary.get_base_dir();
+ String file_name = plugin_config.binary.get_basename().get_file();
+ String release_file_name = file_path.plus_file(file_name + ".release.a");
+ String debug_file_name = file_path.plus_file(file_name + ".debug.a");
+
+ last_updated = MAX(last_updated, FileAccess::get_modified_time(release_file_name));
+ last_updated = MAX(last_updated, FileAccess::get_modified_time(debug_file_name));
+ }
+
+ return last_updated;
+}
+
+static inline PluginConfigTVOS load_plugin_config(Ref config_file, const String &path) {
+ PluginConfigTVOS plugin_config = {};
+
+ if (!config_file.is_valid()) {
+ return plugin_config;
+ }
+
+ Error err = config_file->load(path);
+
+ if (err != OK) {
+ return plugin_config;
+ }
+
+ String config_base_dir = path.get_base_dir();
+
+ plugin_config.name = config_file->get_value(PluginConfigTVOS::CONFIG_SECTION, PluginConfigTVOS::CONFIG_NAME_KEY, String());
+ plugin_config.initialization_method = config_file->get_value(PluginConfigTVOS::CONFIG_SECTION, PluginConfigTVOS::CONFIG_INITIALIZE_KEY, String());
+ plugin_config.deinitialization_method = config_file->get_value(PluginConfigTVOS::CONFIG_SECTION, PluginConfigTVOS::CONFIG_DEINITIALIZE_KEY, String());
+
+ String binary_path = config_file->get_value(PluginConfigTVOS::CONFIG_SECTION, PluginConfigTVOS::CONFIG_BINARY_KEY, String());
+ plugin_config.binary = resolve_local_dependency_path(config_base_dir, binary_path);
+
+ if (config_file->has_section(PluginConfigTVOS::DEPENDENCIES_SECTION)) {
+ Vector linked_dependencies = config_file->get_value(PluginConfigTVOS::DEPENDENCIES_SECTION, PluginConfigTVOS::DEPENDENCIES_LINKED_KEY, Vector());
+ Vector embedded_dependencies = config_file->get_value(PluginConfigTVOS::DEPENDENCIES_SECTION, PluginConfigTVOS::DEPENDENCIES_EMBEDDED_KEY, Vector());
+ Vector system_dependencies = config_file->get_value(PluginConfigTVOS::DEPENDENCIES_SECTION, PluginConfigTVOS::DEPENDENCIES_SYSTEM_KEY, Vector());
+ Vector files = config_file->get_value(PluginConfigTVOS::DEPENDENCIES_SECTION, PluginConfigTVOS::DEPENDENCIES_FILES_KEY, Vector());
+
+ plugin_config.linked_dependencies = resolve_local_dependencies(config_base_dir, linked_dependencies);
+ plugin_config.embedded_dependencies = resolve_local_dependencies(config_base_dir, embedded_dependencies);
+ plugin_config.system_dependencies = resolve_system_dependencies(system_dependencies);
+
+ plugin_config.files_to_copy = resolve_local_dependencies(config_base_dir, files);
+
+ plugin_config.capabilities = config_file->get_value(PluginConfigTVOS::DEPENDENCIES_SECTION, PluginConfigTVOS::DEPENDENCIES_CAPABILITIES_KEY, Vector());
+ }
+
+ if (config_file->has_section(PluginConfigTVOS::PLIST_SECTION)) {
+ List keys;
+ config_file->get_section_keys(PluginConfigTVOS::PLIST_SECTION, &keys);
+
+ for (int i = 0; i < keys.size(); i++) {
+ String value = config_file->get_value(PluginConfigTVOS::PLIST_SECTION, keys[i], String());
+
+ if (value.empty()) {
+ continue;
+ }
+
+ plugin_config.plist[keys[i]] = value;
+ }
+ }
+
+ if (validate_plugin(plugin_config)) {
+ plugin_config.last_updated = get_plugin_modification_time(plugin_config, path);
+ }
+
+ return plugin_config;
+}
+
+#endif // GODOT_PLUGIN_CONFIG_H
diff --git a/platform/tvos/tvos.h b/platform/tvos/tvos.h
new file mode 100644
index 000000000000..8af7d06b1ba0
--- /dev/null
+++ b/platform/tvos/tvos.h
@@ -0,0 +1,54 @@
+/*************************************************************************/
+/* tvos.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TVOS_H
+#define TVOS_H
+
+#include "core/object.h"
+
+class tvOS : public Object {
+
+ GDCLASS(tvOS, Object);
+
+ static void _bind_methods();
+
+public:
+ static void alert(const char *p_alert, const char *p_title);
+
+ String get_model() const;
+ String get_rate_url(int p_app_id) const;
+
+ bool get_overrides_menu_button() const;
+ void set_overrides_menu_button(bool p_flag);
+
+ tvOS();
+};
+
+#endif
diff --git a/platform/tvos/tvos.mm b/platform/tvos/tvos.mm
new file mode 100644
index 000000000000..04108ff4b31d
--- /dev/null
+++ b/platform/tvos/tvos.mm
@@ -0,0 +1,104 @@
+/*************************************************************************/
+/* tvos.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tvos.h"
+
+#import "app_delegate.h"
+#include "os_appletv.h"
+#import "view_controller.h"
+
+#import
+#include
+
+void tvOS::_bind_methods() {
+
+ ClassDB::bind_method(D_METHOD("get_rate_url", "app_id"), &tvOS::get_rate_url);
+
+ ClassDB::bind_method(D_METHOD("get_overrides_menu_button"), &tvOS::get_overrides_menu_button);
+ ClassDB::bind_method(D_METHOD("set_overrides_menu_button", "p_flag"), &tvOS::set_overrides_menu_button);
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "overrides_menu_button"), "set_overrides_menu_button", "get_overrides_menu_button");
+};
+
+void tvOS::alert(const char *p_alert, const char *p_title) {
+ NSString *title = [NSString stringWithUTF8String:p_title];
+ NSString *message = [NSString stringWithUTF8String:p_alert];
+
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
+ UIAlertAction *button = [UIAlertAction actionWithTitle:@"OK"
+ style:UIAlertActionStyleCancel
+ handler:^(id){
+ }];
+
+ [alert addAction:button];
+
+ [AppDelegate.viewController presentViewController:alert animated:YES completion:nil];
+}
+
+String tvOS::get_model() const {
+ size_t size;
+ sysctlbyname("hw.machine", NULL, &size, NULL, 0);
+ char *model = (char *)malloc(size);
+ if (model == NULL) {
+ return "";
+ }
+ sysctlbyname("hw.machine", model, &size, NULL, 0);
+ NSString *platform = [NSString stringWithCString:model encoding:NSUTF8StringEncoding];
+ free(model);
+ const char *str = [platform UTF8String];
+ return String(str != NULL ? str : "");
+}
+
+String tvOS::get_rate_url(int p_app_id) const {
+ String app_url_path = "itms-apps://itunes.apple.com/app/idAPP_ID";
+
+ String ret = app_url_path.replace("APP_ID", String::num(p_app_id));
+
+ printf("returning rate url %ls\n", ret.c_str());
+
+ return ret;
+};
+
+bool tvOS::get_overrides_menu_button() const {
+ if (!OSAppleTV::get_singleton()) {
+ return false;
+ }
+
+ return OSAppleTV::get_singleton()->get_overrides_menu_button();
+}
+
+void tvOS::set_overrides_menu_button(bool p_flag) {
+ if (!OSAppleTV::get_singleton()) {
+ return;
+ }
+
+ OSAppleTV::get_singleton()->set_overrides_menu_button(p_flag);
+}
+
+tvOS::tvOS(){};
diff --git a/platform/tvos/view_controller.h b/platform/tvos/view_controller.h
new file mode 100644
index 000000000000..52fb6fbbf2af
--- /dev/null
+++ b/platform/tvos/view_controller.h
@@ -0,0 +1,47 @@
+/*************************************************************************/
+/* view_controller.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import
+
+@class GodotView;
+@class GodotNativeVideoView;
+@class GodotKeyboardInputView;
+
+@interface ViewController : UIViewController
+
+@property(nonatomic, readonly, strong) GodotView *godotView;
+@property(nonatomic, readonly, strong) GodotNativeVideoView *videoView;
+@property(nonatomic, readonly, strong) GodotKeyboardInputView *keyboardView;
+
+// MARK: Native Video Player
+
+- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack;
+
+@end
diff --git a/platform/tvos/view_controller.mm b/platform/tvos/view_controller.mm
new file mode 100644
index 000000000000..40e91eb8112e
--- /dev/null
+++ b/platform/tvos/view_controller.mm
@@ -0,0 +1,170 @@
+/*************************************************************************/
+/* view_controller.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#import "view_controller.h"
+
+#include "core/project_settings.h"
+#import "godot_view.h"
+#import "godot_view_renderer.h"
+#import "keyboard_input_view.h"
+#import "native_video_view.h"
+#include "os_appletv.h"
+
+@interface ViewController ()
+
+@property(strong, nonatomic) GodotViewRenderer *renderer;
+@property(strong, nonatomic) GodotNativeVideoView *videoView;
+@property(strong, nonatomic) GodotKeyboardInputView *keyboardView;
+
+@property(strong, nonatomic) UIView *godotLoadingOverlay;
+
+@end
+
+@implementation ViewController
+
+- (GodotView *)godotView {
+ return (GodotView *)self.view;
+}
+
+- (void)loadView {
+ GodotView *view = [[GodotView alloc] init];
+ [view initializeRendering];
+
+ GodotViewRenderer *renderer = [[GodotViewRenderer alloc] init];
+
+ self.renderer = renderer;
+ self.view = view;
+
+ view.renderer = self.renderer;
+ view.delegate = self;
+}
+
+- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
+ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+ self = [super initWithCoder:coder];
+
+ if (self) {
+ [self godot_commonInit];
+ }
+
+ return self;
+}
+
+- (void)godot_commonInit {
+ // Initialize view controller values.
+}
+
+- (void)didReceiveMemoryWarning {
+ [super didReceiveMemoryWarning];
+ printf("*********** did receive memory warning!\n");
+};
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ [self observeKeyboard];
+ [self displayLoadingOverlay];
+}
+
+- (void)observeKeyboard {
+ printf("******** setting up keyboard input view\n");
+ self.keyboardView = [GodotKeyboardInputView new];
+ [self.view addSubview:self.keyboardView];
+}
+
+- (void)displayLoadingOverlay {
+ NSBundle *bundle = [NSBundle mainBundle];
+ NSString *storyboardName = @"Launch Screen";
+
+ if ([bundle pathForResource:storyboardName ofType:@"storyboardc"] == nil) {
+ return;
+ }
+
+ UIStoryboard *launchStoryboard = [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
+
+ UIViewController *controller = [launchStoryboard instantiateInitialViewController];
+ self.godotLoadingOverlay = controller.view;
+ self.godotLoadingOverlay.frame = self.view.bounds;
+ self.godotLoadingOverlay.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
+
+ [self.view addSubview:self.godotLoadingOverlay];
+}
+
+- (BOOL)godotViewFinishedSetup:(GodotView *)view {
+ [self.godotLoadingOverlay removeFromSuperview];
+ self.godotLoadingOverlay = nil;
+
+ return YES;
+}
+
+- (void)dealloc {
+ [self.videoView stopVideo];
+ self.videoView = nil;
+
+ self.keyboardView = nil;
+
+ self.renderer = nil;
+
+ if (self.godotLoadingOverlay) {
+ [self.godotLoadingOverlay removeFromSuperview];
+ self.godotLoadingOverlay = nil;
+ }
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+// MARK: Native Video Player
+
+- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack {
+ // If we are showing some video already, reuse existing view for new video.
+ if (self.videoView) {
+ return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack];
+ } else {
+ // Create autoresizing view for video playback.
+ GodotNativeVideoView *videoView = [[GodotNativeVideoView alloc] initWithFrame:self.view.bounds];
+ videoView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ [self.view addSubview:videoView];
+
+ self.videoView = videoView;
+
+ return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack];
+ }
+}
+
+@end
]