Files
factorio-ios/platform/native/shim/keyboard.m
T
SashaGoncharov19 51d085e865 Native iPhone port of Factorio 2.0.77
Run the original macOS ARM64 game code on iOS:
- tools/retarget.py: header-only Mach-O retargeting to an iOS dylib
- platform/native: UIKit host and macOS API shims (AppKit for SDL's
  Cocoa backend, CoreGraphics displays, CoreAudio device, Metal device
  properties, Wuffs PNG decoding, Steam and peripheral stubs)
- touch, overlay, iOS keyboard and hardware keyboard input
- tools/build_native.py: build, sign, install, data copy, launch
- docs: build guide, controls, architecture

Also includes the earlier static-translation research (ARM64 -> WASM ->
native) under docs/research, runtime/, tests/ and tools/.

No game files are included; the build reads them from a local install.
2026-10-23 15:27:58 +04:00

370 lines
18 KiB
Objective-C

// Keyboard emulation: everything becomes AppKit key events with macOS virtual key codes,
// which Factorio's SDL Cocoa backend translates exactly as on a Mac.
// * hardware keyboards (UIPress HID usage -> mac key code)
// * the iOS soft keyboard for text (insertText -> key down/up with characters)
// * an on-screen overlay: WASD stick and common Factorio keys, sticky Shift/Ctrl
#import <UIKit/UIKit.h>
#import "fx.h"
void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags);
// macOS virtual key codes.
enum {
MK_A = 0, MK_S = 1, MK_D = 2, MK_F = 3, MK_W = 13, MK_E = 14, MK_R = 15, MK_T = 17, MK_Q = 12, MK_M = 46,
MK_RETURN = 36, MK_TAB = 48, MK_SPACE = 49, MK_DELETE = 51, MK_ESCAPE = 53,
MK_RCMD = 54, MK_LCMD = 55, MK_LSHIFT = 56, MK_CAPS = 57, MK_LOPT = 58, MK_LCTRL = 59,
MK_RSHIFT = 60, MK_ROPT = 61, MK_RCTRL = 62, MK_NONE = 0xFFFF,
};
// AppKit modifier flags plus the device-dependent side bits SDL uses to tell left from right.
#define F_CAPS (1u << 16)
#define F_SHIFT (1u << 17)
#define F_CTRL (1u << 18)
#define F_OPT (1u << 19)
#define F_CMD (1u << 20)
static const struct { unsigned short key; NSUInteger flag, side; } fx_modifiers[] = {
{MK_LCTRL, F_CTRL, 0x0001}, {MK_LSHIFT, F_SHIFT, 0x0002}, {MK_RSHIFT, F_SHIFT, 0x0004},
{MK_LCMD, F_CMD, 0x0008}, {MK_RCMD, F_CMD, 0x0010}, {MK_LOPT, F_OPT, 0x0020},
{MK_ROPT, F_OPT, 0x0040}, {MK_RCTRL, F_CTRL, 0x2000},
};
static NSUInteger fx_flags; // current modifier state as AppKit reports it
FX_EXPORT NSUInteger fx_current_modifier_flags(void) { return fx_flags; }
static void fx_recompute_flags(NSUInteger sides) {
NSUInteger flags = (fx_flags & F_CAPS) | sides;
for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++)
if (sides & fx_modifiers[i].side) flags |= fx_modifiers[i].flag;
fx_flags = flags;
}
// Press or release one mac key. Modifier keys become flagsChanged events.
FX_EXPORT void fx_key(unsigned short key, BOOL down, NSString *chars) {
if (key == MK_NONE) return;
for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++) {
if (fx_modifiers[i].key != key) continue;
NSUInteger sides = fx_flags & 0xFFFF;
sides = down ? (sides | fx_modifiers[i].side) : (sides & ~fx_modifiers[i].side);
fx_recompute_flags(sides);
fx_post_key_event(12, key, nil, fx_flags);
return;
}
if (key == MK_CAPS) {
if (down) fx_flags ^= F_CAPS;
fx_post_key_event(12, key, nil, fx_flags);
return;
}
fx_post_key_event(down ? 10 : 11, key, chars, fx_flags);
}
FX_EXPORT void fx_tap_key(unsigned short key, NSString *chars) { fx_key(key, YES, chars); fx_key(key, NO, chars); }
// ---- HID usage (UIKeyboardHIDUsage) -> mac virtual key code --------------------------------
static unsigned short fx_mac_key_for_hid(long usage) {
static const unsigned short letters[26] = {0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6};
static const unsigned short digits[10] = {18, 19, 20, 21, 23, 22, 26, 28, 25, 29}; // 1..9, 0
static const unsigned short fkeys[12] = {122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111};
static const unsigned short keypad[10] = {83, 84, 85, 86, 87, 88, 89, 91, 92, 82}; // KP1..KP9, KP0
if (usage >= 4 && usage <= 29) return letters[usage - 4];
if (usage >= 30 && usage <= 39) return digits[usage - 30];
if (usage >= 58 && usage <= 69) return fkeys[usage - 58];
if (usage >= 89 && usage <= 98) return keypad[usage - 89];
switch (usage) {
case 40: return MK_RETURN; case 41: return MK_ESCAPE; case 42: return MK_DELETE; case 43: return MK_TAB;
case 44: return MK_SPACE; case 45: return 27; case 46: return 24; case 47: return 33; case 48: return 30;
case 49: return 42; case 50: return 42; case 51: return 41; case 52: return 39; case 53: return 50;
case 54: return 43; case 55: return 47; case 56: return 44; case 57: return MK_CAPS;
case 73: return 114; case 74: return 115; case 75: return 116; case 76: return 117; case 77: return 119;
case 78: return 121; case 79: return 124; case 80: return 123; case 81: return 125; case 82: return 126;
case 83: return 71; case 84: return 75; case 85: return 67; case 86: return 78; case 87: return 69;
case 88: return 76; case 99: return 65; case 100: return 10; case 103: return 81;
case 104: return 105; case 105: return 107; case 106: return 113; case 107: return 106; // F13..F16
case 224: return MK_LCTRL; case 225: return MK_LSHIFT; case 226: return MK_LOPT; case 227: return MK_LCMD;
case 228: return MK_RCTRL; case 229: return MK_RSHIFT; case 230: return MK_ROPT; case 231: return MK_RCMD;
default: return MK_NONE;
}
}
static NSString *fx_text_chars(NSString *chars) {
if (chars.length == 0) return nil;
unichar c = [chars characterAtIndex:0];
return (c < 0x20 || c == 0x7f || (c >= 0xF700 && c <= 0xF8FF)) ? nil : chars;
}
FX_EXPORT BOOL fx_handle_presses(NSSet<UIPress *> *presses, BOOL down) {
BOOL handled = NO;
for (UIPress *press in presses) {
UIKey *key = press.key;
if (!key) continue;
unsigned short mac = fx_mac_key_for_hid(key.keyCode);
if (mac == MK_NONE) { fx_log("[fx] unmapped HID key usage %ld", (long)key.keyCode); continue; }
fx_key(mac, down, down ? fx_text_chars(key.characters) : nil);
handled = YES;
}
return handled;
}
// Mac key code for a typed character, so soft-keyboard letters also work as hotkeys.
static unsigned short fx_mac_key_for_char(unichar c, BOOL *shift) {
// Index = mac key code 0..47 (US layout); '\1' marks codes that are not characters.
static const char plain[] = "asdfhgzxcv\1bqweryt123465=97-80]ou[ip\1lj'k;\\,/nm.";
static const char shifted[] = "ASDFHGZXCV\1BQWERYT!@#$^%+(&_*)}OU{IP\1LJ\"K:|<?NM>";
*shift = NO;
if (c == ' ') return MK_SPACE;
if (c == '`') return 50;
if (c == '~') { *shift = YES; return 50; }
if (c >= 0x80) return MK_NONE;
for (unsigned short i = 0; i < sizeof plain - 1; i++) {
if (plain[i] == c) return i;
if (shifted[i] == c) { *shift = YES; return i; }
}
return MK_NONE;
}
static UIView *fx_make_accessory_bar(void);
static void fx_release_sticky(void);
// ---- Soft keyboard -------------------------------------------------------------------------
@interface FXTextInputView : UIView <UIKeyInput>
@end
@implementation FXTextInputView
- (BOOL)canBecomeFirstResponder { return YES; }
- (BOOL)hasText { return YES; }
- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; }
- (UITextAutocapitalizationType)autocapitalizationType { return UITextAutocapitalizationTypeNone; }
- (UITextSpellCheckingType)spellCheckingType { return UITextSpellCheckingTypeNo; }
- (UITextSmartQuotesType)smartQuotesType { return UITextSmartQuotesTypeNo; }
- (UITextSmartDashesType)smartDashesType { return UITextSmartDashesTypeNo; }
- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; }
- (UIView *)inputAccessoryView {
static UIView *bar;
if (!bar) bar = fx_make_accessory_bar();
return bar;
}
- (void)insertText:(NSString *)text {
[text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *s, NSRange r, NSRange e, BOOL *stop) {
unichar c = [s characterAtIndex:0];
if (c == '\n') { fx_tap_key(MK_RETURN, nil); return; }
BOOL shift = NO;
unsigned short key = fx_mac_key_for_char(c, &shift);
if (shift) fx_key(MK_LSHIFT, YES, nil);
// Characters without a mac key (e.g. accented letters) still arrive as text via key 0xFFFE.
fx_key(key == MK_NONE ? 0x7F : key, YES, s);
fx_key(key == MK_NONE ? 0x7F : key, NO, nil);
if (shift) fx_key(MK_LSHIFT, NO, nil);
fx_release_sticky();
}];
}
- (void)deleteBackward { fx_tap_key(MK_DELETE, nil); fx_release_sticky(); }
// Hardware keys are handled as raw presses so they are not also delivered as text.
- (void)pressesBegan:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, YES)) [super pressesBegan:p withEvent:e]; }
- (void)pressesEnded:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesEnded:p withEvent:e]; }
- (void)pressesCancelled:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesCancelled:p withEvent:e]; }
@end
// ---- Key buttons -------------------------------------------------------------------------
// A button that presses a mac key while held; sticky buttons latch until the next click or key.
@interface FXKeyButton : UIButton
@property unsigned short key;
@property (copy) NSString *chars;
@property (nonatomic) BOOL sticky, latched;
@end
static NSMutableArray<FXKeyButton *> *fx_sticky_buttons;
@implementation FXKeyButton
+ (instancetype)keyButton:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame {
FXKeyButton *b = [self buttonWithType:UIButtonTypeCustom];
b.frame = frame;
b.key = key;
b.chars = chars;
[b setTitle:title forState:UIControlStateNormal];
b.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
b.titleLabel.adjustsFontSizeToFitWidth = YES;
[b setTitleColor:[UIColor colorWithWhite:1 alpha:0.9] forState:UIControlStateNormal];
b.backgroundColor = [UIColor colorWithWhite:0 alpha:0.35];
b.layer.cornerRadius = 8;
b.layer.borderWidth = 1;
b.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.3].CGColor;
[b addTarget:b action:@selector(fx_down) forControlEvents:UIControlEventTouchDown];
[b addTarget:b action:@selector(fx_up) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel];
return b;
}
- (void)setSticky:(BOOL)sticky {
_sticky = sticky;
if (!fx_sticky_buttons) fx_sticky_buttons = [NSMutableArray array];
if (sticky) [fx_sticky_buttons addObject:self];
}
- (void)fx_set_latched:(BOOL)latched {
if (latched == _latched) return;
_latched = latched;
self.backgroundColor = latched ? [UIColor colorWithRed:0.9 green:0.55 blue:0.1 alpha:0.75] : [UIColor colorWithWhite:0 alpha:0.35];
fx_key(self.key, latched, nil);
}
- (void)fx_down {
if (self.key == MK_NONE) return;
if (self.sticky) {
// Latching one copy of a modifier (overlay or keyboard bar) mirrors to the others.
BOOL latch = !self.latched;
for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key && b != self) { b->_latched = latch; b.backgroundColor = self.backgroundColor; }
[self fx_set_latched:latch];
for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key) b.backgroundColor = self.backgroundColor;
return;
}
fx_key(self.key, YES, self.chars);
}
- (void)fx_up {
if (self.key == MK_NONE || self.sticky) return;
fx_key(self.key, NO, nil);
fx_release_sticky();
}
@end
static void fx_release_sticky(void) {
for (FXKeyButton *b in fx_sticky_buttons) if (b.latched) [b fx_down];
}
// Extra keys shown above the iOS keyboard (which has no Tab, Esc, Ctrl, Alt, arrows or F-keys).
static UIView *fx_make_accessory_bar(void) {
CGFloat h = 46, s = 8;
UIView *bar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, h)];
bar.backgroundColor = [UIColor colorWithWhite:0.12 alpha:0.96];
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:bar.bounds];
scroll.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
scroll.showsHorizontalScrollIndicator = NO;
scroll.delaysContentTouches = NO;
[bar addSubview:scroll];
NSArray *keys = @[
@[@"Esc", @(MK_ESCAPE), @""], @[@"Tab", @(MK_TAB), @""], @[@"Ctrl", @(MK_LCTRL), @"sticky"],
@[@"Alt", @(MK_LOPT), @"sticky"], @[@"⇧", @(MK_LSHIFT), @"sticky"], @[@"←", @123, @""], @[@"↑", @126, @""],
@[@"↓", @125, @""], @[@"→", @124, @""], @[@"Enter", @(MK_RETURN), @""], @[@"Del", @117, @""],
@[@"`", @50, @"`"], @[@"Home", @115, @""], @[@"End", @119, @""], @[@"PgUp", @116, @""], @[@"PgDn", @121, @""],
@[@"F1", @122, @""], @[@"F2", @120, @""], @[@"F3", @99, @""], @[@"F4", @118, @""], @[@"F5", @96, @""],
@[@"F6", @97, @""], @[@"F7", @98, @""], @[@"F8", @100, @""], @[@"F9", @101, @""], @[@"F10", @109, @""],
@[@"F11", @103, @""], @[@"F12", @111, @""],
];
CGFloat x = 56; // clear of the Dynamic Island in landscape
for (NSArray *k in keys) {
NSString *title = k[0], *extra = k[2];
CGFloat w = MAX(44, title.length * 11 + 22);
BOOL sticky = [extra isEqualToString:@"sticky"];
FXKeyButton *b = [FXKeyButton keyButton:title key:[k[1] unsignedShortValue]
chars:(!sticky && extra.length) ? extra : nil frame:CGRectMake(x, 5, w, h - 10)];
b.sticky = sticky;
[scroll addSubview:b];
x += w + s;
}
scroll.contentSize = CGSizeMake(x + 48, h);
return bar;
}
// ---- Overlay ---------------------------------------------------------------------------------
@interface FXStick : UIView
@end
@implementation FXStick {
UIView *_knob;
BOOL _held[4]; // W A S D
}
- (instancetype)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.layer.cornerRadius = frame.size.width / 2;
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.12];
self.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.35].CGColor;
self.layer.borderWidth = 1;
_knob = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width * 0.42, frame.size.width * 0.42)];
_knob.layer.cornerRadius = _knob.bounds.size.width / 2;
_knob.backgroundColor = [UIColor colorWithWhite:1 alpha:0.4];
_knob.userInteractionEnabled = NO;
_knob.center = CGPointMake(frame.size.width / 2, frame.size.height / 2);
[self addSubview:_knob];
}
return self;
}
- (void)fx_update:(CGPoint)p {
CGFloat r = self.bounds.size.width / 2;
CGPoint c = CGPointMake(r, r);
CGFloat dx = p.x - c.x, dy = p.y - c.y, len = hypot(dx, dy);
if (len > r) { dx = dx / len * r; dy = dy / len * r; }
_knob.center = CGPointMake(c.x + dx, c.y + dy);
CGFloat dead = r * 0.3;
BOOL want[4] = {dy < -dead, dx < -dead, dy > dead, dx > dead};
static const unsigned short keys[4] = {MK_W, MK_A, MK_S, MK_D};
static NSString *const chars[4] = {@"w", @"a", @"s", @"d"};
for (int i = 0; i < 4; i++) if (want[i] != _held[i]) { _held[i] = want[i]; fx_key(keys[i], want[i], want[i] ? chars[i] : nil); }
}
- (void)touchesBegan:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; }
- (void)touchesMoved:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; }
- (void)touchesEnded:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2)]; }
- (void)touchesCancelled:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self touchesEnded:t withEvent:e]; }
@end
@interface FXControls : UIView
@end
@implementation FXControls {
FXTextInputView *_text;
NSMutableArray<UIView *> *_gameControls;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
return hit == self ? nil : hit; // empty overlay space belongs to the game
}
- (FXKeyButton *)fx_button:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame {
FXKeyButton *b = [FXKeyButton keyButton:title key:key chars:chars frame:frame];
[self addSubview:b];
return b;
}
- (instancetype)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_gameControls = [NSMutableArray array];
_text = [[FXTextInputView alloc] initWithFrame:CGRectZero];
[self addSubview:_text];
CGFloat W = frame.size.width, H = frame.size.height, s = 40, g = 6;
// Landscape keeps the Dynamic Island on one side, which flips with rotation: inset both.
CGFloat left = 56, right = W - 56;
[self fx_button:@"Esc" key:MK_ESCAPE chars:nil frame:CGRectMake(left, 8, 46, 30)];
[self fx_button:@"Tab" key:MK_TAB chars:nil frame:CGRectMake(left + 52, 8, 46, 30)];
FXKeyButton *kb = [self fx_button:@"⌨︎" key:MK_NONE chars:nil frame:CGRectMake(right - 46, 8, 46, 30)];
[kb addTarget:self action:@selector(fx_toggle_keyboard) forControlEvents:UIControlEventTouchUpInside];
FXKeyButton *hide = [self fx_button:@"◐" key:MK_NONE chars:nil frame:CGRectMake(right - 46 - g - 40, 8, 40, 30)];
[hide addTarget:self action:@selector(fx_toggle_controls) forControlEvents:UIControlEventTouchUpInside];
FXStick *stick = [[FXStick alloc] initWithFrame:CGRectMake(left + 6, H - 128, 116, 116)];
[self addSubview:stick];
[_gameControls addObject:stick];
// Right-hand grid: common Factorio defaults.
NSArray *grid = @[@[@"E", @(MK_E), @"e"], @[@"Q", @(MK_Q), @"q"], @[@"R", @(MK_R), @"r"], @[@"F", @(MK_F), @"f"],
@[@"M", @(MK_M), @"m"], @[@"T", @(MK_T), @"t"], @[@"⇧", @(MK_LSHIFT), @""], @[@"Ctrl", @(MK_LCTRL), @""]];
for (NSUInteger i = 0; i < grid.count; i++) {
NSUInteger col = i % 4, row = i / 4;
CGRect f = CGRectMake(right - (4 - col) * (s + g) + g, H - 12 - (2 - row) * (s + g) + g, s, s);
FXKeyButton *b = [self fx_button:grid[i][0] key:[grid[i][1] unsignedShortValue]
chars:[grid[i][2] length] ? grid[i][2] : nil frame:f];
if (b.key == MK_LSHIFT || b.key == MK_LCTRL) b.sticky = YES;
[_gameControls addObject:b];
}
}
return self;
}
- (void)fx_toggle_keyboard {
if (_text.isFirstResponder) [_text resignFirstResponder];
else [_text becomeFirstResponder];
}
- (void)fx_toggle_controls {
BOOL hidden = !_gameControls.firstObject.hidden;
for (UIView *v in _gameControls) v.hidden = hidden;
}
@end
static FXControls *fx_controls;
FX_EXPORT void fx_install_controls(UIView *root) {
fx_controls = [[FXControls alloc] initWithFrame:root.bounds];
[root addSubview:fx_controls];
}
FX_EXPORT void fx_controls_after_click(void) { fx_release_sticky(); }