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.
This commit is contained in:
commit
51d085e865
89 files changed
+7999
No files matched your search
@@ -0,0 +1,921 @@
|
||||
// AppKit subset implemented on UIKit for the retargeted macOS binary.
|
||||
// Scope: what Factorio's statically linked SDL 2.24 Cocoa backend needs to create one
|
||||
// fullscreen window with a CAMetalLayer and receive mouse-style input from touches.
|
||||
// Unknown selectors on shim classes are logged once and return zero.
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
#import "fx.h"
|
||||
#import "pngdecode.h"
|
||||
|
||||
NSUInteger fx_current_modifier_flags(void);
|
||||
|
||||
// ---- Exported data symbols -------------------------------------------------------------
|
||||
__attribute__((visibility("default"))) id NSApp = nil;
|
||||
__attribute__((visibility("default"))) const double NSAppKitVersionNumber = 2022.0;
|
||||
#define FX_STRING(name) __attribute__((visibility("default"))) NSString *const name = @#name;
|
||||
FX_STRING(NSApplicationDidBecomeActiveNotification)
|
||||
FX_STRING(NSBackingPropertyOldScaleFactorKey)
|
||||
FX_STRING(NSDeviceRGBColorSpace)
|
||||
FX_STRING(NSFilenamesPboardType)
|
||||
FX_STRING(NSWindowDidBecomeKeyNotification)
|
||||
FX_STRING(NSWindowDidChangeBackingPropertiesNotification)
|
||||
FX_STRING(NSWindowDidChangeScreenNotification)
|
||||
FX_STRING(NSWindowDidChangeScreenProfileNotification)
|
||||
FX_STRING(NSWindowDidDeminiaturizeNotification)
|
||||
FX_STRING(NSWindowDidEnterFullScreenNotification)
|
||||
FX_STRING(NSWindowDidExitFullScreenNotification)
|
||||
FX_STRING(NSWindowDidExposeNotification)
|
||||
FX_STRING(NSWindowDidMiniaturizeNotification)
|
||||
FX_STRING(NSWindowDidMoveNotification)
|
||||
FX_STRING(NSWindowDidResignKeyNotification)
|
||||
FX_STRING(NSWindowDidResizeNotification)
|
||||
FX_STRING(NSWindowWillCloseNotification)
|
||||
FX_STRING(NSWindowWillEnterFullScreenNotification)
|
||||
FX_STRING(NSWindowWillExitFullScreenNotification)
|
||||
__attribute__((visibility("default"))) NSString *const NSPasteboardTypeString = @"public.utf8-plain-text";
|
||||
|
||||
__attribute__((visibility("default"))) void NSRectFill(CGRect rect) { (void)rect; }
|
||||
|
||||
// AppKit event constants (values from the macOS SDK).
|
||||
enum {
|
||||
FXLeftMouseDown = 1, FXLeftMouseUp = 2, FXRightMouseDown = 3, FXRightMouseUp = 4,
|
||||
FXMouseMoved = 5, FXLeftMouseDragged = 6, FXRightMouseDragged = 7,
|
||||
FXKeyDown = 10, FXKeyUp = 11, FXFlagsChanged = 12, FXScrollWheel = 22,
|
||||
FXOtherMouseDown = 25, FXOtherMouseUp = 26, FXOtherMouseDragged = 27,
|
||||
};
|
||||
|
||||
// ---- Unknown-selector fallback ----------------------------------------------------------
|
||||
static id fx_zero_imp(id self, SEL _cmd, ...) { return nil; }
|
||||
|
||||
@interface FXShimObject : NSObject
|
||||
@end
|
||||
@implementation FXShimObject
|
||||
+ (BOOL)resolveInstanceMethod:(SEL)sel {
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.' || strncmp(name, "init", 4) == 0) return NO;
|
||||
fx_log("[fx] unimplemented -[%s %s] -> 0", class_getName(self), name);
|
||||
class_addMethod(self, sel, (IMP)fx_zero_imp, "@@:");
|
||||
return YES;
|
||||
}
|
||||
+ (BOOL)resolveClassMethod:(SEL)sel {
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.') return NO;
|
||||
// KVO probes these optional class methods; answering them would disable automatic
|
||||
// change notifications (SDL observes -[NSWindow visible]).
|
||||
if (!strncmp(name, "automaticallyNotifiesObserversOf", 32) || !strncmp(name, "keyPathsForValuesAffecting", 26)) return NO;
|
||||
fx_log("[fx] unimplemented +[%s %s] -> 0", class_getName(self), name);
|
||||
class_addMethod(object_getClass(self), sel, (IMP)fx_zero_imp, "@@:");
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
// ---- Screen geometry --------------------------------------------------------------------
|
||||
// The game sees an AppKit screen of "game points" with backing scale 1. By default one game
|
||||
// point is one physical pixel, so Factorio renders at the panel's native resolution (its own UI
|
||||
// scale setting then controls legibility). Documents/factorio/render-divisor.txt can hold a
|
||||
// divisor (e.g. 1.5) to render fewer pixels. UIKit views and touches convert via fx_game_per_uikit().
|
||||
static CGSize fx_landscape(CGSize s) { return s.width >= s.height ? s : CGSizeMake(s.height, s.width); }
|
||||
static CGSize fx_uikit_points(void) { return fx_landscape(UIScreen.mainScreen.bounds.size); }
|
||||
FX_EXPORT double fx_render_divisor(void) {
|
||||
static double divisor;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
|
||||
NSString *text = [NSString stringWithContentsOfFile:[docs stringByAppendingPathComponent:@"factorio/render-divisor.txt"]
|
||||
encoding:NSUTF8StringEncoding error:nil];
|
||||
divisor = text ? text.doubleValue : 0;
|
||||
if (divisor < 1 || divisor > 4) divisor = 1;
|
||||
});
|
||||
return divisor;
|
||||
}
|
||||
CGSize fx_screen_points(void) {
|
||||
CGSize px = fx_landscape(UIScreen.mainScreen.nativeBounds.size);
|
||||
double d = fx_render_divisor();
|
||||
return CGSizeMake(floor(px.width / d), floor(px.height / d));
|
||||
}
|
||||
double fx_screen_scale(void) { return 1.0; }
|
||||
FX_EXPORT double fx_game_per_uikit(void) { return fx_screen_points().width / fx_uikit_points().width; }
|
||||
|
||||
// ---- NSScreen ---------------------------------------------------------------------------
|
||||
@interface NSScreen : FXShimObject
|
||||
@end
|
||||
@implementation NSScreen
|
||||
+ (NSArray *)screens { static NSArray *s; static dispatch_once_t once; dispatch_once(&once, ^{ s = @[[NSScreen new]]; }); return s; }
|
||||
+ (NSScreen *)mainScreen { return self.screens[0]; }
|
||||
+ (NSScreen *)deepestScreen { return self.screens[0]; }
|
||||
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
- (CGRect)visibleFrame { return self.frame; }
|
||||
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
|
||||
- (NSDictionary *)deviceDescription { return @{@"NSScreenNumber": @(FX_DISPLAY_ID)}; }
|
||||
- (NSString *)localizedName { return @"iPhone"; }
|
||||
- (NSInteger)maximumFramesPerSecond { return UIScreen.mainScreen.maximumFramesPerSecond; }
|
||||
- (id)colorSpace { return nil; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
|
||||
@end
|
||||
|
||||
// ---- NSEvent ----------------------------------------------------------------------------
|
||||
static CGPoint fx_mouse_screen; // AppKit screen coordinates (bottom-left origin, points)
|
||||
static NSUInteger fx_mouse_buttons; // bitmask of pressed buttons
|
||||
|
||||
@class NSWindow;
|
||||
@interface NSEvent : FXShimObject
|
||||
@property NSUInteger type;
|
||||
@property CGPoint locationInWindow;
|
||||
@property (weak) NSWindow *window;
|
||||
@property NSUInteger modifierFlags;
|
||||
@property NSInteger buttonNumber;
|
||||
@property NSInteger clickCount;
|
||||
@property CGFloat deltaX, deltaY, scrollingDeltaX, scrollingDeltaY;
|
||||
@property NSTimeInterval timestamp;
|
||||
@property unsigned short keyCode;
|
||||
@property (copy) NSString *characters;
|
||||
@property NSInteger subtype;
|
||||
@end
|
||||
@implementation NSEvent
|
||||
+ (CGPoint)mouseLocation { return fx_mouse_screen; }
|
||||
+ (NSUInteger)pressedMouseButtons { return fx_mouse_buttons; }
|
||||
+ (NSUInteger)modifierFlags { return fx_current_modifier_flags(); }
|
||||
+ (NSTimeInterval)doubleClickInterval { return 0.3; }
|
||||
+ (instancetype)otherEventWithType:(NSUInteger)type location:(CGPoint)location modifierFlags:(NSUInteger)flags
|
||||
timestamp:(NSTimeInterval)time windowNumber:(NSInteger)wn context:(id)ctx
|
||||
subtype:(short)subtype data1:(NSInteger)d1 data2:(NSInteger)d2 {
|
||||
NSEvent *e = [NSEvent new]; e.type = type; e.locationInWindow = location; e.modifierFlags = flags;
|
||||
e.timestamp = time; e.subtype = subtype; return e;
|
||||
}
|
||||
- (BOOL)hasPreciseScrollingDeltas { return YES; }
|
||||
- (BOOL)isDirectionInvertedFromDevice { return NO; }
|
||||
- (BOOL)isARepeat { return NO; }
|
||||
- (NSString *)charactersIgnoringModifiers { return self.characters; }
|
||||
- (NSUInteger)phase { return 0; }
|
||||
- (NSUInteger)momentumPhase { return 0; }
|
||||
- (NSSet *)touchesMatchingPhase:(NSUInteger)phase inView:(id)view { return [NSSet set]; }
|
||||
- (NSInteger)windowNumber { return 1; }
|
||||
@end
|
||||
|
||||
// ---- NSResponder ------------------------------------------------------------------------
|
||||
@interface NSResponder : FXShimObject
|
||||
@property (strong) NSResponder *nextResponder;
|
||||
@end
|
||||
@implementation NSResponder
|
||||
#define FX_FORWARD(sel) - (void)sel:(NSEvent *)e { [self.nextResponder sel:e]; }
|
||||
FX_FORWARD(mouseDown) FX_FORWARD(mouseUp) FX_FORWARD(mouseDragged) FX_FORWARD(mouseMoved)
|
||||
FX_FORWARD(rightMouseDown) FX_FORWARD(rightMouseUp) FX_FORWARD(rightMouseDragged)
|
||||
FX_FORWARD(otherMouseDown) FX_FORWARD(otherMouseUp) FX_FORWARD(otherMouseDragged)
|
||||
FX_FORWARD(scrollWheel) FX_FORWARD(keyDown) FX_FORWARD(keyUp) FX_FORWARD(flagsChanged)
|
||||
FX_FORWARD(mouseEntered) FX_FORWARD(mouseExited)
|
||||
FX_FORWARD(touchesBeganWithEvent) FX_FORWARD(touchesMovedWithEvent)
|
||||
FX_FORWARD(touchesEndedWithEvent) FX_FORWARD(touchesCancelledWithEvent)
|
||||
- (BOOL)acceptsFirstResponder { return YES; }
|
||||
- (BOOL)becomeFirstResponder { return YES; }
|
||||
- (BOOL)resignFirstResponder { return YES; }
|
||||
// AppKit's text system, reduced to what SDL's field editor needs: printable characters become
|
||||
// insertText:replacementRange:, everything else is a command (ignored by SDL).
|
||||
- (void)interpretKeyEvents:(NSArray *)events {
|
||||
for (NSEvent *e in events) {
|
||||
NSString *c = e.characters;
|
||||
unichar first = c.length ? [c characterAtIndex:0] : 0;
|
||||
if (first >= 0x20 && first != 0x7f && !(first >= 0xF700 && first <= 0xF8FF)) {
|
||||
if ([self respondsToSelector:@selector(insertText:replacementRange:)])
|
||||
[(id)self insertText:c replacementRange:NSMakeRange(NSNotFound, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)doCommandBySelector:(SEL)sel { }
|
||||
- (BOOL)validateMenuItem:(id)item { return YES; }
|
||||
- (id)undoManager { return nil; }
|
||||
@end
|
||||
|
||||
// ---- NSView -----------------------------------------------------------------------------
|
||||
@interface FXBackingView : UIView
|
||||
@property (weak) id appkitView;
|
||||
@end
|
||||
|
||||
@interface NSView : NSResponder
|
||||
@property (nonatomic) CGRect frame;
|
||||
@property (nonatomic, weak) NSView *superview;
|
||||
@property (nonatomic, strong) NSMutableArray *subviewList;
|
||||
@property (nonatomic, weak) NSWindow *window;
|
||||
@property (nonatomic, strong) FXBackingView *backing;
|
||||
@property (nonatomic, strong) CALayer *layer;
|
||||
@property (nonatomic) BOOL wantsLayer;
|
||||
@property (nonatomic) NSUInteger autoresizingMask;
|
||||
@property (nonatomic, getter=isHidden) BOOL hidden;
|
||||
- (void)fx_layout;
|
||||
@end
|
||||
|
||||
@interface NSWindow : NSResponder
|
||||
@property (nonatomic, strong) NSView *contentView;
|
||||
@property (nonatomic, weak) id delegate;
|
||||
@property (nonatomic, getter=isVisible) BOOL visible;
|
||||
@property (nonatomic, getter=isKeyWindow) BOOL keyWindow;
|
||||
@property (nonatomic) NSUInteger styleMask;
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, strong) NSResponder *firstResponder;
|
||||
@property (nonatomic) BOOL releasedWhenClosed, oneShot, acceptsMouseMovedEvents, opaque, movableByWindowBackground;
|
||||
@property (nonatomic) NSInteger level;
|
||||
@property (nonatomic) NSUInteger collectionBehavior;
|
||||
@property (nonatomic) NSInteger tabbingMode;
|
||||
@property (nonatomic) CGFloat alphaValue;
|
||||
@end
|
||||
|
||||
UIView *fx_root_view(void);
|
||||
|
||||
@implementation FXBackingView
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
for (CALayer *l in self.layer.sublayers) if ([l isKindOfClass:CAMetalLayer.class]) l.frame = self.bounds;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation NSView
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if ((self = [super init])) {
|
||||
_subviewList = [NSMutableArray array];
|
||||
_frame = frame;
|
||||
fx_main_sync(^{
|
||||
self->_backing = [[FXBackingView alloc] initWithFrame:CGRectZero];
|
||||
self->_backing.appkitView = self;
|
||||
self->_backing.userInteractionEnabled = NO;
|
||||
self->_backing.backgroundColor = UIColor.clearColor;
|
||||
});
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (instancetype)init { return [self initWithFrame:CGRectZero]; }
|
||||
- (CGRect)bounds { return CGRectMake(0, 0, _frame.size.width, _frame.size.height); }
|
||||
- (void)setFrame:(CGRect)frame { _frame = frame; [self fx_layout]; for (NSView *v in _subviewList) [v fx_layout]; }
|
||||
- (void)setFrameSize:(CGSize)size { self.frame = CGRectMake(_frame.origin.x, _frame.origin.y, size.width, size.height); }
|
||||
- (void)setFrameOrigin:(CGPoint)origin { self.frame = CGRectMake(origin.x, origin.y, _frame.size.width, _frame.size.height); }
|
||||
- (void)fx_layout {
|
||||
// AppKit frames are bottom-left relative to the superview; UIKit is top-left.
|
||||
CGFloat parentHeight = _superview ? _superview.frame.size.height : _frame.size.height;
|
||||
CGFloat k = fx_game_per_uikit(); // game points -> UIKit points
|
||||
CGRect ui = CGRectMake(_frame.origin.x / k, (parentHeight - _frame.origin.y - _frame.size.height) / k,
|
||||
_frame.size.width / k, _frame.size.height / k);
|
||||
fx_main_sync(^{ self->_backing.frame = ui; [self->_backing setNeedsLayout]; [self->_backing layoutIfNeeded]; });
|
||||
}
|
||||
- (NSArray *)subviews { return [_subviewList copy]; }
|
||||
- (void)addSubview:(NSView *)view {
|
||||
[view removeFromSuperview];
|
||||
[_subviewList addObject:view];
|
||||
view.superview = self;
|
||||
view.window = self.window;
|
||||
fx_main_sync(^{ [self->_backing addSubview:view.backing]; });
|
||||
[view fx_layout];
|
||||
}
|
||||
- (void)removeFromSuperview {
|
||||
NSView *parent = _superview;
|
||||
if (!parent) return;
|
||||
[parent.subviewList removeObject:self];
|
||||
_superview = nil;
|
||||
fx_main_sync(^{ [self->_backing removeFromSuperview]; });
|
||||
}
|
||||
- (void)setWindow:(NSWindow *)window { _window = window; for (NSView *v in _subviewList) v.window = window; }
|
||||
- (id)viewWithTag:(NSInteger)tag {
|
||||
if ([self tag] == tag) return self;
|
||||
for (NSView *v in _subviewList) { id found = [v viewWithTag:tag]; if (found) return found; }
|
||||
return nil;
|
||||
}
|
||||
- (NSInteger)tag { return -1; }
|
||||
- (CALayer *)makeBackingLayer { return [CALayer layer]; }
|
||||
- (BOOL)wantsUpdateLayer { return NO; }
|
||||
- (void)updateLayer { }
|
||||
- (void)setWantsLayer:(BOOL)wants {
|
||||
_wantsLayer = wants;
|
||||
if (wants && !_layer) {
|
||||
__block CALayer *layer;
|
||||
fx_main_sync(^{
|
||||
layer = [self makeBackingLayer];
|
||||
layer.frame = self->_backing.bounds;
|
||||
[self->_backing.layer addSublayer:layer];
|
||||
});
|
||||
_layer = layer;
|
||||
fx_log("[fx] -[%s setWantsLayer:YES] -> %s", class_getName(self.class), class_getName(layer.class));
|
||||
}
|
||||
}
|
||||
- (void)setNeedsDisplay:(BOOL)flag { }
|
||||
- (void)setNeedsLayout:(BOOL)flag { }
|
||||
- (void)display { }
|
||||
- (void)drawRect:(CGRect)rect { }
|
||||
- (void)setHidden:(BOOL)hidden { _hidden = hidden; fx_main_sync(^{ self->_backing.hidden = hidden; }); }
|
||||
- (void)setWantsBestResolutionOpenGLSurface:(BOOL)flag { }
|
||||
- (void)setAcceptsTouchEvents:(BOOL)flag { }
|
||||
- (void)setAllowedTouchTypes:(NSUInteger)types { }
|
||||
- (void)registerForDraggedTypes:(NSArray *)types { }
|
||||
- (void)unregisterDraggedTypes { }
|
||||
- (void)addCursorRect:(CGRect)rect cursor:(id)cursor { }
|
||||
- (void)discardCursorRects { }
|
||||
- (void)resetCursorRects { }
|
||||
- (void)lockFocus { }
|
||||
- (void)unlockFocus { }
|
||||
- (id)hitTest:(CGPoint)point { return self; }
|
||||
- (BOOL)mouse:(CGPoint)p inRect:(CGRect)r { return CGRectContainsPoint(r, p); }
|
||||
- (CGPoint)convertPoint:(CGPoint)p fromView:(NSView *)view { return p; }
|
||||
- (CGPoint)convertPoint:(CGPoint)p toView:(NSView *)view { return p; }
|
||||
- (CGRect)convertRect:(CGRect)r toView:(NSView *)view { return r; }
|
||||
- (CGRect)convertRect:(CGRect)r fromView:(NSView *)view { return r; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
|
||||
- (CGSize)convertSizeToBacking:(CGSize)s { double k = fx_screen_scale(); return CGSizeMake(s.width*k, s.height*k); }
|
||||
- (CGPoint)convertPointToBacking:(CGPoint)p { double k = fx_screen_scale(); return CGPointMake(p.x*k, p.y*k); }
|
||||
- (CGRect)convertRectFromBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x/k, r.origin.y/k, r.size.width/k, r.size.height/k); }
|
||||
- (BOOL)isFlipped { return NO; }
|
||||
- (BOOL)mouseDownCanMoveWindow { return NO; }
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent *)e { return YES; }
|
||||
- (BOOL)inLiveResize { return NO; }
|
||||
@end
|
||||
|
||||
// ---- NSWindow ---------------------------------------------------------------------------
|
||||
static NSMutableArray *fx_windows;
|
||||
|
||||
@implementation NSWindow
|
||||
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing
|
||||
defer:(BOOL)defer screen:(id)screen {
|
||||
if ((self = [super init])) {
|
||||
_styleMask = style;
|
||||
_alphaValue = 1;
|
||||
if (!fx_windows) fx_windows = [NSMutableArray array];
|
||||
[fx_windows addObject:self];
|
||||
fx_log("[fx] NSWindow requested %.0fx%.0f style=%lu; using fullscreen %.0fx%.0f",
|
||||
rect.size.width, rect.size.height, (unsigned long)style, fx_screen_points().width, fx_screen_points().height);
|
||||
self.contentView = [[NSView alloc] initWithFrame:[self frame]];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing defer:(BOOL)defer {
|
||||
return [self initWithContentRect:rect styleMask:style backing:backing defer:defer screen:nil];
|
||||
}
|
||||
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
- (CGRect)contentRectForFrameRect:(CGRect)r { return r; }
|
||||
- (CGRect)frameRectForContentRect:(CGRect)r { return r; }
|
||||
- (CGRect)contentLayoutRect { return [self frame]; }
|
||||
- (NSScreen *)screen { return NSScreen.mainScreen; }
|
||||
- (NSInteger)windowNumber { return (NSInteger)[fx_windows indexOfObject:self] + 1; }
|
||||
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
|
||||
- (void)setContentView:(NSView *)view {
|
||||
NSView *old = _contentView;
|
||||
if (old) { old.window = nil; fx_main_sync(^{ [old.backing removeFromSuperview]; }); }
|
||||
_contentView = view;
|
||||
if (!view) return;
|
||||
view.window = self;
|
||||
view.frame = [self frame];
|
||||
fx_main_sync(^{
|
||||
UIView *root = fx_root_view();
|
||||
view.backing.frame = root.bounds;
|
||||
view.backing.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[root insertSubview:view.backing atIndex:0];
|
||||
});
|
||||
if (!_firstResponder) _firstResponder = view;
|
||||
}
|
||||
- (BOOL)makeFirstResponder:(NSResponder *)responder { _firstResponder = responder; return YES; }
|
||||
- (void)makeKeyAndOrderFront:(id)sender {
|
||||
self.visible = YES;
|
||||
if (!_keyWindow) {
|
||||
_keyWindow = YES;
|
||||
NSNotification *n = [NSNotification notificationWithName:NSWindowDidBecomeKeyNotification object:self];
|
||||
if ([_delegate respondsToSelector:@selector(windowDidBecomeKey:)]) [_delegate windowDidBecomeKey:n];
|
||||
[NSNotificationCenter.defaultCenter postNotification:n];
|
||||
}
|
||||
}
|
||||
- (void)makeKeyWindow { [self makeKeyAndOrderFront:nil]; }
|
||||
- (void)makeMainWindow { }
|
||||
- (void)orderFront:(id)sender { self.visible = YES; }
|
||||
- (void)orderFrontRegardless { self.visible = YES; }
|
||||
- (void)orderOut:(id)sender { self.visible = NO; }
|
||||
- (void)orderBack:(id)sender { }
|
||||
- (void)close { self.visible = NO; [fx_windows removeObject:self]; }
|
||||
- (void)performClose:(id)sender { [self close]; }
|
||||
- (BOOL)isMiniaturized { return NO; }
|
||||
- (BOOL)isZoomed { return YES; }
|
||||
- (BOOL)isOnActiveSpace { return YES; }
|
||||
- (BOOL)isMainWindow { return _keyWindow; }
|
||||
- (BOOL)canBecomeKeyWindow { return YES; }
|
||||
- (BOOL)canBecomeMainWindow { return YES; }
|
||||
- (BOOL)hasShadow { return NO; }
|
||||
- (void)setHasShadow:(BOOL)flag { }
|
||||
- (void)setFrame:(CGRect)frame display:(BOOL)flag { }
|
||||
- (void)setFrame:(CGRect)frame display:(BOOL)flag animate:(BOOL)animate { }
|
||||
- (void)setFrameOrigin:(CGPoint)origin { }
|
||||
- (void)setFrameTopLeftPoint:(CGPoint)point { }
|
||||
- (void)setContentSize:(CGSize)size { }
|
||||
- (void)setContentMinSize:(CGSize)size { }
|
||||
- (void)setContentMaxSize:(CGSize)size { }
|
||||
- (void)setBackgroundColor:(id)color { }
|
||||
- (void)setIgnoresMouseEvents:(BOOL)flag { }
|
||||
- (void)setMouseConfinementRect:(CGRect)rect { }
|
||||
- (void)setColorSpace:(id)space { }
|
||||
- (void)registerForDraggedTypes:(NSArray *)types { }
|
||||
- (void)unregisterDraggedTypes { }
|
||||
- (void)invalidateCursorRectsForView:(id)view { }
|
||||
- (void)disableCursorRects { }
|
||||
- (void)enableCursorRects { }
|
||||
- (void)toggleFullScreen:(id)sender { }
|
||||
- (void)miniaturize:(id)sender { }
|
||||
- (void)deminiaturize:(id)sender { }
|
||||
- (void)zoom:(id)sender { }
|
||||
- (void)performZoom:(id)sender { }
|
||||
- (void)performMiniaturize:(id)sender { }
|
||||
- (void)center { }
|
||||
- (void)setShape:(id)shape { }
|
||||
- (id)standardWindowButton:(NSUInteger)b { return nil; }
|
||||
- (void)update { }
|
||||
- (void)display { }
|
||||
- (void)setPreservesContentDuringLiveResize:(BOOL)flag { }
|
||||
- (CGRect)convertRectToScreen:(CGRect)r { return r; }
|
||||
- (CGRect)convertRectFromScreen:(CGRect)r { return r; }
|
||||
- (CGPoint)convertPointToScreen:(CGPoint)p { return p; }
|
||||
- (CGPoint)convertPointFromScreen:(CGPoint)p { return p; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { return [self.contentView convertRectToBacking:r]; }
|
||||
- (CGPoint)mouseLocationOutsideOfEventStream { return fx_mouse_screen; }
|
||||
- (void)sendEvent:(NSEvent *)e {
|
||||
NSView *target = self.contentView;
|
||||
switch (e.type) {
|
||||
case FXLeftMouseDown: [target mouseDown:e]; break;
|
||||
case FXLeftMouseUp: [target mouseUp:e]; break;
|
||||
case FXLeftMouseDragged: [target mouseDragged:e]; break;
|
||||
case FXRightMouseDown: [target rightMouseDown:e]; break;
|
||||
case FXRightMouseUp: [target rightMouseUp:e]; break;
|
||||
case FXRightMouseDragged: [target rightMouseDragged:e]; break;
|
||||
case FXOtherMouseDown: [target otherMouseDown:e]; break;
|
||||
case FXOtherMouseUp: [target otherMouseUp:e]; break;
|
||||
case FXOtherMouseDragged: [target otherMouseDragged:e]; break;
|
||||
case FXMouseMoved: [target mouseMoved:e]; break;
|
||||
case FXScrollWheel: [target scrollWheel:e]; break;
|
||||
case FXKeyDown: [(self.firstResponder ?: target) keyDown:e]; break;
|
||||
case FXKeyUp: [(self.firstResponder ?: target) keyUp:e]; break;
|
||||
case FXFlagsChanged: [(self.firstResponder ?: target) flagsChanged:e]; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
// ---- NSApplication ----------------------------------------------------------------------
|
||||
static NSMutableArray<NSEvent *> *fx_event_queue;
|
||||
|
||||
@interface NSApplication : NSResponder
|
||||
@property (nonatomic, weak) id delegate;
|
||||
@property (nonatomic, strong) id mainMenu, windowsMenu, servicesMenu, applicationIconImage;
|
||||
@property (nonatomic, readonly) NSInteger activationPolicy;
|
||||
- (BOOL)setActivationPolicy:(NSInteger)policy;
|
||||
@end
|
||||
|
||||
@interface NSMenu : FXShimObject
|
||||
@end
|
||||
|
||||
@implementation NSApplication
|
||||
+ (instancetype)sharedApplication {
|
||||
if (!NSApp) {
|
||||
NSApp = [self new];
|
||||
fx_log("[fx] +[%s sharedApplication]", class_getName(self));
|
||||
}
|
||||
return NSApp;
|
||||
}
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
// A non-nil main menu makes SDL skip NSBundle nib loading and menu construction.
|
||||
_mainMenu = [NSMenu new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (BOOL)setActivationPolicy:(NSInteger)policy { _activationPolicy = policy; return YES; }
|
||||
- (void)finishLaunching {
|
||||
NSNotification *n = [NSNotification notificationWithName:@"NSApplicationDidFinishLaunchingNotification" object:self];
|
||||
if ([_delegate respondsToSelector:@selector(applicationDidFinishLaunching:)]) [_delegate applicationDidFinishLaunching:n];
|
||||
}
|
||||
- (void)activateIgnoringOtherApps:(BOOL)flag { }
|
||||
- (BOOL)isActive { return YES; }
|
||||
- (BOOL)isHidden { return NO; }
|
||||
- (BOOL)isRunning { return YES; }
|
||||
- (void)hide:(id)sender { }
|
||||
- (void)unhide:(id)sender { }
|
||||
- (void)unhide { }
|
||||
- (void)unhideAllApplications:(id)sender { }
|
||||
- (void)hideOtherApplications:(id)sender { }
|
||||
- (void)setAppleMenu:(id)menu { }
|
||||
- (void)setPresentationOptions:(NSUInteger)options { }
|
||||
- (NSUInteger)presentationOptions { return 0; }
|
||||
- (NSInteger)requestUserAttention:(NSUInteger)type { return 0; }
|
||||
- (void)cancelUserAttentionRequest:(NSInteger)request { }
|
||||
- (void)orderFrontStandardAboutPanel:(id)sender { }
|
||||
- (void)updateWindows { }
|
||||
- (void)stop:(id)sender { }
|
||||
- (void)run { fx_log("[fx] -[NSApplication run] called; not supported"); }
|
||||
- (void)terminate:(id)sender { exit(0); }
|
||||
- (NSArray *)windows { return [fx_windows copy] ?: @[]; }
|
||||
- (NSArray *)orderedWindows { return self.windows; }
|
||||
- (id)keyWindow { for (NSWindow *w in fx_windows) if (w.isKeyWindow) return w; return nil; }
|
||||
- (id)mainWindow { return self.keyWindow; }
|
||||
- (id)windowWithWindowNumber:(NSInteger)n { return (n >= 1 && n <= (NSInteger)fx_windows.count) ? fx_windows[n - 1] : nil; }
|
||||
- (NSEvent *)currentEvent { return nil; }
|
||||
- (void)postEvent:(NSEvent *)e atStart:(BOOL)atStart {
|
||||
@synchronized (fx_event_queue) { if (atStart) [fx_event_queue insertObject:e atIndex:0]; else [fx_event_queue addObject:e]; }
|
||||
}
|
||||
- (NSEvent *)nextEventMatchingMask:(unsigned long long)mask untilDate:(NSDate *)date inMode:(NSString *)mode dequeue:(BOOL)dequeue {
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
@synchronized (fx_event_queue) {
|
||||
if (fx_event_queue.count) {
|
||||
NSEvent *e = fx_event_queue[0];
|
||||
if (dequeue) [fx_event_queue removeObjectAtIndex:0];
|
||||
return e;
|
||||
}
|
||||
}
|
||||
if (pass == 0) {
|
||||
// Let UIKit deliver touches and run main-queue work, as SDL's UIKit backend does.
|
||||
NSTimeInterval wait = date ? MAX(0, MIN([date timeIntervalSinceNow], 0.05)) : 0;
|
||||
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource) { }
|
||||
if (wait > 0 && !fx_event_queue.count) CFRunLoopRunInMode(kCFRunLoopDefaultMode, wait, true);
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
- (void)discardEventsMatchingMask:(unsigned long long)mask beforeEvent:(NSEvent *)e { }
|
||||
- (void)sendEvent:(NSEvent *)e { [e.window sendEvent:e]; }
|
||||
- (NSInteger)runModalForWindow:(id)window { return 0; }
|
||||
- (void)stopModalWithCode:(NSInteger)code { }
|
||||
- (void)registerUserDefaults { }
|
||||
@end
|
||||
|
||||
// Queue a mouse event from UIKit (main thread).
|
||||
void fx_post_mouse(NSUInteger type, CGPoint uikit_point, NSInteger button, NSInteger clicks) {
|
||||
CGSize s = fx_screen_points();
|
||||
CGFloat k = fx_game_per_uikit();
|
||||
CGPoint p = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
|
||||
CGFloat dx = p.x - fx_mouse_screen.x, dy = -(p.y - fx_mouse_screen.y);
|
||||
fx_mouse_screen = p;
|
||||
if (type == FXLeftMouseDown || type == FXRightMouseDown || type == FXOtherMouseDown) fx_mouse_buttons |= (1u << button);
|
||||
if (type == FXLeftMouseUp || type == FXRightMouseUp || type == FXOtherMouseUp) fx_mouse_buttons &= ~(1u << button);
|
||||
NSEvent *e = [NSEvent new];
|
||||
e.type = type;
|
||||
e.locationInWindow = p;
|
||||
e.window = fx_windows.firstObject;
|
||||
e.buttonNumber = button;
|
||||
e.clickCount = clicks;
|
||||
e.modifierFlags = fx_current_modifier_flags();
|
||||
e.deltaX = dx;
|
||||
e.deltaY = dy;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags) {
|
||||
NSEvent *e = [NSEvent new];
|
||||
e.type = type;
|
||||
e.keyCode = keyCode;
|
||||
e.characters = chars ?: @"";
|
||||
e.modifierFlags = flags;
|
||||
e.locationInWindow = fx_mouse_screen;
|
||||
e.window = fx_windows.firstObject;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
void fx_post_scroll(CGPoint uikit_point, CGFloat dx, CGFloat dy) {
|
||||
NSEvent *e = [NSEvent new];
|
||||
CGSize s = fx_screen_points();
|
||||
e.type = FXScrollWheel;
|
||||
CGFloat k = fx_game_per_uikit();
|
||||
e.locationInWindow = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
|
||||
e.window = fx_windows.firstObject;
|
||||
e.deltaX = e.scrollingDeltaX = dx;
|
||||
e.deltaY = e.scrollingDeltaY = dy;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void fx_appkit_init(void) { fx_event_queue = [NSMutableArray array]; }
|
||||
|
||||
// ---- Inert classes ----------------------------------------------------------------------
|
||||
@implementation NSMenu
|
||||
- (instancetype)initWithTitle:(NSString *)title { return [super init]; }
|
||||
- (id)addItemWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [NSClassFromString(@"NSMenuItem") new]; }
|
||||
- (void)addItem:(id)item { }
|
||||
- (id)itemWithTitle:(NSString *)t { return nil; }
|
||||
+ (void)setMenuBarVisible:(BOOL)visible { }
|
||||
+ (BOOL)menuBarVisible { return NO; }
|
||||
@end
|
||||
|
||||
@interface NSMenuItem : FXShimObject
|
||||
@end
|
||||
@implementation NSMenuItem
|
||||
+ (instancetype)separatorItem { return [self new]; }
|
||||
- (instancetype)initWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [super init]; }
|
||||
@end
|
||||
|
||||
@interface NSCursor : FXShimObject
|
||||
@end
|
||||
@implementation NSCursor
|
||||
#define FX_CURSOR(name) + (instancetype)name { static NSCursor *c; static dispatch_once_t o; dispatch_once(&o, ^{ c = [NSCursor new]; }); return c; }
|
||||
FX_CURSOR(arrowCursor) FX_CURSOR(IBeamCursor) FX_CURSOR(crosshairCursor) FX_CURSOR(closedHandCursor)
|
||||
FX_CURSOR(openHandCursor) FX_CURSOR(pointingHandCursor) FX_CURSOR(resizeLeftRightCursor)
|
||||
FX_CURSOR(resizeUpDownCursor) FX_CURSOR(operationNotAllowedCursor) FX_CURSOR(currentCursor)
|
||||
FX_CURSOR(invisibleCursor) FX_CURSOR(currentSystemCursor)
|
||||
+ (void)hide { }
|
||||
+ (void)unhide { }
|
||||
+ (void)setHiddenUntilMouseMoves:(BOOL)flag { }
|
||||
- (instancetype)initWithImage:(id)image hotSpot:(CGPoint)point { return [super init]; }
|
||||
- (void)set { }
|
||||
- (void)push { }
|
||||
- (void)pop { }
|
||||
@end
|
||||
|
||||
@interface NSImage : FXShimObject
|
||||
@end
|
||||
@implementation NSImage
|
||||
- (instancetype)initWithSize:(CGSize)size { return [super init]; }
|
||||
- (instancetype)initWithData:(NSData *)data { return [super init]; }
|
||||
- (instancetype)initWithContentsOfFile:(NSString *)path { return [super init]; }
|
||||
- (void)addRepresentation:(id)rep { }
|
||||
- (void)lockFocus { }
|
||||
- (void)unlockFocus { }
|
||||
- (void)drawInRect:(CGRect)r fromRect:(CGRect)f operation:(NSUInteger)op fraction:(CGFloat)fr { }
|
||||
@end
|
||||
|
||||
@interface NSColorSpace : FXShimObject
|
||||
+ (instancetype)sRGBColorSpace;
|
||||
@end
|
||||
|
||||
@interface NSBitmapImageRep : FXShimObject
|
||||
@end
|
||||
|
||||
// Decode statistics, logged every 500 images: how many took the direct-copy path, wall time.
|
||||
#include <stdatomic.h>
|
||||
static _Atomic long fx_decode_count, fx_decode_direct, fx_decode_pixels, fx_decode_micros;
|
||||
static void fx_log_format_once(size_t bpc, size_t bpp, uint32_t alpha, uint32_t order, int model) {
|
||||
static NSMutableSet *seen;
|
||||
NSString *key = [NSString stringWithFormat:@"bpc %zu bpp %zu alpha %u order %#x model %d", bpc, bpp, alpha, order, model];
|
||||
@synchronized (NSBitmapImageRep.class) {
|
||||
if (!seen) seen = [NSMutableSet set];
|
||||
if ([seen containsObject:key]) return;
|
||||
[seen addObject:key];
|
||||
}
|
||||
fx_log("[fx] ImageIO layout needing conversion: %s", key.UTF8String);
|
||||
}
|
||||
static void fx_decode_stats(long pixels, double seconds) {
|
||||
static double start;
|
||||
long n = atomic_fetch_add(&fx_decode_count, 1) + 1;
|
||||
atomic_fetch_add(&fx_decode_pixels, pixels);
|
||||
atomic_fetch_add(&fx_decode_micros, (long)(seconds * 1e6));
|
||||
if (n == 1) start = CACurrentMediaTime();
|
||||
if (n % 500 == 0)
|
||||
fx_log("[fx] decoded %ld images (%ld wuffs/direct), %.0f Mpx, %.1fs wall, %.1fs summed decode time", n,
|
||||
(long)fx_decode_direct, fx_decode_pixels / 1e6, CACurrentMediaTime() - start, fx_decode_micros / 1e6);
|
||||
}
|
||||
@implementation NSBitmapImageRep {
|
||||
unsigned char *_data;
|
||||
NSInteger _w, _h, _rowBytes, _bpp, _spp;
|
||||
}
|
||||
- (instancetype)initWithBitmapDataPlanes:(unsigned char **)planes pixelsWide:(NSInteger)w pixelsHigh:(NSInteger)h
|
||||
bitsPerSample:(NSInteger)bps samplesPerPixel:(NSInteger)spp hasAlpha:(BOOL)alpha
|
||||
isPlanar:(BOOL)planar colorSpaceName:(NSString *)cs bytesPerRow:(NSInteger)rowBytes
|
||||
bitsPerPixel:(NSInteger)bpp {
|
||||
if ((self = [super init])) {
|
||||
_w = w; _h = h; _spp = spp; _bpp = bpp ? bpp : bps * spp;
|
||||
_rowBytes = rowBytes ? rowBytes : (w * _bpp + 7) / 8;
|
||||
_data = calloc((size_t)(_rowBytes * h), 1);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
// Decodes like AppKit does for PNG: tightly packed, non-premultiplied, native-endian samples,
|
||||
// keeping the source channel count (gray, gray+alpha, RGB, RGBA) and 8 or 16 bits per sample.
|
||||
// Factorio's preload_image() copies and premultiplies this buffer itself.
|
||||
- (instancetype)initWithData:(NSData *)data {
|
||||
if (!(self = [super init])) return nil;
|
||||
double fx_t0 = CACurrentMediaTime();
|
||||
uint8_t *png; int pw, ph, pc;
|
||||
if (fx_png_decode(data.bytes, data.length, &png, &pw, &ph, &pc)) {
|
||||
_data = png; _w = pw; _h = ph; _spp = pc; _bpp = 8 * pc; _rowBytes = (NSInteger)pw * pc;
|
||||
fx_decode_direct++;
|
||||
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
|
||||
return self;
|
||||
}
|
||||
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data,
|
||||
(__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCache: @NO});
|
||||
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
|
||||
if (source) CFRelease(source);
|
||||
if (!image) return nil;
|
||||
BOOL ok = [self fx_unpack:image] || [self fx_render:image];
|
||||
CGImageRelease(image);
|
||||
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
|
||||
return ok ? self : nil;
|
||||
}
|
||||
+ (instancetype)imageRepWithData:(NSData *)data { return [[self alloc] initWithData:data]; }
|
||||
|
||||
- (BOOL)fx_unpack:(CGImageRef)image {
|
||||
size_t bpc = CGImageGetBitsPerComponent(image), bpp = CGImageGetBitsPerPixel(image);
|
||||
CGColorSpaceModel model = CGColorSpaceGetModel(CGImageGetColorSpace(image));
|
||||
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image);
|
||||
CGBitmapInfo info = CGImageGetBitmapInfo(image);
|
||||
if ((bpc != 8 && bpc != 16) || (info & kCGBitmapFloatComponents)) return NO;
|
||||
size_t color = model == kCGColorSpaceModelMonochrome ? 1 : model == kCGColorSpaceModelRGB ? 3 : 0;
|
||||
if (!color) return NO;
|
||||
BOOL hasAlpha = alpha == kCGImageAlphaLast || alpha == kCGImageAlphaFirst ||
|
||||
alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
|
||||
BOOL premultiplied = alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
|
||||
BOOL alphaFirst = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaNoneSkipFirst;
|
||||
BOOL skip = alpha == kCGImageAlphaNoneSkipLast || alpha == kCGImageAlphaNoneSkipFirst;
|
||||
size_t srcComponents = bpp / bpc, outComponents = color + (hasAlpha ? 1 : 0);
|
||||
if (srcComponents != color + ((hasAlpha || skip) ? 1 : 0)) return NO;
|
||||
CGBitmapInfo order = info & kCGBitmapByteOrderMask;
|
||||
BOOL reversed = (bpc == 8 && (order == kCGBitmapByteOrder32Little || order == kCGBitmapByteOrder16Little));
|
||||
BOOL littleSamples = bpc == 16 && order == kCGBitmapByteOrder16Little;
|
||||
|
||||
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
|
||||
size_t sampleBytes = bpc / 8, srcPixel = bpp / 8;
|
||||
_w = w; _h = h; _spp = outComponents; _bpp = outComponents * bpc; _rowBytes = w * outComponents * sampleBytes;
|
||||
_data = malloc(_rowBytes * h);
|
||||
// Decide from metadata before touching pixels: each pixel access below decodes the PNG.
|
||||
// Direct: decoded layout already matches (non-premultiplied R G B [A], 8-bit or 16-bit LE).
|
||||
BOOL direct = !premultiplied && !alphaFirst && !skip && !reversed && (bpc == 8 || littleSamples);
|
||||
if (!direct) {
|
||||
fx_log_format_once(bpc, bpp, alpha, order, model);
|
||||
// One vectorized vImage decode+conversion into the target layout, keeping the image's
|
||||
// colour space (no colour management changes).
|
||||
vImage_CGImageFormat target = {
|
||||
(uint32_t)bpc, (uint32_t)(outComponents * bpc), CGImageGetColorSpace(image),
|
||||
(hasAlpha ? kCGImageAlphaLast : kCGImageAlphaNone) | (bpc == 16 ? kCGBitmapByteOrder16Little : kCGBitmapByteOrderDefault),
|
||||
0, NULL, kCGRenderingIntentDefault};
|
||||
vImage_Buffer buffer = {_data, h, w, (size_t)_rowBytes};
|
||||
if (vImageBuffer_InitWithCGImage(&buffer, &target, NULL, image, kvImageNoAllocate) == kvImageNoError) return YES;
|
||||
fx_log("[fx] vImage conversion failed; using scalar path");
|
||||
}
|
||||
CFDataRef pixels = CGDataProviderCopyData(CGImageGetDataProvider(image));
|
||||
if (!pixels) return NO;
|
||||
const uint8_t *src = CFDataGetBytePtr(pixels);
|
||||
size_t srcRow = CGImageGetBytesPerRow(image);
|
||||
if (direct) {
|
||||
if (srcRow == (size_t)_rowBytes) memcpy(_data, src, _rowBytes * h);
|
||||
else for (size_t y = 0; y < h; y++) memcpy(_data + y * _rowBytes, src + y * srcRow, _rowBytes);
|
||||
fx_decode_direct++;
|
||||
CFRelease(pixels);
|
||||
return YES;
|
||||
}
|
||||
for (size_t y = 0; y < h; y++) {
|
||||
for (size_t x = 0; x < w; x++) {
|
||||
const uint8_t *p = src + y * srcRow + x * srcPixel;
|
||||
uint32_t s[4] = {0};
|
||||
for (size_t c = 0; c < srcComponents; c++) {
|
||||
size_t i = reversed ? srcComponents - 1 - c : c;
|
||||
s[c] = bpc == 8 ? p[i] : littleSamples ? (uint32_t)(p[2*i] | p[2*i+1] << 8) : (uint32_t)(p[2*i] << 8 | p[2*i+1]);
|
||||
}
|
||||
uint32_t out[4], a = 0;
|
||||
size_t first = alphaFirst ? 1 : 0;
|
||||
for (size_t c = 0; c < color; c++) out[c] = s[first + c];
|
||||
if (hasAlpha) {
|
||||
a = alphaFirst ? s[0] : s[color];
|
||||
uint32_t max = bpc == 8 ? 255 : 65535;
|
||||
if (premultiplied && a && a < max)
|
||||
for (size_t c = 0; c < color; c++) out[c] = MIN(max, (out[c] * max + a / 2) / a);
|
||||
out[color] = a;
|
||||
}
|
||||
uint8_t *d = _data + y * _rowBytes + x * outComponents * sampleBytes;
|
||||
for (size_t c = 0; c < outComponents; c++) {
|
||||
if (bpc == 8) d[c] = (uint8_t)out[c];
|
||||
else ((uint16_t *)d)[c] = (uint16_t)out[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
CFRelease(pixels);
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Fallback for indexed, CMYK and other layouts: draw into RGBA8 and un-premultiply.
|
||||
- (BOOL)fx_render:(CGImageRef)image {
|
||||
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
|
||||
free(_data);
|
||||
_w = w; _h = h; _spp = 4; _bpp = 32; _rowBytes = w * 4;
|
||||
_data = calloc(_rowBytes * h, 1);
|
||||
CGColorSpaceRef rgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef ctx = CGBitmapContextCreate(_data, w, h, 8, _rowBytes, rgb, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
||||
CGColorSpaceRelease(rgb);
|
||||
if (!ctx) return NO;
|
||||
CGContextSetBlendMode(ctx, kCGBlendModeCopy);
|
||||
CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image);
|
||||
CGContextRelease(ctx);
|
||||
for (size_t i = 0; i < w * h; i++) {
|
||||
uint8_t *p = _data + i * 4, a = p[3];
|
||||
if (a && a < 255) for (int c = 0; c < 3; c++) p[c] = (uint8_t)MIN(255, (p[c] * 255 + a / 2) / a);
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
- (void)dealloc { free(_data); }
|
||||
- (unsigned char *)bitmapData { return _data; }
|
||||
- (NSInteger)pixelsWide { return _w; }
|
||||
- (NSInteger)pixelsHigh { return _h; }
|
||||
- (NSInteger)bitsPerPixel { return _bpp; }
|
||||
- (NSInteger)samplesPerPixel { return _spp; }
|
||||
- (NSInteger)bytesPerRow { return _rowBytes; }
|
||||
- (CGSize)size { return CGSizeMake(_w, _h); }
|
||||
- (id)colorSpace { return [NSColorSpace sRGBColorSpace]; }
|
||||
- (id)bitmapImageRepByRetaggingWithColorSpace:(id)cs { return self; }
|
||||
- (id)bitmapImageRepByConvertingToColorSpace:(id)cs renderingIntent:(NSInteger)intent { return self; }
|
||||
@end
|
||||
|
||||
@interface FXNSColor : FXShimObject
|
||||
@end
|
||||
@implementation FXNSColor
|
||||
+ (instancetype)blackColor { return [self new]; }
|
||||
+ (instancetype)clearColor { return [self new]; }
|
||||
+ (instancetype)whiteColor { return [self new]; }
|
||||
+ (instancetype)colorWithRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
|
||||
+ (instancetype)colorWithSRGBRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
|
||||
- (void)set { }
|
||||
- (void)setFill { }
|
||||
- (void)setStroke { }
|
||||
@end
|
||||
|
||||
@implementation NSColorSpace
|
||||
#define FX_SPACE(name) + (instancetype)name { static id s; static dispatch_once_t o; dispatch_once(&o, ^{ s = [self new]; }); return s; }
|
||||
FX_SPACE(sRGBColorSpace) FX_SPACE(genericRGBColorSpace) FX_SPACE(deviceRGBColorSpace)
|
||||
- (NSData *)ICCProfileData { return nil; }
|
||||
- (CGColorSpaceRef)CGColorSpace { return NULL; }
|
||||
@end
|
||||
|
||||
@interface NSGraphicsContext : FXShimObject
|
||||
@end
|
||||
@implementation NSGraphicsContext
|
||||
+ (id)currentContext { return nil; }
|
||||
+ (void)setCurrentContext:(id)ctx { }
|
||||
+ (void)saveGraphicsState { }
|
||||
+ (void)restoreGraphicsState { }
|
||||
+ (id)graphicsContextWithBitmapImageRep:(id)rep { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSBezierPath : FXShimObject
|
||||
@end
|
||||
@implementation NSBezierPath
|
||||
+ (instancetype)bezierPath { return [self new]; }
|
||||
+ (instancetype)bezierPathWithRect:(CGRect)r { return [self new]; }
|
||||
- (void)appendBezierPathWithRect:(CGRect)r { }
|
||||
- (void)addClip { }
|
||||
- (void)fill { }
|
||||
@end
|
||||
|
||||
@interface NSAlert : FXShimObject
|
||||
@property (copy) NSString *messageText, *informativeText;
|
||||
@property NSUInteger alertStyle;
|
||||
@end
|
||||
@implementation NSAlert
|
||||
- (id)addButtonWithTitle:(NSString *)title { fx_log("[fx] NSAlert button: %s", title.UTF8String); return nil; }
|
||||
- (NSInteger)runModal {
|
||||
fx_log("[fx] NSAlert: %s | %s", self.messageText.UTF8String ?: "", self.informativeText.UTF8String ?: "");
|
||||
return 1000; // NSAlertFirstButtonReturn
|
||||
}
|
||||
- (void)beginSheetModalForWindow:(id)w completionHandler:(void (^)(NSInteger))handler { NSInteger r = [self runModal]; if (handler) handler(r); }
|
||||
- (id)window { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSPasteboard : FXShimObject
|
||||
@end
|
||||
@implementation NSPasteboard
|
||||
+ (instancetype)generalPasteboard { static NSPasteboard *p; static dispatch_once_t o; dispatch_once(&o, ^{ p = [NSPasteboard new]; }); return p; }
|
||||
- (NSInteger)changeCount { __block NSInteger c; fx_main_sync(^{ c = UIPasteboard.generalPasteboard.changeCount; }); return c; }
|
||||
- (NSString *)stringForType:(NSString *)type { __block NSString *s; fx_main_sync(^{ s = UIPasteboard.generalPasteboard.string; }); return s; }
|
||||
- (BOOL)setString:(NSString *)s forType:(NSString *)type { fx_main_sync(^{ UIPasteboard.generalPasteboard.string = s; }); return YES; }
|
||||
- (NSInteger)declareTypes:(NSArray *)types owner:(id)owner { return [self changeCount]; }
|
||||
- (NSInteger)clearContents { return [self changeCount]; }
|
||||
- (NSString *)availableTypeFromArray:(NSArray *)types { return types.firstObject; }
|
||||
- (id)propertyListForType:(NSString *)type { return nil; }
|
||||
- (NSData *)dataForType:(NSString *)type { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSWorkspace : FXShimObject
|
||||
@end
|
||||
@implementation NSWorkspace
|
||||
+ (instancetype)sharedWorkspace { static NSWorkspace *w; static dispatch_once_t o; dispatch_once(&o, ^{ w = [NSWorkspace new]; }); return w; }
|
||||
- (BOOL)openURL:(NSURL *)url { fx_log("[fx] openURL %s", url.absoluteString.UTF8String); return NO; }
|
||||
- (void)activateFileViewerSelectingURLs:(NSArray *)urls { }
|
||||
- (id)notificationCenter { return NSNotificationCenter.defaultCenter; }
|
||||
@end
|
||||
|
||||
@interface NSRunningApplication : FXShimObject
|
||||
@end
|
||||
@implementation NSRunningApplication
|
||||
+ (instancetype)currentApplication { return [self new]; }
|
||||
+ (NSArray *)runningApplicationsWithBundleIdentifier:(NSString *)bundle { return @[]; }
|
||||
- (BOOL)activateWithOptions:(NSUInteger)options { return YES; }
|
||||
- (BOOL)isActive { return YES; }
|
||||
- (BOOL)isFinishedLaunching { return YES; }
|
||||
@end
|
||||
|
||||
@interface NSOpenGLPixelFormat : FXShimObject
|
||||
@end
|
||||
@implementation NSOpenGLPixelFormat
|
||||
- (instancetype)initWithAttributes:(const uint32_t *)attributes { fx_log("[fx] OpenGL pixel format requested; unsupported"); return nil; }
|
||||
- (void *)CGLPixelFormatObj { return NULL; }
|
||||
@end
|
||||
|
||||
@interface NSOpenGLContext : FXShimObject
|
||||
@end
|
||||
@implementation NSOpenGLContext
|
||||
- (instancetype)initWithFormat:(id)format shareContext:(id)share { fx_log("[fx] OpenGL context requested; unsupported"); return nil; }
|
||||
+ (void)clearCurrentContext { }
|
||||
+ (id)currentContext { return nil; }
|
||||
- (void)makeCurrentContext { }
|
||||
- (void)flushBuffer { }
|
||||
- (void)update { }
|
||||
- (void)setView:(id)view { }
|
||||
- (id)view { return nil; }
|
||||
- (void *)CGLContextObj { return NULL; }
|
||||
@end
|
||||
@@ -0,0 +1,97 @@
|
||||
// macOS CoreAudio device model for SDL's coreaudio backend (macOS build).
|
||||
// FX_LIB_COREAUDIO: one virtual output device (the iPhone route), answering the AudioObject
|
||||
// properties SDL 2.24 queries. Playback itself uses the real iOS AudioQueue.
|
||||
// FX_LIB_AUDIOTOOLBOX: AudioQueueSetProperty passes through, except macOS-only CurrentDevice.
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <dlfcn.h>
|
||||
#include <string.h>
|
||||
#include "fx.h"
|
||||
|
||||
#define FOURCC(a, b, c, d) ((uint32_t)(a) << 24 | (uint32_t)(b) << 16 | (uint32_t)(c) << 8 | (uint32_t)(d))
|
||||
|
||||
#ifdef FX_LIB_COREAUDIO
|
||||
typedef uint32_t AudioObjectID;
|
||||
typedef struct { uint32_t mSelector, mScope, mElement; } FXPropertyAddress;
|
||||
typedef OSStatus (*FXPropertyListener)(AudioObjectID, uint32_t, const FXPropertyAddress *, void *);
|
||||
|
||||
enum {
|
||||
kSystemObject = 1, kSpeaker = 0x100,
|
||||
kDevices = FOURCC('d', 'e', 'v', '#'), kDefaultOutput = FOURCC('d', 'O', 'u', 't'),
|
||||
kDefaultInput = FOURCC('d', 'I', 'n', ' '), kStreamConfiguration = FOURCC('s', 'l', 'a', 'y'),
|
||||
kName = FOURCC('l', 'n', 'a', 'm'), kNominalSampleRate = FOURCC('n', 's', 'r', 't'),
|
||||
kIsAlive = FOURCC('l', 'i', 'v', 'n'), kHogMode = FOURCC('o', 'i', 'n', 'k'),
|
||||
kUID = FOURCC('u', 'i', 'd', ' '), kScopeInput = FOURCC('i', 'n', 'p', 't'),
|
||||
kUnknownProperty = FOURCC('w', 'h', 'o', '?'), kBadObject = FOURCC('!', 'o', 'b', 'j'),
|
||||
};
|
||||
|
||||
// Writes the property into `out` (when non-NULL) and returns its size, or 0 if unsupported.
|
||||
static uint32_t fx_property(AudioObjectID object, const FXPropertyAddress *a, void *out, OSStatus *status) {
|
||||
*status = 0;
|
||||
if (object == kSystemObject) {
|
||||
switch (a->mSelector) {
|
||||
case kDevices: case kDefaultOutput: if (out) *(AudioObjectID *)out = kSpeaker; return 4;
|
||||
case kDefaultInput: if (out) *(AudioObjectID *)out = 0; return 4; // no capture device
|
||||
}
|
||||
} else if (object == kSpeaker) {
|
||||
int input = a->mScope == kScopeInput;
|
||||
switch (a->mSelector) {
|
||||
case kStreamConfiguration:
|
||||
if (out) {
|
||||
AudioBufferList *list = out;
|
||||
list->mNumberBuffers = input ? 0 : 1;
|
||||
list->mBuffers[0] = (AudioBuffer){2, 0, NULL};
|
||||
}
|
||||
return sizeof(AudioBufferList);
|
||||
case kName: if (out) *(CFStringRef *)out = CFStringCreateCopy(NULL, CFSTR("iPhone")); return sizeof(CFStringRef);
|
||||
case kUID: if (out) *(CFStringRef *)out = CFStringCreateCopy(NULL, CFSTR("fx-ios-output")); return sizeof(CFStringRef);
|
||||
case kNominalSampleRate: if (out) *(Float64 *)out = 48000.0; return sizeof(Float64);
|
||||
case kIsAlive: if (out) *(uint32_t *)out = !input; return 4;
|
||||
case kHogMode: if (out) *(pid_t *)out = -1; return sizeof(pid_t);
|
||||
}
|
||||
} else {
|
||||
*status = kBadObject;
|
||||
return 0;
|
||||
}
|
||||
*status = kUnknownProperty;
|
||||
fx_log("[fx] CoreAudio: unsupported property '%c%c%c%c' on object %u", a->mSelector >> 24, a->mSelector >> 16,
|
||||
a->mSelector >> 8, a->mSelector, object);
|
||||
return 0;
|
||||
}
|
||||
|
||||
FX_EXPORT Boolean AudioObjectHasProperty(AudioObjectID object, const FXPropertyAddress *a) {
|
||||
OSStatus status;
|
||||
return fx_property(object, a, NULL, &status) && !status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectGetPropertyDataSize(AudioObjectID object, const FXPropertyAddress *a, uint32_t qsize,
|
||||
const void *q, uint32_t *size) {
|
||||
OSStatus status;
|
||||
uint32_t n = fx_property(object, a, NULL, &status);
|
||||
if (!status && size) *size = n;
|
||||
return status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectGetPropertyData(AudioObjectID object, const FXPropertyAddress *a, uint32_t qsize,
|
||||
const void *q, uint32_t *size, void *data) {
|
||||
OSStatus status;
|
||||
uint32_t n = fx_property(object, a, NULL, &status);
|
||||
if (status) return status;
|
||||
if (!size || *size < n) return FOURCC('!', 's', 'i', 'z');
|
||||
fx_property(object, a, data, &status);
|
||||
*size = n;
|
||||
return status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectAddPropertyListener(AudioObjectID o, const FXPropertyAddress *a, FXPropertyListener l, void *c) { return 0; }
|
||||
FX_EXPORT OSStatus AudioObjectRemovePropertyListener(AudioObjectID o, const FXPropertyAddress *a, FXPropertyListener l, void *c) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_AUDIOTOOLBOX
|
||||
FX_EXPORT OSStatus AudioQueueSetProperty(AudioQueueRef queue, AudioQueuePropertyID id, const void *data, UInt32 size) {
|
||||
static OSStatus (*real)(AudioQueueRef, AudioQueuePropertyID, const void *, UInt32);
|
||||
if (!real) {
|
||||
void *toolbox = dlopen("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", RTLD_LAZY | RTLD_NOLOAD);
|
||||
real = toolbox ? dlsym(toolbox, "AudioQueueSetProperty") : NULL;
|
||||
}
|
||||
if (id == FOURCC('a', 'q', 'c', 'd')) return 0; // kAudioQueueProperty_CurrentDevice: iOS routes itself
|
||||
return real ? real(queue, id, data, size) : -1;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,284 @@
|
||||
// iOS application host: UIKit lifecycle, touch input, and launch of the original entry point.
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
#import <dlfcn.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <mach-o/loader.h>
|
||||
#import <stdarg.h>
|
||||
#import <sys/stat.h>
|
||||
#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<UIPress *> *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 <UIGestureRecognizerDelegate>
|
||||
@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<UITouch *> *)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<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if (_primary && [touches containsObject:_primary]) fx_post_mouse(6, [_primary locationInView:self], 0, 0);
|
||||
}
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)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<UITouch *> *)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<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
|
||||
|
||||
// ---- 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 <UIApplicationDelegate>
|
||||
@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)); }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// macOS-only CoreGraphics display APIs, re-exported alongside iOS CoreGraphics.
|
||||
// One display (FX_DISPLAY_ID) with the phone's landscape size; mode changes are refused.
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <string.h>
|
||||
#include "fx.h"
|
||||
|
||||
typedef uint32_t CGDirectDisplayID;
|
||||
typedef int32_t CGError;
|
||||
typedef CFDataRef CGDisplayModeRef; // CFType so CFRetain/CFEqual/CFArray callbacks work
|
||||
typedef struct { size_t w, h, pw, ph; double hz; } fx_mode;
|
||||
|
||||
static fx_mode fx_current_mode(void) {
|
||||
CGSize s = fx_screen_points();
|
||||
double k = fx_screen_scale();
|
||||
return (fx_mode){(size_t)s.width, (size_t)s.height, (size_t)(s.width * k), (size_t)(s.height * k), 60.0};
|
||||
}
|
||||
static const fx_mode *fx_m(CGDisplayModeRef m) { return (const fx_mode *)CFDataGetBytePtr(m); }
|
||||
|
||||
FX_EXPORT const CFStringRef kCGDisplayShowDuplicateLowResolutionModes = CFSTR("kCGDisplayShowDuplicateLowResolutionModes");
|
||||
|
||||
FX_EXPORT CGError CGGetOnlineDisplayList(uint32_t max, CGDirectDisplayID *displays, uint32_t *count) {
|
||||
if (displays && max) displays[0] = FX_DISPLAY_ID;
|
||||
if (count) *count = 1;
|
||||
return 0;
|
||||
}
|
||||
FX_EXPORT CGDirectDisplayID CGMainDisplayID(void) { return FX_DISPLAY_ID; }
|
||||
FX_EXPORT int CGDisplayIsMain(CGDirectDisplayID d) { return d == FX_DISPLAY_ID; }
|
||||
FX_EXPORT CGDirectDisplayID CGDisplayMirrorsDisplay(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGRect CGDisplayBounds(CGDirectDisplayID d) { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
FX_EXPORT size_t CGDisplayPixelsWide(CGDirectDisplayID d) { return (size_t)fx_screen_points().width; }
|
||||
FX_EXPORT size_t CGDisplayPixelsHigh(CGDirectDisplayID d) { return (size_t)fx_screen_points().height; }
|
||||
FX_EXPORT CGSize CGDisplayScreenSize(CGDirectDisplayID d) {
|
||||
// Physical size in millimetres at ~460 ppi (iPhone 15 Pro class display).
|
||||
double k = fx_render_divisor(); CGSize s = fx_screen_points();
|
||||
return CGSizeMake(s.width * k / 460.0 * 25.4, s.height * k / 460.0 * 25.4);
|
||||
}
|
||||
FX_EXPORT CGDisplayModeRef CGDisplayCopyDisplayMode(CGDirectDisplayID d) {
|
||||
fx_mode m = fx_current_mode();
|
||||
return CFDataCreate(NULL, (const UInt8 *)&m, sizeof m);
|
||||
}
|
||||
FX_EXPORT CFArrayRef CGDisplayCopyAllDisplayModes(CGDirectDisplayID d, CFDictionaryRef options) {
|
||||
CGDisplayModeRef m = CGDisplayCopyDisplayMode(d);
|
||||
CFArrayRef a = CFArrayCreate(NULL, (const void **)&m, 1, &kCFTypeArrayCallBacks);
|
||||
CFRelease(m);
|
||||
return a;
|
||||
}
|
||||
FX_EXPORT void CGDisplayModeRelease(CGDisplayModeRef m) { if (m) CFRelease(m); }
|
||||
FX_EXPORT size_t CGDisplayModeGetWidth(CGDisplayModeRef m) { return fx_m(m)->w; }
|
||||
FX_EXPORT size_t CGDisplayModeGetHeight(CGDisplayModeRef m) { return fx_m(m)->h; }
|
||||
FX_EXPORT size_t CGDisplayModeGetPixelWidth(CGDisplayModeRef m) { return fx_m(m)->pw; }
|
||||
FX_EXPORT size_t CGDisplayModeGetPixelHeight(CGDisplayModeRef m) { return fx_m(m)->ph; }
|
||||
FX_EXPORT double CGDisplayModeGetRefreshRate(CGDisplayModeRef m) { return fx_m(m)->hz; }
|
||||
FX_EXPORT uint32_t CGDisplayModeGetIOFlags(CGDisplayModeRef m) { return 0x7; } // valid | safe | default
|
||||
FX_EXPORT int CGDisplayModeIsUsableForDesktopGUI(CGDisplayModeRef m) { return 1; }
|
||||
FX_EXPORT CFStringRef CGDisplayModeCopyPixelEncoding(CGDisplayModeRef m) {
|
||||
return CFStringCreateCopy(NULL, CFSTR("--------RRRRRRRRGGGGGGGGBBBBBBBB"));
|
||||
}
|
||||
FX_EXPORT CGError CGDisplaySetDisplayMode(CGDirectDisplayID d, CGDisplayModeRef m, CFDictionaryRef o) { return 1000; }
|
||||
FX_EXPORT CGError CGDisplayCapture(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGCaptureAllDisplays(void) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayRelease(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGReleaseAllDisplays(void) { return 0; }
|
||||
FX_EXPORT int32_t CGShieldingWindowLevel(void) { return 2147483630; }
|
||||
FX_EXPORT CGError CGAcquireDisplayFadeReservation(float seconds, uint32_t *token) { if (token) *token = 1; return 0; }
|
||||
FX_EXPORT CGError CGReleaseDisplayFadeReservation(uint32_t token) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayFade(uint32_t token, float d, float a, float b, float r, float g, float bl, int sync) { return 0; }
|
||||
FX_EXPORT uint32_t CGDisplayIDToOpenGLDisplayMask(CGDirectDisplayID d) { return 1; }
|
||||
FX_EXPORT uint32_t CGDisplayIOServicePort(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGAssociateMouseAndMouseCursorPosition(int connected) { return 0; }
|
||||
FX_EXPORT CGError CGWarpMouseCursorPosition(CGPoint p) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayMoveCursorToPoint(CGDirectDisplayID d, CGPoint p) { return 0; }
|
||||
FX_EXPORT CGError CGGetDisplayTransferByTable(CGDirectDisplayID d, uint32_t cap, float *r, float *g, float *b, uint32_t *count) {
|
||||
if (count) *count = 0;
|
||||
return 1000;
|
||||
}
|
||||
FX_EXPORT CGError CGSetDisplayTransferByTable(CGDirectDisplayID d, uint32_t n, const float *r, const float *g, const float *b) { return 1000; }
|
||||
@@ -0,0 +1,93 @@
|
||||
// Crash diagnostics: interposes sigaction so Factorio's own crash handler runs after ours.
|
||||
// Ours logs the faulting PC, LR, fault address and the frame-pointer chain as image+offset,
|
||||
// which tools/symbolize_native.py resolves against the original factorio.dSYM on the Mac.
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <sys/ucontext.h>
|
||||
#include "fx.h"
|
||||
|
||||
static struct sigaction fx_previous[NSIG];
|
||||
static int fx_reporting;
|
||||
|
||||
static void fx_describe(const char *label, uintptr_t address) {
|
||||
Dl_info info;
|
||||
if (address && dladdr((void *)address, &info) && info.dli_fname) {
|
||||
const char *base = strrchr(info.dli_fname, '/');
|
||||
fx_log("[fx-crash] %s %#lx %s+%#lx %s", label, (unsigned long)address, base ? base + 1 : info.dli_fname,
|
||||
(unsigned long)(address - (uintptr_t)info.dli_fbase), info.dli_sname ? info.dli_sname : "");
|
||||
} else {
|
||||
fx_log("[fx-crash] %s %#lx", label, (unsigned long)address);
|
||||
}
|
||||
}
|
||||
|
||||
static void fx_crash_handler(int sig, siginfo_t *si, void *context) {
|
||||
if (!fx_reporting++) {
|
||||
ucontext_t *uc = context;
|
||||
struct __darwin_arm_thread_state64 *ts = &uc->uc_mcontext->__ss;
|
||||
uintptr_t pc = (uintptr_t)__darwin_arm_thread_state64_get_pc(*ts);
|
||||
uintptr_t lr = (uintptr_t)__darwin_arm_thread_state64_get_lr(*ts);
|
||||
uintptr_t fp = (uintptr_t)__darwin_arm_thread_state64_get_fp(*ts);
|
||||
uintptr_t sp = (uintptr_t)__darwin_arm_thread_state64_get_sp(*ts);
|
||||
fx_log("[fx-crash] signal %d code %d fault address %p thread %p%s", sig, si->si_code, si->si_addr,
|
||||
(void *)pthread_self(), pthread_main_np() ? " (main)" : "");
|
||||
fx_describe("pc", pc);
|
||||
fx_describe("lr", lr);
|
||||
for (int r = 0; r < 29; r++) fx_log("[fx-crash] x%d = %#llx", r, ts->__x[r]);
|
||||
fx_log("[fx-crash] sp = %#lx fp = %#lx", (unsigned long)sp, (unsigned long)fp);
|
||||
// Frame-pointer walk, bounded to plausible stack addresses above sp.
|
||||
for (int depth = 0; depth < 64 && fp && (fp & 7) == 0 && fp >= sp && fp - sp < (512u << 20); depth++) {
|
||||
uintptr_t next = ((uintptr_t *)fp)[0], ret = ((uintptr_t *)fp)[1];
|
||||
char label[16];
|
||||
snprintf(label, sizeof label, "#%d", depth);
|
||||
fx_describe(label, ret);
|
||||
if (next <= fp) break;
|
||||
fp = next;
|
||||
}
|
||||
}
|
||||
struct sigaction *prev = &fx_previous[sig];
|
||||
if (prev->sa_flags & SA_SIGINFO) {
|
||||
if (prev->sa_sigaction) { prev->sa_sigaction(sig, si, context); return; }
|
||||
} else if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN) {
|
||||
prev->sa_handler(sig);
|
||||
return;
|
||||
}
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static int fx_is_fatal(int sig) { return sig == SIGSEGV || sig == SIGBUS || sig == SIGILL || sig == SIGFPE || sig == SIGTRAP || sig == SIGABRT; }
|
||||
|
||||
static int fx_sigaction(int sig, const struct sigaction *act, struct sigaction *old) {
|
||||
if (sig <= 0 || sig >= NSIG || !fx_is_fatal(sig) || !act) return sigaction(sig, act, old);
|
||||
if (old) *old = fx_previous[sig];
|
||||
fx_previous[sig] = *act;
|
||||
struct sigaction ours = *act;
|
||||
ours.sa_sigaction = fx_crash_handler;
|
||||
ours.sa_flags |= SA_SIGINFO;
|
||||
return sigaction(sig, &ours, NULL);
|
||||
}
|
||||
|
||||
static void (*fx_signal(int sig, void (*handler)(int)))(int) {
|
||||
if (sig <= 0 || sig >= NSIG || !fx_is_fatal(sig)) return signal(sig, handler);
|
||||
struct sigaction act = {0}, old;
|
||||
act.sa_handler = handler;
|
||||
fx_sigaction(sig, &act, &old);
|
||||
return old.sa_handler;
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_install_crash_reporter(void) {
|
||||
for (int sig = 1; sig < NSIG; sig++) {
|
||||
if (!fx_is_fatal(sig)) continue;
|
||||
struct sigaction act = {0};
|
||||
act.sa_handler = SIG_DFL;
|
||||
fx_sigaction(sig, &act, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((used, section("__DATA,__interpose"))) static const struct { const void *replacement, *original; }
|
||||
fx_interpose[] = {
|
||||
{(const void *)fx_sigaction, (const void *)sigaction},
|
||||
{(const void *)fx_signal, (const void *)signal},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Objective-C symbols missing from iOS Foundation and Metal.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "fx.h"
|
||||
|
||||
#ifdef FX_LIB_FOUNDATION
|
||||
@interface NSAppleEventManager : NSObject
|
||||
@end
|
||||
@implementation NSAppleEventManager
|
||||
+ (instancetype)sharedAppleEventManager { static NSAppleEventManager *m; static dispatch_once_t o; dispatch_once(&o, ^{ m = [self new]; }); return m; }
|
||||
- (void)setEventHandler:(id)handler andSelector:(SEL)sel forEventClass:(uint32_t)c andEventID:(uint32_t)e { }
|
||||
- (void)removeEventHandlerForEventClass:(uint32_t)c andEventID:(uint32_t)e { }
|
||||
@end
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_METAL
|
||||
#import <Metal/Metal.h>
|
||||
FX_EXPORT NSArray *MTLCopyAllDevicesWithObserver(id *observer, void (^handler)(id, NSString *)) {
|
||||
if (observer) *observer = nil;
|
||||
id device = MTLCreateSystemDefaultDevice();
|
||||
return device ? @[device] : @[];
|
||||
}
|
||||
FX_EXPORT void MTLRemoveDeviceObserver(id observer) { }
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
// Shared declarations for the macOS-API shims used by the retargeted Factorio binary.
|
||||
#pragma once
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define FX_DISPLAY_ID 1u
|
||||
#define FX_EXPORT __attribute__((visibility("default")))
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
FX_EXPORT void fx_log(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
||||
FX_EXPORT CGSize fx_screen_points(void);
|
||||
FX_EXPORT double fx_screen_scale(void);
|
||||
FX_EXPORT double fx_render_divisor(void);
|
||||
FX_EXPORT double fx_game_per_uikit(void);
|
||||
#ifdef __OBJC__
|
||||
FX_EXPORT void fx_main_sync(void (^block)(void));
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/Foundation.h>
|
||||
// Selectors sent to Factorio's own SDL classes (declared so ARC accepts the sends).
|
||||
@protocol FXAppKitCallbacks
|
||||
@optional
|
||||
- (void)mouseDown:(id)e; - (void)mouseUp:(id)e; - (void)mouseDragged:(id)e; - (void)mouseMoved:(id)e;
|
||||
- (void)rightMouseDown:(id)e; - (void)rightMouseUp:(id)e; - (void)rightMouseDragged:(id)e;
|
||||
- (void)otherMouseDown:(id)e; - (void)otherMouseUp:(id)e; - (void)otherMouseDragged:(id)e;
|
||||
- (void)scrollWheel:(id)e; - (void)keyDown:(id)e; - (void)keyUp:(id)e; - (void)flagsChanged:(id)e;
|
||||
- (void)mouseEntered:(id)e; - (void)mouseExited:(id)e;
|
||||
- (void)touchesBeganWithEvent:(id)e; - (void)touchesMovedWithEvent:(id)e;
|
||||
- (void)touchesEndedWithEvent:(id)e; - (void)touchesCancelledWithEvent:(id)e;
|
||||
- (void)windowDidBecomeKey:(NSNotification *)n; - (void)windowDidResignKey:(NSNotification *)n;
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)n;
|
||||
- (NSInteger)tag;
|
||||
- (void)insertText:(id)text replacementRange:(NSRange)range;
|
||||
@end
|
||||
@interface NSObject (FXAppKitCallbacks) <FXAppKitCallbacks>
|
||||
@end
|
||||
#endif
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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(); }
|
||||
@@ -0,0 +1,53 @@
|
||||
// macOS-only MTLDevice properties, added to the device's concrete class before the game starts.
|
||||
// Values describe a built-in Apple GPU. Other selectors iOS lacks are logged and answer zero.
|
||||
#import <Metal/Metal.h>
|
||||
#import <objc/runtime.h>
|
||||
#import "fx.h"
|
||||
|
||||
static id fx_zero(id self, SEL _cmd, ...) { return nil; }
|
||||
static BOOL fx_no(id self, SEL _cmd) { return NO; }
|
||||
static BOOL fx_yes(id self, SEL _cmd) { return YES; }
|
||||
static NSUInteger fx_uint_zero(id self, SEL _cmd) { return 0; }
|
||||
static NSUInteger fx_uint_one(id self, SEL _cmd) { return 1; }
|
||||
|
||||
static void fx_add(Class cls, const char *name, IMP imp, const char *types) {
|
||||
SEL sel = sel_registerName(name);
|
||||
if (!class_getInstanceMethod(cls, sel)) {
|
||||
class_addMethod(cls, sel, imp, types);
|
||||
fx_log("[fx] Metal compat: -[%s %s]", class_getName(cls), name);
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL (*fx_original_resolve)(id, SEL, SEL);
|
||||
static BOOL fx_device_resolve(id cls, SEL _cmd, SEL sel) {
|
||||
if (fx_original_resolve && fx_original_resolve(cls, _cmd, sel)) return YES;
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.') return NO;
|
||||
fx_log("[fx] Metal compat: unknown -[%s %s] -> 0", class_getName(cls), name);
|
||||
class_addMethod(cls, sel, (IMP)fx_zero, "@@:");
|
||||
return YES;
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_patch_metal(void) {
|
||||
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
||||
if (!device) { fx_log("[fx] no Metal device"); return; }
|
||||
Class cls = object_getClass(device);
|
||||
fx_log("[fx] Metal device %s (%s), unified memory %d", device.name.UTF8String, class_getName(cls), device.hasUnifiedMemory);
|
||||
fx_add(cls, "isDepth24Stencil8PixelFormatSupported", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isLowPower", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isHeadless", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isRemovable", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "location", (IMP)fx_uint_zero, "Q@:"); // MTLDeviceLocationBuiltIn
|
||||
fx_add(cls, "locationNumber", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "maxTransferRate", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "peerGroupID", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "peerIndex", (IMP)fx_uint_zero, "I@:");
|
||||
fx_add(cls, "peerCount", (IMP)fx_uint_one, "I@:");
|
||||
fx_add(cls, "supportsBCTextureCompression", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "areProgrammableSamplePositionsSupported", (IMP)fx_yes, "B@:");
|
||||
Method resolve = class_getClassMethod(cls, @selector(resolveInstanceMethod:));
|
||||
fx_original_resolve = (BOOL (*)(id, SEL, SEL))method_getImplementation(resolve);
|
||||
// Overrides only this concrete class; inherited NSObject behavior is kept via fx_original_resolve.
|
||||
if (!class_addMethod(object_getClass(cls), @selector(resolveInstanceMethod:), (IMP)fx_device_resolve, "B@::"))
|
||||
method_setImplementation(resolve, (IMP)fx_device_resolve);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Fast exact PNG decoding with Wuffs for the common Factorio sprite formats.
|
||||
// Produces what AppKit's NSBitmapImageRep gives on macOS: tightly packed, non-premultiplied
|
||||
// 8-bit samples with the source channel count (gray 1, gray+alpha 2, RGB 3, RGBA 4).
|
||||
// Returns 0 for anything else (palette, tRNS, 16-bit, malformed) so the caller can fall back.
|
||||
#define WUFFS_IMPLEMENTATION
|
||||
#define WUFFS_CONFIG__MODULES
|
||||
#define WUFFS_CONFIG__MODULE__BASE
|
||||
#define WUFFS_CONFIG__MODULE__ADLER32
|
||||
#define WUFFS_CONFIG__MODULE__CRC32
|
||||
#define WUFFS_CONFIG__MODULE__DEFLATE
|
||||
#define WUFFS_CONFIG__MODULE__ZLIB
|
||||
#define WUFFS_CONFIG__MODULE__PNG
|
||||
#include "../../../third_party/wuffs/wuffs-v0.3.c"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "pngdecode.h"
|
||||
|
||||
static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]; }
|
||||
|
||||
// Channel count for supported PNGs (from IHDR, no tRNS before IDAT), else 0.
|
||||
static int fx_png_channels(const uint8_t *d, size_t n) {
|
||||
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'};
|
||||
if (n < 33 || memcmp(d, sig, 8) || memcmp(d + 12, "IHDR", 4)) return 0;
|
||||
uint8_t depth = d[24], type = d[25];
|
||||
int channels = type == 0 ? 1 : type == 2 ? 3 : type == 4 ? 2 : type == 6 ? 4 : 0;
|
||||
if (depth != 8 || !channels) return 0;
|
||||
for (size_t at = 8; at + 12 <= n;) {
|
||||
uint32_t len = be32(d + at);
|
||||
const uint8_t *kind = d + at + 4;
|
||||
if (!memcmp(kind, "IDAT", 4)) break;
|
||||
if (!memcmp(kind, "tRNS", 4)) return 0;
|
||||
if ((uint64_t)at + 12 + len > n) return 0;
|
||||
at += 12 + (size_t)len;
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
int fx_png_decode(const uint8_t *data, size_t size, uint8_t **out, int *width, int *height, int *channels) {
|
||||
int ch = fx_png_channels(data, size);
|
||||
if (!ch) return 0;
|
||||
wuffs_png__decoder *dec = wuffs_png__decoder__alloc();
|
||||
if (!dec) return 0;
|
||||
// Skipping zlib/CRC verification does not change decoded pixels (verified byte-identical with
|
||||
// tests/native/png_parity.m); a corrupt file decodes to garbage instead of failing.
|
||||
wuffs_png__decoder__set_quirk_enabled(dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true);
|
||||
int ok = 0;
|
||||
uint8_t *rgba = NULL, *work = NULL;
|
||||
wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader((uint8_t *)data, size, true);
|
||||
wuffs_base__image_config ic = {0};
|
||||
if (wuffs_png__decoder__decode_image_config(dec, &ic, &src).repr) goto done;
|
||||
uint32_t w = wuffs_base__pixel_config__width(&ic.pixcfg), h = wuffs_base__pixel_config__height(&ic.pixcfg);
|
||||
if (!w || !h || (uint64_t)w * h > (1u << 28)) goto done;
|
||||
wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h);
|
||||
size_t pixels = (size_t)w * h;
|
||||
rgba = malloc(pixels * 4);
|
||||
uint64_t work_len = wuffs_png__decoder__workbuf_len(dec).max_incl;
|
||||
work = work_len ? malloc(work_len) : NULL;
|
||||
if (!rgba || (work_len && !work)) goto done;
|
||||
wuffs_base__pixel_buffer pb;
|
||||
if (wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(rgba, pixels * 4)).repr) goto done;
|
||||
if (wuffs_png__decoder__decode_frame(dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC,
|
||||
wuffs_base__make_slice_u8(work, work_len), NULL).repr) goto done;
|
||||
// Pack to the source channel count (gray sources decode with R == G == B).
|
||||
if (ch != 4) {
|
||||
uint8_t *dst = rgba;
|
||||
for (size_t i = 0; i < pixels; i++) {
|
||||
const uint8_t *p = rgba + i * 4;
|
||||
if (ch == 1) { dst[i] = p[0]; }
|
||||
else if (ch == 2) { dst[2 * i] = p[0]; dst[2 * i + 1] = p[3]; }
|
||||
else { uint8_t r = p[0], g = p[1], b = p[2]; dst[3 * i] = r; dst[3 * i + 1] = g; dst[3 * i + 2] = b; }
|
||||
}
|
||||
}
|
||||
*out = rgba; rgba = NULL;
|
||||
*width = (int)w; *height = (int)h; *channels = ch;
|
||||
ok = 1;
|
||||
done:
|
||||
free(rgba);
|
||||
free(work);
|
||||
free(dec);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Exact PNG decoding for common 8-bit formats; see pngdecode.c.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
// On success returns 1 and a malloc'd buffer (caller frees) of width*height*channels bytes.
|
||||
int fx_png_decode(const uint8_t *data, size_t size, uint8_t **out, int *width, int *height, int *channels);
|
||||
@@ -0,0 +1,118 @@
|
||||
// Small macOS-only APIs. Each FX_LIB_* section is compiled into its own shim dylib by
|
||||
// tools/build_native.py, so the original two-level-namespace bindings stay valid.
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include "fx.h"
|
||||
|
||||
#ifdef FX_LIB_COREVIDEO
|
||||
typedef struct { int64_t timeValue; int32_t timeScale; int32_t flags; } fx_cvtime;
|
||||
FX_EXPORT int32_t CVDisplayLinkCreateWithActiveCGDisplays(void **link) { *link = (void *)1; return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkCreateWithCGDisplay(uint32_t display, void **link) { *link = (void *)1; return 0; }
|
||||
FX_EXPORT fx_cvtime CVDisplayLinkGetNominalOutputVideoRefreshPeriod(void *link) { return (fx_cvtime){1, 60, 0}; }
|
||||
FX_EXPORT void CVDisplayLinkRelease(void *link) { }
|
||||
FX_EXPORT int32_t CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(void *l, void *c, void *p) { return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkSetOutputCallback(void *link, void *callback, void *context) { return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkStart(void *link) { fx_log("[fx] CVDisplayLinkStart ignored"); return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_CARBON
|
||||
FX_EXPORT const CFStringRef kTISPropertyUnicodeKeyLayoutData = CFSTR("TISPropertyUnicodeKeyLayoutData");
|
||||
FX_EXPORT void *TISCopyCurrentKeyboardLayoutInputSource(void) { return NULL; }
|
||||
FX_EXPORT void *TISGetInputSourceProperty(void *source, CFStringRef key) { return NULL; }
|
||||
FX_EXPORT uint32_t KBGetLayoutType(int16_t keyboard) { return 0; }
|
||||
FX_EXPORT uint8_t LMGetKbdType(void) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_CORESERVICES
|
||||
FX_EXPORT int32_t LSOpenCFURLRef(CFURLRef url, CFURLRef *launched) { fx_log("[fx] LSOpenCFURLRef ignored"); return -10814; }
|
||||
FX_EXPORT int32_t UCKeyTranslate(const void *layout, uint16_t key, uint16_t action, uint32_t mods, uint32_t kbd,
|
||||
uint32_t opts, uint32_t *dead, unsigned long max, unsigned long *len, uint16_t *chars) {
|
||||
if (len) *len = 0;
|
||||
return -1;
|
||||
}
|
||||
FX_EXPORT double nan(const char *tag) { return __builtin_nan(""); }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_OPENCL
|
||||
FX_EXPORT int32_t clGetPlatformIDs(uint32_t n, void **platforms, uint32_t *count) { if (count) *count = 0; return -1001; }
|
||||
FX_EXPORT int32_t clGetDeviceIDs(void *p, uint64_t type, uint32_t n, void **devices, uint32_t *count) { if (count) *count = 0; return -1; }
|
||||
FX_EXPORT int32_t clGetDeviceInfo(void *d, uint32_t name, size_t size, void *value, size_t *ret) { return -33; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_FORCEFEEDBACK
|
||||
#define FF_UNSUPPORTED ((int32_t)0x80040202)
|
||||
FX_EXPORT int32_t FFIsForceFeedback(uint32_t service) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFCreateDevice(uint32_t service, void **device) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFReleaseDevice(void *device) { return 0; }
|
||||
FX_EXPORT int32_t FFDeviceCreateEffect(void *d, CFUUIDRef u, void *e, void **h) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceReleaseEffect(void *d, void *e) { return 0; }
|
||||
FX_EXPORT int32_t FFDeviceGetForceFeedbackCapabilities(void *d, void *c) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceGetForceFeedbackProperty(void *d, uint32_t p, void *v, uint32_t s) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceSetForceFeedbackProperty(void *d, uint32_t p, void *v) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceSendForceFeedbackCommand(void *d, uint32_t c) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectGetEffectStatus(void *e, uint32_t *s) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectSetParameters(void *e, void *p, uint32_t f) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectStart(void *e, uint32_t i, uint32_t f) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectStop(void *e) { return FF_UNSUPPORTED; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_LDAP
|
||||
// Only reachable from libcurl's LDAP protocol handler; every entry fails.
|
||||
#define LDAP_FAIL 0x51 /* LDAP_SERVER_DOWN */
|
||||
FX_EXPORT void *ldap_init(const char *host, int port) { return NULL; }
|
||||
FX_EXPORT int ldap_set_option(void *ld, int option, const void *value) { return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_simple_bind_s(void *ld, const char *who, const char *cred) { return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_search_s(void *ld, const char *b, int s, const char *f, char **a, int o, void **r) { if (r) *r = NULL; return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_unbind_s(void *ld) { return 0; }
|
||||
FX_EXPORT char *ldap_err2string(int err) { return "LDAP unsupported on iOS port"; }
|
||||
FX_EXPORT void *ldap_first_entry(void *ld, void *r) { return NULL; }
|
||||
FX_EXPORT void *ldap_next_entry(void *ld, void *e) { return NULL; }
|
||||
FX_EXPORT char *ldap_first_attribute(void *ld, void *e, void **b) { return NULL; }
|
||||
FX_EXPORT char *ldap_next_attribute(void *ld, void *e, void *b) { return NULL; }
|
||||
FX_EXPORT char *ldap_get_dn(void *ld, void *e) { return NULL; }
|
||||
FX_EXPORT void **ldap_get_values_len(void *ld, void *e, const char *a) { return NULL; }
|
||||
FX_EXPORT void ldap_value_free_len(void **v) { }
|
||||
FX_EXPORT void ldap_memfree(void *p) { }
|
||||
FX_EXPORT int ldap_msgfree(void *m) { return 0; }
|
||||
FX_EXPORT int ldap_url_parse(const char *url, void **desc) { return LDAP_FAIL; }
|
||||
FX_EXPORT void ldap_free_urldesc(void *d) { }
|
||||
FX_EXPORT void ber_free(void *b, int f) { }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_APPLICATIONSERVICES
|
||||
FX_EXPORT int UAZoomEnabled(void) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_SECURITY
|
||||
#define errSecUnimplemented (-4)
|
||||
FX_EXPORT CFStringRef SecCertificateCopyLongDescription(CFAllocatorRef a, void *cert, CFErrorRef *e) { return NULL; }
|
||||
FX_EXPORT int32_t SecIdentityCreateWithCertificate(CFTypeRef keychain, void *cert, void **identity) { return errSecUnimplemented; }
|
||||
FX_EXPORT int32_t SecItemImport(CFDataRef data, CFStringRef ext, void *fmt, void *type, uint32_t flags,
|
||||
const void *params, void *keychain, CFArrayRef *items) { return errSecUnimplemented; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_IOKIT
|
||||
FX_EXPORT CFDictionaryRef IODisplayCreateInfoDictionary(uint32_t framebuffer, uint32_t options) {
|
||||
return CFDictionaryCreate(NULL, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_STEAM
|
||||
// Steam is excluded from the target. Init reports failure; the game runs without Steam.
|
||||
FX_EXPORT int SteamAPI_RestartAppIfNecessary(uint32_t app) { fx_log("[fx] SteamAPI_RestartAppIfNecessary(%u) -> false", app); return 0; }
|
||||
FX_EXPORT int SteamAPI_Init(void) { fx_log("[fx] SteamAPI_Init -> false"); return 0; }
|
||||
FX_EXPORT void SteamAPI_Shutdown(void) { }
|
||||
FX_EXPORT void SteamAPI_RunCallbacks(void) { }
|
||||
FX_EXPORT int32_t SteamAPI_GetHSteamUser(void) { return 0; }
|
||||
FX_EXPORT void SteamAPI_RegisterCallback(void *cb, int id) { }
|
||||
FX_EXPORT void SteamAPI_UnregisterCallback(void *cb) { }
|
||||
FX_EXPORT void SteamAPI_RegisterCallResult(void *cb, uint64_t call) { }
|
||||
FX_EXPORT void SteamAPI_UnregisterCallResult(void *cb, uint64_t call) { }
|
||||
FX_EXPORT void *SteamInternal_ContextInit(void *context) { fx_log("[fx] SteamInternal_ContextInit -> NULL"); return NULL; }
|
||||
FX_EXPORT void *SteamInternal_FindOrCreateUserInterface(int32_t user, const char *version) { return NULL; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_EMPTY
|
||||
FX_EXPORT int fx_empty_shim;
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
// void fx_call_on_stack(void *stack_top, void (*fn)(void *), void *arg)
|
||||
// Calls fn(arg) with sp moved to stack_top on the current thread, then restores sp.
|
||||
.text
|
||||
.globl _fx_call_on_stack
|
||||
.p2align 2
|
||||
_fx_call_on_stack:
|
||||
stp x29, x30, [sp, #-16]!
|
||||
mov x29, sp
|
||||
mov x9, sp
|
||||
and x0, x0, #~15
|
||||
mov sp, x0
|
||||
stp x9, xzr, [sp, #-16]!
|
||||
mov x0, x2
|
||||
blr x1
|
||||
ldp x9, xzr, [sp], #16
|
||||
mov sp, x9
|
||||
ldp x29, x30, [sp], #16
|
||||
ret
|
||||
Reference in new issue
Block a user