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

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

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

250 lines
14 KiB
Objective-C

#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <CommonCrypto/CommonDigest.h>
#include "factorio_rng.h"
#include "save_header.h"
#include "map_header_bridge.h"
#include "save_layout.h"
#include "target_bridge.h"
#include "deserialiser_bridge.h"
@interface PortDelegate : UIResponder <UIApplicationDelegate, UIDocumentPickerDelegate> {
w2c_factorio__rng _module;
UILabel *_output;
UILabel *_saveOutput;
uint32_t _calls;
NSString *_runID;
}
@property(nonatomic, strong) UIWindow *window;
@end
@implementation PortDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options {
(void)application;
(void)options;
_runID = NSUUID.UUID.UUIDString;
wasm_rt_init();
wasm2c_factorio__rng_instantiate(&_module);
w2c_factorio__rng_seed(&_module, 0, 123456789);
UIViewController *controller = [UIViewController new];
controller.view.backgroundColor = UIColor.systemBackgroundColor;
UILabel *title = [UILabel new];
title.text = @"Factorio port prototype";
title.font = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle1];
title.numberOfLines = 0;
UILabel *scope = [UILabel new];
scope.text = @"Native code without JIT or Steam. Reads version and map headers from supported 2.0.77 saves and generates RNG values.\n\nWorld simulation and gameplay are not implemented yet.";
scope.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
scope.numberOfLines = 0;
_output = [UILabel new];
_output.font = [UIFont monospacedSystemFontOfSize:16 weight:UIFontWeightRegular];
_output.numberOfLines = 0;
UIButton *next = [UIButton buttonWithType:UIButtonTypeSystem];
[next setTitle:@"Generate next 5 values" forState:UIControlStateNormal];
[next addTarget:self action:@selector(generate:) forControlEvents:UIControlEventTouchUpInside];
UIButton *openSave = [UIButton buttonWithType:UIButtonTypeSystem];
[openSave setTitle:@"Open Factorio save (.zip)" forState:UIControlStateNormal];
[openSave addTarget:self action:@selector(openSave:) forControlEvents:UIControlEventTouchUpInside];
_saveOutput = [UILabel new];
_saveOutput.text = @"Choose a save to read its headers. Currently supports 2.0.77 with empty startup settings. The full map will not be loaded.";
_saveOutput.numberOfLines = 0;
_saveOutput.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[title, scope, _output, next, openSave, _saveOutput]];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 24;
stack.translatesAutoresizingMaskIntoConstraints = NO;
UIScrollView *scroll = [UIScrollView new];
scroll.translatesAutoresizingMaskIntoConstraints = NO;
[controller.view addSubview:scroll];
[scroll addSubview:stack];
[NSLayoutConstraint activateConstraints:@[
[scroll.leadingAnchor constraintEqualToAnchor:controller.view.safeAreaLayoutGuide.leadingAnchor],
[scroll.trailingAnchor constraintEqualToAnchor:controller.view.safeAreaLayoutGuide.trailingAnchor],
[scroll.topAnchor constraintEqualToAnchor:controller.view.safeAreaLayoutGuide.topAnchor],
[scroll.bottomAnchor constraintEqualToAnchor:controller.view.safeAreaLayoutGuide.bottomAnchor],
[stack.leadingAnchor constraintEqualToAnchor:scroll.contentLayoutGuide.leadingAnchor constant:24],
[stack.trailingAnchor constraintEqualToAnchor:scroll.contentLayoutGuide.trailingAnchor constant:-24],
[stack.topAnchor constraintEqualToAnchor:scroll.contentLayoutGuide.topAnchor constant:24],
[stack.bottomAnchor constraintEqualToAnchor:scroll.contentLayoutGuide.bottomAnchor constant:-24],
[stack.widthAnchor constraintEqualToAnchor:scroll.frameLayoutGuide.widthAnchor constant:-48]
]];
self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
[self generate:nil];
uint8_t sample[] = {2, 0, 0, 0, 77, 0, 0, 0, 0};
FactorioMapVersion version;
BOOL selfTest = factorio_version_parse(sample, sizeof(sample), &version) == 0 &&
version.major == 2 && version.minor == 0 && version.patch == 77 && version.build == 0 && version.flag == 0;
[self writeReport:@{@"status": selfTest ? @"pass" : @"fail", @"test": @"native AOT MapVersion constructor"}
name:@"DeviceSelfTest.json"];
uint64_t tickSample[] = {11, 22, 33};
FactorioMapHeader header;
FactorioMapVersion modern = {.major=2, .minor=0, .patch=77};
BOOL modernTest = factorio_map_header_parse((const uint8_t *)tickSample, sizeof(tickSample), modern, &header) == 0 &&
header.tick_fields[0] == 11 && header.tick_fields[1] == 22 && header.tick_fields[2] == 33;
uint32_t legacySample[] = {UINT32_MAX, 22, 33};
FactorioMapVersion legacy = {.major=1, .minor=2, .patch=0, .build=373};
BOOL legacyTest = factorio_map_header_parse((const uint8_t *)legacySample, sizeof(legacySample), legacy, &header) == 0 &&
header.tick_fields[0] == UINT64_MAX && header.tick_fields[1] == 22 && header.tick_fields[2] == 33;
[self writeReport:@{@"status": modernTest && legacyTest ? @"pass" : @"fail",
@"test": @"native AOT branching MapHeader constructor", @"modern_64_bit": @(modernTest),
@"legacy_32_bit_sentinel": @(legacyTest), @"input": @"controlled constructor payloads; not a loaded save"}
name:@"MapHeaderSelfTest.json"];
FactorioTargetProbe target = {0};
int targetStatus = factorio_target_probe(&target);
[self writeReport:@{@"status": targetStatus == 0 ? @"pass" : @"fail",
@"code": @(targetStatus), @"test": @"native AOT TargetDeserialiser and memory pool constructors",
@"allocations": @(target.allocations), @"pool_capacity": @(target.pool_capacity),
@"initialized_targets": @(target.initialized_targets), @"heap_bytes": @(target.heap_bytes),
@"scope": @"isolated initialization; no world loading, virtual dispatch or exception unwinding"}
name:@"TargetDeserialiserSelfTest.json"];
FactorioDeserialiserProbe deserialiser = {0};
int deserialiserStatus = factorio_deserialiser_probe(sample, sizeof(sample), &deserialiser);
uint8_t newerSample[] = {2, 0, 0, 0, 78, 0, 0, 0, 0};
int newerStatus = factorio_deserialiser_probe(newerSample, sizeof(newerSample), &deserialiser);
[self writeReport:@{@"status": deserialiserStatus == 0 && newerStatus == 7 ? @"pass" : @"fail",
@"code": @(deserialiserStatus), @"newer_version_status": @(newerStatus),
@"test": @"native AOT MapDeserialiser and owned target initialization",
@"allocations": @(deserialiser.allocations), @"reads": @(deserialiser.reads),
@"target_capacity": @(deserialiser.target_capacity), @"heap_bytes": @(deserialiser.heap_bytes),
@"stream_cursor": @(deserialiser.stream_cursor), @"owns_target": @(deserialiser.owns_target),
@"scope": @"version prefix and isolated initialization; no world loading or C++ unwinding"}
name:@"DeserialiserSelfTest.json"];
NSString *samplePath = [[self documentsPath] stringByAppendingPathComponent:@"ProbeSave.zip"];
if ([[NSFileManager defaultManager] fileExistsAtPath:samplePath]) [self probeURL:[NSURL fileURLWithPath:samplePath]];
return YES;
}
- (void)generate:(id)sender {
(void)sender;
NSMutableString *text = [NSMutableString string];
for (int i = 0; i < 5; ++i) {
uint32_t value = w2c_factorio__rng_get_int(&_module, 0);
[text appendFormat:@"%6u %08x\n", ++_calls, value];
}
_output.text = text;
}
- (NSString *)documentsPath {
return NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
}
- (void)writeReport:(NSDictionary *)report name:(NSString *)name {
NSMutableDictionary *identified = [report mutableCopy];
identified[@"run_id"] = _runID;
identified[@"recorded_at"] = [[NSISO8601DateFormatter new] stringFromDate:NSDate.date];
identified[@"app_version"] = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"unknown";
identified[@"app_build"] = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"unknown";
identified[@"build_id"] = @__DATE__ " " __TIME__;
NSData *data = [NSJSONSerialization dataWithJSONObject:identified options:NSJSONWritingPrettyPrinted error:nil];
[data writeToFile:[[self documentsPath] stringByAppendingPathComponent:name] atomically:YES];
}
- (void)openSave:(id)sender {
(void)sender;
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc]
initForOpeningContentTypes:@[UTTypeZIP] asCopy:YES];
picker.delegate = self;
[self.window.rootViewController presentViewController:picker animated:YES completion:nil];
}
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
(void)controller;
if (urls.count) [self probeURL:urls.firstObject];
}
- (void)probeURL:(NSURL *)url {
_saveOutput.text = @"Reading save headers…";
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
BOOL access = [url startAccessingSecurityScopedResource];
FactorioMapVersion version;
// Stream the input identity rather than loading a potentially large save into RAM.
NSInputStream *stream = [NSInputStream inputStreamWithURL:url];
[stream open];
CC_SHA256_CTX hash;
CC_SHA256_Init(&hash);
uint8_t buffer[65536];
NSInteger count;
while ((count = [stream read:buffer maxLength:sizeof(buffer)]) > 0)
CC_SHA256_Update(&hash, buffer, (CC_LONG)count);
[stream close];
NSString *inputHash = nil;
if (count == 0) {
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256_Final(digest, &hash);
NSMutableString *hex = [NSMutableString stringWithCapacity:64];
for (unsigned i = 0; i < sizeof(digest); ++i) [hex appendFormat:@"%02x", digest[i]];
inputHash = hex;
}
int status = factorio_save_header(url.fileSystemRepresentation, &version);
FactorioSaveLayout layout;
int layoutStatus = status ? status : factorio_save_map_header(url.fileSystemRepresentation, &layout);
uint8_t prefix[9];
FactorioDeserialiserProbe initialized = {0};
int initializationStatus = status ? status : factorio_save_prefix(url.fileSystemRepresentation, prefix, sizeof(prefix));
if (!initializationStatus) initializationStatus = factorio_deserialiser_probe(prefix, sizeof(prefix), &initialized);
if (access) [url stopAccessingSecurityScopedResource];
NSMutableDictionary *report = [@{@"status": @(status), @"scope": @"version header only; whole archive not validated"} mutableCopy];
report[@"probe_id"] = NSUUID.UUID.UUIDString;
if (inputHash) report[@"input_sha256"] = inputHash;
else report[@"input_hash_error"] = @"Could not read complete input for hashing";
NSString *text;
NSMutableDictionary *layoutReport = [@{@"status": @(layoutStatus),
@"scope": @"recovered scenario envelope and translated MapHeader; whole archive not validated",
@"offset_authority": @"recovered metadata layout; not full-engine execution"} mutableCopy];
layoutReport[@"probe_id"] = report[@"probe_id"];
if (inputHash) layoutReport[@"input_sha256"] = inputHash;
NSMutableDictionary *initializationReport = [@{@"status": @(initializationStatus),
@"probe_id": report[@"probe_id"], @"scope": @"original MapDeserialiser initialized from actual save version prefix; world not loaded"} mutableCopy];
if (inputHash) initializationReport[@"input_sha256"] = inputHash;
if (!initializationStatus) {
initializationReport[@"allocations"] = @(initialized.allocations);
initializationReport[@"reads"] = @(initialized.reads);
initializationReport[@"target_capacity"] = @(initialized.target_capacity);
initializationReport[@"heap_bytes"] = @(initialized.heap_bytes);
initializationReport[@"stream_cursor"] = @(initialized.stream_cursor);
initializationReport[@"owns_target"] = @(initialized.owns_target);
initializationReport[@"version"] = @[@(initialized.version.major), @(initialized.version.minor),
@(initialized.version.patch), @(initialized.version.build), @(initialized.version.flag)];
}
if (!layoutStatus) {
layoutReport[@"header_offset"] = @(layout.header_offset);
layoutReport[@"next_offset"] = @(layout.next_offset);
layoutReport[@"mod_count"] = @(layout.mod_count);
NSMutableArray *ticks = [NSMutableArray new];
for (unsigned i=0;i<3;++i) [ticks addObject:[NSString stringWithFormat:@"%016llx", (unsigned long long)layout.header.tick_fields[i]]];
layoutReport[@"tick_fields_hex"] = ticks;
}
if (!status) {
report[@"major"] = @(version.major); report[@"minor"] = @(version.minor);
report[@"patch"] = @(version.patch); report[@"build"] = @(version.build); report[@"flag"] = @(version.flag);
if (!layoutStatus) {
text = [NSString stringWithFormat:@"Version %u.%u.%u · build %u\nMap header at byte %zu\nRaw tick fields: %llu / %llu / %llu\nThe game world is not loaded.",
version.major, version.minor, version.patch, version.build, layout.header_offset,
(unsigned long long)layout.header.tick_fields[0], (unsigned long long)layout.header.tick_fields[1],
(unsigned long long)layout.header.tick_fields[2]];
} else {
text = [NSString stringWithFormat:@"Version %u.%u.%u · build %u\n%s",
version.major, version.minor, version.patch, version.build, factorio_save_layout_status_string(layoutStatus)];
}
} else {
text = [NSString stringWithUTF8String:factorio_save_status_string(status)];
}
dispatch_async(dispatch_get_main_queue(), ^{
self->_saveOutput.text = text;
[self writeReport:report name:@"ProbeResult.json"];
[self writeReport:layoutReport name:@"SaveLayoutResult.json"];
[self writeReport:initializationReport name:@"DeserialiserSaveResult.json"];
});
});
}
@end
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass(PortDelegate.class));
}
}