diff options
Diffstat (limited to 'platform/iphone')
44 files changed, 5192 insertions, 3781 deletions
diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index b72d29149c..ee7b2f4ab5 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -3,18 +3,23 @@ Import("env") iphone_lib = [ - "godot_iphone.cpp", - "os_iphone.cpp", - "semaphore_iphone.cpp", - "gl_view.mm", + "godot_iphone.mm", + "os_iphone.mm", "main.m", "app_delegate.mm", "view_controller.mm", - "game_center.mm", - "in_app_store.mm", - "icloud.mm", "ios.mm", "vulkan_context_iphone.mm", + "display_server_iphone.mm", + "joypad_iphone.mm", + "godot_view.mm", + "display_layer.mm", + "godot_app_delegate.m", + "godot_view_renderer.mm", + "godot_view_gesture_recognizer.mm", + "device_metrics.m", + "keyboard_input_view.mm", + "native_video_view.m", ] env_ios = env.Clone() diff --git a/platform/iphone/app_delegate.h b/platform/iphone/app_delegate.h index 27552d781a..2f082f1e07 100644 --- a/platform/iphone/app_delegate.h +++ b/platform/iphone/app_delegate.h @@ -28,29 +28,20 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#if defined(OPENGL_ENABLED) -#import "gl_view.h" -#endif -#import "view_controller.h" #import <UIKit/UIKit.h> -#import <CoreMotion/CoreMotion.h> +@class ViewController; // FIXME: Add support for both GLES2 and Vulkan when GLES2 is implemented again, // so it can't be done with compilation time branching. //#if defined(OPENGL_ENABLED) //@interface AppDelegate : NSObject <UIApplicationDelegate, GLViewDelegate> { //#endif -#if defined(VULKAN_ENABLED) -@interface AppDelegate : NSObject <UIApplicationDelegate> { -#endif - //@property (strong, nonatomic) UIWindow *window; - ViewController *view_controller; - bool is_focus_out; -}; +//#if defined(VULKAN_ENABLED) +@interface AppDelegate : NSObject <UIApplicationDelegate> +//#endif @property(strong, nonatomic) UIWindow *window; - -+ (ViewController *)getViewController; +@property(strong, class, readonly, nonatomic) ViewController *viewController; @end diff --git a/platform/iphone/app_delegate.mm b/platform/iphone/app_delegate.mm index c4ef185bf1..c1942e77dd 100644 --- a/platform/iphone/app_delegate.mm +++ b/platform/iphone/app_delegate.mm @@ -29,644 +29,61 @@ /*************************************************************************/ #import "app_delegate.h" - -#include "core/project_settings.h" +#include "core/config/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" -#if defined(OPENGL_ENABLED) -#import "gl_view.h" -#endif +#import "godot_view.h" #include "main/main.h" #include "os_iphone.h" +#import "view_controller.h" -#import "GameController/GameController.h" +#import <AVFoundation/AVFoundation.h> #import <AudioToolbox/AudioServices.h> -#define kFilteringFactor 0.1 #define kRenderingFrequency 60 -#define kAccelerometerFrequency 100.0 // Hz - -Error _shell_open(String); -void _set_keep_screen_on(bool p_enabled); - -Error _shell_open(String p_uri) { - NSString *url = [[NSString alloc] initWithUTF8String:p_uri.utf8().get_data()]; - - if (![[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:url]]) - return ERR_CANT_OPEN; - - printf("opening url %ls\n", p_uri.c_str()); - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; - [url release]; - return OK; -}; - -void _set_keep_screen_on(bool p_enabled) { - [[UIApplication sharedApplication] setIdleTimerDisabled:(BOOL)p_enabled]; -}; - -void _vibrate() { - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); -}; - -@implementation AppDelegate - -@synthesize window; extern int gargc; extern char **gargv; -extern int iphone_main(int, int, int, char **, String); + +extern int iphone_main(int, char **, String); extern void iphone_finish(); -CMMotionManager *motionManager; -bool motionInitialised; +@implementation AppDelegate static ViewController *mainViewController = nil; -+ (ViewController *)getViewController { - return mainViewController; -} - -NSMutableDictionary *ios_joysticks = nil; -NSMutableArray *pending_ios_joysticks = nil; - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (ios_joysticks == nil) { - NSArray *keys = [ios_joysticks allKeys]; - for (NSNumber *key in keys) { - GCController *controller = [ios_joysticks 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 _ios_add_joystick(GCController *controller, AppDelegate *delegate) { - // get a new id for our controller - int joy_id = OSIPhone::get_singleton()->get_unused_joy_id(); - if (joy_id != -1) { - // assign our player index - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [delegate getFreePlayerIndex]; - }; - - // tell Godot about our new controller - OSIPhone::get_singleton()->joy_connection_changed( - joy_id, true, [controller.vendorName UTF8String]); - - // add it to our dictionary, this will retain our controllers - [ios_joysticks setObject:controller - forKey:[NSNumber numberWithInt:joy_id]]; - - // set our input handler - [delegate setControllerInputHandler:controller]; - } else { - printf("Couldn't retrieve new joy id\n"); - }; -} - -static void on_focus_out(ViewController *view_controller, bool *is_focus_out) { - if (!*is_focus_out) { - *is_focus_out = true; - if (OS::get_singleton()->get_main_loop()) - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_WM_FOCUS_OUT); - - [view_controller.view stopAnimation]; - if (OS::get_singleton()->native_video_is_playing()) { - OSIPhone::get_singleton()->native_video_focus_out(); - } - AudioDriverCoreAudio *audio = dynamic_cast<AudioDriverCoreAudio *>(AudioDriverCoreAudio::get_singleton()); - if (audio) - audio->stop(); - } -} - -static void on_focus_in(ViewController *view_controller, bool *is_focus_out) { - if (*is_focus_out) { - *is_focus_out = false; - if (OS::get_singleton()->get_main_loop()) - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_WM_FOCUS_IN); - - [view_controller.view startAnimation]; - if (OSIPhone::get_singleton()->native_video_is_playing()) { - OSIPhone::get_singleton()->native_video_unpause(); - } - - AudioDriverCoreAudio *audio = dynamic_cast<AudioDriverCoreAudio *>(AudioDriverCoreAudio::get_singleton()); - if (audio) - audio->start(); - } ++ (ViewController *)viewController { + return mainViewController; } -- (void)controllerWasConnected:(NSNotification *)notification { - // create our dictionary if we don't have one yet - if (ios_joysticks == nil) { - ios_joysticks = [[NSMutableDictionary alloc] init]; - }; - - // get our controller - GCController *controller = (GCController *)notification.object; - if (controller == nil) { - printf("Couldn't retrieve new controller\n"); - } else if ([[ios_joysticks allKeysForObject:controller] count] != 0) { - printf("Controller is already registered\n"); - } else if (frame_count > 1) { - _ios_add_joystick(controller, self); - } else { - if (pending_ios_joysticks == nil) - pending_ios_joysticks = [[NSMutableArray alloc] init]; - [pending_ios_joysticks addObject:controller]; - }; -}; - -- (void)controllerWasDisconnected:(NSNotification *)notification { - if (ios_joysticks != nil) { - // find our joystick, there should be only one in our dictionary - GCController *controller = (GCController *)notification.object; - NSArray *keys = [ios_joysticks allKeysForObject:controller]; - for (NSNumber *key in keys) { - // tell Godot this joystick is no longer there - int joy_id = [key intValue]; - OSIPhone::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // and remove it from our dictionary - [ios_joysticks removeObjectForKey:key]; - }; - }; -}; - -- (int)getJoyIdForController:(GCController *)controller { - if (ios_joysticks != nil) { - // find our joystick, there should be only one in our dictionary - NSArray *keys = [ios_joysticks allKeysForObject:controller]; - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - }; - }; - - return -1; -}; - -- (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... - controller.extendedGamepad.valueChangedHandler = ^( - GCExtendedGamepad *gamepad, GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - - InputDefault::JoyAxis jx; - jx.min = -1; - if (element == gamepad.leftThumbstick) { - jx.value = gamepad.leftThumbstick.xAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_X, jx); - jx.value = -gamepad.leftThumbstick.yAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_Y, jx); - } else if (element == gamepad.rightThumbstick) { - jx.value = gamepad.rightThumbstick.xAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_X, jx); - jx.value = -gamepad.rightThumbstick.yAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_Y, jx); - } else if (element == gamepad.leftTrigger) { - jx.value = gamepad.leftTrigger.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_LEFT, jx); - } else if (element == gamepad.rightTrigger) { - jx.value = gamepad.rightTrigger.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_RIGHT, jx); - }; - }; - } else if (controller.gamepad != nil) { - // gamepad is the standard profile with 4 buttons, shoulder buttons and a - // D-pad - controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, - GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - }; -#ifdef ADD_MICRO_GAMEPAD // disabling this for now, only available on iOS 9+, - // while we are setting that as the minimum, seems our - // build environment doesn't like it - } else if (controller.microGamepad != nil) { - // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad - controller.microGamepad.valueChangedHandler = - ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - }; -#endif - }; - - ///@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 -}; - -- (void)initGameControllers { - // 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)deinitGameControllers { - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:GCControllerDidConnectNotification - object:nil]; - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:GCControllerDidDisconnectNotification - object:nil]; - - if (ios_joysticks != nil) { - [ios_joysticks dealloc]; - ios_joysticks = nil; - }; - - if (pending_ios_joysticks != nil) { - [pending_ios_joysticks dealloc]; - pending_ios_joysticks = nil; - }; -}; - -OS::VideoMode _get_video_mode() { - int backingWidth; - int backingHeight; -#if defined(OPENGL_ENABLED) - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_WIDTH_OES, &backingWidth); - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_HEIGHT_OES, &backingHeight); -#endif - - OS::VideoMode vm; - vm.fullscreen = true; - vm.width = backingWidth; - vm.height = backingHeight; - vm.resizable = false; - return vm; -}; - -static int frame_count = 0; -- (void)drawView:(UIView *)view; -{ - switch (frame_count) { - case 0: { - OS::get_singleton()->set_video_mode(_get_video_mode()); - - if (!OS::get_singleton()) { - exit(0); - }; - ++frame_count; - - NSString *locale_code = [[NSLocale currentLocale] localeIdentifier]; - OSIPhone::get_singleton()->set_locale( - String::utf8([locale_code UTF8String])); - - NSString *uuid; - if ([[UIDevice currentDevice] - respondsToSelector:@selector(identifierForVendor)]) { - uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; - } else { - // before iOS 6, so just generate an identifier and store it - uuid = [[NSUserDefaults standardUserDefaults] - objectForKey:@"identiferForVendor"]; - if (!uuid) { - CFUUIDRef cfuuid = CFUUIDCreate(NULL); - uuid = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, cfuuid); - CFRelease(cfuuid); - [[NSUserDefaults standardUserDefaults] - setObject:uuid - forKey:@"identifierForVendor"]; - } - } - - OSIPhone::get_singleton()->set_unique_id(String::utf8([uuid UTF8String])); - - }; break; - - case 1: { - Main::setup2(); - ++frame_count; - - if (pending_ios_joysticks != nil) { - for (GCController *controller in pending_ios_joysticks) { - _ios_add_joystick(controller, self); - } - [pending_ios_joysticks dealloc]; - pending_ios_joysticks = nil; - } - - // 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 - } - - }; break; - - case 2: { - Main::start(); - ++frame_count; - - }; break; // no fallthrough - - default: { - if (OSIPhone::get_singleton()) { - // OSIPhone::get_singleton()->update_accelerometer(accel[0], accel[1], - // accel[2]); - if (motionInitialised) { - // Just using polling approach for now, we can set this up so it sends - // data to us in intervals, might be better. See Apple reference pages - // for more details: - // https://developer.apple.com/reference/coremotion/cmmotionmanager?language=objc - - // Apple splits our accelerometer date into a gravity and user movement - // component. We add them back together - CMAcceleration gravity = motionManager.deviceMotion.gravity; - CMAcceleration acceleration = - motionManager.deviceMotion.userAcceleration; - - ///@TODO We don't seem to be getting data here, is my device broken or - /// is this code incorrect? - CMMagneticField magnetic = - motionManager.deviceMotion.magneticField.field; - - ///@TODO we can access rotationRate as a CMRotationRate variable - ///(processed date) or CMGyroData (raw data), have to see what works - /// best - CMRotationRate rotation = motionManager.deviceMotion.rotationRate; - - // Adjust for screen orientation. - // [[UIDevice currentDevice] orientation] changes even if we've fixed - // our orientation which is not a good thing when you're trying to get - // your user to move the screen in all directions and want consistent - // output - - ///@TODO Using [[UIApplication sharedApplication] statusBarOrientation] - /// is a bit of a hack. Godot obviously knows the orientation so maybe - /// we - // can use that instead? (note that left and right seem swapped) - - switch ([[UIApplication sharedApplication] statusBarOrientation]) { - case UIDeviceOrientationLandscapeLeft: { - OSIPhone::get_singleton()->update_gravity(-gravity.y, gravity.x, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - -(acceleration.y + gravity.y), (acceleration.x + gravity.x), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - -magnetic.y, magnetic.x, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(-rotation.y, rotation.x, - rotation.z); - }; break; - case UIDeviceOrientationLandscapeRight: { - OSIPhone::get_singleton()->update_gravity(gravity.y, -gravity.x, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - (acceleration.y + gravity.y), -(acceleration.x + gravity.x), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - magnetic.y, -magnetic.x, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(rotation.y, -rotation.x, - rotation.z); - }; break; - case UIDeviceOrientationPortraitUpsideDown: { - OSIPhone::get_singleton()->update_gravity(-gravity.x, gravity.y, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - -(acceleration.x + gravity.x), (acceleration.y + gravity.y), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - -magnetic.x, magnetic.y, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(-rotation.x, rotation.y, - rotation.z); - }; break; - default: { // assume portrait - OSIPhone::get_singleton()->update_gravity(gravity.x, gravity.y, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - acceleration.x + gravity.x, acceleration.y + gravity.y, - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, - magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(rotation.x, rotation.y, - rotation.z); - }; break; - }; - } - - bool quit_request = OSIPhone::get_singleton()->iterate(); - }; - - }; break; - }; -}; - -- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_OS_MEMORY_WARNING); - } -}; - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - CGRect rect = [[UIScreen mainScreen] bounds]; + // TODO: might be required to make an early return, so app wouldn't crash because of timeout. + // TODO: logo screen is not displayed while shaders are compiling + // DummyViewController(Splash/LoadingViewController) -> setup -> GodotViewController - is_focus_out = false; - - [application setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; - // disable idle timer - // application.idleTimerDisabled = YES; + CGRect windowBounds = [[UIScreen mainScreen] bounds]; // Create a full-screen window - window = [[UIWindow alloc] initWithFrame:rect]; - - OS::VideoMode vm = _get_video_mode(); + self.window = [[UIWindow alloc] initWithFrame:windowBounds]; - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, - NSUserDomainMask, YES); + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; - int err = iphone_main(vm.width, vm.height, gargc, gargv, String::utf8([documentsDirectory UTF8String])); + int err = iphone_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; + return NO; }; -#if defined(OPENGL_ENABLED) - // WARNING: We must *always* create the GLView after we have constructed the - // OS with iphone_main. This allows the GLView to access project settings so - // it can properly initialize the OpenGL context - GLView *glView = [[GLView alloc] initWithFrame:rect]; - glView.delegate = self; - - view_controller = [[ViewController alloc] init]; - view_controller.view = glView; - - _set_keep_screen_on(bool(GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true)) ? YES : NO); - glView.useCADisplayLink = - bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO; - printf("cadisaplylink: %d", glView.useCADisplayLink); - glView.animationInterval = 1.0 / kRenderingFrequency; - [glView startAnimation]; -#endif - -#if defined(VULKAN_ENABLED) - view_controller = [[ViewController alloc] init]; -#endif + ViewController *viewController = [[ViewController alloc] init]; + viewController.godotView.useCADisplayLink = bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO; + viewController.godotView.renderingInterval = 1.0 / kRenderingFrequency; - window.rootViewController = view_controller; + self.window.rootViewController = viewController; // Show the window - [window makeKeyAndVisible]; - - // Configure and start accelerometer - if (!motionInitialised) { - motionManager = [[CMMotionManager alloc] init]; - if (motionManager.deviceMotionAvailable) { - motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; - [motionManager startDeviceMotionUpdatesUsingReferenceFrame: - CMAttitudeReferenceFrameXMagneticNorthZVertical]; - motionInitialised = YES; - }; - }; - - [self initGameControllers]; + [self.window makeKeyAndVisible]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -674,40 +91,33 @@ static int frame_count = 0; name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; - // OSIPhone::screen_width = rect.size.width - rect.origin.x; - // OSIPhone::screen_height = rect.size.height - rect.origin.y; - - mainViewController = view_controller; + mainViewController = viewController; // prevent to stop music in another background app [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; - return TRUE; + return YES; }; - (void)onAudioInterruption:(NSNotification *)notification { if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) { if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeBegan]]) { NSLog(@"Audio interruption began"); - on_focus_out(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_out(); } else if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]) { NSLog(@"Audio interruption ended"); - on_focus_in(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_in(); } } }; -- (void)applicationWillTerminate:(UIApplication *)application { - [self deinitGameControllers]; - - if (motionInitialised) { - ///@TODO is this the right place to clean this up? - [motionManager stopDeviceMotionUpdates]; - [motionManager release]; - motionManager = nil; - motionInitialised = NO; - }; +- (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 { iphone_finish(); }; @@ -722,16 +132,15 @@ static int frame_count = 0; // notification panel by swiping from the upper part of the screen. - (void)applicationWillResignActive:(UIApplication *)application { - on_focus_out(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_out(); } - (void)applicationDidBecomeActive:(UIApplication *)application { - on_focus_in(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_in(); } - (void)dealloc { - [window release]; - [super dealloc]; + self.window = nil; } @end diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index 3e6c2f0ecf..0456458326 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -31,12 +31,10 @@ def get_opts(): ("IPHONESDK", "Path to the iPhone SDK", ""), BoolVariable( "use_static_mvk", - "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables validation layers)", + "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables" + " validation layers)", False, ), - BoolVariable("game_center", "Support for game center", True), - BoolVariable("store_kit", "Support for in-app store", True), - BoolVariable("icloud", "Support for iCloud", True), BoolVariable("ios_exceptions", "Enable exceptions", False), ("ios_triple", "Triple for ios toolchain", ""), ] @@ -67,7 +65,7 @@ def configure(env): elif env["target"] == "debug": env.Append(CCFLAGS=["-gdwarf-2", "-O0"]) - env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1), "DEBUG_ENABLED", "DEBUG_MEMORY_ENABLED"]) + env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1), "DEBUG_ENABLED"]) if env["use_lto"]: env.Append(CCFLAGS=["-flto"]) @@ -118,20 +116,33 @@ def configure(env): arch_flag = "i386" if env["arch"] == "x86" else env["arch"] env.Append( CCFLAGS=( - "-arch " + "-fobjc-arc -arch " + arch_flag - + " -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks -fasm-blocks -isysroot $IPHONESDK -mios-simulator-version-min=10.0" + + " -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks" + " -fasm-blocks -isysroot $IPHONESDK -mios-simulator-version-min=13.0" ).split() ) elif env["arch"] == "arm": detect_darwin_sdk_path("iphone", env) env.Append( - CCFLAGS='-fno-objc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits -fpascal-strings -fblocks -isysroot $IPHONESDK -fvisibility=hidden -mthumb "-DIBOutlet=__attribute__((iboutlet))" "-DIBOutletCollection(ClassName)=__attribute__((iboutletcollection(ClassName)))" "-DIBAction=void)__attribute__((ibaction)" -miphoneos-version-min=10.0 -MMD -MT dependencies'.split() + CCFLAGS=( + "-fobjc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing" + " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" + " -fpascal-strings -fblocks -isysroot $IPHONESDK -fvisibility=hidden -mthumb" + ' "-DIBOutlet=__attribute__((iboutlet))"' + ' "-DIBOutletCollection(ClassName)=__attribute__((iboutletcollection(ClassName)))"' + ' "-DIBAction=void)__attribute__((ibaction)" -miphoneos-version-min=11.0 -MMD -MT dependencies'.split() + ) ) elif env["arch"] == "arm64": detect_darwin_sdk_path("iphone", env) env.Append( - CCFLAGS="-fno-objc-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 -miphoneos-version-min=10.0 -isysroot $IPHONESDK".split() + 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 -miphoneos-version-min=11.0" + " -isysroot $IPHONESDK".split() + ) ) env.Append(CPPDEFINES=["NEED_LONG_INT"]) env.Append(CPPDEFINES=["LIBYUV_DISABLE_NEON"]) @@ -143,6 +154,9 @@ def configure(env): else: env.Append(CCFLAGS=["-fno-exceptions"]) + # Temp fix for ABS/MAX/MIN macros in iPhone SDK blocking compilation + env.Append(CCFLAGS=["-Wno-ambiguous-macro"]) + ## Link flags if env["arch"] == "x86" or env["arch"] == "x86_64": @@ -151,7 +165,7 @@ def configure(env): LINKFLAGS=[ "-arch", arch_flag, - "-mios-simulator-version-min=10.0", + "-mios-simulator-version-min=13.0", "-isysroot", "$IPHONESDK", "-Xlinker", @@ -162,9 +176,9 @@ def configure(env): ] ) elif env["arch"] == "arm": - env.Append(LINKFLAGS=["-arch", "armv7", "-Wl,-dead_strip", "-miphoneos-version-min=10.0"]) + env.Append(LINKFLAGS=["-arch", "armv7", "-Wl,-dead_strip", "-miphoneos-version-min=11.0"]) if env["arch"] == "arm64": - env.Append(LINKFLAGS=["-arch", "arm64", "-Wl,-dead_strip", "-miphoneos-version-min=10.0"]) + env.Append(LINKFLAGS=["-arch", "arm64", "-Wl,-dead_strip", "-miphoneos-version-min=11.0"]) env.Append( LINKFLAGS=[ @@ -205,20 +219,11 @@ def configure(env): ] ) - # Feature options - if env["game_center"]: - env.Append(CPPDEFINES=["GAME_CENTER_ENABLED"]) - env.Append(LINKFLAGS=["-framework", "GameKit"]) - - if env["store_kit"]: - env.Append(CPPDEFINES=["STOREKIT_ENABLED"]) - env.Append(LINKFLAGS=["-framework", "StoreKit"]) - - if env["icloud"]: - env.Append(CPPDEFINES=["ICLOUD_ENABLED"]) - env.Prepend( - CPPPATH=["$IPHONESDK/usr/include", "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers",] + CPPPATH=[ + "$IPHONESDK/usr/include", + "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers", + ] ) env["ENV"]["CODESIGN_ALLOCATE"] = "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate" @@ -228,8 +233,7 @@ def configure(env): env.Append(CPPDEFINES=["VULKAN_ENABLED"]) env.Append(LINKFLAGS=["-framework", "IOSurface"]) - if env["use_static_mvk"]: - env.Append(LINKFLAGS=["-framework", "MoltenVK"]) - env["builtin_vulkan"] = False - elif not env["builtin_vulkan"]: - env.Append(LIBS=["vulkan"]) + + # Use Static Vulkan for iOS. Dynamic Framework works fine too. + env.Append(LINKFLAGS=["-framework", "MoltenVK"]) + env["builtin_vulkan"] = False diff --git a/platform/iphone/device_metrics.h b/platform/iphone/device_metrics.h new file mode 100644 index 0000000000..6d0ff49077 --- /dev/null +++ b/platform/iphone/device_metrics.h @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* device_metrics.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <Foundation/Foundation.h> + +@interface GodotDeviceMetrics : NSObject + +@property(nonatomic, class, readonly, strong) NSDictionary<NSArray *, NSNumber *> *dpiList; + +@end diff --git a/platform/iphone/device_metrics.m b/platform/iphone/device_metrics.m new file mode 100644 index 0000000000..747872bc49 --- /dev/null +++ b/platform/iphone/device_metrics.m @@ -0,0 +1,152 @@ +/*************************************************************************/ +/* device_metrics.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "device_metrics.h" + +@implementation GodotDeviceMetrics + ++ (NSDictionary *)dpiList { + return @{ + @[ + @"iPad1,1", + @"iPad2,1", + @"iPad2,2", + @"iPad2,3", + @"iPad2,4", + ] : @132, + @[ + @"iPhone1,1", + @"iPhone1,2", + @"iPhone2,1", + @"iPad2,5", + @"iPad2,6", + @"iPad2,7", + @"iPod1,1", + @"iPod2,1", + @"iPod3,1", + ] : @163, + @[ + @"iPad3,1", + @"iPad3,2", + @"iPad3,3", + @"iPad3,4", + @"iPad3,5", + @"iPad3,6", + @"iPad4,1", + @"iPad4,2", + @"iPad4,3", + @"iPad5,3", + @"iPad5,4", + @"iPad6,3", + @"iPad6,4", + @"iPad6,7", + @"iPad6,8", + @"iPad6,11", + @"iPad6,12", + @"iPad7,1", + @"iPad7,2", + @"iPad7,3", + @"iPad7,4", + @"iPad7,5", + @"iPad7,6", + @"iPad7,11", + @"iPad7,12", + @"iPad8,1", + @"iPad8,2", + @"iPad8,3", + @"iPad8,4", + @"iPad8,5", + @"iPad8,6", + @"iPad8,7", + @"iPad8,8", + @"iPad8,9", + @"iPad8,10", + @"iPad8,11", + @"iPad8,12", + @"iPad11,3", + @"iPad11,4", + ] : @264, + @[ + @"iPhone3,1", + @"iPhone3,2", + @"iPhone3,3", + @"iPhone4,1", + @"iPhone5,1", + @"iPhone5,2", + @"iPhone5,3", + @"iPhone5,4", + @"iPhone6,1", + @"iPhone6,2", + @"iPhone7,2", + @"iPhone8,1", + @"iPhone8,4", + @"iPhone9,1", + @"iPhone9,3", + @"iPhone10,1", + @"iPhone10,4", + @"iPhone11,8", + @"iPhone12,1", + @"iPhone12,8", + @"iPad4,4", + @"iPad4,5", + @"iPad4,6", + @"iPad4,7", + @"iPad4,8", + @"iPad4,9", + @"iPad5,1", + @"iPad5,2", + @"iPad11,1", + @"iPad11,2", + @"iPod4,1", + @"iPod5,1", + @"iPod7,1", + @"iPod9,1", + ] : @326, + @[ + @"iPhone7,1", + @"iPhone8,2", + @"iPhone9,2", + @"iPhone9,4", + @"iPhone10,2", + @"iPhone10,5", + ] : @401, + @[ + @"iPhone10,3", + @"iPhone10,6", + @"iPhone11,2", + @"iPhone11,4", + @"iPhone11,6", + @"iPhone12,3", + @"iPhone12,5", + ] : @458, + }; +} + +@end diff --git a/platform/iphone/in_app_store.h b/platform/iphone/display_layer.h index 44e65e77ed..bfde8f96a3 100644 --- a/platform/iphone/in_app_store.h +++ b/platform/iphone/display_layer.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* in_app_store.h */ +/* display_layer.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,40 +28,31 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifdef STOREKIT_ENABLED +#import <OpenGLES/EAGLDrawable.h> +#import <QuartzCore/QuartzCore.h> -#ifndef IN_APP_STORE_H -#define IN_APP_STORE_H +@protocol DisplayLayer <NSObject> -#include "core/object.h" +- (void)renderDisplayLayer; +- (void)initializeDisplayLayer; +- (void)layoutDisplayLayer; -class InAppStore : public Object { - GDCLASS(InAppStore, Object); - - static InAppStore *instance; - static void _bind_methods(); - - List<Variant> pending_events; - -public: - Error request_product_info(Variant p_params); - Error restore_purchases(); - Error purchase(Variant p_params); - - int get_pending_event_count(); - Variant pop_pending_event(); - void finish_transaction(String product_id); - void set_auto_finish_transaction(bool b); - - void _post_event(Variant p_event); - void _record_purchase(String product_id); - - static InAppStore *get_singleton(); - - InAppStore(); - ~InAppStore(); -}; +@end +// An ugly workaround for iOS simulator +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR +#if defined(__IPHONE_13_0) +API_AVAILABLE(ios(13.0)) +@interface GodotMetalLayer : CAMetalLayer <DisplayLayer> +#else +@interface GodotMetalLayer : CALayer <DisplayLayer> #endif - +#else +@interface GodotMetalLayer : CAMetalLayer <DisplayLayer> #endif +@end + +API_DEPRECATED("OpenGLES is deprecated", ios(2.0, 12.0)) +@interface GodotOpenGLLayer : CAEAGLLayer <DisplayLayer> + +@end diff --git a/platform/iphone/display_layer.mm b/platform/iphone/display_layer.mm new file mode 100644 index 0000000000..7420a5ebe6 --- /dev/null +++ b/platform/iphone/display_layer.mm @@ -0,0 +1,183 @@ +/*************************************************************************/ +/* display_layer.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "display_layer.h" +#include "core/config/project_settings.h" +#include "core/os/keyboard.h" +#include "display_server_iphone.h" +#include "main/main.h" +#include "os_iphone.h" +#include "servers/audio_server.h" + +#import <AudioToolbox/AudioServices.h> +#import <GameController/GameController.h> +#import <OpenGLES/EAGL.h> +#import <OpenGLES/ES1/gl.h> +#import <OpenGLES/ES1/glext.h> +#import <QuartzCore/QuartzCore.h> +#import <UIKit/UIKit.h> + +@implementation GodotMetalLayer + +- (void)initializeDisplayLayer { +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR + if (@available(iOS 13, *)) { + // Simulator supports Metal since iOS 13 + } else { + NSLog(@"iOS Simulator prior to iOS 13 does not support Metal rendering."); + } +#endif +} + +- (void)layoutDisplayLayer { +} + +- (void)renderDisplayLayer { +} + +@end + +@implementation GodotOpenGLLayer { + // The pixel dimensions of the backbuffer + GLint backingWidth; + GLint backingHeight; + + EAGLContext *context; + GLuint viewRenderbuffer, viewFramebuffer; + GLuint depthRenderbuffer; +} + +- (void)initializeDisplayLayer { + // Get our backing layer + + // 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]; + + // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? + + // Create GL ES 2 context + if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES2") { + 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)renderDisplayLayer { + [EAGLContext setCurrentContext:context]; +} + +- (void)dealloc { + if ([EAGLContext currentContext] == context) { + [EAGLContext setCurrentContext:nil]; + } + + if (context) { + context = nil; + } +} + +- (BOOL)createFramebuffer { + 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 CAself) + // 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<EAGLDrawable>)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); + // OSIPhone::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/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h new file mode 100644 index 0000000000..229b1e80db --- /dev/null +++ b/platform/iphone/display_server_iphone.h @@ -0,0 +1,202 @@ +/*************************************************************************/ +/* display_server_iphone.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef display_server_iphone_h +#define display_server_iphone_h + +#include "core/input/input.h" +#include "servers/display_server.h" + +#if defined(VULKAN_ENABLED) +#include "drivers/vulkan/rendering_device_vulkan.h" +#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" + +#include "vulkan_context_iphone.h" + +#import <QuartzCore/CAMetalLayer.h> +#include <vulkan/vulkan_metal.h> +#endif + +class DisplayServerIPhone : public DisplayServer { + GDCLASS(DisplayServerIPhone, DisplayServer) + + _THREAD_SAFE_CLASS_ + +#if defined(VULKAN_ENABLED) + VulkanContextIPhone *context_vulkan; + RenderingDeviceVulkan *rendering_device_vulkan; +#endif + + DisplayServer::ScreenOrientation screen_orientation; + + ObjectID window_attached_instance_id; + + Callable window_event_callback; + Callable window_resize_callback; + Callable input_event_callback; + Callable input_text_callback; + + int virtual_keyboard_height = 0; + + void perform_event(const Ref<InputEvent> &p_event); + + DisplayServerIPhone(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + ~DisplayServerIPhone(); + +public: + String rendering_driver; + + static DisplayServerIPhone *get_singleton(); + + static void register_iphone_driver(); + static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + static Vector<String> get_rendering_drivers_func(); + + // MARK: - Events + + virtual void process_events() override; + + virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + + static void _dispatch_input_events(const Ref<InputEvent> &p_event); + void send_input_event(const Ref<InputEvent> &p_event) const; + void send_input_text(const String &p_text) const; + void send_window_event(DisplayServer::WindowEvent p_event) const; + void _window_callback(const Callable &p_callable, const Variant &p_arg) const; + + // MARK: - Input + + // MARK: Touches + + void touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick); + void touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y); + void touches_cancelled(int p_idx); + + // MARK: Keyboard + + void key(uint32_t p_key, bool p_pressed); + + // MARK: Motion + + void update_gravity(float p_x, float p_y, float p_z); + void update_accelerometer(float p_x, float p_y, float p_z); + void update_magnetometer(float p_x, float p_y, float p_z); + void update_gyroscope(float p_x, float p_y, float p_z); + + // MARK: - + + virtual bool has_feature(Feature p_feature) const override; + virtual String get_name() const override; + + virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; + + virtual int get_screen_count() const override; + virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + + virtual Vector<DisplayServer::WindowID> get_window_list() const override; + + virtual WindowID + get_window_at_screen_position(const Point2i &p_position) const override; + + virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; + virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; + + virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override; + + virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override; + + virtual void window_set_transient(WindowID p_window, WindowID p_parent) override; + + virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override; + virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override; + virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; + + virtual float screen_get_max_scale() const override; + + virtual void screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) override; + virtual DisplayServer::ScreenOrientation screen_get_orientation(int p_screen) const override; + + virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual bool can_any_window_draw() const override; + + virtual bool screen_is_touchscreen(int p_screen) const override; + + virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) override; + virtual void virtual_keyboard_hide() override; + + void virtual_keyboard_set_height(int height); + virtual int virtual_keyboard_get_height() const override; + + virtual void clipboard_set(const String &p_text) override; + virtual String clipboard_get() const override; + + virtual void screen_set_keep_on(bool p_enable) override; + virtual bool screen_is_kept_on() const override; + + virtual Error native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track, int p_screen = SCREEN_OF_MAIN_WINDOW) override; + virtual bool native_video_is_playing() const override; + virtual void native_video_pause() override; + virtual void native_video_unpause() override; + virtual void native_video_stop() override; + + void resize_window(CGSize size); +}; + +#endif /* display_server_iphone_h */ diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm new file mode 100644 index 0000000000..d47d131719 --- /dev/null +++ b/platform/iphone/display_server_iphone.mm @@ -0,0 +1,648 @@ +/*************************************************************************/ +/* display_server_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "display_server_iphone.h" +#import "app_delegate.h" +#include "core/config/project_settings.h" +#include "core/io/file_access_pack.h" +#import "device_metrics.h" +#import "godot_view.h" +#include "ios.h" +#import "keyboard_input_view.h" +#import "native_video_view.h" +#include "os_iphone.h" +#import "view_controller.h" + +#import <Foundation/Foundation.h> +#import <sys/utsname.h> + +static const float kDisplayServerIPhoneAcceleration = 1; + +DisplayServerIPhone *DisplayServerIPhone::get_singleton() { + return (DisplayServerIPhone *)DisplayServer::get_singleton(); +} + +DisplayServerIPhone::DisplayServerIPhone(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + rendering_driver = p_rendering_driver; + +#if defined(OPENGL_ENABLED) + // FIXME: Add support for both GLES2 and Vulkan when GLES2 is implemented + // again, + + if (rendering_driver == "opengl_es") { + bool gl_initialization_error = false; + + // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? + + if (RasterizerGLES2::is_viable() == OK) { + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; + } + + 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; + } + + // rendering_server = memnew(RenderingServerRaster); + // // FIXME: Reimplement threaded rendering + // if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { + // rendering_server = memnew(RenderingServerWrapMT(rendering_server, + // false)); + // } + // rendering_server->init(); + // rendering_server->cursor_set_visible(false, 0); + + // reset this to what it should be, it will have been set to 0 after + // rendering_server->init() is called + // RasterizerStorageGLES2::system_fbo = gl_view_base_fb; + } +#endif + +#if defined(VULKAN_ENABLED) + rendering_driver = "vulkan"; + + context_vulkan = nullptr; + rendering_device_vulkan = nullptr; + + if (rendering_driver == "vulkan") { + context_vulkan = memnew(VulkanContextIPhone); + if (context_vulkan->initialize() != OK) { + memdelete(context_vulkan); + context_vulkan = nullptr; + ERR_FAIL_MSG("Failed to initialize Vulkan context"); + } + + CALayer *layer = [AppDelegate.viewController.godotView initializeRenderingForDriver:@"vulkan"]; + + if (!layer) { + ERR_FAIL_MSG("Failed to create iOS rendering layer."); + } + + Size2i size = Size2i(layer.bounds.size.width, layer.bounds.size.height) * screen_get_max_scale(); + if (context_vulkan->window_create(MAIN_WINDOW_ID, layer, size.width, size.height) != OK) { + memdelete(context_vulkan); + context_vulkan = nullptr; + ERR_FAIL_MSG("Failed to create Vulkan window."); + } + + rendering_device_vulkan = memnew(RenderingDeviceVulkan); + rendering_device_vulkan->initialize(context_vulkan); + + RasterizerRD::make_current(); + } +#endif + + bool keep_screen_on = bool(GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true)); + screen_set_keep_on(keep_screen_on); + + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); + + r_error = OK; +} + +DisplayServerIPhone::~DisplayServerIPhone() { +#if defined(VULKAN_ENABLED) + if (rendering_driver == "vulkan") { + if (rendering_device_vulkan) { + rendering_device_vulkan->finalize(); + memdelete(rendering_device_vulkan); + rendering_device_vulkan = NULL; + } + + if (context_vulkan) { + context_vulkan->window_destroy(MAIN_WINDOW_ID); + memdelete(context_vulkan); + context_vulkan = NULL; + } + } +#endif +} + +DisplayServer *DisplayServerIPhone::create_func(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + return memnew(DisplayServerIPhone(p_rendering_driver, p_mode, p_flags, p_resolution, r_error)); +} + +Vector<String> DisplayServerIPhone::get_rendering_drivers_func() { + Vector<String> drivers; + +#if defined(VULKAN_ENABLED) + drivers.push_back("vulkan"); +#endif +#if defined(OPENGL_ENABLED) + drivers.push_back("opengl_es"); +#endif + + return drivers; +} + +void DisplayServerIPhone::register_iphone_driver() { + register_create_function("iphone", create_func, get_rendering_drivers_func); +} + +// MARK: Events + +void DisplayServerIPhone::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { + window_resize_callback = p_callable; +} + +void DisplayServerIPhone::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) { + window_event_callback = p_callable; +} +void DisplayServerIPhone::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) { + input_event_callback = p_callable; +} + +void DisplayServerIPhone::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) { + input_text_callback = p_callable; +} + +void DisplayServerIPhone::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::process_events() { +} + +void DisplayServerIPhone::_dispatch_input_events(const Ref<InputEvent> &p_event) { + DisplayServerIPhone::get_singleton()->send_input_event(p_event); +} + +void DisplayServerIPhone::send_input_event(const Ref<InputEvent> &p_event) const { + _window_callback(input_event_callback, p_event); +} + +void DisplayServerIPhone::send_input_text(const String &p_text) const { + _window_callback(input_text_callback, p_text); +} + +void DisplayServerIPhone::send_window_event(DisplayServer::WindowEvent p_event) const { + _window_callback(window_event_callback, int(p_event)); +} + +void DisplayServerIPhone::_window_callback(const Callable &p_callable, const Variant &p_arg) const { + if (!p_callable.is_null()) { + const Variant *argp = &p_arg; + Variant ret; + Callable::CallError ce; + p_callable.call((const Variant **)&argp, 1, ret, ce); + } +} + +// MARK: - Input + +// MARK: Touches + +void DisplayServerIPhone::touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick) { + if (!GLOBAL_DEF("debug/disable_touch", false)) { + Ref<InputEventScreenTouch> ev; + ev.instance(); + + ev->set_index(p_idx); + ev->set_pressed(p_pressed); + ev->set_position(Vector2(p_x, p_y)); + perform_event(ev); + } +}; + +void DisplayServerIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y) { + if (!GLOBAL_DEF("debug/disable_touch", false)) { + Ref<InputEventScreenDrag> ev; + ev.instance(); + ev->set_index(p_idx); + ev->set_position(Vector2(p_x, p_y)); + ev->set_relative(Vector2(p_x - p_prev_x, p_y - p_prev_y)); + perform_event(ev); + }; +}; + +void DisplayServerIPhone::perform_event(const Ref<InputEvent> &p_event) { + Input::get_singleton()->parse_input_event(p_event); +}; + +void DisplayServerIPhone::touches_cancelled(int p_idx) { + touch_press(p_idx, -1, -1, false, false); +}; + +// MARK: Keyboard + +void DisplayServerIPhone::key(uint32_t p_key, bool p_pressed) { + Ref<InputEventKey> ev; + ev.instance(); + ev->set_echo(false); + ev->set_pressed(p_pressed); + ev->set_keycode(p_key); + ev->set_physical_keycode(p_key); + ev->set_unicode(p_key); + perform_event(ev); +}; + +// MARK: Motion + +void DisplayServerIPhone::update_gravity(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_gravity(Vector3(p_x, p_y, p_z)); +}; + +void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, float p_z) { + // Found out the Z should not be negated! Pass as is! + Vector3 v_accelerometer = Vector3( + p_x / kDisplayServerIPhoneAcceleration, + p_y / kDisplayServerIPhoneAcceleration, + p_z / kDisplayServerIPhoneAcceleration); + + Input::get_singleton()->set_accelerometer(v_accelerometer); +}; + +void DisplayServerIPhone::update_magnetometer(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_magnetometer(Vector3(p_x, p_y, p_z)); +}; + +void DisplayServerIPhone::update_gyroscope(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_gyroscope(Vector3(p_x, p_y, p_z)); +}; + +// MARK: - + +bool DisplayServerIPhone::has_feature(Feature p_feature) const { + switch (p_feature) { + // case FEATURE_CONSOLE_WINDOW: + // case FEATURE_CURSOR_SHAPE: + // case FEATURE_CUSTOM_CURSOR_SHAPE: + // case FEATURE_GLOBAL_MENU: + // case FEATURE_HIDPI: + // case FEATURE_ICON: + // case FEATURE_IME: + // case FEATURE_MOUSE: + // case FEATURE_MOUSE_WARP: + // case FEATURE_NATIVE_DIALOG: + // case FEATURE_NATIVE_ICON: + // case FEATURE_NATIVE_VIDEO: + // case FEATURE_WINDOW_TRANSPARENCY: + case FEATURE_CLIPBOARD: + case FEATURE_KEEP_SCREEN_ON: + case FEATURE_ORIENTATION: + case FEATURE_TOUCHSCREEN: + case FEATURE_VIRTUAL_KEYBOARD: + return true; + default: + return false; + } +} + +String DisplayServerIPhone::get_name() const { + return "iPhone"; +} + +void DisplayServerIPhone::alert(const String &p_alert, const String &p_title) { + const CharString utf8_alert = p_alert.utf8(); + const CharString utf8_title = p_title.utf8(); + iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); +} + +int DisplayServerIPhone::get_screen_count() const { + return 1; +} + +Point2i DisplayServerIPhone::screen_get_position(int p_screen) const { + return Size2i(); +} + +Size2i DisplayServerIPhone::screen_get_size(int p_screen) const { + CALayer *layer = AppDelegate.viewController.godotView.renderingLayer; + + if (!layer) { + return Size2i(); + } + + return Size2i(layer.bounds.size.width, layer.bounds.size.height) * screen_get_scale(p_screen); +} + +Rect2i DisplayServerIPhone::screen_get_usable_rect(int p_screen) const { + if (@available(iOS 11, *)) { + UIEdgeInsets insets = UIEdgeInsetsZero; + UIView *view = AppDelegate.viewController.godotView; + + if ([view respondsToSelector:@selector(safeAreaInsets)]) { + insets = [view safeAreaInsets]; + } + + float scale = screen_get_scale(p_screen); + Size2i insets_position = Size2i(insets.left, insets.top) * scale; + Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; + + return Rect2i(screen_get_position(p_screen) + insets_position, screen_get_size(p_screen) - insets_size); + } else { + return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); + } +} + +int DisplayServerIPhone::screen_get_dpi(int p_screen) const { + struct utsname systemInfo; + uname(&systemInfo); + + NSString *string = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + + NSDictionary *iOSModelToDPI = [GodotDeviceMetrics dpiList]; + + for (NSArray *keyArray in iOSModelToDPI) { + if ([keyArray containsObject:string]) { + NSNumber *value = iOSModelToDPI[keyArray]; + return [value intValue]; + } + } + + // If device wasn't found in dictionary + // make a best guess from device metrics. + CGFloat scale = [UIScreen mainScreen].scale; + + UIUserInterfaceIdiom idiom = [UIDevice currentDevice].userInterfaceIdiom; + + switch (idiom) { + case UIUserInterfaceIdiomPad: + return scale == 2 ? 264 : 132; + case UIUserInterfaceIdiomPhone: { + if (scale == 3) { + CGFloat nativeScale = [UIScreen mainScreen].nativeScale; + return nativeScale == 3 ? 458 : 401; + } + + return 326; + } + default: + return 72; + } +} + +float DisplayServerIPhone::screen_get_scale(int p_screen) const { + return [UIScreen mainScreen].nativeScale; +} + +Vector<DisplayServer::WindowID> DisplayServerIPhone::get_window_list() const { + Vector<DisplayServer::WindowID> list; + list.push_back(MAIN_WINDOW_ID); + return list; +} + +DisplayServer::WindowID DisplayServerIPhone::get_window_at_screen_position(const Point2i &p_position) const { + return MAIN_WINDOW_ID; +} + +void DisplayServerIPhone::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { + window_attached_instance_id = p_instance; +} + +ObjectID DisplayServerIPhone::window_get_attached_instance_id(WindowID p_window) const { + return window_attached_instance_id; +} + +void DisplayServerIPhone::window_set_title(const String &p_title, WindowID p_window) { + // Probably not supported for iOS +} + +int DisplayServerIPhone::window_get_current_screen(WindowID p_window) const { + return SCREEN_OF_MAIN_WINDOW; +} + +void DisplayServerIPhone::window_set_current_screen(int p_screen, WindowID p_window) { + // Probably not supported for iOS +} + +Point2i DisplayServerIPhone::window_get_position(WindowID p_window) const { + return Point2i(); +} + +void DisplayServerIPhone::window_set_position(const Point2i &p_position, WindowID p_window) { + // Probably not supported for single window iOS app +} + +void DisplayServerIPhone::window_set_transient(WindowID p_window, WindowID p_parent) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::window_set_max_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_max_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerIPhone::window_set_min_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_min_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerIPhone::window_set_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_size(WindowID p_window) const { + CGRect screenBounds = [UIScreen mainScreen].bounds; + return Size2i(screenBounds.size.width, screenBounds.size.height) * screen_get_max_scale(); +} + +Size2i DisplayServerIPhone::window_get_real_size(WindowID p_window) const { + return window_get_size(p_window); +} + +void DisplayServerIPhone::window_set_mode(WindowMode p_mode, WindowID p_window) { + // Probably not supported for iOS +} + +DisplayServer::WindowMode DisplayServerIPhone::window_get_mode(WindowID p_window) const { + return WindowMode::WINDOW_MODE_FULLSCREEN; +} + +bool DisplayServerIPhone::window_is_maximize_allowed(WindowID p_window) const { + return false; +} + +void DisplayServerIPhone::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { + // Probably not supported for iOS +} + +bool DisplayServerIPhone::window_get_flag(WindowFlags p_flag, WindowID p_window) const { + return false; +} + +void DisplayServerIPhone::window_request_attention(WindowID p_window) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::window_move_to_foreground(WindowID p_window) { + // Probably not supported for iOS +} + +float DisplayServerIPhone::screen_get_max_scale() const { + return screen_get_scale(SCREEN_OF_MAIN_WINDOW); +}; + +void DisplayServerIPhone::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) { + screen_orientation = p_orientation; +} + +DisplayServer::ScreenOrientation DisplayServerIPhone::screen_get_orientation(int p_screen) const { + return screen_orientation; +} + +bool DisplayServerIPhone::window_can_draw(WindowID p_window) const { + return true; +} + +bool DisplayServerIPhone::can_any_window_draw() const { + return true; +} + +bool DisplayServerIPhone::screen_is_touchscreen(int p_screen) const { + return true; +} + +void DisplayServerIPhone::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_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 DisplayServerIPhone::virtual_keyboard_hide() { + [AppDelegate.viewController.keyboardView resignFirstResponder]; +} + +void DisplayServerIPhone::virtual_keyboard_set_height(int height) { + virtual_keyboard_height = height * screen_get_max_scale(); +} + +int DisplayServerIPhone::virtual_keyboard_get_height() const { + return virtual_keyboard_height; +} + +void DisplayServerIPhone::clipboard_set(const String &p_text) { + [UIPasteboard generalPasteboard].string = [NSString stringWithUTF8String:p_text.utf8()]; +} + +String DisplayServerIPhone::clipboard_get() const { + NSString *text = [UIPasteboard generalPasteboard].string; + + return String::utf8([text UTF8String]); +} + +void DisplayServerIPhone::screen_set_keep_on(bool p_enable) { + [UIApplication sharedApplication].idleTimerDisabled = p_enable; +} + +bool DisplayServerIPhone::screen_is_kept_on() const { + return [UIApplication sharedApplication].idleTimerDisabled; +} + +Error DisplayServerIPhone::native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track, int p_screen) { + FileAccess *f = FileAccess::open(p_path, FileAccess::READ); + bool exists = f && f->is_open(); + + String user_data_dir = OSIPhone::get_singleton()->get_user_data_dir(); + + if (!exists) { + return FAILED; + } + + String tempFile = OSIPhone::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 DisplayServerIPhone::native_video_is_playing() const { + return [AppDelegate.viewController.videoView isVideoPlaying]; +} + +void DisplayServerIPhone::native_video_pause() { + if (native_video_is_playing()) { + [AppDelegate.viewController.videoView pauseVideo]; + } +} + +void DisplayServerIPhone::native_video_unpause() { + [AppDelegate.viewController.videoView unpauseVideo]; +}; + +void DisplayServerIPhone::native_video_stop() { + if (native_video_is_playing()) { + [AppDelegate.viewController.videoView stopVideo]; + } +} + +void DisplayServerIPhone::resize_window(CGSize viewSize) { + Size2i size = Size2i(viewSize.width, viewSize.height) * screen_get_max_scale(); + +#if defined(VULKAN_ENABLED) + if (rendering_driver == "vulkan") { + if (context_vulkan) { + context_vulkan->window_resize(MAIN_WINDOW_ID, size.x, size.y); + } + } +#endif + + Variant resize_rect = Rect2i(Point2i(), size); + _window_callback(window_resize_callback, resize_rect); +} diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp index 4393a4ae9f..fff82c9467 100644 --- a/platform/iphone/export/export.cpp +++ b/platform/iphone/export/export.cpp @@ -30,19 +30,20 @@ #include "export.h" +#include "core/config/project_settings.h" #include "core/io/image_loader.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/iphone/logo.gen.h" +#include "platform/iphone/plugin/godot_plugin_config.h" #include "string.h" #include <sys/stat.h> @@ -54,6 +55,13 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Ref<ImageTexture> logo; + // Plugins + volatile bool plugins_changed; + Thread *check_for_changes_thread; + volatile bool quit_request; + Mutex plugins_lock; + Vector<PluginConfig> plugins; + typedef Error (*FileHandler)(String p_file, void *p_userdata); static Error _walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata); static Error _codesign(String p_file, void *p_userdata); @@ -70,6 +78,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { String modules_fileref; String modules_buildphase; String modules_buildgrp; + Vector<String> capabilities; }; struct ExportArchitecture { String name; @@ -86,21 +95,25 @@ class EditorExportPlatformIOS : public EditorExportPlatform { struct IOSExportAsset { 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<EditorExportPreset> &p_preset, Vector<uint8_t> &pfile, const IOSConfigData &p_config, bool p_debug); - Error _export_loading_screens(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir); + Error _export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir); + Error _export_loading_screen_file(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir); Error _export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir); Vector<ExportArchitecture> _get_supported_architectures(); Vector<String> _get_preset_architectures(const Ref<EditorExportPreset> &p_preset); void _add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets); - Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets); + Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_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<IOSExportAsset> &r_exported_assets); Error _export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets); + Error _export_ios_plugins(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &dest_dir, Vector<IOSExportAsset> &r_exported_assets, bool p_debug); bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { String pname = p_package; @@ -113,7 +126,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t 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)); @@ -125,42 +138,162 @@ class EditorExportPlatformIOS : public EditorExportPlatform { return true; } + static void _check_for_changes_poll_thread(void *ud) { + EditorExportPlatformIOS *ea = (EditorExportPlatformIOS *)ud; + + while (!ea->quit_request) { + // Nothing to do if we already know the plugins have changed. + if (!ea->plugins_changed) { + MutexLock lock(ea->plugins_lock); + + Vector<PluginConfig> loaded_plugins = get_plugins(); + + if (ea->plugins.size() != loaded_plugins.size()) { + ea->plugins_changed = true; + } 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 = true; + break; + } + } + } + } + + 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) { + break; + } + } + } + } + protected: - virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features); - virtual void get_export_options(List<ExportOption> *r_options); + virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override; + virtual void get_export_options(List<ExportOption> *r_options) override; public: - virtual String get_name() const { return "iOS"; } - virtual String get_os_name() const { return "iOS"; } - virtual Ref<Texture2D> get_logo() const { return logo; } + virtual String get_name() const override { return "iOS"; } + virtual String get_os_name() const override { return "iOS"; } + virtual Ref<Texture2D> get_logo() const override { return logo; } + + virtual bool should_update_export_options() override { + bool export_options_changed = plugins_changed; + if (export_options_changed) { + // don't clear unless we're reporting true, to avoid race + plugins_changed = false; + } + return export_options_changed; + } - virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { + virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { List<String> list; list.push_back("ipa"); return list; } - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0); - virtual void add_module_code(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &p_name, const String &p_fid, const String &p_gid); + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const; + virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; - virtual void get_platform_features(List<String> *r_features) { + virtual void get_platform_features(List<String> *r_features) override { r_features->push_back("mobile"); r_features->push_back("iOS"); } - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { } EditorExportPlatformIOS(); ~EditorExportPlatformIOS(); + + /// List the gdip files in the directory specified by the p_path parameter. + static Vector<String> list_plugin_config_files(const String &p_path, bool p_check_directories) { + Vector<String> 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<String> 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(PLUGIN_CONFIG_EXT)) { + dir_files.push_back(file); + } + } + da->list_dir_end(); + } + + return dir_files; + } + + static Vector<PluginConfig> get_plugins() { + Vector<PluginConfig> loaded_plugins; + + String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().plus_file("ios/plugins"); + + if (DirAccess::exists(plugins_dir)) { + Vector<String> plugins_filenames = list_plugin_config_files(plugins_dir, true); + + if (!plugins_filenames.empty()) { + Ref<ConfigFile> config_file = memnew(ConfigFile); + for (int i = 0; i < plugins_filenames.size(); i++) { + PluginConfig 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<PluginConfig> get_enabled_plugins(const Ref<EditorExportPreset> &p_presets) { + Vector<PluginConfig> enabled_plugins; + Vector<PluginConfig> all_plugins = get_plugins(); + for (int i = 0; i < all_plugins.size(); i++) { + PluginConfig plugin = all_plugins[i]; + bool enabled = p_presets->get("plugins/" + plugin.name); + if (enabled) { + enabled_plugins.push_back(plugin); + } + } + + return enabled_plugins; + } }; void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name"); - if (driver == "GLES2") { - r_features->push_back("etc"); - } else if (driver == "Vulkan") { + r_features->push_back("pvrtc"); + if (driver == "Vulkan") { // FIXME: Review if this is correct. r_features->push_back("etc2"); } @@ -205,12 +338,16 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *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"), "")); + Vector<ExportArchitecture> architectures = _get_supported_architectures(); + for (int i = 0; i < architectures.size(); ++i) { + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + architectures[i].name), architectures[i].is_default)); + } + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_store_team_id"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_debug"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_debug", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Developer"), "iPhone Developer")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_debug", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 1)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_release"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_release", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Distribution"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_release", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 0)); @@ -223,12 +360,14 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/arkit"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/camera"), false)); + Vector<PluginConfig> 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 = false; + plugins = found_plugins; r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/access_wifi"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/game_center"), true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/in_app_purchases"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "capabilities/push_notifications"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data/accessible_from_files_app"), false)); @@ -255,16 +394,18 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "optional_icons/spotlight_40x40", PROPERTY_HINT_FILE, "*.png"), "")); // Spotlight r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "optional_icons/spotlight_80x80", PROPERTY_HINT_FILE, "*.png"), "")); // Spotlight on devices with retina display + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_launch_screen_storyboard"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "storyboard/image_scale_mode", PROPERTY_HINT_ENUM, "Same as Logo,Center,Scale To Fit,Scale To Fill,Scale"), 0)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE, "*.png"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE, "*.png"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_custom_bg_color"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "storyboard/custom_bg_color"), Color())); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "launch_screens/generate_missing"), false)); for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, loading_screen_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png"), "")); } - - Vector<ExportArchitecture> architectures = _get_supported_architectures(); - for (int i = 0; i < architectures.size(); ++i) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + architectures[i].name), architectures[i].is_default)); - } } void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &pfile, const IOSConfigData &p_config, bool p_debug) { @@ -274,6 +415,12 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ "ad-hoc", "enterprise" }; + static const String storyboard_image_scale_mode[] = { + "center", + "scaleAspectFit", + "scaleAspectFill", + "scaleToFill" + }; String str; String strnew; str.parse_utf8((const char *)pfile.ptr(), pfile.size()); @@ -331,18 +478,6 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$docs_in_place", ((bool)p_preset->get("user_data/accessible_from_files_app")) ? "<true/>" : "<false/>") + "\n"; } else if (lines[i].find("$docs_sharing") != -1) { strnew += lines[i].replace("$docs_sharing", ((bool)p_preset->get("user_data/accessible_from_itunes_sharing")) ? "<true/>" : "<false/>") + "\n"; - } else if (lines[i].find("$access_wifi") != -1) { - bool is_on = p_preset->get("capabilities/access_wifi"); - strnew += lines[i].replace("$access_wifi", is_on ? "1" : "0") + "\n"; - } else if (lines[i].find("$game_center") != -1) { - bool is_on = p_preset->get("capabilities/game_center"); - strnew += lines[i].replace("$game_center", is_on ? "1" : "0") + "\n"; - } else if (lines[i].find("$in_app_purchases") != -1) { - bool is_on = p_preset->get("capabilities/in_app_purchases"); - strnew += lines[i].replace("$in_app_purchases", is_on ? "1" : "0") + "\n"; - } else if (lines[i].find("$push_notifications") != -1) { - bool is_on = p_preset->get("capabilities/push_notifications"); - strnew += lines[i].replace("$push_notifications", is_on ? "1" : "0") + "\n"; } else if (lines[i].find("$entitlements_push_notifications") != -1) { bool is_on = p_preset->get("capabilities/push_notifications"); strnew += lines[i].replace("$entitlements_push_notifications", is_on ? "<key>aps-environment</key><string>development</string>" : "") + "\n"; @@ -352,15 +487,14 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ // I've removed armv7 as we can run on 64bit only devices // Note that capabilities listed here are requirements for the app to be installed. // They don't enable anything. + Vector<String> capabilities_list = p_config.capabilities; - if ((bool)p_preset->get("capabilities/arkit")) { - capabilities += "<string>arkit</string>\n"; - } - if ((bool)p_preset->get("capabilities/game_center")) { - capabilities += "<string>gamekit</string>\n"; + if ((bool)p_preset->get("capabilities/access_wifi") && !capabilities_list.has("wifi")) { + capabilities_list.push_back("wifi"); } - if ((bool)p_preset->get("capabilities/access_wifi")) { - capabilities += "<string>wifi</string>\n"; + + for (int idx = 0; idx < capabilities_list.size(); idx++) { + capabilities += "<string>" + capabilities_list[idx] + "</string>\n"; } strnew += lines[i].replace("$required_device_capabilities", capabilities); @@ -390,6 +524,60 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ } else if (lines[i].find("$photolibrary_usage_description") != -1) { String description = p_preset->get("privacy/photolibrary_usage_description"); strnew += lines[i].replace("$photolibrary_usage_description", description) + "\n"; + } else if (lines[i].find("$plist_launch_screen_name") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "<key>UILaunchStoryboardName</key>\n<string>Launch Screen</string>" : ""; + strnew += lines[i].replace("$plist_launch_screen_name", value) + "\n"; + } else if (lines[i].find("$pbx_launch_screen_file_reference") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "90DD2D9D24B36E8000717FE1 = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = \"Launch Screen.storyboard\"; sourceTree = \"<group>\"; };" : ""; + strnew += lines[i].replace("$pbx_launch_screen_file_reference", value) + "\n"; + } else if (lines[i].find("$pbx_launch_screen_copy_files") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */," : ""; + strnew += lines[i].replace("$pbx_launch_screen_copy_files", value) + "\n"; + } else if (lines[i].find("$pbx_launch_screen_build_phase") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */," : ""; + strnew += lines[i].replace("$pbx_launch_screen_build_phase", value) + "\n"; + } else if (lines[i].find("$pbx_launch_screen_build_reference") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */; };" : ""; + strnew += lines[i].replace("$pbx_launch_screen_build_reference", value) + "\n"; + } else if (lines[i].find("$pbx_launch_image_usage_setting") != -1) { + bool is_on = p_preset->get("storyboard/use_launch_screen_storyboard"); + String value = is_on ? "" : "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;"; + strnew += lines[i].replace("$pbx_launch_image_usage_setting", value) + "\n"; + } else if (lines[i].find("$launch_screen_image_mode") != -1) { + int image_scale_mode = p_preset->get("storyboard/image_scale_mode"); + String value; + + switch (image_scale_mode) { + case 0: { + String logo_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + bool is_on = ProjectSettings::get_singleton()->get("application/boot_splash/fullsize"); + // If custom logo is not specified, Godot does not scale default one, so we should do the same. + value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center"; + } break; + default: { + value = storyboard_image_scale_mode[image_scale_mode - 1]; + } + } + + strnew += lines[i].replace("$launch_screen_image_mode", value) + "\n"; + } else if (lines[i].find("$launch_screen_background_color") != -1) { + bool use_custom = p_preset->get("storyboard/use_custom_bg_color"); + Color color = use_custom ? p_preset->get("storyboard/custom_bg_color") : ProjectSettings::get_singleton()->get("application/boot_splash/bg_color"); + const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\""; + + Dictionary value_dictionary; + value_dictionary["red"] = color.r; + value_dictionary["green"] = color.g; + value_dictionary["blue"] = color.b; + value_dictionary["alpha"] = color.a; + String value = value_format.format(value_dictionary, "$_"); + + strnew += lines[i].replace("$launch_screen_background_color", value) + "\n"; } else { strnew += lines[i] + "\n"; } @@ -499,7 +687,6 @@ static const IconInfo icon_infos[] = { { "optional_icons/spotlight_80x80", "iphone", "Icon-80.png", "80", "2x", "40x40", false }, { "optional_icons/spotlight_80x80", "ipad", "Icon-80.png", "80", "2x", "40x40", false } - }; Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir) { @@ -591,7 +778,75 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr return OK; } -Error EditorExportPlatformIOS::_export_loading_screens(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { +Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { + const String custom_launch_image_2x = p_preset->get("storyboard/custom_image@2x"); + const String custom_launch_image_3x = p_preset->get("storyboard/custom_image@3x"); + + if (custom_launch_image_2x.length() > 0 && custom_launch_image_3x.length() > 0) { + Ref<Image> image; + String image_path = p_dest_dir.plus_file("splash@2x.png"); + image.instance(); + Error err = image->load(custom_launch_image_2x); + + if (err) { + image.unref(); + return err; + } + + if (image->save_png(image_path) != OK) { + return ERR_FILE_CANT_WRITE; + } + + image.unref(); + image_path = p_dest_dir.plus_file("splash@3x.png"); + image.instance(); + err = image->load(custom_launch_image_3x); + + if (err) { + image.unref(); + return err; + } + + if (image->save_png(image_path) != OK) { + return ERR_FILE_CANT_WRITE; + } + } else { + Ref<Image> splash; + + const String splash_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + + if (!splash_path.empty()) { + splash.instance(); + const Error err = splash->load(splash_path); + if (err) { + splash.unref(); + } + } + + if (splash.is_null()) { + splash = Ref<Image>(memnew(Image(boot_splash_png))); + } + + // Using same image for both @2x and @3x + // because Godot's own boot logo uses single image for all resolutions. + // Also not using @1x image, because devices using this image variant + // are not supported by iOS 9, which is minimal target. + const String splash_png_path_2x = p_dest_dir.plus_file("splash@2x.png"); + const String splash_png_path_3x = p_dest_dir.plus_file("splash@3x.png"); + + if (splash->save_png(splash_png_path_2x) != OK) { + return ERR_FILE_CANT_WRITE; + } + + if (splash->save_png(splash_png_path_3x) != OK) { + return ERR_FILE_CANT_WRITE; + } + } + + return OK; +} + +Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { DirAccess *da = DirAccess::open(p_dest_dir); ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); @@ -776,15 +1031,6 @@ struct ExportLibsData { }; void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets) { - Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); - Vector<String> frameworks; - for (int i = 0; i < export_plugins.size(); ++i) { - Vector<String> plugin_frameworks = export_plugins[i]->get_ios_frameworks(); - for (int j = 0; j < plugin_frameworks.size(); ++j) { - frameworks.push_back(plugin_frameworks[j]); - } - } - // 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 }; @@ -809,15 +1055,19 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese String type; if (asset.exported_path.ends_with(".framework")) { - 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"; + 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")) { - 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"; + 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")) { @@ -853,28 +1103,6 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese // Note, frameworks like gamekit are always included in our project.pbxprof file // even if turned off in capabilities. - // We do need our ARKit framework - if ((bool)p_preset->get("capabilities/arkit")) { - String build_id = (++current_id).str(); - String ref_id = (++current_id).str(); - - if (pbx_frameworks_build.length() > 0) { - pbx_frameworks_build += ",\n"; - pbx_frameworks_refs += ",\n"; - } - - pbx_frameworks_build += build_id; - pbx_frameworks_refs += ref_id; - - Dictionary format_dict; - format_dict["build_id"] = build_id; - format_dict["ref_id"] = ref_id; - format_dict["name"] = "ARKit.framework"; - format_dict["file_path"] = "System/Library/Frameworks/ARKit.framework"; - format_dict["file_type"] = "wrapper.framework"; - pbx_files += file_info_format.format(format_dict, "$_"); - } - 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); @@ -890,153 +1118,199 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } } -Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets) { +Error EditorExportPlatformIOS::_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<IOSExportAsset> &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 + "'."); - 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 - IOSExportAsset exported_asset = { asset, p_is_framework }; - r_exported_assets.push_back(exported_asset); + + 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 iOS 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 { - DirAccess *da = DirAccess::create_for_path(asset); - if (!da) { - memdelete(filesystem_da); - ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + asset + "."); - } - bool file_exists = da->file_exists(asset); - bool dir_exists = da->dir_exists(asset); - if (!file_exists && !dir_exists) { - memdelete(da); - memdelete(filesystem_da); - return ERR_FILE_NOT_FOUND; - } + file_name = *p_custom_file_name; + } - String base_dir = asset.get_base_dir().replace("res://", ""); - String destination_dir; - String destination; - String asset_path; - bool create_framework = false; - - if (p_is_framework && asset.ends_with(".dylib")) { - // For iOS we need to turn .dylib into .framework - // to be able to send application to AppStore - destination_dir = p_out_dir.plus_file("dylibs").plus_file(base_dir); - - String file_name = asset.get_basename().get_file(); - String framework_name = file_name + ".framework"; - - destination_dir = destination_dir.plus_file(framework_name); - destination = destination_dir.plus_file(file_name); - asset_path = destination_dir; - create_framework = true; - } else if (p_is_framework && (asset.ends_with(".framework") || asset.ends_with(".xcframework"))) { - destination_dir = p_out_dir.plus_file("dylibs").plus_file(base_dir); - - String file_name = asset.get_file(); - destination = destination_dir.plus_file(file_name); - asset_path = destination; - } else { - destination_dir = p_out_dir.plus_file(base_dir); + String framework_name = file_name + ".framework"; - String file_name = asset.get_file(); - destination = destination_dir.plus_file(file_name); - asset_path = destination; - } + 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); - 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; - } - } + 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; + } - Error err = dir_exists ? da->copy_dir(asset, destination) : da->copy(asset, destination); + 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); - if (err) { - memdelete(filesystem_da); - return err; - } - IOSExportAsset exported_asset = { asset_path, p_is_framework }; - r_exported_assets.push_back(exported_asset); + memdelete(filesystem_da); + return make_dir_err; + } + } - if (create_framework) { - String file_name = asset.get_basename().get_file(); - String framework_name = file_name + ".framework"; + Error err = dir_exists ? da->copy_dir(p_asset, destination) : da->copy(p_asset, destination); + memdelete(da); + if (err) { + memdelete(filesystem_da); + return err; + } + IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; + r_exported_assets.push_back(exported_asset); - // Performing `install_name_tool -id @rpath/{name}.framework/{name} ./{name}` on dylib - { - List<String> 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); + if (create_framework) { + String file_name; - OS::get_singleton()->execute("install_name_tool", install_name_args, true); - } + if (!p_custom_file_name) { + file_name = p_asset.get_basename().get_file(); + } else { + file_name = *p_custom_file_name; + } - // Creating Info.plist - { - String info_plist_format = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" - "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" - "<plist version=\"1.0\">\n" - "<dict>\n" - "<key>CFBundleShortVersionString</key>\n" - "<string>1.0</string>\n" - "<key>CFBundleIdentifier</key>\n" - "<string>com.gdnative.framework.$name</string>\n" - "<key>CFBundleName</key>\n" - "<string>$name</string>\n" - "<key>CFBundleExecutable</key>\n" - "<string>$name</string>\n" - "<key>DTPlatformName</key>\n" - "<string>iphoneos</string>\n" - "<key>CFBundleInfoDictionaryVersion</key>\n" - "<string>6.0</string>\n" - "<key>CFBundleVersion</key>\n" - "<string>1</string>\n" - "<key>CFBundlePackageType</key>\n" - "<string>FMWK</string>\n" - "<key>MinimumOSVersion</key>\n" - "<string>10.0</string>\n" - "</dict>\n" - "</plist>"; - - String info_plist = info_plist_format.replace("$name", file_name); - - FileAccess *f = FileAccess::open(asset_path.plus_file("Info.plist"), FileAccess::WRITE); - if (f) { - f->store_string(info_plist); - f->close(); - memdelete(f); - } - } + String framework_name = file_name + ".framework"; + + // Performing `install_name_tool -id @rpath/{name}.framework/{name} ./{name}` on dylib + { + List<String> 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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + "<plist version=\"1.0\">\n" + "<dict>\n" + "<key>CFBundleShortVersionString</key>\n" + "<string>1.0</string>\n" + "<key>CFBundleIdentifier</key>\n" + "<string>com.gdnative.framework.$name</string>\n" + "<key>CFBundleName</key>\n" + "<string>$name</string>\n" + "<key>CFBundleExecutable</key>\n" + "<string>$name</string>\n" + "<key>DTPlatformName</key>\n" + "<string>iphoneos</string>\n" + "<key>CFBundleInfoDictionaryVersion</key>\n" + "<string>6.0</string>\n" + "<key>CFBundleVersion</key>\n" + "<string>1</string>\n" + "<key>CFBundlePackageType</key>\n" + "<string>FMWK</string>\n" + "<key>MinimumOSVersion</key>\n" + "<string>10.0</string>\n" + "</dict>\n" + "</plist>"; + + 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 EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &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 + IOSExportAsset 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 EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets) { Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { - Vector<String> frameworks = export_plugins[i]->get_ios_frameworks(); - Error err = _export_additional_assets(p_out_dir, frameworks, true, r_exported_assets); + Vector<String> linked_frameworks = export_plugins[i]->get_ios_frameworks(); + Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets); + ERR_FAIL_COND_V(err, err); + + Vector<String> embedded_frameworks = export_plugins[i]->get_ios_embedded_frameworks(); + err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> project_static_libs = export_plugins[i]->get_ios_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, r_exported_assets); + err = _export_additional_assets(p_out_dir, project_static_libs, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files(); - err = _export_additional_assets(p_out_dir, ios_bundle_files, false, r_exported_assets); + err = _export_additional_assets(p_out_dir, ios_bundle_files, false, false, r_exported_assets); ERR_FAIL_COND_V(err, err); } @@ -1044,7 +1318,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir 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, r_exported_assets); + Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); return OK; @@ -1062,20 +1336,173 @@ Vector<String> EditorExportPlatformIOS::_get_preset_architectures(const Ref<Edit return enabled_archs; } -void EditorExportPlatformIOS::add_module_code(const Ref<EditorExportPreset> &p_preset, EditorExportPlatformIOS::IOSConfigData &p_config_data, const String &p_name, const String &p_fid, const String &p_gid) { - if ((bool)p_preset->get("capabilities/" + p_name)) { - //add module static library - print_line("ADDING MODULE: " + p_name); +Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &dest_dir, Vector<IOSExportAsset> &r_exported_assets, bool p_debug) { + String plugin_definition_cpp_code; + String plugin_initialization_cpp_code; + String plugin_deinitialization_cpp_code; - p_config_data.modules_buildfile += p_gid + " /* libgodot_" + p_name + "_module.a in Frameworks */ = {isa = PBXBuildFile; fileRef = " + p_fid + " /* libgodot_" + p_name + "_module.a */; };\n\t\t"; - p_config_data.modules_fileref += p_fid + " /* libgodot_" + p_name + "_module.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = godot_" + p_name + "_module ; path = \"libgodot_" + p_name + "_module.a\"; sourceTree = \"<group>\"; };\n\t\t"; - p_config_data.modules_buildphase += p_gid + " /* libgodot_" + p_name + "_module.a */,\n\t\t\t\t"; - p_config_data.modules_buildgrp += p_fid + " /* libgodot_" + p_name + "_module.a */,\n\t\t\t\t"; - } else { - //add stub function for disabled module - p_config_data.cpp_code += "void register_" + p_name + "_types() { /*stub*/ };\n"; - p_config_data.cpp_code += "void unregister_" + p_name + "_types() { /*stub*/ };\n"; + Vector<String> plugin_linked_dependencies; + Vector<String> plugin_embedded_dependencies; + Vector<String> plugin_files; + + Vector<PluginConfig> enabled_plugins = get_enabled_plugins(p_preset); + + Vector<String> added_linked_dependenciy_names; + Vector<String> added_embedded_dependenciy_names; + HashMap<String, String> plist_values; + + Error err; + + for (int i = 0; i < enabled_plugins.size(); i++) { + PluginConfig plugin = enabled_plugins[i]; + + // Export plugin binary. + if (!plugin.supports_targets) { + err = _copy_asset(dest_dir, plugin.binary, nullptr, true, true, r_exported_assets); + } else { + String plugin_binary_dir = plugin.binary.get_base_dir(); + String plugin_name_prefix = plugin.binary.get_basename().get_file(); + String plugin_file = plugin_name_prefix + "." + (p_debug ? "debug" : "release") + ".a"; + String result_file_name = plugin.binary.get_file(); + + err = _copy_asset(dest_dir, plugin_binary_dir.plus_file(plugin_file), &result_file_name, true, true, 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.has(name)) { + 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.has(name)) { + 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.has(name)) { + 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.has(capability)) { + 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>" + key + "</key><string>" + value + "</string>\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_ios_plugins_initialize();\n" + "void godot_ios_plugins_deinitialize();\n" + "// Exported Plugins\n\n" + "$definition" + "// Use Plugins\n" + "void godot_ios_plugins_initialize() {\n" + "$initialization" + "}\n\n" + "void godot_ios_plugins_deinitialize() {\n" + "$deinitialization" + "}\n"; + + p_config_data.cpp_code += plugin_cpp_code.format(plugin_format, "$_"); } + return OK; } Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { @@ -1172,6 +1599,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p files_to_parse.insert("godot_ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata"); files_to_parse.insert("godot_ios.xcodeproj/xcshareddata/xcschemes/godot_ios.xcscheme"); files_to_parse.insert("godot_ios/godot_ios.entitlements"); + files_to_parse.insert("godot_ios/Launch Screen.storyboard"); IOSConfigData config_data = { pkg_name, @@ -1183,9 +1611,12 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p "", "", "", - "" + "", + Vector<String>() }; + Vector<IOSExportAsset> assets; + DirAccess *tmp_app_path = DirAccess::create_for_path(dest_dir); ERR_FAIL_COND_V(!tmp_app_path, ERR_CANT_CREATE); @@ -1198,8 +1629,8 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return ERR_CANT_OPEN; } - add_module_code(p_preset, config_data, "arkit", "F9B95E6E2391205500AF0000", "F9C95E812391205C00BF0000"); - add_module_code(p_preset, config_data, "camera", "F9B95E6E2391205500AF0001", "F9C95E812391205C00BF0001"); + err = _export_ios_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); @@ -1241,21 +1672,8 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p is_execute = true; #endif file = "godot_ios.a"; - } else if (file.begins_with("libgodot_arkit")) { - if ((bool)p_preset->get("capabilities/arkit") && file.ends_with(String(p_debug ? "debug" : "release") + ".fat.a")) { - file = "libgodot_arkit_module.a"; - } else { - ret = unzGoToNextFile(src_pkg_zip); - continue; //ignore! - } - } else if (file.begins_with("libgodot_camera")) { - if ((bool)p_preset->get("capabilities/camera") && file.ends_with(String(p_debug ? "debug" : "release") + ".fat.a")) { - file = "libgodot_camera_module.a"; - } else { - ret = unzGoToNextFile(src_pkg_zip); - continue; //ignore! - } } + if (file == project_file) { project_file_data = data; } @@ -1347,13 +1765,48 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return err; } - err = _export_loading_screens(p_preset, dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"); + bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard"); + + String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; + String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; + + DirAccess *launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + + if (!launch_screen_da) { + return ERR_CANT_CREATE; + } + + if (use_storyboard) { + print_line("Using Launch Storyboard"); + + if (launch_screen_da->change_dir(launch_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(launch_image_path); + } + + err = _export_loading_screen_file(p_preset, splash_image_path); + } else { + print_line("Using Launch Images"); + + const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard"; + + launch_screen_da->remove(launch_screen_path); + + if (launch_screen_da->change_dir(splash_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(splash_image_path); + } + + err = _export_loading_screen_images(p_preset, launch_image_path); + } + + memdelete(launch_screen_da); + if (err) { return err; } print_line("Exporting additional assets"); - Vector<IOSExportAsset> 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"; @@ -1471,7 +1924,7 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset } } - String etc_error = test_etc2(); + String etc_error = test_etc2_or_pvrtc(); if (etc_error != String()) { valid = false; err += etc_error; @@ -1488,9 +1941,17 @@ EditorExportPlatformIOS::EditorExportPlatformIOS() { Ref<Image> img = memnew(Image(_iphone_logo)); logo.instance(); logo->create_from_image(img); + + plugins_changed = true; + quit_request = false; + + check_for_changes_thread = Thread::create(_check_for_changes_poll_thread, this); } EditorExportPlatformIOS::~EditorExportPlatformIOS() { + quit_request = true; + Thread::wait_to_finish(check_for_changes_thread); + memdelete(check_for_changes_thread); } void register_iphone_exporter() { diff --git a/platform/iphone/game_center.mm b/platform/iphone/game_center.mm deleted file mode 100644 index 8d470da1a8..0000000000 --- a/platform/iphone/game_center.mm +++ /dev/null @@ -1,387 +0,0 @@ -/*************************************************************************/ -/* game_center.mm */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef GAME_CENTER_ENABLED - -#include "game_center.h" - -#ifdef __IPHONE_9_0 - -#import <GameKit/GameKit.h> -extern "C" { - -#else - -extern "C" { -#import <GameKit/GameKit.h> - -#endif - -#import "app_delegate.h" -}; - -GameCenter *GameCenter::instance = NULL; - -void GameCenter::_bind_methods() { - ClassDB::bind_method(D_METHOD("is_authenticated"), &GameCenter::is_authenticated); - - ClassDB::bind_method(D_METHOD("post_score"), &GameCenter::post_score); - ClassDB::bind_method(D_METHOD("award_achievement"), &GameCenter::award_achievement); - ClassDB::bind_method(D_METHOD("reset_achievements"), &GameCenter::reset_achievements); - ClassDB::bind_method(D_METHOD("request_achievements"), &GameCenter::request_achievements); - ClassDB::bind_method(D_METHOD("request_achievement_descriptions"), &GameCenter::request_achievement_descriptions); - ClassDB::bind_method(D_METHOD("show_game_center"), &GameCenter::show_game_center); - ClassDB::bind_method(D_METHOD("request_identity_verification_signature"), &GameCenter::request_identity_verification_signature); - - ClassDB::bind_method(D_METHOD("get_pending_event_count"), &GameCenter::get_pending_event_count); - ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); -}; - -void GameCenter::return_connect_error(const char *p_error_description) { - authenticated = false; - Dictionary ret; - ret["type"] = "authentication"; - ret["result"] = "error"; - ret["error_code"] = 0; - ret["error_description"] = p_error_description; - pending_events.push_back(ret); -} - -void GameCenter::connect() { - //if this class isn't available, game center isn't implemented - if ((NSClassFromString(@"GKLocalPlayer")) == nil) { - return_connect_error("GameCenter not available"); - return; - } - - GKLocalPlayer *player = [GKLocalPlayer localPlayer]; - if (![player respondsToSelector:@selector(authenticateHandler)]) { - return_connect_error("GameCenter doesn't respond to 'authenticateHandler'"); - return; - } - - ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; - if (!root_controller) { - return_connect_error("Window doesn't have root ViewController"); - return; - } - - // This handler is called several times. First when the view needs to be shown, then again - // after the view is cancelled or the user logs in. Or if the user's already logged in, it's - // called just once to confirm they're authenticated. This is why no result needs to be specified - // in the presentViewController phase. In this case, more calls to this function will follow. - player.authenticateHandler = (^(UIViewController *controller, NSError *error) { - if (controller) { - [root_controller presentViewController:controller animated:YES completion:nil]; - } else { - Dictionary ret; - ret["type"] = "authentication"; - if (player.isAuthenticated) { - ret["result"] = "ok"; - ret["player_id"] = [player.playerID UTF8String]; - GameCenter::get_singleton()->authenticated = true; - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - ret["error_description"] = [error.localizedDescription UTF8String]; - GameCenter::get_singleton()->authenticated = false; - }; - - pending_events.push_back(ret); - }; - }); -}; - -bool GameCenter::is_authenticated() { - return authenticated; -}; - -Error GameCenter::post_score(Variant p_score) { - Dictionary params = p_score; - ERR_FAIL_COND_V(!params.has("score") || !params.has("category"), ERR_INVALID_PARAMETER); - float score = params["score"]; - String category = params["category"]; - - NSString *cat_str = [[[NSString alloc] initWithUTF8String:category.utf8().get_data()] autorelease]; - GKScore *reporter = [[[GKScore alloc] initWithLeaderboardIdentifier:cat_str] autorelease]; - reporter.value = score; - - ERR_FAIL_COND_V([GKScore respondsToSelector:@selector(reportScores)], ERR_UNAVAILABLE); - - [GKScore reportScores:@[ reporter ] - withCompletionHandler:^(NSError *error) { - Dictionary ret; - ret["type"] = "post_score"; - if (error == nil) { - ret["result"] = "ok"; - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - ret["error_description"] = [error.localizedDescription UTF8String]; - }; - - pending_events.push_back(ret); - }]; - - return OK; -}; - -Error GameCenter::award_achievement(Variant p_params) { - Dictionary params = p_params; - ERR_FAIL_COND_V(!params.has("name") || !params.has("progress"), ERR_INVALID_PARAMETER); - String name = params["name"]; - float progress = params["progress"]; - - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; - GKAchievement *achievement = [[[GKAchievement alloc] initWithIdentifier:name_str] autorelease]; - ERR_FAIL_COND_V(!achievement, FAILED); - - ERR_FAIL_COND_V([GKAchievement respondsToSelector:@selector(reportAchievements)], ERR_UNAVAILABLE); - - achievement.percentComplete = progress; - achievement.showsCompletionBanner = NO; - if (params.has("show_completion_banner")) { - achievement.showsCompletionBanner = params["show_completion_banner"] ? YES : NO; - } - - [GKAchievement reportAchievements:@[ achievement ] - withCompletionHandler:^(NSError *error) { - Dictionary ret; - ret["type"] = "award_achievement"; - if (error == nil) { - ret["result"] = "ok"; - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - }; - - pending_events.push_back(ret); - }]; - - return OK; -}; - -void GameCenter::request_achievement_descriptions() { - [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray *descriptions, NSError *error) { - Dictionary ret; - ret["type"] = "achievement_descriptions"; - if (error == nil) { - ret["result"] = "ok"; - PackedStringArray names; - PackedStringArray titles; - PackedStringArray unachieved_descriptions; - PackedStringArray achieved_descriptions; - PackedInt32Array maximum_points; - Array hidden; - Array replayable; - - for (int i = 0; i < [descriptions count]; i++) { - GKAchievementDescription *description = [descriptions objectAtIndex:i]; - - const char *str = [description.identifier UTF8String]; - names.push_back(String::utf8(str != NULL ? str : "")); - - str = [description.title UTF8String]; - titles.push_back(String::utf8(str != NULL ? str : "")); - - str = [description.unachievedDescription UTF8String]; - unachieved_descriptions.push_back(String::utf8(str != NULL ? str : "")); - - str = [description.achievedDescription UTF8String]; - achieved_descriptions.push_back(String::utf8(str != NULL ? str : "")); - - maximum_points.push_back(description.maximumPoints); - - hidden.push_back(description.hidden == YES); - - replayable.push_back(description.replayable == YES); - } - - ret["names"] = names; - ret["titles"] = titles; - ret["unachieved_descriptions"] = unachieved_descriptions; - ret["achieved_descriptions"] = achieved_descriptions; - ret["maximum_points"] = maximum_points; - ret["hidden"] = hidden; - ret["replayable"] = replayable; - - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - }; - - pending_events.push_back(ret); - }]; -}; - -void GameCenter::request_achievements() { - [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error) { - Dictionary ret; - ret["type"] = "achievements"; - if (error == nil) { - ret["result"] = "ok"; - PackedStringArray names; - PackedFloat32Array percentages; - - for (int i = 0; i < [achievements count]; i++) { - GKAchievement *achievement = [achievements objectAtIndex:i]; - const char *str = [achievement.identifier UTF8String]; - names.push_back(String::utf8(str != NULL ? str : "")); - - percentages.push_back(achievement.percentComplete); - } - - ret["names"] = names; - ret["progress"] = percentages; - - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - }; - - pending_events.push_back(ret); - }]; -}; - -void GameCenter::reset_achievements() { - [GKAchievement resetAchievementsWithCompletionHandler:^(NSError *error) { - Dictionary ret; - ret["type"] = "reset_achievements"; - if (error == nil) { - ret["result"] = "ok"; - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - }; - - pending_events.push_back(ret); - }]; -}; - -Error GameCenter::show_game_center(Variant p_params) { - ERR_FAIL_COND_V(!NSProtocolFromString(@"GKGameCenterControllerDelegate"), FAILED); - - Dictionary params = p_params; - - GKGameCenterViewControllerState view_state = GKGameCenterViewControllerStateDefault; - if (params.has("view")) { - String view_name = params["view"]; - if (view_name == "default") { - view_state = GKGameCenterViewControllerStateDefault; - } else if (view_name == "leaderboards") { - view_state = GKGameCenterViewControllerStateLeaderboards; - } else if (view_name == "achievements") { - view_state = GKGameCenterViewControllerStateAchievements; - } else if (view_name == "challenges") { - view_state = GKGameCenterViewControllerStateChallenges; - } else { - return ERR_INVALID_PARAMETER; - } - } - - GKGameCenterViewController *controller = [[GKGameCenterViewController alloc] init]; - ERR_FAIL_COND_V(!controller, FAILED); - - ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; - ERR_FAIL_COND_V(!root_controller, FAILED); - - controller.gameCenterDelegate = root_controller; - controller.viewState = view_state; - if (view_state == GKGameCenterViewControllerStateLeaderboards) { - controller.leaderboardIdentifier = nil; - if (params.has("leaderboard_name")) { - String name = params["leaderboard_name"]; - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; - controller.leaderboardIdentifier = name_str; - } - } - - [root_controller presentViewController:controller animated:YES completion:nil]; - - return OK; -}; - -Error GameCenter::request_identity_verification_signature() { - ERR_FAIL_COND_V(!is_authenticated(), ERR_UNAUTHORIZED); - - GKLocalPlayer *player = [GKLocalPlayer localPlayer]; - [player generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) { - Dictionary ret; - ret["type"] = "identity_verification_signature"; - if (error == nil) { - ret["result"] = "ok"; - ret["public_key_url"] = [publicKeyUrl.absoluteString UTF8String]; - ret["signature"] = [[signature base64EncodedStringWithOptions:0] UTF8String]; - ret["salt"] = [[salt base64EncodedStringWithOptions:0] UTF8String]; - ret["timestamp"] = timestamp; - ret["player_id"] = [player.playerID UTF8String]; - } else { - ret["result"] = "error"; - ret["error_code"] = (int64_t)error.code; - ret["error_description"] = [error.localizedDescription UTF8String]; - }; - - pending_events.push_back(ret); - }]; - - return OK; -}; - -void GameCenter::game_center_closed() { - Dictionary ret; - ret["type"] = "show_game_center"; - ret["result"] = "ok"; - pending_events.push_back(ret); -} - -int GameCenter::get_pending_event_count() { - return pending_events.size(); -}; - -Variant GameCenter::pop_pending_event() { - Variant front = pending_events.front()->get(); - pending_events.pop_front(); - - return front; -}; - -GameCenter *GameCenter::get_singleton() { - return instance; -}; - -GameCenter::GameCenter() { - ERR_FAIL_COND(instance != NULL); - instance = this; - authenticated = false; -}; - -GameCenter::~GameCenter() {} - -#endif diff --git a/platform/iphone/gl_view.h b/platform/iphone/gl_view.h deleted file mode 100644 index 975aa4b70a..0000000000 --- a/platform/iphone/gl_view.h +++ /dev/null @@ -1,123 +0,0 @@ -/*************************************************************************/ -/* gl_view.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#import <AVFoundation/AVFoundation.h> -#import <MediaPlayer/MediaPlayer.h> -#import <OpenGLES/EAGL.h> -#import <OpenGLES/ES1/gl.h> -#import <OpenGLES/ES1/glext.h> -#import <UIKit/UIKit.h> - -@protocol GLViewDelegate; - -@interface GLView : UIView <UIKeyInput> { -@private - // The pixel dimensions of the backbuffer - GLint backingWidth; - GLint backingHeight; - - EAGLContext *context; - - // OpenGL names for the renderbuffer and framebuffers used to render to this view - GLuint viewRenderbuffer, viewFramebuffer; - - // OpenGL name for the depth buffer that is attached to viewFramebuffer, if it exists (0 if it does not exist) - GLuint depthRenderbuffer; - - BOOL useCADisplayLink; - // 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 - CADisplayLink *displayLink; - - // An animation timer that, when animation is started, will periodically call -drawView at the given rate. - // Only used if CADisplayLink is not - NSTimer *animationTimer; - - NSTimeInterval animationInterval; - - // Delegate to do our drawing, called by -drawView, which can be called manually or via the animation timer. - id<GLViewDelegate> delegate; - - // Flag to denote that the -setupView method of a delegate has been called. - // Resets to NO whenever the delegate changes. - BOOL delegateSetup; - BOOL active; - float screen_scale; -} - -@property(nonatomic, assign) id<GLViewDelegate> delegate; - -// AVPlayer-related properties -@property(strong, nonatomic) AVAsset *avAsset; -@property(strong, nonatomic) AVPlayerItem *avPlayerItem; -@property(strong, nonatomic) AVPlayer *avPlayer; -@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer; - -@property(strong, nonatomic) UIWindow *backgroundWindow; - -@property(nonatomic) UITextAutocorrectionType autocorrectionType; - -- (void)startAnimation; -- (void)stopAnimation; -- (void)drawView; - -- (BOOL)canBecomeFirstResponder; - -- (void)open_keyboard; -- (void)hide_keyboard; -- (void)deleteBackward; -- (BOOL)hasText; -- (void)insertText:(NSString *)p_text; - -- (id)initGLES; -- (BOOL)createFramebuffer; -- (void)destroyFramebuffer; - -- (void)audioRouteChangeListenerCallback:(NSNotification *)notification; -- (void)keyboardOnScreen:(NSNotification *)notification; -- (void)keyboardHidden:(NSNotification *)notification; - -@property NSTimeInterval animationInterval; -@property(nonatomic, assign) BOOL useCADisplayLink; - -@end - -@protocol GLViewDelegate <NSObject> - -@required - -// Draw with OpenGL ES -- (void)drawView:(GLView *)view; - -@optional - -// Called whenever you need to do some initialization before rendering. -- (void)setupView:(GLView *)view; - -@end diff --git a/platform/iphone/gl_view.mm b/platform/iphone/gl_view.mm deleted file mode 100644 index 1169ebc6b4..0000000000 --- a/platform/iphone/gl_view.mm +++ /dev/null @@ -1,702 +0,0 @@ -/*************************************************************************/ -/* gl_view.mm */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#import "gl_view.h" - -#include "core/os/keyboard.h" -#include "core/project_settings.h" -#include "os_iphone.h" -#include "servers/audio_server.h" - -#import <OpenGLES/EAGLDrawable.h> -#import <QuartzCore/QuartzCore.h> - -/* -@interface GLView (private) - -- (id)initGLES; -- (BOOL)createFramebuffer; -- (void)destroyFramebuffer; -@end -*/ - -int gl_view_base_fb; -static String keyboard_text; -static GLView *_instance = NULL; - -static bool video_found_error = false; -static bool video_playing = false; -static CMTime video_current_time; - -void _show_keyboard(String); -void _hide_keyboard(); -bool _play_video(String, float, String, String); -bool _is_video_playing(); -void _pause_video(); -void _focus_out_video(); -void _unpause_video(); -void _stop_video(); -CGFloat _points_to_pixels(CGFloat); - -void _show_keyboard(String p_existing) { - keyboard_text = p_existing; - printf("instance on show is %p\n", _instance); - [_instance open_keyboard]; -}; - -void _hide_keyboard() { - printf("instance on hide is %p\n", _instance); - [_instance hide_keyboard]; - keyboard_text = ""; -}; - -Rect2 _get_ios_window_safe_area(float p_window_width, float p_window_height) { - UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0); - if (_instance != nil && [_instance respondsToSelector:@selector(safeAreaInsets)]) { - insets = [_instance safeAreaInsets]; - } - ERR_FAIL_COND_V(insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0, - Rect2(0, 0, p_window_width, p_window_height)); - UIEdgeInsets window_insets = UIEdgeInsetsMake(_points_to_pixels(insets.top), _points_to_pixels(insets.left), _points_to_pixels(insets.bottom), _points_to_pixels(insets.right)); - return Rect2(window_insets.left, window_insets.top, p_window_width - window_insets.right - window_insets.left, p_window_height - window_insets.bottom - window_insets.top); -} - -bool _play_video(String p_path, float p_volume, String p_audio_track, String p_subtitle_track) { - p_path = ProjectSettings::get_singleton()->globalize_path(p_path); - - NSString *file_path = [[[NSString alloc] initWithUTF8String:p_path.utf8().get_data()] autorelease]; - - _instance.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:file_path]]; - - _instance.avPlayerItem = [[AVPlayerItem alloc] initWithAsset:_instance.avAsset]; - [_instance.avPlayerItem addObserver:_instance forKeyPath:@"status" options:0 context:nil]; - - _instance.avPlayer = [[AVPlayer alloc] initWithPlayerItem:_instance.avPlayerItem]; - _instance.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:_instance.avPlayer]; - - [_instance.avPlayer addObserver:_instance forKeyPath:@"status" options:0 context:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:_instance - selector:@selector(playerItemDidReachEnd:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_instance.avPlayer currentItem]]; - - [_instance.avPlayer addObserver:_instance forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0]; - - [_instance.avPlayerLayer setFrame:_instance.bounds]; - [_instance.layer addSublayer:_instance.avPlayerLayer]; - [_instance.avPlayer play]; - - AVMediaSelectionGroup *audioGroup = [_instance.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:[NSString stringWithUTF8String:p_audio_track.utf8()]]) { - AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters]; - [audioInputParams setVolume:p_volume atTime:kCMTimeZero]; - [audioInputParams setTrackID:[track trackID]]; - [allAudioParams addObject:audioInputParams]; - - AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; - [audioMix setInputParameters:allAudioParams]; - - [_instance.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup]; - [_instance.avPlayer.currentItem setAudioMix:audioMix]; - - break; - } - } - - AVMediaSelectionGroup *subtitlesGroup = [_instance.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:[NSString stringWithUTF8String:p_subtitle_track.utf8()]]) { - [_instance.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup]; - break; - } - } - - video_playing = true; - - return true; -} - -bool _is_video_playing() { - if (_instance.avPlayer.error) { - printf("Error during playback\n"); - } - return (_instance.avPlayer.rate > 0 && !_instance.avPlayer.error); -} - -void _pause_video() { - video_current_time = _instance.avPlayer.currentTime; - [_instance.avPlayer pause]; - video_playing = false; -} - -void _focus_out_video() { - printf("focus out pausing video\n"); - [_instance.avPlayer pause]; -}; - -void _unpause_video() { - [_instance.avPlayer play]; - video_playing = true; -}; - -void _stop_video() { - [_instance.avPlayer pause]; - [_instance.avPlayerLayer removeFromSuperlayer]; - _instance.avPlayer = nil; - video_playing = false; -} - -CGFloat _points_to_pixels(CGFloat points) { - float pixelPerInch; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { - pixelPerInch = 132; - } else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { - pixelPerInch = 163; - } else { - pixelPerInch = 160; - } - CGFloat pointsPerInch = 72.0; - return (points / pointsPerInch * pixelPerInch); -} - -@implementation GLView - -@synthesize animationInterval; - -static const int max_touches = 8; -static UITouch *touches[max_touches]; - -static void init_touches() { - for (int i = 0; i < max_touches; i++) { - touches[i] = NULL; - }; -}; - -static int get_touch_id(UITouch *p_touch) { - int first = -1; - for (int i = 0; i < max_touches; i++) { - if (first == -1 && touches[i] == NULL) { - first = i; - continue; - }; - if (touches[i] == p_touch) - return i; - }; - - if (first != -1) { - touches[first] = p_touch; - return first; - }; - - return -1; -}; - -static int remove_touch(UITouch *p_touch) { - int remaining = 0; - for (int i = 0; i < max_touches; i++) { - if (touches[i] == NULL) - continue; - if (touches[i] == p_touch) - touches[i] = NULL; - else - ++remaining; - }; - return remaining; -}; - -static void clear_touches() { - for (int i = 0; i < max_touches; i++) { - touches[i] = NULL; - }; -}; - -// 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. -+ (Class)layerClass { - return [CAEAGLLayer class]; -} - -//The GL view is stored in the nib file. When it's unarchived it's sent -initWithCoder: -- (id)initWithCoder:(NSCoder *)coder { - active = FALSE; - if ((self = [super initWithCoder:coder])) { - self = [self initGLES]; - } - return self; -} - -- (id)initGLES { - // Get our backing layer - CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; - - // Configure it so that it is opaque, does not retain the contents of the backbuffer when displayed, and uses RGBA8888 color. - eaglLayer.opaque = YES; - eaglLayer.drawableProperties = [NSDictionary - dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:FALSE], - kEAGLDrawablePropertyRetainedBacking, - kEAGLColorFormatRGBA8, - kEAGLDrawablePropertyColorFormat, - nil]; - - // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? - - // Create GL ES 2 context - if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES2") { - 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 nil; - } - } - - if (![EAGLContext setCurrentContext:context]) { - NSLog(@"Failed to set EAGLContext!"); - return nil; - } - if (![self createFramebuffer]) { - NSLog(@"Failed to create frame buffer!"); - return nil; - } - - // Default the animation interval to 1/60th of a second. - animationInterval = 1.0 / 60.0; - return self; -} - -- (id<GLViewDelegate>)delegate { - return delegate; -} - -// Update the delegate, and if it needs a -setupView: call, set our internal flag so that it will be called. -- (void)setDelegate:(id<GLViewDelegate>)d { - delegate = d; - delegateSetup = ![delegate respondsToSelector:@selector(setupView:)]; -} - -@synthesize useCADisplayLink; - -// 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 { - [EAGLContext setCurrentContext:context]; - [self destroyFramebuffer]; - [self createFramebuffer]; - [self drawView]; -} - -- (BOOL)createFramebuffer { - // Generate IDs for a framebuffer object and a color renderbuffer - UIScreen *mainscr = [UIScreen mainScreen]; - printf("******** screen size %i, %i\n", (int)mainscr.currentMode.size.width, (int)mainscr.currentMode.size.height); - self.contentScaleFactor = mainscr.nativeScale; - - 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<EAGLDrawable>)self.layer]; - 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); - OSIPhone::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; - } -} - -- (void)startAnimation { - if (active) - return; - active = TRUE; - printf("start animation!\n"); - if (useCADisplayLink) { - // Approximate frame rate - // assumes device refreshes at 60 fps - int frameInterval = (int)floor(animationInterval * 60.0f); - - displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; - [displayLink setFrameInterval:frameInterval]; - - // Setup DisplayLink in main thread - [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } else { - animationTimer = [NSTimer scheduledTimerWithTimeInterval:animationInterval target:self selector:@selector(drawView) userInfo:nil repeats:YES]; - } - - if (video_playing) { - _unpause_video(); - } -} - -- (void)stopAnimation { - if (!active) - return; - active = FALSE; - printf("******** stop animation!\n"); - - if (useCADisplayLink) { - [displayLink invalidate]; - displayLink = nil; - } else { - [animationTimer invalidate]; - animationTimer = nil; - } - - clear_touches(); - - if (video_playing) { - // save position - } -} - -- (void)setAnimationInterval:(NSTimeInterval)interval { - animationInterval = interval; - if ((useCADisplayLink && displayLink) || (!useCADisplayLink && animationTimer)) { - [self stopAnimation]; - [self startAnimation]; - } -} - -// Updates the OpenGL view when the timer fires -- (void)drawView { - if (!active) { - printf("draw view not active!\n"); - return; - }; - if (useCADisplayLink) { - // Pause the CADisplayLink to avoid recursion - [displayLink setPaused:YES]; - - // Process all input events - while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) - ; - - // We are good to go, resume the CADisplayLink - [displayLink setPaused:NO]; - } - - // Make sure that you are drawing to the current context - [EAGLContext setCurrentContext:context]; - - // If our drawing delegate needs to have the view setup, then call -setupView: and flag that it won't need to be called again. - if (!delegateSetup) { - [delegate setupView:self]; - delegateSetup = YES; - } - - glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer); - - [delegate drawView:self]; - - 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)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseBegan) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - CGPoint touchPoint = [touch locationInView:self]; - OSIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, true, touch.tapCount > 1); - }; - }; -} - -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseMoved) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - CGPoint touchPoint = [touch locationInView:self]; - CGPoint prev_point = [touch previousLocationInView:self]; - OSIPhone::get_singleton()->touch_drag(tid, prev_point.x * self.contentScaleFactor, prev_point.y * self.contentScaleFactor, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor); - }; - }; -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseEnded) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - remove_touch(touch); - CGPoint touchPoint = [touch locationInView:self]; - OSIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, false, false); - }; - }; -} - -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - OSIPhone::get_singleton()->touches_cancelled(); - clear_touches(); -}; - -- (BOOL)canBecomeFirstResponder { - return YES; -}; - -- (void)open_keyboard { - //keyboard_text = p_existing; - [self becomeFirstResponder]; -}; - -- (void)hide_keyboard { - //keyboard_text = p_existing; - [self resignFirstResponder]; -}; - -- (void)keyboardOnScreen:(NSNotification *)notification { - NSDictionary *info = notification.userInfo; - NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; - - CGRect rawFrame = [value CGRectValue]; - CGRect keyboardFrame = [self convertRect:rawFrame fromView:nil]; - - OSIPhone::get_singleton()->set_virtual_keyboard_height(_points_to_pixels(keyboardFrame.size.height)); -} - -- (void)keyboardHidden:(NSNotification *)notification { - OSIPhone::get_singleton()->set_virtual_keyboard_height(0); -} - -- (void)deleteBackward { - if (keyboard_text.length()) - keyboard_text.erase(keyboard_text.length() - 1, 1); - OSIPhone::get_singleton()->key(KEY_BACKSPACE, true); -}; - -- (BOOL)hasText { - return keyboard_text.length() ? YES : NO; -}; - -- (void)insertText:(NSString *)p_text { - String character; - character.parse_utf8([p_text UTF8String]); - keyboard_text = keyboard_text + character; - OSIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true); - printf("inserting text with character %lc\n", (CharType)character[0]); -}; - -- (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 (_is_video_playing()) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [_instance.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; - } -} - -// When created via code however, we get initWithFrame -- (id)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - _instance = self; - printf("after init super %p\n", self); - if (self != nil) { - self = [self initGLES]; - printf("after init gles %p\n", self); - } - init_touches(); - self.multipleTouchEnabled = YES; - self.autocorrectionType = UITextAutocorrectionTypeNo; - - printf("******** adding observer for sound routing changes\n"); - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(audioRouteChangeListenerCallback:) - name:AVAudioSessionRouteChangeNotification - object:nil]; - - printf("******** adding observer for keyboard show/hide\n"); - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(keyboardOnScreen:) - name:UIKeyboardDidShowNotification - object:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(keyboardHidden:) - name:UIKeyboardDidHideNotification - object:nil]; - - //self.autoresizesSubviews = YES; - //[self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth]; - - return self; -} - -//- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers { -// return YES; -//} - -//- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{ -// return YES; -//} - -// Stop animating and release resources when they are no longer needed. -- (void)dealloc { - [self stopAnimation]; - - if ([EAGLContext currentContext] == context) { - [EAGLContext setCurrentContext:nil]; - } - - [context release]; - context = nil; - - [super dealloc]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (object == _instance.avPlayerItem && [keyPath isEqualToString:@"status"]) { - if (_instance.avPlayerItem.status == AVPlayerStatusFailed || _instance.avPlayer.status == AVPlayerStatusFailed) { - _stop_video(); - video_found_error = true; - } - - if (_instance.avPlayer.status == AVPlayerStatusReadyToPlay && - _instance.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && - CMTIME_COMPARE_INLINE(video_current_time, ==, kCMTimeZero)) { - //NSLog(@"time: %@", video_current_time); - - [_instance.avPlayer seekToTime:video_current_time]; - video_current_time = kCMTimeZero; - } - } - - if (object == _instance.avPlayer && [keyPath isEqualToString:@"rate"]) { - NSLog(@"Player playback rate changed: %.5f", _instance.avPlayer.rate); - if (_is_video_playing() && _instance.avPlayer.rate == 0.0 && !_instance.avPlayer.error) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [_instance.avPlayer play]; // NOTE: change this line according your current player implementation - NSLog(@"resumed play"); - }); - - NSLog(@" . . . PAUSED (or just started)"); - } - } -} - -- (void)playerItemDidReachEnd:(NSNotification *)notification { - _stop_video(); -} - -@end diff --git a/platform/iphone/godot_app_delegate.h b/platform/iphone/godot_app_delegate.h new file mode 100644 index 0000000000..ebb21c499b --- /dev/null +++ b/platform/iphone/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +typedef NSObject<UIApplicationDelegate> ApplicationDelegateService; + +@interface GodotApplicalitionDelegate : NSObject <UIApplicationDelegate> + +@property(class, readonly, strong) NSArray<ApplicationDelegateService *> *services; + ++ (void)addService:(ApplicationDelegateService *)service; + +@end diff --git a/platform/iphone/godot_app_delegate.m b/platform/iphone/godot_app_delegate.m new file mode 100644 index 0000000000..a5aad26bd5 --- /dev/null +++ b/platform/iphone/godot_app_delegate.m @@ -0,0 +1,497 @@ +/*************************************************************************/ +/* godot_app_delegate.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "godot_app_delegate.h" + +#import "app_delegate.h" + +@interface GodotApplicalitionDelegate () + +@end + +@implementation GodotApplicalitionDelegate + +static NSMutableArray<ApplicationDelegateService *> *services = nil; + ++ (NSArray<ApplicationDelegateService *> *)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<UIApplicationLaunchOptionsKey, id> *)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<UIApplicationLaunchOptionsKey, id> *)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<UISceneSession *> *)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<NSString *> *)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: Remote Notification + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + } +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didFailToRegisterForRemoteNotificationsWithError:error]; + } +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; + } + + completionHandler(UIBackgroundFetchResultNoData); +} + +// 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<id<UIUserActivityRestoring>> *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]; + } +} + +- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler { + for (ApplicationDelegateService *service in services) { + if (![service respondsToSelector:_cmd]) { + continue; + } + + [service application:application performActionForShortcutItem:shortcutItem completionHandler:completionHandler]; + } +} + +// 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<UIApplicationOpenURLOptionsKey, id> *)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/iphone/godot_iphone.cpp b/platform/iphone/godot_iphone.mm index b9d217c9d2..a4119bcafd 100644 --- a/platform/iphone/godot_iphone.cpp +++ b/platform/iphone/godot_iphone.mm @@ -1,5 +1,5 @@ /*************************************************************************/ -/* godot_iphone.cpp */ +/* godot_iphone.mm */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "core/ustring.h" +#include "core/string/ustring.h" #include "main/main.h" #include "os_iphone.h" @@ -38,19 +38,49 @@ static OSIPhone *os = nullptr; -extern "C" { -int add_path(int p_argc, char **p_args); -int add_cmdline(int p_argc, char **p_args); +int add_path(int, char **); +int add_cmdline(int, char **); +int iphone_main(int, char **, String); + +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 iphone_main(int, int, int, char **, String); +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 iphone_main(int width, int height, int argc, char **argv, String data_dir) { +int iphone_main(int argc, char **argv, String data_dir) { size_t len = strlen(argv[0]); while (len--) { - if (argv[0][len] == '/') + if (argv[0][len] == '/') { break; + } } if (len >= 0) { @@ -65,7 +95,10 @@ int iphone_main(int width, int height, int argc, char **argv, String data_dir) { char cwd[512]; getcwd(cwd, sizeof(cwd)); printf("cwd %s\n", cwd); - os = new OSIPhone(width, height, data_dir); + os = new OSIPhone(data_dir); + + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE char *fargv[64]; for (int i = 0; i < argc; i++) { @@ -76,10 +109,14 @@ int iphone_main(int width, int height, int argc, char **argv, String data_dir) { 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) + if (err != OK) { return 255; + } + + os->initialize_modules(); return 0; }; diff --git a/platform/iphone/game_center.h b/platform/iphone/godot_view.h index 0d3ef5b696..a8f4cb38d9 100644 --- a/platform/iphone/game_center.h +++ b/platform/iphone/godot_view.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* game_center.h */ +/* godot_view.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,48 +28,27 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifdef GAME_CENTER_ENABLED +#import <UIKit/UIKit.h> -#ifndef GAME_CENTER_H -#define GAME_CENTER_H +class String; -#include "core/object.h" +@protocol DisplayLayer; +@protocol GodotViewRendererProtocol; -class GameCenter : public Object { - GDCLASS(GameCenter, Object); +@interface GodotView : UIView - static GameCenter *instance; - static void _bind_methods(); +@property(assign, nonatomic) id<GodotViewRendererProtocol> renderer; - List<Variant> pending_events; +@property(assign, readonly, nonatomic) BOOL isActive; - bool authenticated; +@property(assign, nonatomic) BOOL useCADisplayLink; +@property(strong, readonly, nonatomic) CALayer<DisplayLayer> *renderingLayer; +@property(assign, readonly, nonatomic) BOOL canRender; - void return_connect_error(const char *p_error_description); +@property(assign, nonatomic) NSTimeInterval renderingInterval; -public: - void connect(); - bool is_authenticated(); +- (CALayer<DisplayLayer> *)initializeRenderingForDriver:(NSString *)driverName; +- (void)stopRendering; +- (void)startRendering; - Error post_score(Variant p_score); - Error award_achievement(Variant p_params); - void reset_achievements(); - void request_achievements(); - void request_achievement_descriptions(); - Error show_game_center(Variant p_params); - Error request_identity_verification_signature(); - - void game_center_closed(); - - int get_pending_event_count(); - Variant pop_pending_event(); - - static GameCenter *get_singleton(); - - GameCenter(); - ~GameCenter(); -}; - -#endif - -#endif +@end diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm new file mode 100644 index 0000000000..0c50842cfa --- /dev/null +++ b/platform/iphone/godot_view.mm @@ -0,0 +1,458 @@ +/*************************************************************************/ +/* godot_view.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "godot_view.h" +#include "core/os/keyboard.h" +#include "core/string/ustring.h" +#import "display_layer.h" +#include "display_server_iphone.h" +#import "godot_view_gesture_recognizer.h" +#import "godot_view_renderer.h" + +#import <CoreMotion/CoreMotion.h> + +static const int max_touches = 8; + +@interface GodotView () { + UITouch *godot_touches[max_touches]; +} + +@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<DisplayLayer> *renderingLayer; + +@property(strong, nonatomic) CMMotionManager *motionManager; + +@property(strong, nonatomic) GodotViewGestureRecognizer *delayGestureRecognizer; + +@end + +@implementation GodotView + +- (CALayer<DisplayLayer> *)initializeRenderingForDriver:(NSString *)driverName { + if (self.renderingLayer) { + return self.renderingLayer; + } + + CALayer<DisplayLayer> *layer; + + if ([driverName isEqualToString:@"vulkan"]) { + layer = [GodotMetalLayer layer]; + } else if ([driverName isEqualToString:@"opengl_es"]) { + if (@available(iOS 13, *)) { + NSLog(@"OpenGL ES is deprecated on iOS 13"); + } +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR + return nil; +#else + layer = [GodotOpenGLLayer layer]; +#endif + } else { + return nil; + } + + 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; +} + +- (void)dealloc { + [self stopRendering]; + + self.renderer = nil; + + if (self.renderingLayer) { + [self.renderingLayer removeFromSuperlayer]; + self.renderingLayer = nil; + } + + if (self.motionManager) { + [self.motionManager stopDeviceMotionUpdates]; + self.motionManager = 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; + + [self initTouches]; + + // Configure and start accelerometer + if (!self.motionManager) { + self.motionManager = [[CMMotionManager alloc] init]; + if (self.motionManager.deviceMotionAvailable) { + self.motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; + [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXMagneticNorthZVertical]; + } else { + self.motionManager = nil; + } + } + + // Initialize delay gesture recognizer + GodotViewGestureRecognizer *gestureRecognizer = [[GodotViewGestureRecognizer alloc] init]; + self.delayGestureRecognizer = gestureRecognizer; + [self addGestureRecognizer:self.delayGestureRecognizer]; +} + +- (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; + } + + [self clearTouches]; +} + +- (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)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 renderDisplayLayer]; + + if (!self.renderer) { + return; + } + + if ([self.renderer setupView:self]) { + return; + } + + [self handleMotion]; + [self.renderer renderOnView:self]; +} + +- (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]; + } +} + +- (void)layoutSubviews { + if (self.renderingLayer) { + self.renderingLayer.frame = self.bounds; + [self.renderingLayer layoutDisplayLayer]; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->resize_window(self.bounds.size); + } + } + + [super layoutSubviews]; +} + +// MARK: - Input + +// MARK: Touches + +- (void)initTouches { + for (int i = 0; i < max_touches; i++) { + godot_touches[i] = NULL; + } +} + +- (int)getTouchIDForTouch:(UITouch *)p_touch { + int first = -1; + for (int i = 0; i < max_touches; i++) { + if (first == -1 && godot_touches[i] == NULL) { + first = i; + continue; + } + if (godot_touches[i] == p_touch) { + return i; + } + } + + if (first != -1) { + godot_touches[first] = p_touch; + return first; + } + + return -1; +} + +- (int)removeTouch:(UITouch *)p_touch { + int remaining = 0; + for (int i = 0; i < max_touches; i++) { + if (godot_touches[i] == NULL) { + continue; + } + if (godot_touches[i] == p_touch) { + godot_touches[i] = NULL; + } else { + ++remaining; + } + } + return remaining; +} + +- (void)clearTouches { + for (int i = 0; i < max_touches; i++) { + godot_touches[i] = NULL; + } +} + +- (void)touchesBegan:(NSSet *)touchesSet withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touchesSet containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + CGPoint touchPoint = [touch locationInView:self]; + DisplayServerIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, true, touch.tapCount > 1); + } + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + CGPoint touchPoint = [touch locationInView:self]; + CGPoint prev_point = [touch previousLocationInView:self]; + DisplayServerIPhone::get_singleton()->touch_drag(tid, prev_point.x * self.contentScaleFactor, prev_point.y * self.contentScaleFactor, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor); + } + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + [self removeTouch:touch]; + CGPoint touchPoint = [touch locationInView:self]; + DisplayServerIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, false, false); + } + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + DisplayServerIPhone::get_singleton()->touches_cancelled(tid); + } + } + [self clearTouches]; +} + +// MARK: Motion + +- (void)handleMotion { + if (!self.motionManager) { + return; + } + + // Just using polling approach for now, we can set this up so it sends + // data to us in intervals, might be better. See Apple reference pages + // for more details: + // https://developer.apple.com/reference/coremotion/cmmotionmanager?language=objc + + // Apple splits our accelerometer date into a gravity and user movement + // component. We add them back together + CMAcceleration gravity = self.motionManager.deviceMotion.gravity; + CMAcceleration acceleration = self.motionManager.deviceMotion.userAcceleration; + + ///@TODO We don't seem to be getting data here, is my device broken or + /// is this code incorrect? + CMMagneticField magnetic = self.motionManager.deviceMotion.magneticField.field; + + ///@TODO we can access rotationRate as a CMRotationRate variable + ///(processed date) or CMGyroData (raw data), have to see what works + /// best + CMRotationRate rotation = self.motionManager.deviceMotion.rotationRate; + + // Adjust for screen orientation. + // [[UIDevice currentDevice] orientation] changes even if we've fixed + // our orientation which is not a good thing when you're trying to get + // your user to move the screen in all directions and want consistent + // output + + ///@TODO Using [[UIApplication sharedApplication] statusBarOrientation] + /// is a bit of a hack. Godot obviously knows the orientation so maybe + /// we + // can use that instead? (note that left and right seem swapped) + + UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; + + if (@available(iOS 13, *)) { + interfaceOrientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; +#endif + } + + switch (interfaceOrientation) { + case UIInterfaceOrientationLandscapeLeft: { + DisplayServerIPhone::get_singleton()->update_gravity(-gravity.y, gravity.x, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(-(acceleration.y + gravity.y), (acceleration.x + gravity.x), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(-magnetic.y, magnetic.x, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(-rotation.y, rotation.x, rotation.z); + } break; + case UIInterfaceOrientationLandscapeRight: { + DisplayServerIPhone::get_singleton()->update_gravity(gravity.y, -gravity.x, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer((acceleration.y + gravity.y), -(acceleration.x + gravity.x), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(magnetic.y, -magnetic.x, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(rotation.y, -rotation.x, rotation.z); + } break; + case UIInterfaceOrientationPortraitUpsideDown: { + DisplayServerIPhone::get_singleton()->update_gravity(-gravity.x, gravity.y, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(-(acceleration.x + gravity.x), (acceleration.y + gravity.y), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(-magnetic.x, magnetic.y, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(-rotation.x, rotation.y, rotation.z); + } break; + default: { // assume portrait + DisplayServerIPhone::get_singleton()->update_gravity(gravity.x, gravity.y, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(rotation.x, rotation.y, rotation.z); + } break; + } +} + +@end diff --git a/platform/iphone/godot_view_gesture_recognizer.h b/platform/iphone/godot_view_gesture_recognizer.h new file mode 100644 index 0000000000..1431a9fb89 --- /dev/null +++ b/platform/iphone/godot_view_gesture_recognizer.h @@ -0,0 +1,46 @@ +/*************************************************************************/ +/* godot_view_gesture_recognizer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +// GLViewGestureRecognizer 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 <UIKit/UIKit.h> + +@interface GodotViewGestureRecognizer : UIGestureRecognizer + +@property(nonatomic, readonly, assign) NSTimeInterval delayTimeInterval; + +- (instancetype)init; + +@end diff --git a/platform/iphone/godot_view_gesture_recognizer.mm b/platform/iphone/godot_view_gesture_recognizer.mm new file mode 100644 index 0000000000..91cc07fcf6 --- /dev/null +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -0,0 +1,171 @@ +/*************************************************************************/ +/* godot_view_gesture_recognizer.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "godot_view_gesture_recognizer.h" + +#include "core/config/project_settings.h" + +// Minimum distance for touches to move to fire +// a delay timer before scheduled time. +// Should be the low enough to not cause issues with dragging +// but big enough to allow click to work. +const CGFloat kGLGestureMovementDistance = 0.5; + +@interface GodotViewGestureRecognizer () + +@property(nonatomic, readwrite, assign) NSTimeInterval delayTimeInterval; + +@end + +@interface GodotViewGestureRecognizer () + +// Timer used to delay begin touch message. +// Should work as simple emulation of UIDelayedAction +@property(strong, nonatomic) NSTimer *delayTimer; + +// Delayed touch parameters +@property(strong, nonatomic) NSSet *delayedTouches; +@property(strong, nonatomic) UIEvent *delayedEvent; + +@end + +@implementation GodotViewGestureRecognizer + +- (instancetype)init { + self = [super init]; + + self.cancelsTouchesInView = YES; + self.delaysTouchesBegan = YES; + self.delaysTouchesEnded = YES; + + self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); + + return self; +} + +- (void)dealloc { + if (self.delayTimer) { + [self.delayTimer invalidate]; + self.delayTimer = nil; + } + + if (self.delayedTouches) { + self.delayedTouches = nil; + } + + if (self.delayedEvent) { + self.delayedEvent = nil; + } +} + +- (void)delayTouches:(NSSet *)touches andEvent:(UIEvent *)event { + [self.delayTimer fire]; + + self.delayedTouches = touches; + self.delayedEvent = event; + + self.delayTimer = [NSTimer + scheduledTimerWithTimeInterval:self.delayTimeInterval + target:self + selector:@selector(fireDelayedTouches:) + userInfo:nil + repeats:NO]; +} + +- (void)fireDelayedTouches:(id)timer { + [self.delayTimer invalidate]; + self.delayTimer = nil; + + if (self.delayedTouches) { + [self.view touchesBegan:self.delayedTouches withEvent:self.delayedEvent]; + } + + self.delayedTouches = nil; + self.delayedEvent = nil; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseBegan]; + [self delayTouches:cleared andEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseMoved]; + + if (self.delayTimer) { + // We should check if movement was significant enough to fire an event + // for dragging to work correctly. + for (UITouch *touch in cleared) { + CGPoint from = [touch locationInView:self.view]; + CGPoint to = [touch previousLocationInView:self.view]; + CGFloat xDistance = from.x - to.x; + CGFloat yDistance = from.y - to.y; + + CGFloat distance = sqrt(xDistance * xDistance + yDistance * yDistance); + + // Early exit, since one of touches has moved enough to fire a drag event. + if (distance > kGLGestureMovementDistance) { + [self.delayTimer fire]; + [self.view touchesMoved:cleared withEvent:event]; + return; + } + } + + return; + } + + [self.view touchesMoved:cleared withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [self.delayTimer fire]; + + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseEnded]; + [self.view touchesEnded:cleared withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [self.delayTimer fire]; + [self.view touchesCancelled:touches withEvent:event]; +}; + +- (NSSet *)copyClearedTouches:(NSSet *)touches phase:(UITouchPhase)phaseToSave { + NSMutableSet *cleared = [touches mutableCopy]; + + for (UITouch *touch in touches) { + if (touch.phase != phaseToSave) { + [cleared removeObject:touch]; + } + } + + return cleared; +} + +@end diff --git a/platform/iphone/godot_view_renderer.h b/platform/iphone/godot_view_renderer.h new file mode 100644 index 0000000000..ea8998c808 --- /dev/null +++ b/platform/iphone/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +@protocol GodotViewRendererProtocol <NSObject> + +@property(assign, readonly, nonatomic) BOOL hasFinishedSetup; + +- (BOOL)setupView:(UIView *)view; +- (void)renderOnView:(UIView *)view; + +@end + +@interface GodotViewRenderer : NSObject <GodotViewRendererProtocol> + +@end diff --git a/platform/iphone/godot_view_renderer.mm b/platform/iphone/godot_view_renderer.mm new file mode 100644 index 0000000000..f2ff417e6f --- /dev/null +++ b/platform/iphone/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "godot_view_renderer.h" +#include "core/config/project_settings.h" +#include "core/os/keyboard.h" +#import "display_server_iphone.h" +#include "main/main.h" +#include "os_iphone.h" +#include "servers/audio_server.h" + +#import <AudioToolbox/AudioServices.h> +#import <CoreMotion/CoreMotion.h> +#import <GameController/GameController.h> +#import <QuartzCore/QuartzCore.h> +#import <UIKit/UIKit.h> + +@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; + OSIPhone::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 (!OSIPhone::get_singleton()) { + return; + } + + OSIPhone::get_singleton()->iterate(); +} + +@end diff --git a/platform/iphone/icloud.mm b/platform/iphone/icloud.mm deleted file mode 100644 index c768274b1f..0000000000 --- a/platform/iphone/icloud.mm +++ /dev/null @@ -1,359 +0,0 @@ -/*************************************************************************/ -/* icloud.mm */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef ICLOUD_ENABLED - -#include "icloud.h" - -#ifndef __IPHONE_9_0 -extern "C" { -#endif - -#import "app_delegate.h" - -#import <Foundation/Foundation.h> - -#ifndef __IPHONE_9_0 -}; -#endif - -ICloud *ICloud::instance = NULL; - -void ICloud::_bind_methods() { - ClassDB::bind_method(D_METHOD("remove_key"), &ICloud::remove_key); - ClassDB::bind_method(D_METHOD("set_key_values"), &ICloud::set_key_values); - ClassDB::bind_method(D_METHOD("get_key_value"), &ICloud::get_key_value); - ClassDB::bind_method(D_METHOD("synchronize_key_values"), &ICloud::synchronize_key_values); - ClassDB::bind_method(D_METHOD("get_all_key_values"), &ICloud::get_all_key_values); - - ClassDB::bind_method(D_METHOD("get_pending_event_count"), &ICloud::get_pending_event_count); - ClassDB::bind_method(D_METHOD("pop_pending_event"), &ICloud::pop_pending_event); -}; - -int ICloud::get_pending_event_count() { - return pending_events.size(); -}; - -Variant ICloud::pop_pending_event() { - Variant front = pending_events.front()->get(); - pending_events.pop_front(); - - return front; -}; - -ICloud *ICloud::get_singleton() { - return instance; -}; - -//convert from apple's abstract type to godot's abstract type.... -Variant nsobject_to_variant(NSObject *object) { - if ([object isKindOfClass:[NSString class]]) { - const char *str = [(NSString *)object UTF8String]; - return String::utf8(str != NULL ? str : ""); - } else if ([object isKindOfClass:[NSData class]]) { - PackedByteArray ret; - NSData *data = (NSData *)object; - if ([data length] > 0) { - ret.resize([data length]); - { - // PackedByteArray::Write w = ret.write(); - copymem((void *)ret.ptr(), [data bytes], [data length]); - } - } - return ret; - } else if ([object isKindOfClass:[NSArray class]]) { - Array result; - NSArray *array = (NSArray *)object; - for (unsigned int i = 0; i < [array count]; ++i) { - NSObject *value = [array objectAtIndex:i]; - result.push_back(nsobject_to_variant(value)); - } - return result; - } else if ([object isKindOfClass:[NSDictionary class]]) { - Dictionary result; - NSDictionary *dic = (NSDictionary *)object; - - NSArray *keys = [dic allKeys]; - int count = [keys count]; - for (int i = 0; i < count; ++i) { - NSObject *k = [keys objectAtIndex:i]; - NSObject *v = [dic objectForKey:k]; - - result[nsobject_to_variant(k)] = nsobject_to_variant(v); - } - return result; - } else if ([object isKindOfClass:[NSNumber class]]) { - //Every type except numbers can reliably identify its type. The following is comparing to the *internal* representation, which isn't guaranteed to match the type that was used to create it, and is not advised, particularly when dealing with potential platform differences (ie, 32/64 bit) - //To avoid errors, we'll cast as broadly as possible, and only return int or float. - //bool, char, int, uint, longlong -> int - //float, double -> float - NSNumber *num = (NSNumber *)object; - if (strcmp([num objCType], @encode(BOOL)) == 0) { - return Variant((int)[num boolValue]); - } else if (strcmp([num objCType], @encode(char)) == 0) { - return Variant((int)[num charValue]); - } else if (strcmp([num objCType], @encode(int)) == 0) { - return Variant([num intValue]); - } else if (strcmp([num objCType], @encode(unsigned int)) == 0) { - return Variant((int)[num unsignedIntValue]); - } else if (strcmp([num objCType], @encode(long long)) == 0) { - return Variant((int)[num longValue]); - } else if (strcmp([num objCType], @encode(float)) == 0) { - return Variant([num floatValue]); - } else if (strcmp([num objCType], @encode(double)) == 0) { - return Variant((float)[num doubleValue]); - } else { - return Variant(); - } - } else if ([object isKindOfClass:[NSDate class]]) { - //this is a type that icloud supports...but how did you submit it in the first place? - //I guess this is a type that *might* show up, if you were, say, trying to make your game - //compatible with existing cloud data written by another engine's version of your game - WARN_PRINT("NSDate unsupported, returning null Variant"); - return Variant(); - } else if ([object isKindOfClass:[NSNull class]] or object == nil) { - return Variant(); - } else { - WARN_PRINT("Trying to convert unknown NSObject type to Variant"); - return Variant(); - } -} - -NSObject *variant_to_nsobject(Variant v) { - if (v.get_type() == Variant::STRING) { - return [[[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()] autorelease]; - } else if (v.get_type() == Variant::REAL) { - return [NSNumber numberWithDouble:(double)v]; - } else if (v.get_type() == Variant::INT) { - return [NSNumber numberWithLongLong:(long)(int)v]; - } else if (v.get_type() == Variant::BOOL) { - return [NSNumber numberWithBool:BOOL((bool)v)]; - } else if (v.get_type() == Variant::DICTIONARY) { - NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease]; - Dictionary dic = v; - Array keys = dic.keys(); - for (unsigned int i = 0; i < keys.size(); ++i) { - NSString *key = [[[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()] autorelease]; - NSObject *value = variant_to_nsobject(dic[keys[i]]); - - if (key == NULL || value == NULL) { - return NULL; - } - - [result setObject:value forKey:key]; - } - return result; - } else if (v.get_type() == Variant::ARRAY) { - NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease]; - Array arr = v; - for (unsigned int i = 0; i < arr.size(); ++i) { - NSObject *value = variant_to_nsobject(arr[i]); - if (value == NULL) { - //trying to add something unsupported to the array. cancel the whole array - return NULL; - } - [result addObject:value]; - } - return result; - } else if (v.get_type() == Variant::PACKED_BYTE_ARRAY) { - PackedByteArray arr = v; - // PackedByteArray::Read r = arr.read(); - NSData *result = [NSData dataWithBytes:arr.ptr() length:arr.size()]; - return result; - } - WARN_PRINT(String("Could not add unsupported type to iCloud: '" + Variant::get_type_name(v.get_type()) + "'").utf8().get_data()); - return NULL; -} - -Error ICloud::remove_key(Variant p_param) { - String param = p_param; - NSString *key = [[[NSString alloc] initWithUTF8String:param.utf8().get_data()] autorelease]; - - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - - if (![[store dictionaryRepresentation] objectForKey:key]) { - return ERR_INVALID_PARAMETER; - } - - [store removeObjectForKey:key]; - return OK; -} - -//return an array of the keys that could not be set -Variant ICloud::set_key_values(Variant p_params) { - Dictionary params = p_params; - Array keys = params.keys(); - - Array error_keys; - - for (unsigned int i = 0; i < keys.size(); ++i) { - String variant_key = keys[i]; - Variant variant_value = params[variant_key]; - - NSString *key = [[[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()] autorelease]; - if (key == NULL) { - error_keys.push_back(variant_key); - continue; - } - - NSObject *value = variant_to_nsobject(variant_value); - - if (value == NULL) { - error_keys.push_back(variant_key); - continue; - } - - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - [store setObject:value forKey:key]; - } - - return error_keys; -} - -Variant ICloud::get_key_value(Variant p_param) { - String param = p_param; - - NSString *key = [[[NSString alloc] initWithUTF8String:param.utf8().get_data()] autorelease]; - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - - if (![[store dictionaryRepresentation] objectForKey:key]) { - return Variant(); - } - - Variant result = nsobject_to_variant([[store dictionaryRepresentation] objectForKey:key]); - - return result; -} - -Variant ICloud::get_all_key_values() { - Dictionary result; - - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - NSDictionary *store_dictionary = [store dictionaryRepresentation]; - - NSArray *keys = [store_dictionary allKeys]; - int count = [keys count]; - for (int i = 0; i < count; ++i) { - NSString *k = [keys objectAtIndex:i]; - NSObject *v = [store_dictionary objectForKey:k]; - - const char *str = [k UTF8String]; - if (str != NULL) { - result[String::utf8(str)] = nsobject_to_variant(v); - } - } - - return result; -} - -Error ICloud::synchronize_key_values() { - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - BOOL result = [store synchronize]; - if (result == YES) { - return OK; - } else { - return FAILED; - } -} - -/* -Error ICloud::initial_sync() { - //you sometimes have to write something to the store to get it to download new data. go apple! - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - if ([store boolForKey:@"isb"]) - { - [store setBool:NO forKey:@"isb"]; - } - else - { - [store setBool:YES forKey:@"isb"]; - } - return synchronize(); -} - -*/ -ICloud::ICloud() { - ERR_FAIL_COND(instance != NULL); - instance = this; - //connected = false; - - [[NSNotificationCenter defaultCenter] - addObserverForName:NSUbiquitousKeyValueStoreDidChangeExternallyNotification - object:[NSUbiquitousKeyValueStore defaultStore] - queue:nil - usingBlock:^(NSNotification *notification) { - NSDictionary *userInfo = [notification userInfo]; - NSInteger change = [[userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey] integerValue]; - - Dictionary ret; - ret["type"] = "key_value_changed"; - - //PackedStringArray result_keys; - //Array result_values; - Dictionary keyValues; - String reason = ""; - - if (change == NSUbiquitousKeyValueStoreServerChange) { - reason = "server"; - } else if (change == NSUbiquitousKeyValueStoreInitialSyncChange) { - reason = "initial_sync"; - } else if (change == NSUbiquitousKeyValueStoreQuotaViolationChange) { - reason = "quota_violation"; - } else if (change == NSUbiquitousKeyValueStoreAccountChange) { - reason = "account"; - } - - ret["reason"] = reason; - - NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; - - NSArray *keys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; - for (NSString *key in keys) { - const char *str = [key UTF8String]; - if (str == NULL) { - continue; - } - - NSObject *object = [store objectForKey:key]; - - //figure out what kind of object it is - Variant value = nsobject_to_variant(object); - - keyValues[String::utf8(str)] = value; - } - - ret["changed_values"] = keyValues; - pending_events.push_back(ret); - }]; -} - -ICloud::~ICloud() {} - -#endif diff --git a/platform/iphone/in_app_store.mm b/platform/iphone/in_app_store.mm deleted file mode 100644 index 548dcc549d..0000000000 --- a/platform/iphone/in_app_store.mm +++ /dev/null @@ -1,326 +0,0 @@ -/*************************************************************************/ -/* in_app_store.mm */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef STOREKIT_ENABLED - -#include "in_app_store.h" - -extern "C" { -#import <Foundation/Foundation.h> -#import <StoreKit/StoreKit.h> -}; - -bool auto_finish_transactions = true; -NSMutableDictionary *pending_transactions = [NSMutableDictionary dictionary]; - -@interface SKProduct (LocalizedPrice) -@property(nonatomic, readonly) NSString *localizedPrice; -@end - -//----------------------------------// -// SKProduct extension -//----------------------------------// -@implementation SKProduct (LocalizedPrice) -- (NSString *)localizedPrice { - NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; - [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; - [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; - [numberFormatter setLocale:self.priceLocale]; - NSString *formattedString = [numberFormatter stringFromNumber:self.price]; - [numberFormatter release]; - return formattedString; -} - -@end - -InAppStore *InAppStore::instance = NULL; - -void InAppStore::_bind_methods() { - ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); - ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); - ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); - - ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); - ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); - ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); - ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); -}; - -@interface ProductsDelegate : NSObject <SKProductsRequestDelegate> { -}; - -@end - -@implementation ProductsDelegate - -- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { - NSArray *products = response.products; - Dictionary ret; - ret["type"] = "product_info"; - ret["result"] = "ok"; - PackedStringArray titles; - PackedStringArray descriptions; - PackedFloat32Array prices; - PackedStringArray ids; - PackedStringArray localized_prices; - PackedStringArray currency_codes; - - for (NSUInteger i = 0; i < [products count]; i++) { - SKProduct *product = [products objectAtIndex:i]; - - const char *str = [product.localizedTitle UTF8String]; - titles.push_back(String::utf8(str != NULL ? str : "")); - - str = [product.localizedDescription UTF8String]; - descriptions.push_back(String::utf8(str != NULL ? str : "")); - prices.push_back([product.price doubleValue]); - ids.push_back(String::utf8([product.productIdentifier UTF8String])); - localized_prices.push_back(String::utf8([product.localizedPrice UTF8String])); - currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String])); - }; - ret["titles"] = titles; - ret["descriptions"] = descriptions; - ret["prices"] = prices; - ret["ids"] = ids; - ret["localized_prices"] = localized_prices; - ret["currency_codes"] = currency_codes; - - PackedStringArray invalid_ids; - - for (NSString *ipid in response.invalidProductIdentifiers) { - invalid_ids.push_back(String::utf8([ipid UTF8String])); - }; - ret["invalid_ids"] = invalid_ids; - - InAppStore::get_singleton()->_post_event(ret); - - [request release]; -}; - -@end - -Error InAppStore::request_product_info(Variant p_params) { - Dictionary params = p_params; - ERR_FAIL_COND_V(!params.has("product_ids"), ERR_INVALID_PARAMETER); - - PackedStringArray pids = params["product_ids"]; - printf("************ request product info! %i\n", pids.size()); - - NSMutableArray *array = [[[NSMutableArray alloc] initWithCapacity:pids.size()] autorelease]; - for (int i = 0; i < pids.size(); i++) { - printf("******** adding %ls to product list\n", pids[i].c_str()); - NSString *pid = [[[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()] autorelease]; - [array addObject:pid]; - }; - - NSSet *products = [[[NSSet alloc] initWithArray:array] autorelease]; - SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:products]; - - ProductsDelegate *delegate = [[ProductsDelegate alloc] init]; - - request.delegate = delegate; - [request start]; - - return OK; -}; - -Error InAppStore::restore_purchases() { - printf("restoring purchases!\n"); - [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; - - return OK; -}; - -@interface TransObserver : NSObject <SKPaymentTransactionObserver> { -}; -@end - -@implementation TransObserver - -- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - printf("transactions updated!\n"); - for (SKPaymentTransaction *transaction in transactions) { - switch (transaction.transactionState) { - case SKPaymentTransactionStatePurchased: { - printf("status purchased!\n"); - String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); - String transactionId = String::utf8([transaction.transactionIdentifier UTF8String]); - InAppStore::get_singleton()->_record_purchase(pid); - Dictionary ret; - ret["type"] = "purchase"; - ret["result"] = "ok"; - ret["product_id"] = pid; - ret["transaction_id"] = transactionId; - - NSData *receipt = nil; - int sdk_version = 6; - - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { - NSURL *receiptFileURL = nil; - NSBundle *bundle = [NSBundle mainBundle]; - if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) { - // Get the transaction receipt file path location in the app bundle. - receiptFileURL = [bundle appStoreReceiptURL]; - - // Read in the contents of the transaction file. - receipt = [NSData dataWithContentsOfURL:receiptFileURL]; - sdk_version = 7; - - } else { - // Fall back to deprecated transaction receipt, - // which is still available in iOS 7. - - // Use SKPaymentTransaction's transactionReceipt. - receipt = transaction.transactionReceipt; - } - - } else { - receipt = transaction.transactionReceipt; - } - - NSString *receipt_to_send = nil; - if (receipt != nil) { - receipt_to_send = [receipt base64EncodedStringWithOptions:0]; - } - Dictionary receipt_ret; - receipt_ret["receipt"] = String::utf8(receipt_to_send != nil ? [receipt_to_send UTF8String] : ""); - receipt_ret["sdk"] = sdk_version; - ret["receipt"] = receipt_ret; - - InAppStore::get_singleton()->_post_event(ret); - - if (auto_finish_transactions) { - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - } else { - [pending_transactions setObject:transaction forKey:transaction.payment.productIdentifier]; - } - - }; break; - case SKPaymentTransactionStateFailed: { - printf("status transaction failed!\n"); - String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); - Dictionary ret; - ret["type"] = "purchase"; - ret["result"] = "error"; - ret["product_id"] = pid; - ret["error"] = String::utf8([transaction.error.localizedDescription UTF8String]); - InAppStore::get_singleton()->_post_event(ret); - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - } break; - case SKPaymentTransactionStateRestored: { - printf("status transaction restored!\n"); - String pid = String::utf8([transaction.originalTransaction.payment.productIdentifier UTF8String]); - InAppStore::get_singleton()->_record_purchase(pid); - Dictionary ret; - ret["type"] = "restore"; - ret["result"] = "ok"; - ret["product_id"] = pid; - InAppStore::get_singleton()->_post_event(ret); - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - } break; - default: { - printf("status default %i!\n", (int)transaction.transactionState); - }; break; - }; - }; -}; - -@end - -Error InAppStore::purchase(Variant p_params) { - ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); - if (![SKPaymentQueue canMakePayments]) - return ERR_UNAVAILABLE; - - printf("purchasing!\n"); - Dictionary params = p_params; - ERR_FAIL_COND_V(!params.has("product_id"), ERR_INVALID_PARAMETER); - - NSString *pid = [[[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()] autorelease]; - SKPayment *payment = [SKPayment paymentWithProductIdentifier:pid]; - SKPaymentQueue *defq = [SKPaymentQueue defaultQueue]; - [defq addPayment:payment]; - printf("purchase sent!\n"); - - return OK; -}; - -int InAppStore::get_pending_event_count() { - return pending_events.size(); -}; - -Variant InAppStore::pop_pending_event() { - Variant front = pending_events.front()->get(); - pending_events.pop_front(); - - return front; -}; - -void InAppStore::_post_event(Variant p_event) { - pending_events.push_back(p_event); -}; - -void InAppStore::_record_purchase(String product_id) { - String skey = "purchased/" + product_id; - NSString *key = [[[NSString alloc] initWithUTF8String:skey.utf8().get_data()] autorelease]; - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; - [[NSUserDefaults standardUserDefaults] synchronize]; -}; - -InAppStore *InAppStore::get_singleton() { - return instance; -}; - -InAppStore::InAppStore() { - ERR_FAIL_COND(instance != NULL); - instance = this; - auto_finish_transactions = false; - - TransObserver *observer = [[TransObserver alloc] init]; - [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; - //pending_transactions = [NSMutableDictionary dictionary]; -}; - -void InAppStore::finish_transaction(String product_id) { - NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding]; - - if ([pending_transactions objectForKey:prod_id]) { - [[SKPaymentQueue defaultQueue] finishTransaction:[pending_transactions objectForKey:prod_id]]; - [pending_transactions removeObjectForKey:prod_id]; - } -}; - -void InAppStore::set_auto_finish_transaction(bool b) { - auto_finish_transactions = b; -} - -InAppStore::~InAppStore() {} - -#endif diff --git a/platform/iphone/ios.h b/platform/iphone/ios.h index 2b29e6f268..5415c36cd9 100644 --- a/platform/iphone/ios.h +++ b/platform/iphone/ios.h @@ -31,7 +31,7 @@ #ifndef IOS_H #define IOS_H -#include "core/object.h" +#include "core/object/class_db.h" class iOS : public Object { GDCLASS(iOS, Object); diff --git a/platform/iphone/ios.mm b/platform/iphone/ios.mm index 5923f558a5..d4e099063f 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -29,17 +29,28 @@ /*************************************************************************/ #include "ios.h" -#include <sys/sysctl.h> - +#import "app_delegate.h" +#import "view_controller.h" #import <UIKit/UIKit.h> +#include <sys/sysctl.h> void iOS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rate_url", "app_id"), &iOS::get_rate_url); }; void iOS::alert(const char *p_alert, const char *p_title) { - UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:[NSString stringWithUTF8String:p_title] message:[NSString stringWithUTF8String:p_alert] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] autorelease]; - [alert show]; + 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 iOS::get_model() const { @@ -58,25 +69,11 @@ String iOS::get_model() const { } String iOS::get_rate_url(int p_app_id) const { - String templ = "itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=APP_ID"; - String templ_iOS7 = "itms-apps://itunes.apple.com/app/idAPP_ID"; - String templ_iOS8 = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=APP_ID&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software"; - - //ios7 before - String ret = templ; - - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 7.1) { - // iOS 7 needs a different templateReviewURL @see https://github.com/arashpayan/appirater/issues/131 - ret = templ_iOS7; - } else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { - // iOS 8 needs a different templateReviewURL also @see https://github.com/arashpayan/appirater/issues/182 - ret = templ_iOS8; - } + String app_url_path = "itms-apps://itunes.apple.com/app/idAPP_ID"; - // ios7 for everything? - ret = templ_iOS7.replace("APP_ID", String::num(p_app_id)); + String ret = app_url_path.replace("APP_ID", String::num(p_app_id)); - printf("returning rate url %ls\n", ret.c_str()); + printf("returning rate url %s\n", ret.utf8().get_data()); return ret; }; diff --git a/platform/iphone/icloud.h b/platform/iphone/joypad_iphone.h index b11e22fec6..85e26e1dc8 100644 --- a/platform/iphone/icloud.h +++ b/platform/iphone/joypad_iphone.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* icloud.h */ +/* joypad_iphone.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,37 +28,23 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifdef ICLOUD_ENABLED +#import <GameController/GameController.h> -#ifndef ICLOUD_H -#define ICLOUD_H +@interface JoypadIPhoneObserver : NSObject -#include "core/object.h" +- (void)startObserving; +- (void)startProcessing; +- (void)finishObserving; -class ICloud : public Object { - GDCLASS(ICloud, Object); +@end - static ICloud *instance; - static void _bind_methods(); - - List<Variant> pending_events; +class JoypadIPhone { +private: + JoypadIPhoneObserver *observer; public: - Error remove_key(Variant p_param); - Variant set_key_values(Variant p_param); - Variant get_key_value(Variant p_param); - Error synchronize_key_values(); - Variant get_all_key_values(); - - int get_pending_event_count(); - Variant pop_pending_event(); - - static ICloud *get_singleton(); + JoypadIPhone(); + ~JoypadIPhone(); - ICloud(); - ~ICloud(); + void start_processing(); }; - -#endif - -#endif diff --git a/platform/iphone/joypad_iphone.mm b/platform/iphone/joypad_iphone.mm new file mode 100644 index 0000000000..22d7dd4bd2 --- /dev/null +++ b/platform/iphone/joypad_iphone.mm @@ -0,0 +1,345 @@ +/*************************************************************************/ +/* joypad_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "joypad_iphone.h" +#include "core/config/project_settings.h" +#include "drivers/coreaudio/audio_driver_coreaudio.h" +#include "main/main.h" + +#import "godot_view.h" + +#include "os_iphone.h" + +JoypadIPhone::JoypadIPhone() { + observer = [[JoypadIPhoneObserver alloc] init]; + [observer startObserving]; +} + +JoypadIPhone::~JoypadIPhone() { + if (observer) { + [observer finishObserving]; + observer = nil; + } +} + +void JoypadIPhone::start_processing() { + if (observer) { + [observer startProcessing]; + } +} + +@interface JoypadIPhoneObserver () + +@property(assign, nonatomic) BOOL isObserving; +@property(assign, nonatomic) BOOL isProcessing; +@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; +@property(strong, nonatomic) NSMutableArray *joypadsQueue; + +@end + +@implementation JoypadIPhoneObserver + +- (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]; + + // 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; +}; + +- (void)addiOSJoypad:(GCController *)controller { + // get a new id for our controller + int joy_id = Input::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 + Input::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; + + 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]; + Input::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) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonB) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, + gamepad.buttonB.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.buttonY) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, + gamepad.buttonY.isPressed); + } else if (element == gamepad.leftShoulder) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, + gamepad.leftShoulder.isPressed); + } else if (element == gamepad.rightShoulder) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, + gamepad.rightShoulder.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, + gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, + gamepad.dpad.right.isPressed); + }; + + Input::JoyAxis jx; + jx.min = -1; + if (element == gamepad.leftThumbstick) { + jx.value = gamepad.leftThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_X, jx); + jx.value = -gamepad.leftThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_Y, jx); + } else if (element == gamepad.rightThumbstick) { + jx.value = gamepad.rightThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_X, jx); + jx.value = -gamepad.rightThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_Y, jx); + } else if (element == gamepad.leftTrigger) { + jx.value = gamepad.leftTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_LEFT, jx); + } else if (element == gamepad.rightTrigger) { + jx.value = gamepad.rightTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_RIGHT, 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) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_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/iphone/keyboard_input_view.h b/platform/iphone/keyboard_input_view.h new file mode 100644 index 0000000000..5382a2e9a9 --- /dev/null +++ b/platform/iphone/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +@interface GodotKeyboardInputView : UITextView + +- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end; + +@end diff --git a/platform/iphone/keyboard_input_view.mm b/platform/iphone/keyboard_input_view.mm new file mode 100644 index 0000000000..1a37403de7 --- /dev/null +++ b/platform/iphone/keyboard_input_view.mm @@ -0,0 +1,195 @@ +/*************************************************************************/ +/* keyboard_input_view.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "keyboard_input_view.h" + +#include "core/os/keyboard.h" +#include "display_server_iphone.h" +#include "os_iphone.h" + +@interface GodotKeyboardInputView () <UITextViewDelegate> + +@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 textContainer:(NSTextContainer *)textContainer { + self = [super initWithFrame:frame textContainer:textContainer]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.hidden = YES; + self.delegate = self; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(observeTextChange:) + name:UITextViewTextDidChangeNotification + object:self]; +} + +- (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++) { + DisplayServerIPhone::get_singleton()->key(KEY_BACKSPACE, true); + DisplayServerIPhone::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; + } + + DisplayServerIPhone::get_singleton()->key(character, true); + DisplayServerIPhone::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/iphone/logo.png b/platform/iphone/logo.png Binary files differindex 405b6f93ca..966d8aa70a 100644 --- a/platform/iphone/logo.png +++ b/platform/iphone/logo.png diff --git a/platform/iphone/main.m b/platform/iphone/main.m index 164db2a74b..351b40a881 100644 --- a/platform/iphone/main.m +++ b/platform/iphone/main.m @@ -28,24 +28,30 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#import "app_delegate.h" +#import "godot_app_delegate.h" #import <UIKit/UIKit.h> #include <stdio.h> +#include <vulkan/vulkan.h> int gargc; char **gargv; int main(int argc, char *argv[]) { +#if defined(VULKAN_ENABLED) + //MoltenVK - enable full component swizzling support + setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); +#endif + printf("*********** main.m\n"); gargc = argc; gargv = argv; - NSAutoreleasePool *pool = [NSAutoreleasePool new]; - AppDelegate *app = [AppDelegate alloc]; printf("running app main\n"); - UIApplicationMain(argc, argv, nil, @"AppDelegate"); - printf("main done, pool release\n"); - [pool release]; + @autoreleasepool { + NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]); + UIApplicationMain(argc, argv, nil, className); + } + printf("main done\n"); return 0; } diff --git a/platform/iphone/native_video_view.h b/platform/iphone/native_video_view.h new file mode 100644 index 0000000000..d8687b3538 --- /dev/null +++ b/platform/iphone/native_video_view.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* native_video_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +@interface GodotNativeVideoView : UIView + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; +- (BOOL)isVideoPlaying; +- (void)pauseVideo; +- (void)unpauseVideo; +- (void)stopVideo; + +@end diff --git a/platform/iphone/native_video_view.m b/platform/iphone/native_video_view.m new file mode 100644 index 0000000000..1193946f2b --- /dev/null +++ b/platform/iphone/native_video_view.m @@ -0,0 +1,266 @@ +/*************************************************************************/ +/* native_video_view.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "native_video_view.h" +#import <AVFoundation/AVFoundation.h> + +@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)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/iphone/os_iphone.cpp b/platform/iphone/os_iphone.cpp deleted file mode 100644 index 41dd623e69..0000000000 --- a/platform/iphone/os_iphone.cpp +++ /dev/null @@ -1,632 +0,0 @@ -/*************************************************************************/ -/* os_iphone.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef IPHONE_ENABLED - -#include "os_iphone.h" - -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif - -#if defined(VULKAN_ENABLED) -#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" -// #import <QuartzCore/CAMetalLayer.h> -#include <vulkan/vulkan_metal.h> -#endif - -#include "servers/rendering/rendering_server_raster.h" -#include "servers/rendering/rendering_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" - -#include "semaphore_iphone.h" - -#include <dlfcn.h> - -int OSIPhone::get_video_driver_count() const { - return 2; -}; - -const char *OSIPhone::get_video_driver_name(int p_driver) const { - switch (p_driver) { - case VIDEO_DRIVER_GLES2: - return "GLES2"; - } - ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); -}; - -OSIPhone *OSIPhone::get_singleton() { - return (OSIPhone *)OS::get_singleton(); -}; - -extern int gl_view_base_fb; // from gl_view.mm - -void OSIPhone::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); -}; - -void OSIPhone::set_unique_id(String p_id) { - unique_id = p_id; -}; - -String OSIPhone::get_unique_id() const { - return unique_id; -}; - -void OSIPhone::initialize_core() { - OS_Unix::initialize_core(); - - set_data_dir(data_dir); -}; - -int OSIPhone::get_current_video_driver() const { - return video_driver_index; -} - -Error OSIPhone::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - video_driver_index = p_video_driver; - -#if defined(OPENGL_ENABLED) - bool gl_initialization_error = false; - - // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? - - if (RasterizerGLES2::is_viable() == OK) { - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - } else { - gl_initialization_error = true; - } - - 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; - } -#endif - -#if defined(VULKAN_ENABLED) - RasterizerRD::make_current(); -#endif - - rendering_server = memnew(RenderingServerRaster); - // FIXME: Reimplement threaded rendering - if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { - rendering_server = memnew(RenderingServerWrapMT(rendering_server, false)); - } - rendering_server->init(); - //rendering_server->cursor_set_visible(false, 0); - -#if defined(OPENGL_ENABLED) - // reset this to what it should be, it will have been set to 0 after rendering_server->init() is called - RasterizerStorageGLES2::system_fbo = gl_view_base_fb; -#endif - - AudioDriverManager::initialize(p_audio_driver); - - input = memnew(InputDefault); - -#ifdef GAME_CENTER_ENABLED - game_center = memnew(GameCenter); - Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); - game_center->connect(); -#endif - -#ifdef STOREKIT_ENABLED - store_kit = memnew(InAppStore); - Engine::get_singleton()->add_singleton(Engine::Singleton("InAppStore", store_kit)); -#endif - -#ifdef ICLOUD_ENABLED - icloud = memnew(ICloud); - Engine::get_singleton()->add_singleton(Engine::Singleton("ICloud", icloud)); - //icloud->connect(); -#endif - ios = memnew(iOS); - Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); - - return OK; -}; - -MainLoop *OSIPhone::get_main_loop() const { - return main_loop; -}; - -void OSIPhone::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 OSIPhone::iterate() { - if (!main_loop) - return true; - - if (main_loop) { - for (int i = 0; i < event_count; i++) { - input->parse_input_event(event_queue[i]); - }; - }; - event_count = 0; - - return Main::iteration(); -}; - -void OSIPhone::key(uint32_t p_key, bool p_pressed) { - Ref<InputEventKey> ev; - ev.instance(); - ev->set_echo(false); - ev->set_pressed(p_pressed); - ev->set_keycode(p_key); - ev->set_physical_keycode(p_key); - ev->set_unicode(p_key); - queue_event(ev); -}; - -void OSIPhone::touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick) { - if (!GLOBAL_DEF("debug/disable_touch", false)) { - Ref<InputEventScreenTouch> ev; - ev.instance(); - - ev->set_index(p_idx); - ev->set_pressed(p_pressed); - ev->set_position(Vector2(p_x, p_y)); - queue_event(ev); - }; - - touch_list.pressed[p_idx] = p_pressed; -}; - -void OSIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y) { - if (!GLOBAL_DEF("debug/disable_touch", false)) { - Ref<InputEventScreenDrag> ev; - ev.instance(); - ev->set_index(p_idx); - ev->set_position(Vector2(p_x, p_y)); - ev->set_relative(Vector2(p_x - p_prev_x, p_y - p_prev_y)); - queue_event(ev); - }; -}; - -void OSIPhone::queue_event(const Ref<InputEvent> &p_event) { - ERR_FAIL_INDEX(event_count, MAX_EVENTS); - - event_queue[event_count++] = p_event; -}; - -void OSIPhone::touches_cancelled() { - for (int i = 0; i < MAX_MOUSE_COUNT; i++) { - if (touch_list.pressed[i]) { - // send a mouse_up outside the screen - touch_press(i, -1, -1, false, false); - }; - }; -}; - -static const float ACCEL_RANGE = 1; - -void OSIPhone::update_gravity(float p_x, float p_y, float p_z) { - input->set_gravity(Vector3(p_x, p_y, p_z)); -}; - -void OSIPhone::update_accelerometer(float p_x, float p_y, float p_z) { - // Found out the Z should not be negated! Pass as is! - input->set_accelerometer(Vector3(p_x / (float)ACCEL_RANGE, p_y / (float)ACCEL_RANGE, p_z / (float)ACCEL_RANGE)); - - /* - if (p_x != last_accel.x) { - //printf("updating accel x %f\n", p_x); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_0; - ev.joy_motion.axis_value = (p_x / (float)ACCEL_RANGE); - last_accel.x = p_x; - queue_event(ev); - }; - if (p_y != last_accel.y) { - //printf("updating accel y %f\n", p_y); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_1; - ev.joy_motion.axis_value = (p_y / (float)ACCEL_RANGE); - last_accel.y = p_y; - queue_event(ev); - }; - if (p_z != last_accel.z) { - //printf("updating accel z %f\n", p_z); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_2; - ev.joy_motion.axis_value = ( (1.0 - p_z) / (float)ACCEL_RANGE); - last_accel.z = p_z; - queue_event(ev); - }; - */ -}; - -void OSIPhone::update_magnetometer(float p_x, float p_y, float p_z) { - input->set_magnetometer(Vector3(p_x, p_y, p_z)); -}; - -void OSIPhone::update_gyroscope(float p_x, float p_y, float p_z) { - input->set_gyroscope(Vector3(p_x, p_y, p_z)); -}; - -int OSIPhone::get_unused_joy_id() { - return input->get_unused_joy_id(); -}; - -void OSIPhone::joy_connection_changed(int p_idx, bool p_connected, String p_name) { - input->joy_connection_changed(p_idx, p_connected, p_name); -}; - -void OSIPhone::joy_button(int p_device, int p_button, bool p_pressed) { - input->joy_button(p_device, p_button, p_pressed); -}; - -void OSIPhone::joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value) { - input->joy_axis(p_device, p_axis, p_value); -}; - -void OSIPhone::delete_main_loop() { - if (main_loop) { - main_loop->finish(); - memdelete(main_loop); - }; - - main_loop = nullptr; -}; - -void OSIPhone::finalize() { - delete_main_loop(); - - memdelete(input); - memdelete(ios); - -#ifdef GAME_CENTER_ENABLED - memdelete(game_center); -#endif - -#ifdef STOREKIT_ENABLED - memdelete(store_kit); -#endif - -#ifdef ICLOUD_ENABLED - memdelete(icloud); -#endif - - rendering_server->finish(); - memdelete(rendering_server); - // memdelete(rasterizer); - - // Free unhandled events before close - for (int i = 0; i < MAX_EVENTS; i++) { - event_queue[i].unref(); - }; - event_count = 0; -}; - -void OSIPhone::set_mouse_show(bool p_show) {} -void OSIPhone::set_mouse_grab(bool p_grab) {} - -bool OSIPhone::is_mouse_grab_enabled() const { - return true; -}; - -Point2 OSIPhone::get_mouse_position() const { - return Point2(); -}; - -int OSIPhone::get_mouse_button_state() const { - return 0; -}; - -void OSIPhone::set_window_title(const String &p_title) {} - -void OSIPhone::alert(const String &p_alert, const String &p_title) { - const CharString utf8_alert = p_alert.utf8(); - const CharString utf8_title = p_title.utf8(); - iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); -} - -Error OSIPhone::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 OSIPhone::close_dynamic_library(void *p_library_handle) { - if (p_library_handle == RTLD_SELF) { - return OK; - } - return OS_Unix::close_dynamic_library(p_library_handle); -} - -HashMap<String, void *> OSIPhone::dynamic_symbol_lookup_table; -void register_dynamic_symbol(char *name, void *address) { - OSIPhone::dynamic_symbol_lookup_table[String(name)] = address; -} - -Error OSIPhone::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 = OSIPhone::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 OSIPhone::set_video_mode(const VideoMode &p_video_mode, int p_screen) { - video_mode = p_video_mode; -}; - -OS::VideoMode OSIPhone::get_video_mode(int p_screen) const { - return video_mode; -}; - -void OSIPhone::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const { - p_list->push_back(video_mode); -}; - -bool OSIPhone::can_draw() const { - if (native_video_is_playing()) - return false; - return true; -}; - -int OSIPhone::set_base_framebuffer(int p_fb) { -#if defined(OPENGL_ENABLED) - // gl_view_base_fb has not been updated yet - RasterizerStorageGLES2::system_fbo = p_fb; -#endif - - return 0; -}; - -bool OSIPhone::has_virtual_keyboard() const { - return true; -}; - -extern void _show_keyboard(String p_existing); -extern void _hide_keyboard(); -extern Error _shell_open(String p_uri); -extern void _set_keep_screen_on(bool p_enabled); -extern void _vibrate(); - -void OSIPhone::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, int p_max_input_length, int p_cursor_start, int p_cursor_end) { - _show_keyboard(p_existing_text); -}; - -void OSIPhone::hide_virtual_keyboard() { - _hide_keyboard(); -}; - -void OSIPhone::set_virtual_keyboard_height(int p_height) { - virtual_keyboard_height = p_height; -} - -int OSIPhone::get_virtual_keyboard_height() const { - return virtual_keyboard_height; -} - -Error OSIPhone::shell_open(String p_uri) { - return _shell_open(p_uri); -}; - -void OSIPhone::set_keep_screen_on(bool p_enabled) { - OS::set_keep_screen_on(p_enabled); - _set_keep_screen_on(p_enabled); -}; - -String OSIPhone::get_user_data_dir() const { - return data_dir; -}; - -String OSIPhone::get_name() const { - return "iOS"; -}; - -String OSIPhone::get_model_name() const { - String model = ios->get_model(); - if (model != "") - return model; - - return OS_Unix::get_model_name(); -} - -Size2 OSIPhone::get_window_size() const { - return Vector2(video_mode.width, video_mode.height); -} - -extern Rect2 _get_ios_window_safe_area(float p_window_width, float p_window_height); - -Rect2 OSIPhone::get_window_safe_area() const { - return _get_ios_window_safe_area(video_mode.width, video_mode.height); -} - -bool OSIPhone::has_touchscreen_ui_hint() const { - return true; -} - -void OSIPhone::set_locale(String p_locale) { - locale_code = p_locale; -} - -String OSIPhone::get_locale() const { - return locale_code; -} - -extern bool _play_video(String p_path, float p_volume, String p_audio_track, String p_subtitle_track); -extern bool _is_video_playing(); -extern void _pause_video(); -extern void _unpause_video(); -extern void _stop_video(); -extern void _focus_out_video(); - -Error OSIPhone::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 tempFile = get_user_data_dir(); - if (!exists) - return FAILED; - - if (p_path.begins_with("res://")) { - if (PackedData::get_singleton()->has_path(p_path)) { - print("Unable to play %S using the native player as it resides in a .pck file\n", p_path.c_str()); - 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:/", get_user_data_dir()); - - memdelete(f); - - print("Playing video: %S\n", p_path.c_str()); - if (_play_video(p_path, p_volume, p_audio_track, p_subtitle_track)) - return OK; - return FAILED; -} - -bool OSIPhone::native_video_is_playing() const { - return _is_video_playing(); -} - -void OSIPhone::native_video_pause() { - if (native_video_is_playing()) - _pause_video(); -} - -void OSIPhone::native_video_unpause() { - _unpause_video(); -}; - -void OSIPhone::native_video_focus_out() { - _focus_out_video(); -}; - -void OSIPhone::native_video_stop() { - if (native_video_is_playing()) - _stop_video(); -} - -void OSIPhone::vibrate_handheld(int p_duration_ms) { - // iOS does not support duration for vibration - _vibrate(); -} - -bool OSIPhone::_check_internal_feature_support(const String &p_feature) { - return p_feature == "mobile"; -} - -// 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 *ios_init_callbacks = nullptr; -static int ios_init_callbacks_count = 0; -static int ios_init_callbacks_capacity = 0; - -void add_ios_init_callback(init_callback cb) { - if (ios_init_callbacks_count == ios_init_callbacks_capacity) { - void *new_ptr = realloc(ios_init_callbacks, sizeof(cb) * 32); - if (new_ptr) { - ios_init_callbacks = (init_callback *)(new_ptr); - ios_init_callbacks_capacity += 32; - } - } - if (ios_init_callbacks_capacity > ios_init_callbacks_count) { - ios_init_callbacks[ios_init_callbacks_count] = cb; - ++ios_init_callbacks_count; - } -} - -OSIPhone::OSIPhone(int width, int height, String p_data_dir) { - for (int i = 0; i < ios_init_callbacks_count; ++i) { - ios_init_callbacks[i](); - } - free(ios_init_callbacks); - ios_init_callbacks = nullptr; - ios_init_callbacks_count = 0; - ios_init_callbacks_capacity = 0; - - main_loop = nullptr; - rendering_server = nullptr; - - VideoMode vm; - vm.fullscreen = true; - vm.width = width; - vm.height = height; - vm.resizable = false; - set_video_mode(vm); - event_count = 0; - virtual_keyboard_height = 0; - - // can't call set_data_dir from here, since it requires DirAccess - // which is initialized in initialize_core - data_dir = p_data_dir; - - Vector<Logger *> loggers; - loggers.push_back(memnew(SyslogLogger)); -#ifdef DEBUG_ENABLED - // it seems 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); -}; - -OSIPhone::~OSIPhone() { -} - -#endif diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index 955eb15d57..04a0a478d5 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -33,180 +33,92 @@ #ifndef OS_IPHONE_H #define OS_IPHONE_H -#include "core/input/input.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #include "drivers/unix/os_unix.h" -#include "game_center.h" -#include "icloud.h" -#include "in_app_store.h" #include "ios.h" +#include "joypad_iphone.h" #include "servers/audio_server.h" #include "servers/rendering/rasterizer.h" -#include "servers/rendering_server.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" #include "platform/iphone/vulkan_context_iphone.h" #endif +extern void godot_ios_plugins_initialize(); +extern void godot_ios_plugins_deinitialize(); + class OSIPhone : public OS_Unix { private: - enum { - MAX_MOUSE_COUNT = 8, - MAX_EVENTS = 64, - }; - static HashMap<String, void *> dynamic_symbol_lookup_table; friend void register_dynamic_symbol(char *name, void *address); - RenderingServer *rendering_server; - AudioDriverCoreAudio audio_driver; -#ifdef GAME_CENTER_ENABLED - GameCenter *game_center; -#endif -#ifdef STOREKIT_ENABLED - InAppStore *store_kit; -#endif -#ifdef ICLOUD_ENABLED - ICloud *icloud; -#endif iOS *ios; - MainLoop *main_loop; - -#if defined(VULKAN_ENABLED) - VulkanContextIPhone *context_vulkan; - RenderingDeviceVulkan *rendering_device_vulkan; -#endif - 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(); + JoypadIPhone *joypad_iphone; - virtual void finalize(); + MainLoop *main_loop; - struct MouseList { - bool pressed[MAX_MOUSE_COUNT]; - MouseList() { - for (int i = 0; i < MAX_MOUSE_COUNT; i++) - pressed[i] = false; - }; - }; + virtual void initialize_core() override; + virtual void initialize() override; - MouseList touch_list; + virtual void initialize_joypads() override { + } - Vector3 last_accel; + virtual void set_main_loop(MainLoop *p_main_loop) override; + virtual MainLoop *get_main_loop() const override; - Ref<InputEvent> event_queue[MAX_EVENTS]; - int event_count; - void queue_event(const Ref<InputEvent> &p_event); + virtual void delete_main_loop() override; - String data_dir; - String unique_id; - String locale_code; + virtual void finalize() override; - InputDefault *input; + String user_data_dir; - int virtual_keyboard_height; + bool is_focused = false; - int video_driver_index; + void deinitialize_modules(); public: - bool iterate(); - - uint8_t get_orientations() const; - - void touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick); - void touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y); - void touches_cancelled(); - void key(uint32_t p_key, bool p_pressed); - void set_virtual_keyboard_height(int p_height); - - int set_base_framebuffer(int p_fb); - - void update_gravity(float p_x, float p_y, float p_z); - void update_accelerometer(float p_x, float p_y, float p_z); - void update_magnetometer(float p_x, float p_y, float p_z); - void update_gyroscope(float p_x, float p_y, float p_z); - - int get_unused_joy_id(); - 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); - static OSIPhone *get_singleton(); - 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 alert(const String &p_alert, const String &p_title = "ALERT!"); - - 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 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<VideoMode> *p_list, int p_screen = 0) const; + OSIPhone(String p_data_dir); + ~OSIPhone(); - virtual void set_keep_screen_on(bool p_enabled); + void initialize_modules(); - virtual bool can_draw() const; + bool iterate(); - virtual bool has_virtual_keyboard() const; - virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), 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; + void start(); - virtual Size2 get_window_size() const; - virtual Rect2 get_window_safe_area() const; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error close_dynamic_library(void *p_library_handle) override; + virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; - virtual bool has_touchscreen_ui_hint() const; + virtual void alert(const String &p_alert, + const String &p_title = "ALERT!") override; - void set_data_dir(String p_dir); + virtual String get_name() const override; + virtual String get_model_name() const override; - virtual String get_name() const; - virtual String get_model_name() const; + virtual Error shell_open(String p_uri) override; - Error shell_open(String p_uri); + void set_user_data_dir(String p_dir); + virtual String get_user_data_dir() const override; - String get_user_data_dir() const; + virtual String get_locale() const override; - void set_locale(String p_locale); - String get_locale() const; + virtual String get_unique_id() const override; - void set_unique_id(String p_id); - String get_unique_id() const; + virtual void vibrate_handheld(int p_duration_ms = 500) override; - 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(); - virtual void vibrate_handheld(int p_duration_ms = 500); + virtual bool _check_internal_feature_support(const String &p_feature) override; - virtual bool _check_internal_feature_support(const String &p_feature); - OSIPhone(int width, int height, String p_data_dir); - ~OSIPhone(); + void on_focus_out(); + void on_focus_in(); }; #endif // OS_IPHONE_H -#endif +#endif // IPHONE_ENABLED diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm new file mode 100644 index 0000000000..b87e6f37a0 --- /dev/null +++ b/platform/iphone/os_iphone.mm @@ -0,0 +1,330 @@ +/*************************************************************************/ +/* os_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifdef IPHONE_ENABLED + +#include "os_iphone.h" +#import "app_delegate.h" +#include "core/config/project_settings.h" +#include "core/io/file_access_pack.h" +#include "core/os/dir_access.h" +#include "core/os/file_access.h" +#include "display_server_iphone.h" +#include "drivers/unix/syslog_logger.h" +#import "godot_view.h" +#include "main/main.h" +#import "view_controller.h" + +#import <AudioToolbox/AudioServices.h> +#import <UIKit/UIKit.h> +#import <dlfcn.h> + +#if defined(VULKAN_ENABLED) +#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" +#import <QuartzCore/CAMetalLayer.h> +#include <vulkan/vulkan_metal.h> +#endif + +// 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 *ios_init_callbacks = nullptr; +static int ios_init_callbacks_count = 0; +static int ios_init_callbacks_capacity = 0; +HashMap<String, void *> OSIPhone::dynamic_symbol_lookup_table; + +void add_ios_init_callback(init_callback cb) { + if (ios_init_callbacks_count == ios_init_callbacks_capacity) { + void *new_ptr = realloc(ios_init_callbacks, sizeof(cb) * 32); + if (new_ptr) { + ios_init_callbacks = (init_callback *)(new_ptr); + ios_init_callbacks_capacity += 32; + } + } + if (ios_init_callbacks_capacity > ios_init_callbacks_count) { + ios_init_callbacks[ios_init_callbacks_count] = cb; + ++ios_init_callbacks_count; + } +} + +void register_dynamic_symbol(char *name, void *address) { + OSIPhone::dynamic_symbol_lookup_table[String(name)] = address; +} + +OSIPhone *OSIPhone::get_singleton() { + return (OSIPhone *)OS::get_singleton(); +} + +OSIPhone::OSIPhone(String p_data_dir) { + for (int i = 0; i < ios_init_callbacks_count; ++i) { + ios_init_callbacks[i](); + } + free(ios_init_callbacks); + ios_init_callbacks = nullptr; + ios_init_callbacks_count = 0; + ios_init_callbacks_capacity = 0; + + main_loop = nullptr; + + // can't call set_data_dir from here, since it requires DirAccess + // which is initialized in initialize_core + user_data_dir = p_data_dir; + + Vector<Logger *> loggers; + loggers.push_back(memnew(SyslogLogger)); +#ifdef DEBUG_ENABLED + // it seems 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); + + DisplayServerIPhone::register_iphone_driver(); +} + +OSIPhone::~OSIPhone() {} + +void OSIPhone::initialize_core() { + OS_Unix::initialize_core(); + + set_user_data_dir(user_data_dir); +} + +void OSIPhone::initialize() { + initialize_core(); +} + +void OSIPhone::initialize_modules() { + ios = memnew(iOS); + Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); + + joypad_iphone = memnew(JoypadIPhone); +} + +void OSIPhone::deinitialize_modules() { + if (joypad_iphone) { + memdelete(joypad_iphone); + } + + if (ios) { + memdelete(ios); + } + + godot_ios_plugins_deinitialize(); +} + +void OSIPhone::set_main_loop(MainLoop *p_main_loop) { + godot_ios_plugins_initialize(); + + main_loop = p_main_loop; + + if (main_loop) { + main_loop->init(); + } +} + +MainLoop *OSIPhone::get_main_loop() const { + return main_loop; +} + +void OSIPhone::delete_main_loop() { + if (main_loop) { + main_loop->finish(); + memdelete(main_loop); + }; + + main_loop = nullptr; +} + +bool OSIPhone::iterate() { + if (!main_loop) { + return true; + } + + if (DisplayServer::get_singleton()) { + DisplayServer::get_singleton()->process_events(); + } + + return Main::iteration(); +} + +void OSIPhone::start() { + Main::start(); + + if (joypad_iphone) { + joypad_iphone->start_processing(); + } +} + +void OSIPhone::finalize() { + deinitialize_modules(); + + // Already gets called + // delete_main_loop(); +} + +// MARK: Dynamic Libraries + +Error OSIPhone::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 OSIPhone::close_dynamic_library(void *p_library_handle) { + if (p_library_handle == RTLD_SELF) { + return OK; + } + return OS_Unix::close_dynamic_library(p_library_handle); +} + +Error OSIPhone::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 = OSIPhone::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 OSIPhone::alert(const String &p_alert, const String &p_title) { + const CharString utf8_alert = p_alert.utf8(); + const CharString utf8_title = p_title.utf8(); + iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); +} + +String OSIPhone::get_name() const { + return "iOS"; +}; + +String OSIPhone::get_model_name() const { + String model = ios->get_model(); + if (model != "") + return model; + + return OS_Unix::get_model_name(); +} + +Error OSIPhone::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 OSIPhone::set_user_data_dir(String p_dir) { + DirAccess *da = DirAccess::open(p_dir); + + user_data_dir = da->get_current_dir(); + printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); + memdelete(da); +} + +String OSIPhone::get_user_data_dir() const { + return user_data_dir; +} + +String OSIPhone::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("-", "_"); +} + +String OSIPhone::get_unique_id() const { + NSString *uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; + return String::utf8([uuid UTF8String]); +} + +void OSIPhone::vibrate_handheld(int p_duration_ms) { + // iOS does not support duration for vibration + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + +bool OSIPhone::_check_internal_feature_support(const String &p_feature) { + return p_feature == "mobile"; +} + +void OSIPhone::on_focus_out() { + if (is_focused) { + is_focused = false; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT); + } + + [AppDelegate.viewController.godotView stopRendering]; + + if (DisplayServerIPhone::get_singleton() && DisplayServerIPhone::get_singleton()->native_video_is_playing()) { + DisplayServerIPhone::get_singleton()->native_video_pause(); + } + + audio_driver.stop(); + } +} + +void OSIPhone::on_focus_in() { + if (!is_focused) { + is_focused = true; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_IN); + } + + [AppDelegate.viewController.godotView startRendering]; + + if (DisplayServerIPhone::get_singleton() && DisplayServerIPhone::get_singleton()->native_video_is_playing()) { + DisplayServerIPhone::get_singleton()->native_video_unpause(); + } + + audio_driver.start(); + } +} + +#endif // IPHONE_ENABLED diff --git a/platform/iphone/platform_config.h b/platform/iphone/platform_config.h index bc190ba956..ec39ad0ba4 100644 --- a/platform/iphone/platform_config.h +++ b/platform/iphone/platform_config.h @@ -30,8 +30,13 @@ #include <alloca.h> -#define GLES2_INCLUDE_H <ES2/gl.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/iphone/plugin/godot_plugin_config.h b/platform/iphone/plugin/godot_plugin_config.h new file mode 100644 index 0000000000..5323f94989 --- /dev/null +++ b/platform/iphone/plugin/godot_plugin_config.h @@ -0,0 +1,265 @@ +/*************************************************************************/ +/* godot_plugin_config.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_PLUGIN_CONFIG_H +#define GODOT_PLUGIN_CONFIG_H + +#include "core/error/error_list.h" +#include "core/io/config_file.h" +#include "core/string/ustring.h" + +static const char *PLUGIN_CONFIG_EXT = ".gdip"; + +static const char *CONFIG_SECTION = "config"; +static const char *CONFIG_NAME_KEY = "name"; +static const char *CONFIG_BINARY_KEY = "binary"; +static const char *CONFIG_INITIALIZE_KEY = "initialization"; +static const char *CONFIG_DEINITIALIZE_KEY = "deinitialization"; + +static const char *DEPENDENCIES_SECTION = "dependencies"; +static const char *DEPENDENCIES_LINKED_KEY = "linked"; +static const char *DEPENDENCIES_EMBEDDED_KEY = "embedded"; +static const char *DEPENDENCIES_SYSTEM_KEY = "system"; +static const char *DEPENDENCIES_CAPABILITIES_KEY = "capabilities"; +static const char *DEPENDENCIES_FILES_KEY = "files"; + +static const char *PLIST_SECTION = "plist"; + +/* + 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 PluginConfig { + // 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<String> linked_dependencies; + Vector<String> embedded_dependencies; + Vector<String> system_dependencies; + + Vector<String> files_to_copy; + Vector<String> capabilities; + + // Optional plist section + // Supports only string types for now + HashMap<String, String> 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<String> resolve_local_dependencies(String plugin_config_dir, Vector<String> p_paths) { + Vector<String> 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<String> resolve_system_dependencies(Vector<String> p_paths) { + Vector<String> 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(PluginConfig &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 && FileAccess::exists(plugin_config.binary)) { + plugin_config.valid_config = true; + plugin_config.supports_targets = false; + } else if (fields_value) { + 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"); + + if (FileAccess::exists(release_file_name) && FileAccess::exists(debug_file_name)) { + plugin_config.valid_config = true; + plugin_config.supports_targets = true; + } + } + + return plugin_config.valid_config; +} + +static inline uint64_t get_plugin_modification_time(const PluginConfig &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 PluginConfig load_plugin_config(Ref<ConfigFile> config_file, const String &path) { + PluginConfig 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(CONFIG_SECTION, CONFIG_NAME_KEY, String()); + plugin_config.initialization_method = config_file->get_value(CONFIG_SECTION, CONFIG_INITIALIZE_KEY, String()); + plugin_config.deinitialization_method = config_file->get_value(CONFIG_SECTION, CONFIG_DEINITIALIZE_KEY, String()); + + String binary_path = config_file->get_value(CONFIG_SECTION, CONFIG_BINARY_KEY, String()); + plugin_config.binary = resolve_local_dependency_path(config_base_dir, binary_path); + + if (config_file->has_section(DEPENDENCIES_SECTION)) { + Vector<String> linked_dependencies = config_file->get_value(DEPENDENCIES_SECTION, DEPENDENCIES_LINKED_KEY, Vector<String>()); + Vector<String> embedded_dependencies = config_file->get_value(DEPENDENCIES_SECTION, DEPENDENCIES_EMBEDDED_KEY, Vector<String>()); + Vector<String> system_dependencies = config_file->get_value(DEPENDENCIES_SECTION, DEPENDENCIES_SYSTEM_KEY, Vector<String>()); + Vector<String> files = config_file->get_value(DEPENDENCIES_SECTION, DEPENDENCIES_FILES_KEY, Vector<String>()); + + 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(DEPENDENCIES_SECTION, DEPENDENCIES_CAPABILITIES_KEY, Vector<String>()); + } + + if (config_file->has_section(PLIST_SECTION)) { + List<String> keys; + config_file->get_section_keys(PLIST_SECTION, &keys); + + for (int i = 0; i < keys.size(); i++) { + String value = config_file->get_value(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/iphone/view_controller.h b/platform/iphone/view_controller.h index f6bbe11d97..ff76359842 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -28,23 +28,20 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#import <GameKit/GameKit.h> #import <UIKit/UIKit.h> -@interface ViewController : UIViewController <GKGameCenterControllerDelegate> { -}; +@class GodotView; +@class GodotNativeVideoView; +@class GodotKeyboardInputView; -- (BOOL)shouldAutorotateToInterfaceOrientation: - (UIInterfaceOrientation)p_orientation; +@interface ViewController : UIViewController -- (void)didReceiveMemoryWarning; +@property(nonatomic, readonly, strong) GodotView *godotView; +@property(nonatomic, readonly, strong) GodotNativeVideoView *videoView; +@property(nonatomic, readonly, strong) GodotKeyboardInputView *keyboardView; -- (void)viewDidLoad; +// MARK: Native Video Player -- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures; - -- (BOOL)prefersStatusBarHidden; - -- (BOOL)prefersHomeIndicatorAutoHidden; +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; @end diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 279bcc1226..7e44d30851 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -29,96 +29,150 @@ /*************************************************************************/ #import "view_controller.h" - +#include "core/config/project_settings.h" +#include "display_server_iphone.h" +#import "godot_view.h" +#import "godot_view_renderer.h" +#import "keyboard_input_view.h" +#import "native_video_view.h" #include "os_iphone.h" -#include "core/project_settings.h" +#import <AVFoundation/AVFoundation.h> +#import <GameController/GameController.h> -extern "C" { +@interface ViewController () -int add_path(int, char **); -int add_cmdline(int, char **); +@property(strong, nonatomic) GodotViewRenderer *renderer; +@property(strong, nonatomic) GodotNativeVideoView *videoView; +@property(strong, nonatomic) GodotKeyboardInputView *keyboardView; -int add_path(int p_argc, char **p_args) { - NSString *str = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_path"]; - if (!str) - return p_argc; +@end - p_args[p_argc++] = "--path"; - [str retain]; // memory leak lol (maybe make it static here and delete it in ViewController destructor? @todo - p_args[p_argc++] = (char *)[str cString]; - p_args[p_argc] = NULL; +@implementation ViewController - return p_argc; -}; +- (GodotView *)godotView { + return (GodotView *)self.view; +} -int add_cmdline(int p_argc, char **p_args) { - NSArray *arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"]; - if (!arr) - return p_argc; +- (void)loadView { + GodotView *view = [[GodotView alloc] init]; + GodotViewRenderer *renderer = [[GodotViewRenderer alloc] init]; - for (int i = 0; i < [arr count]; i++) { - NSString *str = [arr objectAtIndex:i]; - if (!str) - continue; - [str retain]; // @todo delete these at some point - p_args[p_argc++] = (char *)[str cString]; - }; + self.renderer = renderer; + self.view = view; - p_args[p_argc] = NULL; + view.renderer = self.renderer; +} - return p_argc; -}; -}; // extern "C" +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; -@interface ViewController () + if (self) { + [self godot_commonInit]; + } -@end + return self; +} -@implementation ViewController +- (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]; + if (@available(iOS 11.0, *)) { [self setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; } } +- (void)observeKeyboard { + printf("******** setting up keyboard input view\n"); + self.keyboardView = [GodotKeyboardInputView new]; + [self.view addSubview:self.keyboardView]; + + printf("******** adding observer for keyboard show/hide\n"); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardOnScreen:) + name:UIKeyboardDidShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardHidden:) + name:UIKeyboardDidHideNotification + object:nil]; +} + +- (void)dealloc { + [self.videoView stopVideo]; + + self.videoView = nil; + + self.keyboardView = nil; + + self.renderer = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +// MARK: Orientation + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { return UIRectEdgeAll; } - (BOOL)shouldAutorotate { - switch (OS::get_singleton()->get_screen_orientation()) { - case OS::SCREEN_SENSOR: - case OS::SCREEN_SENSOR_LANDSCAPE: - case OS::SCREEN_SENSOR_PORTRAIT: + if (!DisplayServerIPhone::get_singleton()) { + return NO; + } + + switch (DisplayServerIPhone::get_singleton()->screen_get_orientation(DisplayServer::SCREEN_OF_MAIN_WINDOW)) { + case DisplayServer::SCREEN_SENSOR: + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: + case DisplayServer::SCREEN_SENSOR_PORTRAIT: return YES; default: return NO; } -}; +} - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - switch (OS::get_singleton()->get_screen_orientation()) { - case OS::SCREEN_PORTRAIT: + if (!DisplayServerIPhone::get_singleton()) { + return UIInterfaceOrientationMaskAll; + } + + switch (DisplayServerIPhone::get_singleton()->screen_get_orientation(DisplayServer::SCREEN_OF_MAIN_WINDOW)) { + case DisplayServer::SCREEN_PORTRAIT: return UIInterfaceOrientationMaskPortrait; - case OS::SCREEN_REVERSE_LANDSCAPE: + case DisplayServer::SCREEN_REVERSE_LANDSCAPE: return UIInterfaceOrientationMaskLandscapeRight; - case OS::SCREEN_REVERSE_PORTRAIT: + case DisplayServer::SCREEN_REVERSE_PORTRAIT: return UIInterfaceOrientationMaskPortraitUpsideDown; - case OS::SCREEN_SENSOR_LANDSCAPE: + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: return UIInterfaceOrientationMaskLandscape; - case OS::SCREEN_SENSOR_PORTRAIT: + case DisplayServer::SCREEN_SENSOR_PORTRAIT: return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown; - case OS::SCREEN_SENSOR: + case DisplayServer::SCREEN_SENSOR: return UIInterfaceOrientationMaskAll; - case OS::SCREEN_LANDSCAPE: + case DisplayServer::SCREEN_LANDSCAPE: return UIInterfaceOrientationMaskLandscapeLeft; } }; @@ -135,12 +189,42 @@ int add_cmdline(int p_argc, char **p_args) { } } -#ifdef GAME_CENTER_ENABLED -- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController { - //[gameCenterViewController dismissViewControllerAnimated:YES completion:^{GameCenter::get_singleton()->game_center_closed();}];//version for signaling when overlay is completely gone - GameCenter::get_singleton()->game_center_closed(); - [gameCenterViewController dismissViewControllerAnimated:YES completion:nil]; +// MARK: Keyboard + +- (void)keyboardOnScreen:(NSNotification *)notification { + NSDictionary *info = notification.userInfo; + NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; + + CGRect rawFrame = [value CGRectValue]; + CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->virtual_keyboard_set_height(keyboardFrame.size.height); + } +} + +- (void)keyboardHidden:(NSNotification *)notification { + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->virtual_keyboard_set_height(0); + } +} + +// 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]; + } } -#endif @end diff --git a/platform/iphone/vulkan_context_iphone.h b/platform/iphone/vulkan_context_iphone.h index cadd701636..5c3d5fe33e 100644 --- a/platform/iphone/vulkan_context_iphone.h +++ b/platform/iphone/vulkan_context_iphone.h @@ -32,13 +32,14 @@ #define VULKAN_CONTEXT_IPHONE_H #include "drivers/vulkan/vulkan_context.h" -// #import <UIKit/UIKit.h> + +#import <UIKit/UIKit.h> class VulkanContextIPhone : public VulkanContext { virtual const char *_get_platform_surface_extension() const; public: - int window_create(void *p_window, int p_width, int p_height); + Error window_create(DisplayServer::WindowID p_window_id, CALayer *p_metal_layer, int p_width, int p_height); VulkanContextIPhone(); ~VulkanContextIPhone(); diff --git a/platform/iphone/vulkan_context_iphone.mm b/platform/iphone/vulkan_context_iphone.mm index 44c940dc3a..d62e826957 100644 --- a/platform/iphone/vulkan_context_iphone.mm +++ b/platform/iphone/vulkan_context_iphone.mm @@ -35,21 +35,21 @@ const char *VulkanContextIPhone::_get_platform_surface_extension() const { return VK_MVK_IOS_SURFACE_EXTENSION_NAME; } -int VulkanContextIPhone::window_create(void *p_window, int p_width, int p_height) { +Error VulkanContextIPhone::window_create(DisplayServer::WindowID p_window_id, CALayer *p_metal_layer, int p_width, int p_height) { VkIOSSurfaceCreateInfoMVK createInfo; - createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; + createInfo.sType = VK_STRUCTURE_TYPE_IOS_SURFACE_CREATE_INFO_MVK; createInfo.pNext = NULL; createInfo.flags = 0; - createInfo.pView = p_window; + createInfo.pView = (__bridge const void *)p_metal_layer; VkSurfaceKHR surface; - VkResult err = vkCreateIOSSurfaceMVK(_get_instance(), &createInfo, NULL, &surface); - ERR_FAIL_COND_V(err, -1); - return _window_create(surface, p_width, p_height); -} + VkResult err = + vkCreateIOSSurfaceMVK(_get_instance(), &createInfo, NULL, &surface); + ERR_FAIL_COND_V(err, ERR_CANT_CREATE); -VulkanContextIPhone::VulkanContextIPhone() { + return _window_create(p_window_id, surface, p_width, p_height); } -VulkanContextIPhone::~VulkanContextIPhone() { -} +VulkanContextIPhone::VulkanContextIPhone() {} + +VulkanContextIPhone::~VulkanContextIPhone() {} |