diff --git a/README.md b/README.md index 0ca9b0e..3aedada 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,135 @@ -# Factorio on iPhone (personal port) +# Factorio on iPhone -Run your own copy of **Factorio 2.0.77** natively on an iPhone. The original macOS ARM64 game -code from your local install runs unchanged on iOS; this repository supplies the glue: a Mach-O -retargeting step, a small set of shims for the macOS APIs iOS lacks, touch and keyboard -controls, and build/sign/install tooling. +Play your own copy of **Factorio** on your iPhone. You build the app on your Mac from the +Factorio you already own on Steam, and install it on your phone with a cable. -**This repository contains no game files.** You need a legitimately owned copy of Factorio for -macOS (the Steam build). The build reads the executable and data from your local install and -installs them only on your own device. This is an unofficial, personal project and is not -affiliated with or endorsed by Wube Software. +This project contains **no game files**. Everything comes from your own installed copy. It is an +unofficial fan project, not made or supported by Wube Software. -## Status +--- -Tested on an iPhone 15 Pro (iOS 27.0.1) with Xcode 26.6 (iOS SDK 26.5), using the Steam macOS -build of Factorio 2.0.77 (base game). +## What you need -| Area | State | -| --- | --- | -| Startup to main menu | ~16 s | -| Rendering | Metal, native panel resolution (2556×1179 on iPhone 15 Pro) | -| New game, playing, autosave | Working | -| Audio | Working (plays with the silent switch on, mixes with other apps) | -| Touch controls, on-screen overlay | Working | -| Hardware keyboard, iOS keyboard with extra keys | Working | -| Loading saves, long sessions, large factories | Not yet verified | -| Mods, multiplayer, Space Age | Not tested | +- A **Mac** (tested on Apple Silicon). +- **Factorio 2.0.77 for Mac, bought on Steam** and installed. +- An **iPhone** (tested on iPhone 15 Pro) and its **cable**. +- A free or paid **Apple account**. +- About **3 GB** of free space on the phone. -No JIT and no emulation: the game's own ARM64 machine code runs natively, signed with your -developer certificate. +--- -## Requirements +## Install, step by step -- A Mac (tested on Apple Silicon) with **Xcode**, including the iOS platform, and **Python 3**. -- **Factorio 2.0.77 for macOS from Steam**, installed at the default Steam location (or pass - `--game`). The build checks the exact executable hash and refuses other versions. -- An iPhone with an A-series chip that supports Metal, **Developer Mode** enabled, paired with - the Mac. -- An Apple developer account signed into Xcode, with a development provisioning profile that - covers the app's bundle identifier (a wildcard team profile works). See - [docs/building.md](docs/building.md#signing). +### Step 1: Install Xcode -## Quick start +1. Open the **App Store** on your Mac, search for **Xcode** and install it (it is large and takes + a while). +2. Open Xcode once. When it asks which platforms to install, choose **iOS**. + +### Step 2: Sign in to Xcode with your Apple account + +1. In Xcode open **Xcode → Settings → Accounts**. +2. Click **+**, choose **Apple ID** and sign in. + +### Step 3: Connect your iPhone + +1. Plug the iPhone into the Mac, unlock it, and tap **Trust**. +2. On the iPhone open **Settings → Privacy & Security → Developer Mode**, turn it **on**, and let + the phone restart. (If you don't see **Developer Mode** yet, do Step 4 first and come back.) + +### Step 4: Let Xcode set up signing (one time only) + +Your iPhone only runs apps signed with your Apple account. A tiny empty Xcode project sets this +up once: + +1. In Xcode choose **File → New → Project… → iOS → App → Next**. +2. Fill in: + - **Product Name:** `factorio` + - **Team:** your Apple account + - **Organization Identifier:** `com.` followed by your name, for example `com.alexsmith` +3. Click **Next** and save it anywhere. +4. At the top of the Xcode window, pick your **iPhone** as the run destination and press + **▶ (Run)**. A blank white app appears on the phone; that means it worked. If the phone says + **Untrusted Developer**, open **Settings → General → VPN & Device Management**, tap your + account and choose **Trust**, then press **▶** again. + +You can now close Xcode and delete that project. The real Factorio app will replace the blank one. + +### Step 5: Download this project + +Open the **Terminal** app on your Mac and run: ```sh -# 1. Build, sign and install the app; copy the game data (~1.5 GB) into the app once. -python3 tools/build_native.py --install --push-data - -# 2. Launch with the console attached (or tap the Factorio icon on the home screen). -python3 tools/build_native.py --no-build --launch +git clone https://git.entityseven.com/entityseven/factorio-ios.git +cd factorio-ios ``` -Later rebuilds only need `python3 tools/build_native.py --install`. Game data and saves live in -the app's Documents folder and survive reinstalls. Full instructions, options and -troubleshooting: [docs/building.md](docs/building.md). +### Step 6: Install Factorio on the phone -## Controls +Keep the iPhone **plugged in and unlocked**, then run: -| Input | Action | +```sh +./install.sh +``` + +This builds the app, installs it, and copies the game data (about 1.5 GB) to the phone. It takes +a few minutes. When it says **Done**, the **Factorio** icon is on your home screen. + + +--- + +## Playing + +- **Tap** = left click. **Hold** = right click (hold on ore or trees to mine them). +- **Two-finger drag** = zoom and scroll. +- **Left stick** = walk. Buttons **E Q R F M T**, **Esc**, **Tab**, **⇧**, **Ctrl** = the same keys + as on a computer. +- **⌨︎** opens the keyboard. **◐** hides the on-screen buttons. +- A Bluetooth keyboard works too. + +The text is small at first: in the game open **Settings → Interface** and set **UI scale** to +about **175–200 %**. + +The game takes about 15 seconds to start. Your saves stay on the phone, also when you update the +app. + +All controls: [docs/controls.md](docs/controls.md). + +--- + +## Updating + +```sh +cd factorio-ios +git pull +./install.sh +``` + +--- + +## Problems? + +| What you see | What to do | | --- | --- | -| Tap / drag | Left mouse button | -| Long press (hold) | Right mouse button held: mining, removing; drag to keep going | -| Two-finger drag | Mouse wheel (zoom, scroll lists) | -| Left stick | W A S D movement | -| E Q R F M T buttons | Inventory, pipette, rotate, pick up, map, technologies | -| ⇧ and Ctrl buttons | Sticky modifiers for the next click or key | -| Esc, Tab | Escape and Tab | -| ⌨︎ | iOS keyboard plus a bar with Esc, Tab, Ctrl, Alt, ⇧, arrows, Home/End, PgUp/PgDn, F1–F12 | -| ◐ | Hide or show the on-screen controls | +| `Factorio not found` | Install Factorio from Steam on this Mac and let it finish downloading. | +| `wrong Factorio version` | This version of the port needs Factorio **2.0.77**. In Steam, open the game's **Properties** and keep it on that version. | +| `no provisioning profile found` | Do **Step 4** again. The Xcode project must be named `factorio`. | +| `no iPhone found` | Plug in the phone, unlock it, tap **Trust**, and check **Developer Mode** (Step 3). | +| `Xcode is not installed` / `iOS platform is missing` | Do **Step 1**. | +| `device was not unlocked` | Unlock the phone and run `./install.sh` again. | +| App says **Game data missing** | Run `./install.sh` again with the phone unlocked. | +| App stops opening after 7 days | Free Apple accounts sign apps for 7 days. Run `./install.sh` again; your saves are kept. | +| The game closes while playing | Run `python3 tools/build_native.py --no-build --launch` and send the output as a bug report. | -A Bluetooth or USB keyboard works directly with Factorio's normal key bindings. Details: -[docs/controls.md](docs/controls.md). +--- -Factorio's interface is small at native resolution; set **Settings → Interface → UI scale** to -around 175–200 %. +## More information -## How it works +- [docs/building.md](docs/building.md): advanced build options and troubleshooting. +- [docs/native-port.md](docs/native-port.md): how the port works. +- [docs/research/](docs/research/static-translation.md): earlier research (ARM64 → WASM + translation). -1. `tools/retarget.py` turns the unmodified macOS executable into an iOS-loadable dynamic - library by editing only Mach-O header fields (platform, file type, page-zero size, - framework paths). It verifies that no byte after the header changes. -2. `platform/native/shim/` implements the ~150 macOS symbols iOS lacks: an AppKit layer on - UIKit sized to the SDL 2.24 Cocoa backend Factorio links statically, CoreGraphics display - and CoreAudio device models, PNG decoding through Wuffs, Metal device properties, and - stubs for Steam, Carbon, ForceFeedback, OpenCL and LDAP. -3. A small host app starts UIKit, loads the game library and calls its original `main` on the - main thread with a larger stack. - -The design, every shim, and the verification behind it are described in -[docs/native-port.md](docs/native-port.md). - -## Repository layout - -| Path | Contents | -| --- | --- | -| `tools/build_native.py` | Build, sign, install, copy data, launch | -| `tools/retarget.py` | Header-only Mach-O retargeting of the game executable | -| `tools/native_audit.py` | Lists game imports that iOS lacks (needs shims) | -| `tools/symbolize_native.py` | Symbolizes crash reports against the game's debug symbols | -| `tools/make_icon.m` | Builds the app icon from the game's own icon | -| `platform/native/` | iOS host app and macOS API shims | -| `tests/native/` | PNG decoder parity test against macOS ImageIO | -| `docs/` | Build guide, controls, architecture | -| `docs/research/`, `runtime/`, `tests/*.py`, other `tools/` | Earlier static-translation research (ARM64 → WASM → native), kept for reference | - -## Third-party software - -- [Wuffs](https://github.com/google/wuffs) v0.3.4 (Apache-2.0): PNG decoding. Downloaded at - build time into `third_party/` and checked against a pinned SHA-256; not vendored. -- [SDL](https://github.com/libsdl-org/SDL) 2.24 (zlib): statically linked inside the game - binary; its source was used as a reference for the AppKit shim and is not included. -- [WABT](https://github.com/WebAssembly/wabt) (Apache-2.0): used only by the research track. - -Factorio is a trademark of Wube Software Ltd. All game code and assets remain theirs. +**Third-party software:** [Wuffs](https://github.com/google/wuffs) (Apache-2.0) is downloaded +during the build to decode PNG images. Factorio itself uses [SDL](https://github.com/libsdl-org/SDL) +(zlib). Factorio is a trademark of Wube Software Ltd.; all game code and assets belong to them. diff --git a/docs/building.md b/docs/building.md index dfb29a0..7a23e35 100644 --- a/docs/building.md +++ b/docs/building.md @@ -32,28 +32,33 @@ The default game location is ## Signing The app is signed with your **Apple Development** certificate and a development provisioning -profile. The build script picks them automatically: +profile from `~/Library/Developer/Xcode/UserData/Provisioning Profiles`. The build chooses: -- it looks in `~/Library/Developer/Xcode/UserData/Provisioning Profiles` for a profile whose - App ID is exactly the app's bundle identifier, or a wildcard (`TEAMID.*`) profile; -- it signs with the keychain identity whose certificate is embedded in that profile; -- the team ID comes from the profile (override with `--team` / `FX_TEAM`). +1. with `--bundle-id` (or `FX_BUNDLE_ID`): a profile for exactly that App ID, else a wildcard + (`TEAMID.*`) profile; +2. otherwise: the newest profile whose App ID contains `factorio`, and that App ID becomes the + app's bundle identifier; +3. otherwise: a wildcard profile, with bundle identifier `local.factorio.native`. -If you have no suitable profile yet, let Xcode create one: create any new iOS App project, set -its bundle identifier to `local.factorio.native` (or your own identifier, then pass the same one -to `--bundle-id`), choose your team under *Signing & Capabilities* with automatic signing, select -your iPhone and build it once. Xcode then downloads a profile that the script can use. +It signs with the keychain identity whose certificate is embedded in the chosen profile; the team +comes from the profile (override with `--team` / `FX_TEAM`). Expired profiles are ignored. -Free Apple accounts can sign apps for their own devices, but the installed app expires after -seven days and has to be reinstalled. This has only been tested with a paid account. +The README's step 4 creates a suitable profile: an empty Xcode iOS App project named `factorio`, +with automatic signing for your team, run once on the iPhone. Its App ID +(`com..factorio`) is unique to you, which Apple requires for explicit App IDs. + +Free Apple accounts work the same way, but the installed app expires after seven days and has to +be reinstalled with `./install.sh` (saves are kept). This has been tested with a paid account +only. ## 3. Build and install ```sh -python3 tools/build_native.py --install --push-data +./install.sh # same as: python3 tools/build_native.py --install --push-data ``` -What happens: +`install.sh` runs the steps below with short progress messages and writes the full output to +`build/install.log`. What happens: 1. **Inputs.** The ARM64 slice of the game executable is extracted with `lipo` into `build/factorio.arm64`. The Wuffs PNG decoder source (v0.3.4) is downloaded into @@ -90,7 +95,7 @@ installing takes the longest; the main menu normally appears after about 16 seco | --- | --- | --- | | `--game PATH` | `FX_GAME` | Steam location (see above) | | `--team ID` | `FX_TEAM` | Taken from the provisioning profile | -| `--bundle-id ID` | `FX_BUNDLE_ID` | `local.factorio.native` | +| `--bundle-id ID` | `FX_BUNDLE_ID` | From the provisioning profile (see [Signing](#signing)) | | `--device ID` | `FX_DEVICE` | First paired device | | `--no-build` | | Install/launch the existing bundle | @@ -110,7 +115,7 @@ files under the phone → Files → Factorio. A save from the Mac can be copied ```sh xcrun devicectl device copy to --device --domain-type appDataContainer \ - --domain-identifier local.factorio.native \ + --domain-identifier \ --source ~/Library/Application\ Support/factorio/saves/my-save.zip \ --destination Documents/factorio/write/saves/my-save.zip ``` @@ -119,8 +124,8 @@ xcrun devicectl device copy to --device --domain-type appDataContainer \ | Symptom | Cause and fix | | --- | --- | -| `unexpected input …; only the inspected 2.0.77 ARM64 slice is supported` | The installed game is a different version. Install 2.0.77, or delete `build/factorio.arm64` if it was extracted from another install. | -| `no provisioning profile covers …` | See [Signing](#signing). | +| `wrong Factorio version` | The installed game is not 2.0.77. Select that version in Steam (game Properties → Betas) or pass `--game` to a 2.0.77 install. | +| `no provisioning profile found` | See [Signing](#signing). | | `Unable to launch … because the device was not, or could not be, unlocked` | Unlock the phone and run the launch again. | | App shows "Game data missing" | Run `python3 tools/build_native.py --no-build --push-data`. | | App closes during play | Run with `--launch`, then `python3 tools/symbolize_native.py` to turn the `[fx-crash]` lines in `build/native/console.log` into function names. If there is no crash report, iOS may have stopped it for memory; check Settings → Privacy & Security → Analytics & Improvements → Analytics Data for a `JetsamEvent` report. | diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..e18b860 --- /dev/null +++ b/install.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Builds Factorio for iPhone from your local Steam copy, installs it on the connected iPhone and +# copies the game data. Safe to run again: data that is already on the phone is skipped and saves +# are kept. Extra arguments are passed to tools/build_native.py (see docs/building.md). +cd "$(dirname "$0")" || exit 1 +mkdir -p build +LOG=build/install.log +: > "$LOG" + +fail() { + echo + echo "Failed: $1" + echo "Details are in $LOG" + exit 1 +} + +step() { + # step "message" args... : run build_native.py with args, logging its output. + message=$1; shift + printf '%s... ' "$message" + if python3 tools/build_native.py "$@" >> "$LOG" 2>&1; then + echo "ok" + else + echo + if grep -q "not, or could not be, unlocked" "$LOG"; then + fail "device was not unlocked. Unlock the iPhone and run ./install.sh again." + fi + # build_native.py ends with a one-line explanation of what went wrong. + reason=$(grep -v '^+' "$LOG" | grep -v '^\s*$' | tail -n 3) + fail "$reason" + fi +} + +command -v xcrun > /dev/null 2>&1 || fail "Xcode is not installed (README step 1)." +xcrun --sdk iphoneos --show-sdk-path > /dev/null 2>&1 || fail "Xcode's iOS platform is missing (README step 1)." +command -v python3 > /dev/null 2>&1 || fail "python3 not found; open Xcode once to finish installing its tools." + +step "Building the app" "$@" +step "Installing on the iPhone" --no-build --install "$@" +step "Copying game data to the iPhone (first time takes a few minutes)" --no-build --push-data "$@" +echo +echo "Done. Open Factorio from the iPhone home screen." diff --git a/tools/build_native.py b/tools/build_native.py index b6e7b41..dff66e5 100644 --- a/tools/build_native.py +++ b/tools/build_native.py @@ -12,7 +12,8 @@ Usage: Options (each also readable from the environment variable shown): --game PATH factorio.app/Contents of a local install (FX_GAME; default: Steam location) --team ID Apple developer team ID (FX_TEAM; default: from your provisioning profiles) - --bundle-id ID app bundle identifier (FX_BUNDLE_ID; default: local.factorio.native) + --bundle-id ID app bundle identifier (FX_BUNDLE_ID; default: the App ID of a provisioning profile + containing "factorio", else local.factorio.native with a wildcard profile) --device ID devicectl device identifier or name (FX_DEVICE; default: first paired device) The game executable is the unmodified ARM64 slice with only Mach-O header changes (see @@ -24,6 +25,7 @@ import json import os import plistlib import subprocess +import time import sys import urllib.request from pathlib import Path @@ -36,7 +38,9 @@ SRC = ROOT / "platform/native" MIN_IOS = "17.0" # Set from command-line options / environment in main(). GAME = STEAM_GAME -BUNDLE_ID = "local.factorio.native" +BUNDLE_ID = None # resolved from the provisioning profile unless given +DEFAULT_BUNDLE_ID = "local.factorio.native" +SLICE_SHA256 = "2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7" # 2.0.77 Steam, arm64 TEAM = None SLICE = ROOT / "build/factorio.arm64" @@ -113,12 +117,14 @@ def build_icon(): def prepare_inputs(): """Extract the ARM64 slice from the local install and fetch the pinned Wuffs source.""" - if not SLICE.exists(): - binary = GAME / "MacOS/factorio" - if not binary.exists(): - raise SystemExit(f"Factorio executable not found at {binary}; pass --game PATH/factorio.app/Contents") - SLICE.parent.mkdir(parents=True, exist_ok=True) - run(["lipo", "-thin", "arm64", binary, "-output", SLICE]) + binary = GAME / "MacOS/factorio" + if not binary.exists(): + raise SystemExit(f"Factorio not found at {GAME}.\nInstall Factorio from Steam on this Mac, or pass --game PATH/factorio.app/Contents.") + SLICE.parent.mkdir(parents=True, exist_ok=True) + # Re-extract every build so a Steam update is noticed. + run(["lipo", "-thin", "arm64", binary, "-output", SLICE]) + if hashlib.sha256(SLICE.read_bytes()).hexdigest() != SLICE_SHA256: + raise SystemExit("wrong Factorio version: this port needs Factorio 2.0.77 for macOS from Steam.") if not WUFFS.exists() or hashlib.sha256(WUFFS.read_bytes()).hexdigest() != WUFFS_SHA256: WUFFS.parent.mkdir(parents=True, exist_ok=True) print("+ fetch", WUFFS_URL, flush=True) @@ -164,24 +170,42 @@ def build(): sign() -def find_profile(): - """A development profile covering BUNDLE_ID (explicit or wildcard), optionally for TEAM.""" +def load_profiles(): folder = Path.home() / "Library/Developer/Xcode/UserData/Provisioning Profiles" - candidates = [] + profiles = [] for path in sorted(folder.glob("*.mobileprovision")): plist = plistlib.loads(subprocess.check_output(["security", "cms", "-D", "-i", str(path)])) team = plist["TeamIdentifier"][0] - app_id = plist["Entitlements"]["application-identifier"] if TEAM and team != TEAM: continue - if app_id == f"{team}.{BUNDLE_ID}": - candidates.insert(0, (path, plist, team)) # explicit App ID wins over wildcard - elif app_id == f"{team}.*": - candidates.append((path, plist, team)) - if not candidates: - raise SystemExit(f"no provisioning profile covers {BUNDLE_ID}" + (f" for team {TEAM}" if TEAM else "") + - ". Create one in Xcode (any project, automatic signing, same team) or pass --team.") - return candidates[0] + if plist.get("ExpirationDate") and plist["ExpirationDate"].timestamp() < time.time(): + continue + bundle = plist["Entitlements"]["application-identifier"].split(".", 1)[1] + profiles.append((path, plist, team, bundle)) + return profiles + + +def find_profile(): + """Choose the signing profile and the bundle identifier it allows. + + Preference: an explicit App ID equal to --bundle-id; otherwise an explicit App ID containing + "factorio" (what the README's one-time Xcode project creates); otherwise a wildcard profile. + """ + global BUNDLE_ID + profiles = load_profiles() + if BUNDLE_ID: + matches = [p for p in profiles if p[3] == BUNDLE_ID] + [p for p in profiles if p[3] == "*"] + else: + named = sorted((p for p in profiles if "factorio" in p[3].lower() and p[3] != "*"), + key=lambda p: p[1].get("CreationDate"), reverse=True) + matches = named + [p for p in profiles if p[3] == "*"] + if not matches: + raise SystemExit("no provisioning profile found for the app.\n" + "Do README step 4 once (an Xcode project named \"factorio\" run on your iPhone).") + path, plist, team, bundle = matches[0] + if not BUNDLE_ID: + BUNDLE_ID = bundle if bundle != "*" else DEFAULT_BUNDLE_ID + return path, plist, team def signing_identity(profile): @@ -222,7 +246,7 @@ def device(requested=None): for d in data["result"]["devices"]: if d.get("connectionProperties", {}).get("pairingState") == "paired": return d["identifier"] - raise SystemExit("no paired device") + raise SystemExit("no iPhone found: plug it in, unlock it, tap Trust, and turn on Developer Mode.") def main(): @@ -234,10 +258,12 @@ def main(): parser.add_argument("--no-build", action="store_true") parser.add_argument("--game", default=os.environ.get("FX_GAME", str(STEAM_GAME))) parser.add_argument("--team", default=os.environ.get("FX_TEAM")) - parser.add_argument("--bundle-id", default=os.environ.get("FX_BUNDLE_ID", BUNDLE_ID)) + parser.add_argument("--bundle-id", default=os.environ.get("FX_BUNDLE_ID")) parser.add_argument("--device", default=os.environ.get("FX_DEVICE")) args = parser.parse_args() GAME, BUNDLE_ID, TEAM = Path(args.game), args.bundle_id, args.team + find_profile() + print(f"+ bundle identifier {BUNDLE_ID}", flush=True) if not args.no_build: build() if args.install or args.push_data or args.launch: