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.
9.3 KiB
Save stream positioning and guest calls
2026-10-20. This batch connects the translated map-header constructor to a bounded real-save probe. It does not load a playable world.
Results and authority
The approved _autosave1.zip has version 2.0.77.0, flag 0. The recovered metadata layout places MapHeader at decoded map-stream offset 333 and the next byte after its three fields at 357. The three raw fields are 720001 / 720001 / 720001. Their individual meanings are not yet established.
The offset is derived from the instruction-level serialized-field order described below. It is not a recorded full-game execution trace. At that derived position, the original ARM64 MapHeader constructor and the translated constructor agree on all three values, with exactly three eight-byte reads. build/save-layout/desktop-real-save.json records the source-save hash, offsets, values and native comparison. Signed build 3 ran on the iPhone 15 Pro and returned the same offsets and values from the same approved save. Its input SHA-256, launch ID and probe ID match the companion reports. Results and the signed binary hash are retained in build/save-layout/device-verification.json.
The scenario envelope is a host-side reconstruction in runtime/save_layout.c; it is not translated original Scenario code. MapVersion and MapHeader execute original byte-guarded translated functions. The envelope adapter deliberately supports only 2.0.77.0/flag 0 and an empty dictionary for startup PropertyTree settings. Other versions, nonempty/other settings, malformed booleans, excessive lengths, recursion and read budgets fail explicitly.
Recovered stream order
- MapDeserialiser reads four little-endian uint16 version components and one flag byte. Its version object starts at object offset
0x18. The selected valid-version path initializes helpers without consuming additional serialized fields. - The normal save path calls ScenarioExecutionContext's deserializing constructor before Map::load. ScenarioLocation consumes three compact-length strings; Difficulty consumes one byte.
- ScenarioExecutionContext reads booleans, a string, additional booleans, a compact three-component Version, fixed-width metadata fields, a vector of ModIDs, and startup PropertyTree settings. ModIDs contain a string, three compact uint16 version components and a four-byte field.
- The modern path reads two EndingInfo values and a final boolean. EndingInfo includes LocalisedStrings, a vector of LocalisedStrings and an ordinary string. LocalisedString starts with a type byte: zero consumes no further bytes; other values read a string, a one-byte child count, and recursive child strings.
- The selected path then calls Map::load and Map::loadData. Map::loadData begins by constructing MapHeader. The retained Map constructor assembly does not preserve/use its incoming reader before its first call; its observed constructor dependencies initialize engine state rather than this serialized envelope.
Compact string/vector lengths use a byte unless it is 0xff, which introduces a little-endian uint32. Compact Version/ModVersion components use the same sentinel with a uint16 extension. These two encodings must not be conflated. This batch does not implement recursive PropertyTree settings; rejecting them avoids assuming their complete wire representation.
The envelope adapter reports the next required prefix size when input is short. The ZIP adapter retries bounded prefixes, up to 128 reads and a 1 MiB prefix budget. No full extraction is required. Whole-member CRCs, the remainder of the map stream, object references, mods, entities, or world integrity are not validated.
Compiler/runtime changes
tools/a64_program.py adds a multi-function dispatcher over declared original instruction addresses. It shares the guest's registers, stack, memory and one instruction budget across direct BL calls, tail branches and RET. Original stack save/restore instructions implement nested return-address preservation. No native recursion, runtime compilation or JIT is used.
RET accepts the external return sentinel or known guest-call continuations. Unknown computed returns produce status 4; external sentinels aliasing declared code are rejected. Indirect calls and arbitrary computed returns remain unsupported. The call graph/flag analysis is conservative; a rejected program is not automatically evidence of invalid original game code. Host stream errors still return an explicit status rather than implementing C++ unwinding.
MapHeader now enters through the original four-byte public C1 tail branch at 0x10010582c, then reaches its 332-byte C2 body at 0x1014c6aac. Both inputs are byte guarded. The desktop oracle relocates the alias's B to its copied body and redirects only declared stream reads. The iOS library excludes the oracle.
Verification
- 80,000 native comparisons for hand-authored multi-function instruction fixtures: nested calls, recursive LR saves, tail calls, shared-memory mutation, preserved callee-saved registers, memory guards, exact shared-budget exhaustion and invalid return/entry statuses. These are compiler tests, not recovered game functions.
- 20,000 original MapHeader comparisons now run through its original public entry and body in the multi-function dispatcher. Modern/legacy paths and failure behavior still pass.
- 62 bounded ZIP prefix cases compare exact decoded bytes and check output guards/failure atomicity across stored/deflated outer members, data descriptors, buffer boundaries, empty/oversized requests and truncated streams.
- 140 scenario-envelope cases cover variable string lengths, sentinel encodings, mod counts, nested LocalisedStrings, every truncation of a controlled fixture, malformed values, unsupported settings and recursion/size limits.
- Existing MapVersion equivalence and fifteen archive-version fixtures pass.
- All six translated code/constant/entry ranges match complete exported REA byte evidence.
- The real-save constructor comparison passes at offset 333 on macOS. The iPhone real-save probe, modern/legacy constructor self-tests and version regression pass in one launch. The signed artifact passes a Steam import/dependency audit and contains none of the desktop native-oracle symbols.
Reproduce after the RNG build:
python3 tools/build_version.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
python3 tests/check_a64_program.py
python3 tools/build_map_header.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
python3 tests/check_save_prefixes.py
python3 tools/build_save_layout.py
python3 tests/check_save_layout.py
python3 tools/check_rea_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio' evidence/save-layout-final.json --include-version --include-map-header --include-map-header-entry
build/save-layout/verify_native '/absolute/path/to/save.zip'
python3 tools/package_ios.py
REA evidence
Complete canonical bundle: evidence/save-layout-final.json (64 records). The first session lost its Hopper connection after the main constructor graph was captured; its evidence was preserved in save-layout-pass1.json. The restarted session completed the remaining readers and closed successfully into build/save-layout-rea.snapshot.json. Matching LLVM symbol/instruction checks were used while the second analysis prepared; they are supplementary, not REA records.
| Observation | REA evidence ID |
|---|---|
| MapDeserialiser constructor | ev_3143ceb6dee72f0a56235e02cd4a5ed942c3826860ea21c315847bb186053650 |
| ScenarioExecutionContext serialized order | ev_a4333ddb98e9e43cd6eacf68a19936091fb88eb905df0037617599b1f64952c9 |
| ScenarioLocation strings | ev_17cb5c99e94825134f5ec68367c0dc43d89c520bffd31830815fd785006d3cf4 |
| String loader | ev_36139f8e3f048ef8ad9744727fa699868cea0cb6c45785a552284adf3b3091a9 |
| Boolean validation | ev_8654783b6211640b058d9dd0904eef6b49fcd73230410956eb6b4aff686835cf |
| Version component widths | ev_705e76d3df68f5080b2401c46ae2a8c9f0eff826727469271c101c8d73be0d68 |
| ModVersion component widths | ev_eedbdacc998513468e3f7550844f2421f16ae817f129f10863cc7810aa002b93 |
| EndingInfo order | ev_8eb93855422407b11b683ef67324e000b41916c072b18543e9b94bcffba8d1dc |
| Recursive LocalisedString | ev_8c14f94ef7ba1133a664ab94262e1d527f6e3cd687a80d0fc61a8f7f3a1cf9df |
| LocalisedString vector count | ev_b723c046c47b29ec10d022700ed74861aad1449048c57233526c6083c83f105f |
| Public MapHeader entry bytes | ev_525aeb761646ca31af2fbacfc51ec786e6036b6259fd09505063144d0a5feba1 |
Hopper merged Difficulty's entry with a preceding procedure. Its assembly includes the relevant block at 0x1001bf6ac; procedure names and reported lengths alone are not reliable function-boundary authority. Source bytes, decoded call targets, matching symbols and the specific observed blocks were used together.
Next engine work
- Translate the compact Version/ModVersion readers and other metadata utilities as composable guest functions, reducing reconstructed host behavior where useful.
- Implement a scalable guest memory image and explicit allocator/global/import contracts needed by the actual MapDeserialiser and Map constructors.
- Expand supported PropertyTree settings using recovered wire cases and validated guest code.
- Recover and translate MapGenSettings and the earliest Map::loadData state allocations. Entity/world reconstruction, simulation, graphics, audio and controls remain ahead.