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