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,249 @@
|
||||
#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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user