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.
251 lines
26 KiB
Markdown
251 lines
26 KiB
Markdown
# Factorio 2.0.77 → native iPhone: implementation plan
|
|
|
|
Written 2026-10-20. This is a proposed implementation sequence, not a claim that the remaining translation is feasible or complete. Function discovery will refine the scope and dependencies. No reliable completion date is available before the translation and loader feasibility gates pass.
|
|
|
|
## 1. Target and definition of done
|
|
|
|
Build a native application for the connected iPhone 15 Pro that runs a small Factorio world locally, using the installed macOS ARM64 executable as the reconstruction input. Use REA MCP for focused binary evidence. Translate selected game code ahead of time through ARM64 → WebAssembly → wasm2c C → Clang iOS machine code. All translation happens on the Mac during the build; the phone executes signed native code.
|
|
|
|
Initial gameplay scope:
|
|
|
|
- Factorio 2.0.77 and a pinned, recorded set of its required base-game data. Identify expansion/mod dependencies explicitly before choosing a fixture.
|
|
- Local single player, a small controlled world, and touch controls.
|
|
- Load a world, move the character, inspect inventory, mine, craft, place a miner and belt, and observe item transport.
|
|
- Save locally, exit, relaunch, and resume the same world.
|
|
- No Steam integration. Multiplayer, Steam cloud/achievements/overlay, arbitrary mods, expansion compatibility, large factories, and App Store distribution are deferred.
|
|
|
|
Completion requires an actual device demonstration of this sequence, reproducible builds, save round-trip checks, and measured memory/performance results. A menu, header parser, or rendered screenshot alone does not meet this definition.
|
|
|
|
## 2. What exists now
|
|
|
|
Verified foundations:
|
|
|
|
- Pinned installed executable and unchanged ARM64 slice, with matching debug UUID.
|
|
- REA evidence for RNG, the map-version constructor, parts of startup, and Steam integration.
|
|
- Two RNG functions translated with specialized byte-guarded lifts; 876,544 output comparisons and 1,024 seed-constructor comparisons pass.
|
|
- A bounded automatic integer lifter translating the original map-version constructor; 20,000 native comparisons pass.
|
|
- A native ZIP/zlib adapter that reads only the first nine decoded map bytes; fifteen archive fixtures pass.
|
|
- A signed prototype installed on the iPhone. Its AOT self-test passes. With user approval, the real autosave was copied to the app and its header reports version 2.0.77 successfully.
|
|
- Steam import/dependency audits pass for the current prototype.
|
|
|
|
Missing: whole-world loading, general control flow and calls, a complete memory/runtime model, simulation, game rendering, audio, gameplay input, and save writing. The current reader does not validate complete archive contents or load entities.
|
|
|
|
## 3. Architecture to build
|
|
|
|
```text
|
|
Pinned local Mach-O + matching symbols + required data files
|
|
|
|
|
REA MCP evidence
|
|
|
|
|
function/CFG/import/relocation manifests and semantic IR
|
|
|
|
|
ARM64 → validated WASM modules
|
|
|
|
|
wasm2c → generated C
|
|
|
|
|
Clang → static iOS libraries
|
|
|
|
|
native host: storage / threads / Metal / audio / input
|
|
|
|
|
signed iPhone app
|
|
```
|
|
|
|
Keep engine object layouts and guest addresses inside the guest model. Native services receive validated values through explicit bridges. Every lifted function is identified by source address, exact bytes/hash, supported instruction set, calls, and evidence provenance. Unknown instructions or unresolved executable targets stop the build or trap with diagnostics; they never silently become no-ops.
|
|
|
|
Glue5 is a reference for the WASM-to-native build and compatibility layer. Its documented input is an existing supported `game.wasm`; it does not supply Factorio's missing native-binary-to-WASM translation. Its graphics and import interfaces must not be assumed to match Factorio. Reference: https://github.com/mempage4k/glue5.
|
|
|
|
## 4. Phase A — freeze inputs and make evidence repeatable
|
|
|
|
1. Record executable/slice hashes, UUID, game version, selected data/mod versions, tool revisions, Xcode/SDK, device OS, and build configuration in one machine-readable manifest.
|
|
2. Inventory installed data files without copying them into source control. Define a local asset-import manifest with hashes and relative paths.
|
|
3. Record the current device baseline: self-test, real-save header result, app build identity, and source-save hash. Future reports include unique run IDs, timestamps, and input hashes to distinguish fresh results from stale files.
|
|
4. Reuse the isolated REA MCP workflow and its longer startup timeout. Export evidence after each bounded investigation and close the binary session cleanly.
|
|
5. Store each finding with function address/range, byte hash, evidence IDs, observed callers/callees, unresolved references, and confidence. Keep decompiler hypotheses distinct from instruction-level observations.
|
|
6. Add resumable investigation batches so a lost Hopper session does not erase completed work. Determine whether persistent analysis is available; do not depend on saving databases in the demo.
|
|
|
|
Deliverables: target manifest, asset manifest, evidence index, repeatable baseline report.
|
|
|
|
Exit gate: another clean build reproduces the existing RNG and header results from the pinned input. Changed input is rejected clearly.
|
|
|
|
## 5. Phase B — recover the smallest engine dependency graph
|
|
|
|
1. Continue REA analysis at `Scenario::loadFactory` (`0x1000fa1d0`), `Map::load` (`0x1014c93f8`), and `Map::loadData` (`0x1014c9618`). These are candidate roots from symbol inventory; their complete behavior remains to be recovered.
|
|
2. Follow direct calls and identify virtual calls, callbacks, jump tables, global reads, static constructors, TLS accesses, allocations, and exceptions.
|
|
3. Inspect reachable startup dependencies from `GlobalContext::init` (`0x100161584`). Its current 151 direct callees and 643 basic blocks are an observed frontier, not the complete graph.
|
|
4. Recover the required object layouts, vtables, serialized field order, ownership rules, and initialization order using matching debug metadata where present and instructions where necessary.
|
|
5. Classify each dependency as: lift original code, implement a documented host adapter, exclude an optional service, or unresolved. Give every host adapter a concrete contract.
|
|
6. Build a coverage report: discovered reachable functions, translated functions, unsupported instruction classes, unresolved indirect targets, and unavailable imports. Do not equate function count with working gameplay.
|
|
7. Choose the smallest legal local test world and required base-data configuration. Establish desktop observations: load result, entity counts, tick number, inventory, and stable state summaries where accessible.
|
|
|
|
Deliverables: dependency graph, object-layout notes, import contracts, prioritized translation queue, small-world fixture description.
|
|
|
|
Exit gate: enough of the loader graph is understood to select the next executable slice and enumerate its blockers. No claim of graph completeness while indirect targets remain unresolved.
|
|
|
|
## 6. Phase C — extend the ARM64 lifter into a real compiler
|
|
|
|
Implement incrementally, guided by Phase B rather than attempting every ARM instruction first.
|
|
|
|
1. Separate decoding, instruction semantics, control-flow IR, validation, and WAT emission. Preserve original PC on every IR operation and in generated diagnostics.
|
|
2. Implement architectural integer semantics: register width and zero extension, SP versus zero register, signed/unsigned operations, shifts/rotates, multiplication/division, comparisons, and NZCV flags.
|
|
3. Add basic blocks, conditional/unconditional branches, loops, multiple returns, and switch/jump-table control flow. Use structured WASM regions where possible and an explicit block dispatcher where necessary.
|
|
4. Add direct guest calls with a defined register/stack ABI. Correctly preserve link register, callee-saved state, stack alignment, and nested/reentrant invocation.
|
|
5. Model indirect calls, virtual dispatch, function pointers, and tail calls through validated guest-code identities. Discover address-taken targets and their relocations; unresolved targets remain explicit failures.
|
|
6. Add PC-relative addresses, literal pools, global data references, signed and indexed loads/stores, and additional stack addressing modes.
|
|
7. Add floating point and the SIMD operations actually reached by the chosen slice. Preserve required rounding, conversion, comparison, and NaN behavior; test edge cases separately.
|
|
8. Define atomic and memory-order semantics before translating threaded paths. Initially run only paths whose single-threaded execution has been shown to preserve behavior.
|
|
9. Recover exception/unwind behavior. Choose and validate an explicit guest unwind mechanism or narrowly verified boundary replacements. The current status-returning header adapter is not a general C++ exception implementation.
|
|
10. Add build caching by source hash, compiler version, runtime ABI, and options. Split generated C into manageable units to control compiler time and memory.
|
|
|
|
For each new instruction family, run finite native-oracle comparisons on macOS, architectural edge cases, randomized inputs, guard-memory checks, and unsupported-encoding checks. Then translate a real function using that feature. Original instruction oracles remain desktop-only and are never embedded as executable game code in the iOS app.
|
|
|
|
Deliverables: CFG-capable compiler, instruction coverage report, guest-call ABI, diagnostic PC mapping, differential test suite.
|
|
|
|
Exit gate: a multi-function loader utility with branches and nested calls matches the desktop oracle and runs on the phone through AOT output.
|
|
|
|
## 7. Phase D — memory image and native runtime
|
|
|
|
1. Inventory Mach-O sections, initial data, zero-fill regions, rebases/binds, vtables, function pointers, constructor tables, and TLS. Reconstruct only the reachable required image, with explicit relocation records.
|
|
2. Decide guest address representation using measured requirements. Compare rebased offsets, segmented translation, and supported WASM memory configurations. Preserve 64-bit guest pointer semantics even if physical linear-memory offsets are narrower.
|
|
3. Validate the chosen WASM/WABT/wasm2c memory model early. The prototype's fixed 64 KiB memory is insufficient for a world; do not assume memory64 or multi-memory support without testing the pinned toolchain.
|
|
4. Implement guest heap, stack, globals, bounds checks, alignment behavior, allocation failure, ownership diagnostics, and configurable memory limits. Keep guest pointers separate from native pointers.
|
|
5. Implement required allocator/libc/math, time, entropy, filesystem, compression, logging, threading, synchronization, and TLS adapters with explicit ABI conversion.
|
|
6. Audit C/C++ runtime dependencies: constructors/destructors, RTTI, vtables, exceptions, and standard-library layouts. Reuse compatible native libraries only at demonstrated boundaries; matching names do not establish compatible object layouts.
|
|
7. Map app read-only resources and writable saves/configuration to the iOS sandbox. Support interrupted writes and useful error messages.
|
|
8. Implement deterministic host time/random inputs for replay tests, separate from UI and wall-clock services.
|
|
9. Verify build outputs contain no runtime code generator, executable guest-memory mapping, or dynamic game-code loading. Audit imports and any memory-protection APIs in addition to checking linked dependencies.
|
|
|
|
Deliverables: image loader/rebaser used at build time, guest memory manager, runtime ABI, import implementations and diagnostics.
|
|
|
|
Exit gate: a substantial translated subsystem allocates, accesses globals, calls native services, and exits cleanly on Mac and iPhone without unsupported imports or memory faults.
|
|
|
|
## 8. Phase E — local startup without Steam
|
|
|
|
1. Recover all reached methods and layouts of `SteamContextInterface`, its base implementation, and `DummySteamContext` before selecting replacements.
|
|
2. Route translated startup to local optional services at the interface boundary. Preserve the difference between unavailable Steam functionality and successful local functionality.
|
|
3. Handle both the factory (`0x100267594`) and the inlined initialization in `GlobalContext::init` beginning at `0x100161608`. The graphical app must select local services independently of headless mode.
|
|
4. Implement local storage, locale lookup, native text entry, and required callbacks. Exclude cloud, overlay, achievements, matchmaking, Steam identity, and Steam networking from the selected path.
|
|
5. Remove Steam bindings and references from generated artifacts where reachable code no longer requires them. Audit imports, dependency lists, call targets, and runtime traces.
|
|
6. Reach the minimal engine initialization checkpoint in a native harness before connecting the graphical shell.
|
|
|
|
Exit gate: the selected startup runs with no Steam library present, performs no Steam calls, and reaches the required local initialization state. Current prototype audits alone do not satisfy this engine gate.
|
|
|
|
## 9. Phase F — complete world loading on the Mac
|
|
|
|
1. Extend archive handling from a nine-byte probe to the loader's complete required members/streams. Validate full decompression, checksums where applicable, size limits, member naming, and failure cleanup.
|
|
2. Recover deserializer contracts, version handling, references/object IDs, allocation order, and error propagation.
|
|
3. Load required base prototypes and script state. Identify the embedded scripting runtime and its host interface; translate or replace it only after compatibility is established.
|
|
4. Reconstruct surfaces, chunks, tiles, entities, inventories, recipes, forces, and other structures reached by the fixture. Reject unsupported saves with diagnostics rather than partially pretending to load them.
|
|
5. Run the translated loader in a headless macOS harness first. Report map dimensions, entity counts/types, inventory totals, tick, and unresolved references.
|
|
6. Compare those results against the original game's observable state and decoded format evidence. Add intermediate checkpoints to find the first divergence.
|
|
7. Test truncated/corrupt streams, allocation failures, unsupported data/mod sets, and cleanup after failed loads.
|
|
|
|
Exit gate: the complete controlled world loads into translated engine state with validated reference integrity and matching observable summaries. This stage still does not imply simulation works.
|
|
|
|
## 10. Phase G — deterministic simulation
|
|
|
|
1. Recover the scheduler, tick entry points, entity update dispatch, command processing, scripting callbacks, and required worker behavior.
|
|
2. Advance one tick with fixed inputs; compare state checkpoints with desktop observations or suitable native harnesses.
|
|
3. Add minimal systems in dependency order: character movement, inventory transactions, mining, crafting, placement, resource production, belt movement, and required power/fluid systems if the fixture needs them.
|
|
4. Verify RNG state, entity identifiers, event ordering, numeric behavior, and script state across repeated runs.
|
|
5. Introduce replayable command logs and periodic canonical state summaries. Record exactly what each summary covers; a partial digest is not a whole-engine equivalence proof.
|
|
6. Run 1, 60, 600, and then at least 3,600 ticks. Investigate the first differing checkpoint before increasing duration.
|
|
7. Add a headless iPhone harness using the same world and command log. Compare its summaries with the translated Mac build.
|
|
|
|
Exit gate: the selected factory actions execute repeatably across Mac and iPhone, with matching validated state summaries and no unresolved required execution paths.
|
|
|
|
## 11. Phase H — bring the complete headless world to iPhone
|
|
|
|
1. Package the required translated modules and import only the recorded user-supplied assets needed by the fixture.
|
|
2. Add asynchronous loading, progress, cancellation, diagnostics, and clean recovery from unsupported input.
|
|
3. Run full-world loading and replay on the physical phone. Track peak/resident memory, load duration, tick duration, and failures.
|
|
4. Handle background/foreground transitions without advancing uncontrolled simulation time. Define autosave behavior and safe cancellation points.
|
|
5. Measure memory-pressure behavior and graceful failure before enabling larger worlds.
|
|
|
|
Exit gate: the phone loads the full fixture and runs the validated simulation without graphics. Investigate platform differences here before adding renderer complexity.
|
|
|
|
## 12. Phase I — graphics, audio, and touch
|
|
|
|
1. Recover the renderer boundary and active backend from evidence. Existing Metal linkage alone does not establish a reusable iOS renderer.
|
|
2. Choose a backend after recovery: adapt an evidenced Metal command interface, or implement a native Metal renderer over a recovered render-command/state boundary. Avoid translating every desktop graphics API unless required.
|
|
3. Implement texture/sprite loading, atlases, camera transforms, batching, layering, terrain, entities, animation, and required effects. Recover shaders and rendering state as needed.
|
|
4. Present through a native iOS Metal surface with orientation, safe-area, scale, resize, and lifecycle handling.
|
|
5. Implement input as engine commands: pan/zoom, selection, movement, inventory, crafting, build preview, place/remove, and pause. Resolve gesture conflicts and provide native text input where needed.
|
|
6. Recover and implement the required audio mixer/device boundary, including pause/resume and interruptions. Keep audio work off the simulation/UI critical paths.
|
|
7. Add loading/error UI and visible diagnostics suitable for device testing.
|
|
|
|
Exit gate: demonstrate the complete target gameplay sequence on the phone with correct world interaction and stable rendering. Compare captured scenes against the desktop reference, allowing deliberate mobile UI differences.
|
|
|
|
## 13. Phase J — save writing and resume
|
|
|
|
1. Recover the serializer and ownership dependencies for the selected world state.
|
|
2. Write to a temporary sandbox file, finalize the archive, and atomically replace the destination only after success.
|
|
3. Validate translated save → translated reload first, comparing defined state summaries and replay continuation.
|
|
4. Where original-format compatibility is implemented, test translated save → original Factorio load, and original save → translated reload. Declare compatibility only for tested versions/data configurations.
|
|
5. Test interruption, low storage, corrupt output, cancellation, repeated saves, and app termination. Preserve the last good save.
|
|
6. Add Files import/export and clear local save selection. Continue to obtain explicit approval for any new private-file transfers outside previously authorized scope.
|
|
|
|
Exit gate: play, save, terminate, relaunch, and resume the same factory on the iPhone without lost state. Record interoperability separately from local round-trip success.
|
|
|
|
## 14. Phase K — performance and reliable builds
|
|
|
|
1. Establish a fixed benchmark world, camera, command replay, asset configuration, and device conditions.
|
|
2. Profile simulation, indirect-call dispatch, bounds/address translation, allocation, renderer batching, texture memory, and generated C size/build time.
|
|
3. Optimize measured hotspots. Re-run equivalence checks after changes to translated semantics, call dispatch, memory layout, or floating point behavior.
|
|
4. For the initial small world, use provisional targets of sustained 60 simulation ticks/second and at least 30 rendered frames/second. Revise these explicitly if feasibility measurements require it; neither is currently achieved or promised.
|
|
5. Run a 30-minute device session recording tick/frame percentiles, peak memory, thermal degradation, battery use, and crashes. Set memory budgets from observed safe device behavior.
|
|
6. Pin dependencies, add one documented build command, preserve compiler manifests, and audit the final application for Steam dependencies and runtime code generation.
|
|
7. Test clean installation, asset import, save migration policy, background/resume, offline use, and diagnostic export. Keep personal assets, saves, and signing material out of distributable tool sources.
|
|
|
|
Exit gate: reproducible signed development build plus a device acceptance report covering all target actions, saves, performance, known limits, and exact tested versions. Wider distribution is a separate decision.
|
|
|
|
## 15. Order, checkpoints, and feasibility decisions
|
|
|
|
Critical path: A → B → C/D → E → F → G → H → I → J → K. Discovery and runtime work may inform one another; revisit graph coverage after every new executable path. Do not finish the entire engine before validating a small end-to-end slice.
|
|
|
|
| Checkpoint | Concrete result | Decision if it fails |
|
|
| --- | --- | --- |
|
|
| Existing baseline | Reproducible RNG/header prototype | Fix provenance/build drift first |
|
|
| General translation | Branching multi-function subsystem on iPhone | Reassess compiler design before translating more functions |
|
|
| Runtime feasibility | Required memory/ABI model works within device limits | Revisit memory representation and supported scope |
|
|
| Loader | Complete small-world state on Mac | Resolve data/runtime dependencies before graphics |
|
|
| Simulation | Repeatable factory actions on both platforms | Find first state divergence |
|
|
| Interactive slice | Touch-controlled rendered factory on phone | Profile and resolve platform boundary issues |
|
|
| Persistence | Save/relaunch/resume succeeds | Correct serializer before broadening compatibility |
|
|
| Acceptance | Sustained device session meets documented targets | Optimize measured bottlenecks or explicitly reduce scope |
|
|
|
|
The largest unknowns are dependency-graph size, indirect control flow, C++ unwinding/object ABI, scripting/prototype initialization, memory overhead, and renderer compatibility. If a gate shows an impractical amount of reconstruction, report the evidence and revise the plan with the user; do not substitute a mock game for the requested port.
|
|
|
|
## 16. Immediate next implementation batch
|
|
|
|
1. Add run IDs, timestamps, input hashes, and build identities to device reports.
|
|
2. Freeze the current target and asset manifests; reproduce the current checks once.
|
|
3. Reopen the unchanged ARM64 slice through REA MCP and export bounded dossiers for the three loader roots and their first required dependencies.
|
|
4. Identify one small branching deserialization function with a tractable native oracle.
|
|
5. Implement CFG/basic-block IR, integer flags/comparisons, and the branch forms needed by that function.
|
|
6. Translate it, compare against original ARM64 behavior on macOS, and execute its AOT output on the iPhone.
|
|
7. Publish a coverage/blocker report and select the next dependency from observed evidence.
|
|
|
|
This batch proves the essential next capability: progressing from isolated straight-line primitives to a real branching engine subsystem.
|
|
|
|
## 17. First implementation batch status (2026-10-20)
|
|
|
|
Completed: installed-data and target manifest tooling, identified device reports, initial bounded control flow/NZCV/select support, loader direct-callee recovery, and automatic translation of the branching MapHeader constructor with 20,000 native comparisons. Compiler fixtures pass 1,860,000 comparisons. See [translation-progress.md](translation-progress.md) for physical-device results and exact remaining scope. General guest-to-guest calls, real-save stream positioning for MapHeader, and complete world loading remain pending.
|
|
|
|
## 18. Stream positioning and guest-call batch (2026-10-20)
|
|
|
|
Implemented bounded direct guest calls/returns, a variable-length ZIP prefix API, and a restricted 2.0.77 scenario-envelope reader. The approved real save yields MapHeader at decoded offset 333; original ARM64 constructor and translated values agree. General engine initialization, scalable memory/import support, nonempty startup settings and complete world loading remain pending. See [save-layout-progress.md](save-layout-progress.md).
|
|
|
|
## 19. Allocation and initialization batch (2026-10-20)
|
|
|
|
Translated original TargetDeserialiser and its type-aware memory-pool constructor as a shared guest program. Added fixed configurable memory, a bounded 16-byte-aligned arena, an explicit x0-returning host adapter, mapped static-page identities, ASR/logical immediates and a restricted zero-vector store idiom. The original graph matches 512 desktop ARM64 object graphs; 66,048 new instruction comparisons and the existing 80,000 guest-program comparisons pass. Signed build 4 initializes all 64 pool objects on the iPhone using four allocations and 2,104 guest heap bytes. Same-launch save-header regressions pass. See [initialization-progress.md](initialization-progress.md).
|
|
|
|
This completes an initial allocation service, not general engine initialization or a complete allocator. MapDeserialiser integration, full image/global mapping, virtual calls, destruction/unwinding, MapGenSettings, entity loading, rendering and gameplay remain ahead.
|
|
|
|
## 20. MapDeserialiser integration batch (2026-10-20)
|
|
|
|
Translated the original MapDeserialiser C2 initialization CFG with its actual TargetDeserialiser C1 dependency and shared original pool constructor: 662 instructions in one bounded guest program. Added definitely-zero vector dataflow, zero Q-store pairs/writeback and bounded bzero. Desktop checks pass 512 original object graphs, supplied/owned target paths, nine truncations, eight original version-error branches and twelve allocation failures. Added 4,096 native vector-addressing comparisons; prior compiler and header regressions pass.
|
|
|
|
Signed build 5 passes the original initialization graph on the iPhone from both a controlled prefix and the approved actual save's prefix, consuming nine stream bytes and 10,712 guest heap bytes through twelve allocations. The full world remains unloaded. All new source input ranges now match complete exported REA byte observations, closing the prior batch's pending byte-export check. See [deserialiser-progress.md](deserialiser-progress.md).
|
|
|
|
Next: MapGenSettings and subsequent Map::loadData dependencies. The current modules are isolated initialization probes, not a persistent integrated loader. General virtual dispatch, destruction/unwinding, full static-image mapping, object references, entities, rendering and gameplay remain pending.
|