#import #import #import #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 { 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 *)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)); } }