// iOS application host: UIKit lifecycle, touch input, and launch of the original entry point. #import #import #import #import #import #import #import #import "fx.h" extern char **environ; void fx_post_mouse(NSUInteger type, CGPoint point, NSInteger button, NSInteger clicks); void fx_post_scroll(CGPoint point, CGFloat dx, CGFloat dy); void fx_call_on_stack(void *stack_top, void (*fn)(void *), void *arg); void fx_install_crash_reporter(void); BOOL fx_handle_presses(NSSet *presses, BOOL down); void fx_install_controls(UIView *root); void fx_controls_after_click(void); void fx_patch_metal(void); static FILE *fx_log_file; static UIView *fx_root; static UILabel *fx_status; FX_EXPORT void fx_log(const char *format, ...) { char buffer[2048]; va_list args; va_start(args, format); vsnprintf(buffer, sizeof buffer, format, args); va_end(args); fprintf(stderr, "%s\n", buffer); if (fx_log_file) { fprintf(fx_log_file, "%s\n", buffer); fflush(fx_log_file); } } void fx_main_sync(void (^block)(void)) { if (NSThread.isMainThread) block(); else dispatch_sync(dispatch_get_main_queue(), block); } UIView *fx_root_view(void) { return fx_root; } static void fx_set_status(NSString *text) { fx_log("[fx] status: %s", text.UTF8String); fx_main_sync(^{ fx_status.text = text; fx_status.hidden = text.length == 0; }); } // ---- Touch input: one finger = left mouse; two-finger pan = wheel; long press = right click. @interface FXTouchView : UIView @end @implementation FXTouchView { UITouch *_primary; BOOL _rightHeld; CGPoint _panLast; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.multipleTouchEnabled = YES; self.backgroundColor = UIColor.blackColor; UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; pan.minimumNumberOfTouches = 2; pan.cancelsTouchesInView = NO; pan.delegate = self; [self addGestureRecognizer:pan]; UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hold:)]; hold.minimumPressDuration = 0.6; hold.cancelsTouchesInView = NO; hold.delegate = self; [self addGestureRecognizer:hold]; } return self; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (_primary || event.allTouches.count > 1) return; _primary = touches.anyObject; CGPoint p = [_primary locationInView:self]; fx_post_mouse(5, p, 0, 0); // move first so SDL knows the cursor position fx_post_mouse(1, p, 0, _primary.tapCount ?: 1); } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (_primary && [touches containsObject:_primary]) fx_post_mouse(6, [_primary locationInView:self], 0, 0); } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_primary && [touches containsObject:_primary]) { fx_post_mouse(2, [_primary locationInView:self], 0, _primary.tapCount ?: 1); _primary = nil; fx_controls_after_click(); } } // Overlay buttons and the stick handle their own touches. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)g shouldReceiveTouch:(UITouch *)touch { return touch.view == self; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } - (void)pan:(UIPanGestureRecognizer *)g { CGPoint t = [g translationInView:self], at = [g locationInView:self]; if (g.state == UIGestureRecognizerStateBegan) { if (_primary) { fx_post_mouse(2, [_primary locationInView:self], 0, 1); _primary = nil; } _panLast = t; } else if (g.state == UIGestureRecognizerStateChanged) { fx_post_scroll(at, (t.x - _panLast.x) / 10.0, (t.y - _panLast.y) / 10.0); _panLast = t; } } // Long press = hold the right mouse button until the finger lifts (mining, deconstructing, // dragging to remove); moving while held drags with the right button. - (void)hold:(UILongPressGestureRecognizer *)g { CGPoint p = [g locationInView:self]; switch (g.state) { case UIGestureRecognizerStateBegan: { if (g.numberOfTouches != 1) return; if (_primary) { fx_post_mouse(2, p, 0, 1); _primary = nil; } _rightHeld = YES; fx_post_mouse(3, p, 1, 1); // Factorio refreshes the hovered entity on mouse motion; nudge it so mining starts // without the finger having to move. fx_post_mouse(7, p, 1, 0); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ if (self->_rightHeld) fx_post_mouse(7, p, 1, 0); }); break; } case UIGestureRecognizerStateChanged: if (_rightHeld) fx_post_mouse(7, p, 1, 0); break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: if (_rightHeld) { _rightHeld = NO; fx_post_mouse(4, p, 1, 1); fx_controls_after_click(); } break; default: break; } } @end @interface FXRootViewController : UIViewController @end @implementation FXRootViewController - (void)loadView { self.view = [[FXTouchView alloc] initWithFrame:UIScreen.mainScreen.bounds]; } - (BOOL)prefersStatusBarHidden { return YES; } - (BOOL)prefersHomeIndicatorAutoHidden { return YES; } - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { return UIRectEdgeAll; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskLandscape; } - (BOOL)canBecomeFirstResponder { return YES; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self becomeFirstResponder]; } // Hardware keyboard (when the soft-keyboard input view is not first responder). - (void)pressesBegan:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, YES)) [super pressesBegan:p withEvent:e]; } - (void)pressesEnded:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesEnded:p withEvent:e]; } - (void)pressesCancelled:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesCancelled:p withEvent:e]; } @end // ---- Launch ---------------------------------------------------------------------------- typedef int (*fx_main_fn)(int, char **, char **, char **); static struct { fx_main_fn entry; int argc; char **argv; char **apple; int result; } fx_game; static void fx_run_main(void *unused) { fx_game.result = fx_game.entry(fx_game.argc, fx_game.argv, environ, fx_game.apple); } static fx_main_fn fx_find_entry(void) { for (uint32_t i = 0; i < _dyld_image_count(); i++) { const char *name = _dyld_get_image_name(i); size_t n = strlen(name); if (n < 15 || strcmp(name + n - 15, "/Factorio.dylib")) continue; const struct mach_header_64 *header = (const void *)_dyld_get_image_header(i); const struct load_command *lc = (const void *)(header + 1); for (uint32_t c = 0; c < header->ncmds; c++) { if (lc->cmd == LC_MAIN) { uint64_t off = ((const struct entry_point_command *)lc)->entryoff; fx_log("[fx] Factorio.dylib at %p slide %#lx, entry offset %#llx", header, (long)_dyld_get_image_vmaddr_slide(i), off); return (fx_main_fn)((const char *)header + off); } lc = (const void *)((const char *)lc + lc->cmdsize); } } return NULL; } static NSString *fx_write_config(NSString *root) { NSString *config = [root stringByAppendingPathComponent:@"config/config.ini"]; NSString *data = [root stringByAppendingPathComponent:@"data"]; NSString *write = [root stringByAppendingPathComponent:@"write"]; [NSFileManager.defaultManager createDirectoryAtPath:config.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; [NSFileManager.defaultManager createDirectoryAtPath:write withIntermediateDirectories:YES attributes:nil error:nil]; NSString *text = [NSString stringWithContentsOfFile:config encoding:NSUTF8StringEncoding error:nil] ?: @"[path]\n"; // The container path can change between installs, so rewrite both path keys every launch. NSMutableArray *lines = [NSMutableArray array]; for (NSString *line in [text componentsSeparatedByString:@"\n"]) if (![line hasPrefix:@"read-data="] && ![line hasPrefix:@"write-data="]) [lines addObject:line]; NSUInteger at = [lines indexOfObject:@"[path]"]; if (at == NSNotFound) { [lines insertObject:@"[path]" atIndex:0]; at = 0; } [lines insertObject:[@"write-data=" stringByAppendingString:write] atIndex:at + 1]; [lines insertObject:[@"read-data=" stringByAppendingString:data] atIndex:at + 1]; [[lines componentsJoinedByString:@"\n"] writeToFile:config atomically:YES encoding:NSUTF8StringEncoding error:nil]; return config; } static void fx_launch(void) { NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; NSString *root = [docs stringByAppendingPathComponent:@"factorio"]; NSString *probe = [root stringByAppendingPathComponent:@"data/core/info.json"]; if (![NSFileManager.defaultManager fileExistsAtPath:probe]) { fx_set_status(@"Game data missing.\nCopy Factorio's data folder to Documents/factorio/data\n(see tools/build_native.py --push-data)"); return; } NSString *config = fx_write_config(root); fx_patch_metal(); // iOS plays nothing until the app's audio session is active. Playback + mix keeps game sound // audible with the silent switch on, without stopping other apps' audio. NSError *audioError = nil; AVAudioSession *session = AVAudioSession.sharedInstance; if (![session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&audioError] || ![session setActive:YES error:&audioError]) fx_log("[fx] audio session: %s", audioError.localizedDescription.UTF8String); else fx_log("[fx] audio session active, %.0f Hz, %ld output channels", session.sampleRate, (long)session.outputNumberOfChannels); setenv("SDL_JOYSTICK_HIDAPI", "0", 1); setenv("SDL_JOYSTICK_IOKIT", "0", 1); setenv("SDL_MAC_BACKGROUND_APP", "0", 1); NSString *dylib = [NSBundle.mainBundle.privateFrameworksPath stringByAppendingPathComponent:@"Factorio.dylib"]; fx_set_status(@"Loading Factorio…"); fx_log("[fx] dlopen %s", dylib.UTF8String); double t0 = CACurrentMediaTime(); void *handle = dlopen(dylib.UTF8String, RTLD_NOW | RTLD_GLOBAL); if (!handle) { fx_set_status([NSString stringWithFormat:@"dlopen failed:\n%s", dlerror()]); return; } fx_log("[fx] dlopen ok in %.2fs", CACurrentMediaTime() - t0); fx_game.entry = fx_find_entry(); if (!fx_game.entry) { fx_set_status(@"LC_MAIN not found in Factorio.dylib"); return; } static char *argv[8], *apple[2]; int argc = 0; argv[argc++] = strdup(NSBundle.mainBundle.executablePath.UTF8String); argv[argc++] = "--config"; argv[argc++] = strdup(config.UTF8String); argv[argc] = NULL; apple[0] = strdup([@"executable_path=" stringByAppendingString:NSBundle.mainBundle.executablePath].UTF8String); apple[1] = NULL; fx_game.argc = argc; fx_game.argv = argv; fx_game.apple = apple; fx_set_status(@""); // macOS gives the main thread 8 MiB; iOS gives 1 MiB. Run the original main on a larger stack // while staying on the main thread, which UIKit and SDL's Cocoa code require. size_t size = 256u << 20; char *stack = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (stack == MAP_FAILED) { fx_set_status(@"stack allocation failed"); return; } mprotect(stack, 16384, PROT_NONE); // guard page fx_log("[fx] calling original main"); fx_call_on_stack(stack + size, fx_run_main, NULL); fx_log("[fx] original main returned %d", fx_game.result); fx_set_status([NSString stringWithFormat:@"Factorio exited with code %d", fx_game.result]); } @interface FXAppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end @implementation FXAppDelegate - (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)options { NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; fx_log_file = fopen([docs stringByAppendingPathComponent:@"fx-log.txt"].UTF8String, "w"); fx_install_crash_reporter(); fx_log("[fx] host start, screen %.0fx%.0f pt @%.2fx", fx_screen_points().width, fx_screen_points().height, fx_screen_scale()); self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; FXRootViewController *vc = [FXRootViewController new]; self.window.rootViewController = vc; fx_root = vc.view; fx_status = [[UILabel alloc] initWithFrame:CGRectInset(fx_root.bounds, 40, 40)]; fx_status.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; fx_status.textColor = UIColor.whiteColor; fx_status.numberOfLines = 0; fx_status.textAlignment = NSTextAlignmentCenter; [fx_root addSubview:fx_status]; fx_install_controls(fx_root); [self.window makeKeyAndVisible]; fx_set_status(@"Starting…"); // Return from launch first; the game loop then runs inside a main-queue block and keeps // UIKit alive by re-entering the run loop from -[NSApplication nextEventMatchingMask:…]. dispatch_async(dispatch_get_main_queue(), ^{ fx_launch(); }); return YES; } @end FX_EXPORT int fx_host_main(int argc, char **argv) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass(FXAppDelegate.class)); } }