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
+15
@@ -0,0 +1,15 @@
|
||||
# Generated output, including anything derived from the game binary or data.
|
||||
/build/
|
||||
# Fetched tool sources (see README: wabt, SDL reference, Wuffs are fetched at pinned versions).
|
||||
/third_party/
|
||||
# Local reverse-engineering evidence exported from the user's installed copy.
|
||||
/evidence/
|
||||
/rea-evidence.json
|
||||
# Generated by xcodegen from platform/ios/project.yml.
|
||||
/platform/ios/FactorioPort.xcodeproj/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
*.ipa
|
||||
*.dSYM/
|
||||
*.mobileprovision
|
||||
@@ -0,0 +1,114 @@
|
||||
# Factorio on iPhone (personal port)
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
## 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).
|
||||
|
||||
| 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 |
|
||||
|
||||
No JIT and no emulation: the game's own ARM64 machine code runs natively, signed with your
|
||||
developer certificate.
|
||||
|
||||
## Requirements
|
||||
|
||||
- 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).
|
||||
|
||||
## Quick start
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | Action |
|
||||
| --- | --- |
|
||||
| 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 |
|
||||
|
||||
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 %.
|
||||
|
||||
## How it works
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Building, installing and running
|
||||
|
||||
All commands run from the repository root on the Mac. Nothing needs to be installed with pip;
|
||||
the scripts use only the Python standard library and Xcode's command-line tools.
|
||||
|
||||
## 1. Prepare the Mac
|
||||
|
||||
1. Install **Xcode** from the App Store, open it once, and install the **iOS** platform when it
|
||||
asks (Xcode → Settings → Components).
|
||||
2. Sign into your Apple developer account: Xcode → Settings → Accounts.
|
||||
3. Install **Factorio for macOS through Steam** and let it finish downloading. The build expects
|
||||
version **2.0.77**; it checks the SHA-256 of the executable's ARM64 slice
|
||||
(`2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7`) and stops if it differs. Turning off automatic Steam updates for the game
|
||||
keeps the build working until the port is updated for a newer version.
|
||||
|
||||
The default game location is
|
||||
`~/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents`. Pass
|
||||
`--game /path/to/factorio.app/Contents` (or set `FX_GAME`) if yours is elsewhere.
|
||||
|
||||
## 2. Prepare the iPhone
|
||||
|
||||
1. Connect the iPhone by cable, unlock it and tap **Trust** for this Mac.
|
||||
2. Enable **Developer Mode**: Settings → Privacy & Security → Developer Mode, then restart.
|
||||
3. Check that the Mac sees it:
|
||||
|
||||
```sh
|
||||
xcrun devicectl list devices
|
||||
```
|
||||
|
||||
The phone should be listed as `available (paired)`.
|
||||
|
||||
## Signing
|
||||
|
||||
The app is signed with your **Apple Development** certificate and a development provisioning
|
||||
profile. The build script picks them automatically:
|
||||
|
||||
- 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`).
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 3. Build and install
|
||||
|
||||
```sh
|
||||
python3 tools/build_native.py --install --push-data
|
||||
```
|
||||
|
||||
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
|
||||
`third_party/wuffs/` and checked against its pinned SHA-256.
|
||||
2. **Retarget.** `tools/retarget.py` writes `build/native/Factorio.dylib`, changing only Mach-O
|
||||
header bytes, and records the changes in `build/native/retarget.json`.
|
||||
3. **Shims and host.** The shim libraries and the small host executable are compiled for iOS
|
||||
with clang; the app icon is generated from the game's own icon.
|
||||
4. **Bundle and sign.** `build/native/Factorio.app` is assembled, signed, and installed with
|
||||
`devicectl`.
|
||||
5. **Data.** `--push-data` copies the game's `data/` folder (~1.5 GB) into the app's
|
||||
`Documents/factorio/data` on the phone. This is only needed the first time, after deleting the
|
||||
app, or after a game update.
|
||||
|
||||
The bundle never contains game data. Game settings, saves and logs are written to
|
||||
`Documents/factorio/` inside the app's container and survive reinstalls.
|
||||
|
||||
## 4. Run
|
||||
|
||||
Tap the **Factorio** icon on the home screen, or launch it from the Mac with the console
|
||||
attached:
|
||||
|
||||
```sh
|
||||
python3 tools/build_native.py --no-build --launch
|
||||
```
|
||||
|
||||
The phone must be unlocked for a launch from the Mac. Console output (the shim's `[fx]` lines and
|
||||
Factorio's own log) is shown and saved to `build/native/console.log`. The first start after
|
||||
installing takes the longest; the main menu normally appears after about 16 seconds.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Environment | Default |
|
||||
| --- | --- | --- |
|
||||
| `--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` |
|
||||
| `--device ID` | `FX_DEVICE` | First paired device |
|
||||
| `--no-build` | | Install/launch the existing bundle |
|
||||
|
||||
`python3 tools/build_native.py --help` prints the same list.
|
||||
|
||||
## Rendering resolution
|
||||
|
||||
By default the game renders at the panel's native resolution. To render fewer pixels (for
|
||||
battery or frame rate), put a divisor in `Documents/factorio/render-divisor.txt` on the phone,
|
||||
for example `1.5`, using Finder's file sharing for the app or `devicectl device copy to`. Valid
|
||||
values are 1 to 4. Restart the app afterwards.
|
||||
|
||||
## Copying saves
|
||||
|
||||
Saves are in `Documents/factorio/write/saves/`. With the phone connected, Finder shows the app's
|
||||
files under the phone → Files → Factorio. A save from the Mac can be copied in the same way:
|
||||
|
||||
```sh
|
||||
xcrun devicectl device copy to --device <id> --domain-type appDataContainer \
|
||||
--domain-identifier local.factorio.native \
|
||||
--source ~/Library/Application\ Support/factorio/saves/my-save.zip \
|
||||
--destination Documents/factorio/write/saves/my-save.zip
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| 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). |
|
||||
| `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. |
|
||||
| Image looks blurry | Check that `render-divisor.txt` is absent or contains `1`. Text size is set separately with Factorio's UI scale. |
|
||||
|
||||
## Diagnostics
|
||||
|
||||
- `python3 tools/native_audit.py build/factorio.arm64` lists, per library, the game's imports
|
||||
that the iOS SDK does not provide. Every missing symbol must be implemented by a shim.
|
||||
- `python3 tools/symbolize_native.py [console.log]` symbolizes crash reports using the
|
||||
`factorio.dSYM` that ships with the macOS game.
|
||||
- `tests/native/png_parity.m` checks the PNG decoder against macOS ImageIO for every PNG in the
|
||||
game data; the build command is in [native-port.md](native-port.md#verification).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Controls
|
||||
|
||||
Factorio is played with a mouse and keyboard. The port turns touches into mouse events and
|
||||
offers three ways to press keys. The game receives ordinary macOS key events with Mac key codes,
|
||||
so Factorio's own key bindings and settings apply unchanged.
|
||||
|
||||
## Touch
|
||||
|
||||
| Gesture | Sent to the game | Typical use |
|
||||
| --- | --- | --- |
|
||||
| Tap | Left click (double tap = double click) | Buttons, placing, opening entities |
|
||||
| Drag with one finger | Left button held while moving | Placing rows of items, dragging in GUIs |
|
||||
| Long press (0.6 s) and keep holding | Right button held; moving drags with it | Mining resources, trees and rocks; removing buildings |
|
||||
| Two-finger drag | Mouse wheel | Zooming the map view, scrolling lists |
|
||||
|
||||
Touch positions map 1:1 to the game's pixels, so taps land exactly under the finger.
|
||||
|
||||
## On-screen overlay
|
||||
|
||||
| Button | Key |
|
||||
| --- | --- |
|
||||
| Left stick | W A S D (walking); diagonals press two keys |
|
||||
| E | Inventory |
|
||||
| Q | Pipette / clear cursor |
|
||||
| R | Rotate |
|
||||
| F | Pick up items |
|
||||
| M | Map |
|
||||
| T | Technologies |
|
||||
| ⇧ | Shift, sticky |
|
||||
| Ctrl | Control, sticky |
|
||||
| Esc (top left) | Escape: menu, close windows |
|
||||
| Tab (top left) | Tab |
|
||||
| ◐ (top right) | Hide or show the stick and the key buttons |
|
||||
| ⌨︎ (top right) | Open or close the iOS keyboard |
|
||||
|
||||
The key labels match Factorio's default bindings. If you rebind keys in the game, the buttons
|
||||
still send the same physical keys.
|
||||
|
||||
**Sticky modifiers.** Tap ⇧ or Ctrl and the button turns orange. The modifier stays pressed for
|
||||
your next click, long press or typed key, then releases. Tap it again to release it without
|
||||
using it. Example: Ctrl, then tap an assembler to transfer all items.
|
||||
|
||||
## iOS keyboard and extra keys
|
||||
|
||||
The ⌨︎ button opens the standard iOS keyboard for chat, save names and search fields. Every
|
||||
character you type is also a key press, so letters work as hotkeys while the keyboard is open.
|
||||
|
||||
Above the keyboard is a scrollable bar with the keys iOS keyboards lack: Esc, Tab, Ctrl, Alt
|
||||
and ⇧ (sticky), arrow keys, Enter, Del, \`, Home, End, Page Up, Page Down and F1–F12.
|
||||
|
||||
Typing uses the US key layout; accented characters are sent as text only.
|
||||
|
||||
## Hardware keyboard
|
||||
|
||||
A Bluetooth or USB-C keyboard is passed through key by key, including modifiers, function keys
|
||||
and key releases, so held keys and combinations behave as on a Mac. iOS reserves some
|
||||
Command-key shortcuts for itself.
|
||||
|
||||
## Interface size
|
||||
|
||||
At native resolution Factorio's interface is small on a phone. Set **Settings → Interface → UI
|
||||
scale** in the game to about 175–200 %. This does not lower the rendering resolution.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Architecture of the native port
|
||||
|
||||
The port runs the **original macOS ARM64 machine code** of Factorio on iOS. Both platforms are
|
||||
ARM64 Darwin; what differs is the set of system libraries and frameworks around the code. The
|
||||
port therefore changes nothing in the game's code and instead (1) makes iOS willing to load the
|
||||
executable and (2) supplies the macOS APIs that iOS does not have.
|
||||
|
||||
```
|
||||
factorio (macOS, universal) ──lipo──▶ factorio.arm64 ──retarget.py──▶ Factorio.dylib
|
||||
│ links
|
||||
FactorioNative (host) ──▶ fx_AppKit.dylib ◀──────────────────────────┤
|
||||
UIApplicationMain UIKit bridge, input, boot ├─▶ fx_CoreGraphics, fx_CoreAudio, … (shims,
|
||||
calls original main() │ each re-exporting the iOS framework)
|
||||
└─▶ iOS libSystem, Metal, Foundation, …
|
||||
```
|
||||
|
||||
## Why this works
|
||||
|
||||
`tools/native_audit.py` resolves every import of the game binary against the iOS SDK's library
|
||||
stubs. Every libSystem function the game uses exists on iOS. The renderer is Metal, and its
|
||||
shaders ship as `.metal` source that Metal compiles at runtime, so they work on iOS too. Only
|
||||
about 150 symbols are missing, almost all in windowing, display and peripheral APIs (AppKit,
|
||||
CoreGraphics display functions, CVDisplayLink, Carbon keyboard layout, ForceFeedback, LDAP,
|
||||
OpenCL) plus the Steam API.
|
||||
|
||||
No code is generated at runtime. The game library is signed like any other app library.
|
||||
|
||||
## Retargeting (`tools/retarget.py`)
|
||||
|
||||
Only Mach-O header fields change (1,153 bytes). The script refuses any change after the load
|
||||
commands, and refuses any input other than the pinned 2.0.77 ARM64 slice.
|
||||
|
||||
- `MH_EXECUTE` → `MH_DYLIB`, with an `LC_ID_DYLIB` added in the header padding. iOS will not
|
||||
load a second executable, but it will load a library.
|
||||
- `__PAGEZERO` shrinks to one page directly below `__TEXT`, so the image can slide like a
|
||||
library. Segment indices stay the same, so the rebase and bind opcodes remain valid.
|
||||
- `LC_BUILD_VERSION`: platform macOS → iOS.
|
||||
- Framework load paths: macOS `Versions/A` paths become iOS paths, and frameworks or symbols iOS
|
||||
lacks are redirected to `@loader_path/fx_*.dylib` shims. Where the iOS framework exists, the
|
||||
shim re-exports it and only adds the missing symbols, so the game's two-level namespace
|
||||
bindings still resolve.
|
||||
|
||||
The host reads `LC_MAIN` from the loaded library at runtime to find the original `main`.
|
||||
|
||||
## Startup (`platform/native/shim/boot.m`, `stack.s`)
|
||||
|
||||
`FactorioNative` calls `UIApplicationMain`. After launch, a main-queue block `dlopen`s
|
||||
`Factorio.dylib` and calls the original `main(argc, argv, envp, apple)` on the **main thread**,
|
||||
because UIKit and SDL's Cocoa code require it. iOS gives the main thread a 1 MiB stack, against
|
||||
8 MiB on macOS, so `fx_call_on_stack` switches to a 256 MiB stack with a guard page for the
|
||||
duration of `main`. Before calling `main`, the host:
|
||||
|
||||
- writes `Documents/factorio/config/config.ini` with `read-data` and `write-data` paths into the
|
||||
app container, which can move between installs;
|
||||
- activates the audio session (Playback, mixing with other apps);
|
||||
- adds macOS-only `MTLDevice` properties (`metal_compat.m`);
|
||||
- installs the crash reporter (`crash.c`).
|
||||
|
||||
The game then runs its own loop. Each time SDL pumps events, the shim re-enters the UIKit run
|
||||
loop (`CFRunLoopRunInMode`), the same technique SDL's own UIKit backend uses, so touches,
|
||||
timers and system events keep flowing while the game owns the main thread.
|
||||
|
||||
## AppKit on UIKit (`appkit.m`)
|
||||
|
||||
Factorio links SDL 2.24 statically with the Cocoa video driver. The shim implements the AppKit
|
||||
surface that driver uses, checked against the SDL 2.24.2 source:
|
||||
|
||||
- **NSApplication:** a shared instance, an event queue fed from UIKit,
|
||||
`nextEventMatchingMask:untilDate:inMode:dequeue:` (re-entering the run loop), and
|
||||
`sendEvent:` routing to windows. It also returns a non-nil main menu, so SDL skips nib loading.
|
||||
- **NSWindow / NSView:** each view is backed by a `UIView`. A view that asks for a layer gets
|
||||
the layer from its `makeBackingLayer`, which for SDL's Metal view is a `CAMetalLayer`. AppKit's
|
||||
bottom-left coordinates are converted to UIKit's. Windows are always fullscreen.
|
||||
- **NSEvent:** mouse, scroll, key and flags-changed events with AppKit field semantics, plus
|
||||
`+mouseLocation`, `+pressedMouseButtons` and `+modifierFlags`.
|
||||
- **NSScreen and the CoreGraphics display list** (`coregraphics.c`): one display. Its size is
|
||||
the panel's native pixel size with a backing scale of 1, so the game renders at full
|
||||
resolution. Touches and view frames are scaled between UIKit points and game pixels.
|
||||
`render-divisor.txt` can lower the resolution.
|
||||
- **NSResponder:** the AppKit responder chain, including `interpretKeyEvents:` → `insertText:`
|
||||
for SDL's text input field.
|
||||
- **NSBitmapImageRep:** on macOS Factorio decodes every PNG through it. See below.
|
||||
- **NSColor:** UIKitCore already contains a private `NSColor` class, so the shim's class is
|
||||
named `FXNSColor` and exported under the AppKit symbol name with a linker alias.
|
||||
- **Other classes** (cursors, menus, alerts, pasteboard, workspace, OpenGL) are inert or mapped
|
||||
to UIKit equivalents. An alert's text is written to the log.
|
||||
|
||||
Unknown selectors on shim classes are logged once (`[fx] unimplemented …`) and answer zero, so
|
||||
missing pieces show up in the log instead of as crashes.
|
||||
|
||||
## Image decoding (`appkit.m`, `pngdecode.c`)
|
||||
|
||||
Factorio's macOS sprite loader calls `+[NSBitmapImageRep imageRepWithData:]` for each of about
|
||||
4,700 images (~3 gigapixels) and expects AppKit's layout: tightly packed, non-premultiplied
|
||||
samples with the source channel count. Factorio then premultiplies them itself.
|
||||
|
||||
- `pngdecode.c` decodes 8-bit grayscale, gray+alpha, RGB and RGBA PNGs without a `tRNS` chunk
|
||||
with **Wuffs** v0.3.4. That covers about 87 % of the sprites.
|
||||
- Everything else goes through ImageIO. ImageIO on iOS returns premultiplied RGBA, which a
|
||||
single vImage conversion turns back into the expected layout.
|
||||
|
||||
The decode path is chosen from the image metadata before any pixels are read, so no image is
|
||||
decoded twice. Startup to the main menu dropped from 84 s (scalar conversion) to about 16 s.
|
||||
|
||||
## Audio (`audio.c`)
|
||||
|
||||
SDL's macOS CoreAudio driver enumerates devices through `AudioObject*` property calls, which
|
||||
answer nothing useful on iOS. `fx_CoreAudio` models one output device (stereo, 48 kHz, alive,
|
||||
not hogged) and answers exactly the properties SDL 2.24 queries. Playback uses the real iOS
|
||||
`AudioQueue`. `fx_AudioToolbox` passes `AudioQueueSetProperty` through, except the macOS-only
|
||||
`kAudioQueueProperty_CurrentDevice`.
|
||||
|
||||
## Input (`boot.m`, `keyboard.m`)
|
||||
|
||||
- **Touch → mouse:** tap = left click; a long press holds the right button (with a zero-distance
|
||||
motion so Factorio updates the hovered entity immediately); a two-finger pan sends wheel
|
||||
events.
|
||||
- **Keys:** every source produces AppKit key events with Mac virtual key codes and
|
||||
device-dependent modifier bits, which SDL maps exactly as on a Mac:
|
||||
- hardware keyboards (`UIPress` HID usage → Mac key code);
|
||||
- the iOS keyboard (characters → key code, plus Shift when needed);
|
||||
- the overlay and the extra-keys bar.
|
||||
|
||||
## Other shims
|
||||
|
||||
| Shim | Provides |
|
||||
| --- | --- |
|
||||
| `metal_compat.m` | `isDepth24Stencil8PixelFormatSupported`, `isLowPower`, `isRemovable`, `location`, … on the GPU's device class; logs other unknown device selectors |
|
||||
| `small.c` | CVDisplayLink (refresh period), Carbon keyboard layout (none), ForceFeedback, OpenCL and LDAP (all unavailable), Security and CoreServices extras, Steam API (initialization reports failure; the game continues without Steam) |
|
||||
| `frameworks.m` | `NSAppleEventManager`, `MTLCopyAllDevicesWithObserver` |
|
||||
| `crash.c` | Interposes `sigaction`, so a fault is logged with registers and the frame chain before Factorio's own handler runs |
|
||||
|
||||
## Verification
|
||||
|
||||
- `retarget.py` checks the input hash and confirms that no byte after the header changed.
|
||||
- `native_audit.py` plus a check of the shims' exports confirm that every import resolves.
|
||||
- PNG parity: every game PNG decoded by `pngdecode.c` is byte-identical to macOS ImageIO's exact
|
||||
non-premultiplied output (4,231 identical, 534 use the ImageIO path):
|
||||
|
||||
```sh
|
||||
clang -O2 -fobjc-arc -Iplatform/native/shim tests/native/png_parity.m platform/native/shim/pngdecode.c \
|
||||
-framework Foundation -framework ImageIO -framework CoreGraphics -framework QuartzCore -o /tmp/png_parity
|
||||
/tmp/png_parity "$HOME/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/data"
|
||||
```
|
||||
|
||||
- On the device, verification is by running the game: the console log, unimplemented-selector
|
||||
messages, and crash reports symbolized with `tools/symbolize_native.py`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Only the Steam macOS build of 2.0.77 is accepted. Other versions need their imports
|
||||
re-audited.
|
||||
- Memory: the wildcard development profile does not include the increased-memory-limit
|
||||
entitlement. Large factories may hit the default iOS memory limit; an explicit App ID with
|
||||
that capability lifts it.
|
||||
- Not yet verified: loading existing saves, long sessions and thermals, mods, multiplayer,
|
||||
Space Age.
|
||||
- iOS is told the device has no audio input, so there is no voice or capture.
|
||||
- Keyboard layout handling assumes US.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Original MapDeserialiser initialization (2026-10-20)
|
||||
|
||||
Build **5** initializes the original MapDeserialiser on the iPhone 15 Pro using both a controlled version header and the approved actual save's nine-byte version prefix. The original guest code performs five reads, creates its helper structures and owned TargetDeserialiser, and returns successfully. Reports record **12 allocations**, **10,712 guest heap bytes**, a target-pool capacity of **64** and a stream cursor of **9**. A controlled newer-version prefix takes the original error branch and returns the adapter's version-error status 7.
|
||||
|
||||
This is an isolated constructor probe. The module is freed after validation; its object graph is not yet connected to a persistent world loader. The rest of the save stream, object references, entities and gameplay are not loaded. The scenario-envelope probe remains a separate host-side reconstruction.
|
||||
|
||||
## Original input and evidence
|
||||
|
||||
| Code/data | Address | Bytes | Complete REA byte record |
|
||||
| --- | --- | --- | --- |
|
||||
| MapDeserialiser C2 normal CFG including version-error branches | `0x1014def40` | 1272 | `ev_904c99c8305aec9932b9bfe4c722d12a961c34e254d80c97a09db109dc390bba` |
|
||||
| Actual TargetDeserialiser C1 normal body called by that CFG | `0x1012c1044` | 116 | `ev_d0a2867c6c04389e519058440e52ecc612c2212435f9920a725fb219e27fc663` |
|
||||
| Shared TypeAwareMemoryPool C2 normal CFG | `0x10244a4bc` | 1260 | `ev_bdbccc8d7c9a58178fcb6476fcf32651ee9daa7abfd66ac2b4e39b2b8d489b3e` |
|
||||
| Pool vtable identity range | `0x102fc2d98` | 32 | `ev_ee2b757ce4a5c73c150c5cd29112e7dd6db99529211abfa59085d500e4c2cbb1` |
|
||||
|
||||
The prior C2 TargetDeserialiser input at `0x101ab7c74` also has a complete matching byte record, `ev_5c30d5ca0d76c6b2727297e08f671d59e0944c9bffadd7b58cb62f8650e32b37`. This closes the previous batch's pending byte check. The C1 constructor is a distinct body with different stack saves; it is translated at its actual call target, without substituting the earlier C2 implementation.
|
||||
|
||||
The unchanged ARM64 SHA-256 is pinned to `2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7`. Function range hashes and all 662 translated instruction words are in `build/deserialiser/manifest.json`. Complete REA records are in `evidence/deserialiser-final.json`; the native session was snapshotted to `build/deserialiser-rea.snapshot.json` and closed. Earlier constructor assembly is retained as `ev_3143ceb6dee72f0a56235e02cd4a5ed942c3826860ea21c315847bb186053650`.
|
||||
|
||||
## Guest execution and boundaries
|
||||
|
||||
The original constructor stores the reader pointer at object offset `+8`, the packed version at `+0x18`, its flag at `+0x20`, the target pointer at `+0x28` and the target ownership byte at `+0x4c6`. The initialized-state byte at `+0x10` becomes 1. Helper allocation order and self/heap pointer relationships are compared against the original ARM64 execution.
|
||||
|
||||
The stream service is a bounded memory adapter selected for the observed readOrThrow contract; it does not implement the game's ReadStream virtual dispatch. Operator new uses the existing 16-byte-aligned per-run guest arena. The new bzero adapter validates its destination in the allocated arena prefix. All guest pointers remain offsets into the fixed 1 MiB module memory.
|
||||
|
||||
Zero-vector stores now require a MOVI-zero definition on **every** incoming path. Integer instructions preserve these facts; calls clear them, and copy loads overwrite the relevant vector fact. The supported stores include unscaled/scaled Q stores, pre/post writeback and Q-store pairs. This does not add general SIMD arithmetic, floating point or incoming vector-register state.
|
||||
|
||||
Version rejection aborts with status 7 at the original call to `__cxa_allocate_exception`. The original exception object, message and unwinding behavior are not implemented. Destruction helpers and delete return unsupported-service status 6 if reached. Ordinary constructor inputs skip those cleanup blocks; allocation failures abort with status 5 and discard the module. Static ADRP pages used for vtable/type/callback identities are declared explicitly; those addresses do not provide callable native pointers or a complete relocated image. No exception landing pads are executed.
|
||||
|
||||
The constructor's original checks accept 2.0.77.0 and reject 2.0.77.1; they also reject major zero, the tested newer versions/nonzero flag and the original 1.2.0 build-0..27 interval. This constructor-level acceptance does not establish that the rest of a save version or mod set is supported. The separate save-envelope adapter remains restricted to 2.0.77.0/flag 0/empty startup settings.
|
||||
|
||||
## Verification
|
||||
|
||||
- **512 original ARM64 object-graph comparisons** vary guest object and heap offsets, accepted headers, supplied/owned targets and stream-ownership values. They compare the complete 1,224-byte constructor output and allocated graph after normalizing reader, self, heap and vtable pointers. Register and unallocated-memory guards pass.
|
||||
- **9 short-header cases**, **8 original version-error branches**, all **12 allocation-failure boundaries**, and exact heap capacity pass. Public wrapper outputs remain unchanged on failure.
|
||||
- **4,096 native zero-vector comparisons** cover pair/single stores, v31, positive/negative offsets, pre/post writeback, branches, merges and loops; four invalid vector cases fail closed.
|
||||
- Prior **66,048 instruction comparisons**, **80,000 guest-program comparisons**, **512 TargetDeserialiser graphs** and **20,000 MapHeader comparisons** still pass.
|
||||
- All new constructor/code/data ranges match complete REA observations. The checker reports this independently of behavioral comparisons.
|
||||
- Build 5's controlled and actual-save initialization reports pass on the iPhone, alongside same-launch TargetDeserialiser, version and modern/legacy MapHeader checks. The actual save's header offset and raw tick values still match the existing desktop result, and all save reports share the same input hash and probe ID.
|
||||
- The signed binary has no Steam dependency/import and contains none of the desktop original-code oracle entry symbols.
|
||||
|
||||
The desktop oracle redirects declared BL services and relocates the pool's vtable ADRP/ADD reference. Its version-error service aborts before exception construction, matching the guest adapter's selected stopping point. Tests therefore compare original initialization and branch decisions, not native C++ exception semantics.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
python3 tools/build_target_deserialiser.py build/factorio.arm64
|
||||
python3 tools/build_deserialiser.py build/factorio.arm64
|
||||
python3 tests/check_a64_zero_vectors.py
|
||||
python3 tests/check_a64_initialization.py
|
||||
python3 tests/check_a64_program.py
|
||||
python3 tools/build_map_header.py build/factorio.arm64
|
||||
python3 tools/check_initialization_rea.py build/factorio.arm64 evidence/deserialiser-final.json
|
||||
python3 tools/package_ios.py
|
||||
```
|
||||
|
||||
Generated artifacts and verification reports live under `build/deserialiser/`. After signing, installing, launching and downloading the seven `iphone-*-result.json` reports, `python3 tools/check_deserialiser_device.py` verifies build identity, common launch/probe/input identities and the signed binary, writing `device-verification.json`.
|
||||
|
||||
Next: translate MapGenSettings and the following Map::loadData dependencies, then connect these isolated verified components into a persistent loader. Full static-image mapping, indirect calls, object references, destruction/unwinding, simulation, graphics, sound and touch controls remain ahead.
|
||||
@@ -0,0 +1,250 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Loader allocation and initialization (2026-10-20)
|
||||
|
||||
Signed **build 4** runs the original TargetDeserialiser/memory-pool initialization graph on the iPhone 15 Pro, through ARM64 → WASM → wasm2c → native iOS. Its report records four allocations, a capacity of 64, 64 initialized targets and **2,104 bytes** consumed by the aligned guest arena. The version constructor, both MapHeader branches and the actual-save header probe also pass in the same launch. The save's header remains at decoded byte 333 with three raw values of 720001. This initializes one isolated loader dependency; it does not load a world or provide gameplay.
|
||||
|
||||
## Original code and adapters
|
||||
|
||||
| Input | Address | Bytes | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| TargetDeserialiser C2 normal body | `0x101ab7c74` | 108 | `2ada8a5d403d8d30f9bc053f3cad0e74d08103d6c96bcdbf4db44d481ba6d4d0` |
|
||||
| TypeAwareMemoryPool C2 normal CFG | `0x10244a4bc` | 1260 | `0e450e3ad613dded236f6627d78a07894d48b46e845baa40877d6929043b171a` |
|
||||
| Source vtable identity range | `0x102fc2d98` | 32 | `c991e960829a78707ab67384d3dbf5d296aaa8fc9f33878ae7c1e7571f779dce` |
|
||||
|
||||
The complete source ARM64 SHA-256 is pinned to `2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7`. These inputs are checked before generation. The translated program contains 342 original instructions and shares guest registers, stack and an instruction budget between both functions.
|
||||
|
||||
The allocator is a **host-side service**, not translated operator new. It returns 16-byte-aligned offsets into a bounded per-run arena, leaves bytes uninitialized and reports exhaustion explicitly. A new opt-in import ABI snapshots the guest register context and reloads only x0 after a successful call. Other guest registers retain their local values; host calls invalidate the compiler's NZCV facts. Failed calls abort the run, with partial guest state discarded when the module is freed.
|
||||
|
||||
Guest memory is a fixed 1 MiB, with a 64 KiB arena at offset 131072, an isolated stack and a controlled output object. Addresses are checked at 64-bit width before narrowing. The ADRP source page `0x102fc2000` maps to guest page 65536. The constructor stores the resulting vtable **identity** at guest offset `0x10da8`; no vtable bytes, native pointers or callable virtual methods are installed there. Virtual dispatch remains unsupported.
|
||||
|
||||
New instruction support is limited to non-flag-setting logical immediates, ASR immediates, declared ADRP pages, BRK traps and contiguous MOVI-zero/Q-store sequences. The vector idiom cannot transport values across calls or branches. General SIMD remains unsupported.
|
||||
|
||||
The constructors' exception landing pads at `0x101ab7ce0..0x101ab7d0c` and `0x10244a9a8..0x10244a9d4` are excluded. Delete and length-error imports return an explicit unsupported-service status; allocation failure returns status 5. No C++ exception unwinding, destructor or general free service is supplied. No startup, Steam, world or rendering code is introduced into the iOS binary.
|
||||
|
||||
## Validation
|
||||
|
||||
- **512 original ARM64 object-graph comparisons**, varying guest object and heap offsets. Each compares the complete 144-byte output object and the complete allocated graph, normalizing native heap pointers and the vtable identity. Guards include alignment gaps, padding, unallocated RAM and preserved x19–x30/SP.
|
||||
- Eight insufficient-heap cases exercise aborts before each allocation can succeed; exact 2,104-byte capacity succeeds. Overflow-size, zero-size allocation, invalid arena/context and instruction-budget checks also pass.
|
||||
- **66,048 native instruction comparisons** cover 129 ASR/logical-mask forms, signed input edges and randomized values. Extended-memory writes, near-end accesses, addresses above 4 GiB and twelve rejection cases pass.
|
||||
- Existing **80,000 guest-program comparisons** and **20,000 original MapHeader comparisons** still pass after the shared compiler changes.
|
||||
- Physical build 4 reports share launch ID `D0C96658-439A-466B-99A8-768D93E86E57`, build identity `Oct 20 2026 19:31:52` and report time `2026-10-20T15:33:10Z`.
|
||||
- The signed executable's imports/dependencies contain no Steam integration, and desktop oracle symbols are absent.
|
||||
|
||||
The desktop oracle executes copied original ARM64 words, relocating only direct BL calls and the ADRP/ADD vtable identity reference. The iOS target links only generated C and runtime adapters. Finite comparisons cover constructor initialization and allocation aborts; they do not establish arbitrary mutation paths or whole-engine equivalence.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
python3 tools/build_target_deserialiser.py build/factorio.arm64
|
||||
python3 tests/check_a64_initialization.py
|
||||
python3 tests/check_a64_program.py
|
||||
python3 tools/build_map_header.py build/factorio.arm64
|
||||
python3 tools/package_ios.py
|
||||
```
|
||||
|
||||
Generated sources, function/call manifests and desktop validation are under `build/target-deserialiser/`. Downloaded device reports are `iphone-*-result.json`; `python3 tools/check_target_device.py` checks them against the signed build and existing desktop save result, then writes `device-verification.json`. Original instructions and generated game code remain in ignored local artifacts.
|
||||
|
||||
## REA evidence and resolved provenance check
|
||||
|
||||
REA observed the original constructor assembly in records `ev_1a00dc8f1f23ce6c1e54f8d189d163fbdd012621890e0288643b8f83a8b11611` (TargetDeserialiser) and `ev_b08fa6c3b275200505c3e78c71046526444b5c0ec603ef09d18d5db53dbf0eab` (memory pool). Complete MCP responses are retained in `evidence/1792510440817-2-procedure_assembly.json` and `evidence/1792510440863-3-procedure_assembly.json`. The local evidence index combines these unchanged records with the previous export in `evidence/target-initialization-final.json`.
|
||||
|
||||
The three source ranges were initially hash-pinned directly from the unchanged Mach-O and independently decoded with LLVM; the extra REA byte export was pending at the end of build 4. **Resolved in the build 5 batch:** complete matching REA `read_bytes` observations are now exported in `evidence/deserialiser-final.json`. `tools/check_initialization_rea.py` validates all three ranges and writes a passing `build/target-deserialiser/rea-provenance.json`. See [deserialiser-progress.md](deserialiser-progress.md) for the records. Later compiler support extends the original contiguous zero-store idiom to definitely-zero facts checked across control flow.
|
||||
|
||||
## Next dependency
|
||||
|
||||
The next step from build 4 was integration into the original MapDeserialiser constructor, including its additional allocations, bzero and version-error handling. **Build 5 now completes that isolated initialization probe**; see [deserialiser-progress.md](deserialiser-progress.md). Next is MapGenSettings at the beginning of Map::loadData. Full image relocation, object reference reconstruction, nonempty startup settings, entities, simulation, graphics, sound and touch controls remain unimplemented.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Factorio iPhone port: initial investigation
|
||||
|
||||
Status: two RNG functions, the map-version constructor, and a branching map-header constructor statically translated to WASM and compiled into a native, Steam-free iOS prototype. The prototype reads save-version headers and runs on the connected iPhone 15 Pro. No playable game port implemented. See static-translation.md, world-loading.md and translation-progress.md for reproduction and exact validation scope.
|
||||
|
||||
## Installed target
|
||||
|
||||
- Steam installation: `~/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app`.
|
||||
- Version: 2.0.77, read from the installed Info.plist.
|
||||
- Executable SHA-256: `00b014d78df07e05ab1cee853904c8f7b3814ca652c043461a9d49fbad46cdf6`.
|
||||
- REA identifies a universal Mach-O with x86_64 and arm64 slices.
|
||||
- Selected ARM64 slice: offset 106659840, size 101261968 bytes; build platform MACOS, minimum OS 11.0, SDK 11.0.
|
||||
- Adjacent `factorio.dSYM/Contents/Resources/DWARF/factorio` exists. Its ARM64 UUID matches the inspected executable: `EBA8DC1C-9981-3657-8656-16F6A8424198`. Full type/source metadata recovery has not been performed.
|
||||
|
||||
## Porting implications
|
||||
|
||||
The ARM64 slice is a macOS executable, not an iOS executable. Its dependency inventory includes AppKit, Cocoa, Carbon, ForceFeedback, desktop OpenGL/OpenCL/OpenAL, and `@loader_path/libsteam_api.dylib`. A platform marker change or IPA wrapper alone cannot replace these interfaces.
|
||||
|
||||
Metal and GameController are also linked. This is evidence of dependencies only; it does not establish the renderer's architecture, active execution paths, or reusable source availability.
|
||||
|
||||
A source-based port needs an iOS build of the simulation engine plus platform adapters for application lifecycle, graphics presentation, audio, touch/controller input, writable storage, threading and networking. Dependencies must be built for iOS. The existing Lua/data files do not by themselves provide a replacement for the native engine.
|
||||
|
||||
A binary-based reconstruction first needs focused recovered interfaces and behavior specifications. Pseudocode is not original C++ and does not by itself produce a buildable engine. Save compatibility, multiplayer determinism, mod support and performance remain unverified.
|
||||
|
||||
## Implementation milestones
|
||||
|
||||
1. Binary reconstruction is the selected path. The RNG prototype establishes a bounded translation pipeline; recover startup and simulation dependencies next.
|
||||
2. Build an iOS application hosting an engine boundary, with Metal presentation and touch commands. A shell alone must not be presented as a Factorio port.
|
||||
3. Implement and validate one vertical slice: load a small world, advance simulation, display entities, place a miner and transport its output.
|
||||
4. Validate save/load, deterministic behavior where required, device memory, sustained frame rate and thermal behavior on a specified iPhone.
|
||||
5. Expand features only after the vertical slice works.
|
||||
|
||||
## Evidence and checks
|
||||
|
||||
`rea-evidence.json` is the exported REA bundle. Mach-O inspection parent: `ev_83862cd660d0d09fdefd22e0fe60f714fd098b266c655142eb1a30a812ebbefc`. Architecture, build metadata and dependency pages were inspected. Import/export completeness is explicitly not guaranteed by REA; symbol counts do not prove recovered functions or complete source.
|
||||
|
||||
`xcodebuild -showsdks` lists iOS 26.5 and iOS Simulator 26.5 SDKs. Unsigned and locally signed device builds compile. The app is installed and running on the iPhone 15 Pro. The macOS native equivalence harness passes 876,544 RNG calls, 1,024 RNG seed constructor calls and 20,000 map-version constructor comparisons; unsupported code and changed source bytes are rejected. The phone's native map-version self-test and generated ZIP probe also pass.
|
||||
|
||||
The initial REA procedure search failed because the Hopper launcher relies on Spotlight. A reversible `/Applications/Hopper Disassembler.app` symlink to the existing user-installed app resolves vendor-helper discovery. No indexing settings or saved MCP client or REA configuration were changed. Full analysis took 5m27s, exceeding the original MCP session's 120s startup timeout and leaving retained cleanup state; the generated document was manually closed. The Hopper demo cannot save databases. A separate MCP client with a 600s timeout uses a locally extracted, unmodified ARM64 slice (`build/factorio.arm64`, SHA-256 `2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7`) for focused function evidence. See evidence files for the actual success/failure of each operation.
|
||||
|
||||
Steam integration is excluded from the target. Current artifact import and dependency checks pass. Whole-game Steam exclusion remains dependent on lifting the engine and verifying the local startup graph; see [steam-boundary.md](steam-boundary.md).
|
||||
|
||||
The longer isolated REA run succeeded. Canonical function Evidence is retained in `evidence/factorio-functions-final.json`; `tools/check_rea_rng.py` confirms all three code/constant ranges match the original installed binary's ARM64 slice. The successful isolated session was saved to `build/factorio-rea.snapshot.json` and closed. Its results identify an inlined Steam startup path in `GlobalContext::init`, so omitting only the standalone Steam factory would be incomplete.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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
|
||||
|
||||
1. 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.
|
||||
2. The normal save path calls ScenarioExecutionContext's deserializing constructor before Map::load. ScenarioLocation consumes three compact-length strings; Difficulty consumes one byte.
|
||||
3. 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.
|
||||
4. 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.
|
||||
5. 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:
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
1. Translate the compact Version/ModVersion readers and other metadata utilities as composable guest functions, reducing reconstructed host behavior where useful.
|
||||
2. Implement a scalable guest memory image and explicit allocator/global/import contracts needed by the actual MapDeserialiser and Map constructors.
|
||||
3. Expand supported PropertyTree settings using recovered wire cases and validated guest code.
|
||||
4. Recover and translate MapGenSettings and the earliest Map::loadData state allocations. Entity/world reconstruction, simulation, graphics, audio and controls remain ahead.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Static-translation research (ARM64 → WASM → native)
|
||||
|
||||
> This is the earlier, function-by-function translation track. The playable port uses the
|
||||
> retargeted original binary instead; see [../native-port.md](../native-port.md). Commands below
|
||||
> are run from the repository root.
|
||||
|
||||
The intended target is local single-player Factorio, without Steam integration or runtime JIT. Current deliverable: **two RNG functions, the original map-version and map-header constructors, and the MapDeserialiser/TargetDeserialiser/memory-pool initialization graphs translated to WebAssembly and compiled through wasm2c into a native iOS prototype**. The application can read version headers, initialize the original deserializer from a save's version prefix and, for a limited 2.0.77 profile, locate and decode map-header fields from save ZIPs. Full world loading, rendering, simulation and gameplay are not implemented.
|
||||
|
||||
Pipeline: local ARM64 Mach-O → REA-inspected instructions → byte-guarded WASM translation → wasm2c → Clang → native iOS application. RNG functions use reviewed specialized translations; `MapVersion` uses the new bounded automatic integer lifter. The lifter now also supports bounded integer control flow, comparison flags and immediate branches, verified with separate native instruction fixtures. This is not yet a whole-program ARM64-to-WASM compiler. Glue5 supplies the model for the WASM-to-native half; it expects an existing WASM game build and does not supply the missing Factorio binary lifter.
|
||||
|
||||
## Reproduce on Apple Silicon macOS
|
||||
|
||||
Requires Xcode, Python 3, CMake, Git and xcodegen. WABT is built locally at a pinned revision. The following commands fetch only tool sources:
|
||||
|
||||
```sh
|
||||
git clone --depth 1 --branch 1.0.42 https://github.com/WebAssembly/wabt.git third_party/wabt
|
||||
git -C third_party/wabt submodule update --init third_party/picosha2
|
||||
```
|
||||
|
||||
Then supply your installed game executable:
|
||||
|
||||
```sh
|
||||
python3 tools/build_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tests/check_lift_guards.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tools/build_version.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tools/build_map_header.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tools/build_save_layout.py
|
||||
python3 tools/build_target_deserialiser.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tools/build_deserialiser.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tests/check_a64_guards.py
|
||||
python3 tests/check_a64_cfg.py
|
||||
python3 tests/check_a64_program.py
|
||||
python3 tests/check_a64_initialization.py
|
||||
python3 tests/check_a64_zero_vectors.py
|
||||
python3 tests/check_save_prefixes.py
|
||||
python3 tests/check_save_layout.py
|
||||
python3 tests/check_save_headers.py
|
||||
python3 tools/package_ios.py
|
||||
```
|
||||
|
||||
The specialized lift rejects unrecognized function or constant bytes before producing artifacts. Supported inspected build: Factorio 2.0.77 ARM64. Original installation files are read only. Generated game code and evidence stay in ignored local directories.
|
||||
|
||||
Outputs:
|
||||
|
||||
- `build/rng/factorio_rng.wasm`, generated C/header and `libfactorio_rng_ios.a`.
|
||||
- `build/rng/manifest.json`: source identity, addresses and hashes.
|
||||
- `build/rng/verification.json`: native equivalence and Steam import audit.
|
||||
- `build/version/factorio_version.wasm`, generated C and `libfactorio_version_ios.a`.
|
||||
- `build/map-header/factorio_map_header.wasm`, generated C and `libfactorio_map_header_ios.a`.
|
||||
- `build/map-header/device-verification.json`: both version-dependent MapHeader paths pass on the iPhone using controlled payloads; same-launch version and real-save header checks also pass.
|
||||
- `build/save-layout/device-verification.json`: build 3 reads the actual save's MapHeader at decoded offset 333; its three raw fields match the Mac/original-constructor result. Supported envelope: 2.0.77.0, flag 0, empty startup settings.
|
||||
- `build/target-deserialiser/libfactorio_target_ios.a`: original loader initialization graph with a bounded guest heap.
|
||||
- `build/target-deserialiser/device-verification.json`: build 4 initializes 64 pool objects with four allocations on the iPhone; same-launch version, map-header and real-save regressions pass.
|
||||
- `build/deserialiser/device-verification.json`: build 5 initializes the original MapDeserialiser from the actual save's nine-byte prefix; 12 allocations, 10,712 guest heap bytes, same-launch regressions pass.
|
||||
- `build/FactorioRNG-unsigned.ipa`: historical filename; now includes the RNG and save-header prototype, requires signing to install.
|
||||
- `build/ios-verification.json` and `build/ios-build.log`.
|
||||
|
||||
## Verified scope
|
||||
|
||||
`RandomGenerator::getInt()` and its unsigned-integer seed constructor are translated. The verification executable compares 876,544 generated values and their mutated state against original ARM64 instruction words, plus 1,024 constructor calls. It covers 6,848 starting states and five linear-memory offsets, including unaligned states and the end of memory. Guard bytes check for unintended writes. These finite comparisons are evidence, not proof of equivalence for every state.
|
||||
|
||||
The native oracle is linked only into macOS verification executables. The iOS app links wasm2c output and runtime, with ordinary allocated data memory and explicit bounds checks. It does not load original machine code or compile WASM at runtime. The RNG prototype was installed and launched on the connected iPhone 15 Pro; the user confirmed it worked. The updated save-header prototype also runs on that device: its native constructor self-test and generated-archive probe pass. Results are in `build/version/iphone-self-test.json` and `build/version/iphone-save-probe.json`.
|
||||
|
||||
The automatically lifted version constructor passes 20,000 native comparisons; ZIP/zlib header reading passes fifteen fixtures. It reads version 2.0.77 from the user's autosave locally on macOS. With explicit user approval, the actual autosave was also copied to the iPhone; its native header reader returned version 2.0.77 successfully. See [world-loading.md](world-loading.md) for exact scope, REA provenance, limitations and next entry points.
|
||||
|
||||
## REA workflow
|
||||
|
||||
REA MCP performs artifact inspection and focused disassembly. Initial Mach-O evidence is in `rea-evidence.json`; subsequent local MCP responses are in `evidence/`. `evidence/factorio-functions-final.json` retains successful RNG disassembly, exact byte reads, and Steam/startup dossiers. LLVM's tools independently supplied the initial bounded instruction listings and debug UUID comparison. Evidence from these tools is kept distinct.
|
||||
|
||||
The extracted ARM64 image is byte-identical to the original universal binary's ARM64 slice. After exporting the canonical REA bundle, validate its three byte ranges against the installed input:
|
||||
|
||||
```sh
|
||||
python3 tools/check_rea_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio' evidence/factorio-functions-final.json
|
||||
```
|
||||
|
||||
This passes and writes `build/rng/rea-provenance.json`. The completed isolated REA session was exported to `build/factorio-rea.snapshot.json` and closed successfully.
|
||||
|
||||
The installed Hopper vendor helper requires `/Applications/Hopper Disassembler.app` when Spotlight is unavailable. A symlink there pointing to the user-installed copy of Hopper (in `~/Applications`) resolves this. No Hopper binary was changed. The original full analysis took 5m27s, beyond REA's default 120s timeout; the demo also cannot save `.hop` databases. `tools/rea-session.mjs` starts a separate REA MCP process with a 600s startup timeout, without changing saved MCP client or REA configuration. Pass the vendor `Contents/MacOS/hopper` helper, not the GUI executable.
|
||||
|
||||
## Next engine milestones
|
||||
|
||||
The detailed staged plan is in [implementation-plan.md](implementation-plan.md). Completed work and limits are in [translation-progress.md](translation-progress.md), [save-layout-progress.md](save-layout-progress.md), [initialization-progress.md](initialization-progress.md) and [deserialiser-progress.md](deserialiser-progress.md).
|
||||
|
||||
1. Lift the startup graph and replace its inlined Steam initialization with local services. REA identified the interface boundary and an inlined copy in `GlobalContext::init`; changing the factory alone is insufficient.
|
||||
2. Extend the bounded integer lifter beyond straight-line code: full guest memory mapping, indirect-call dispatch, relocations, TLS, atomics, SIMD and C++ exception handling. Initial bounded direct guest calls, tail calls and known returns are implemented in `a64_program.py`. Initial bounded branches, NZCV support and original MapHeader constructor validation pass.
|
||||
3. Translate world loading and simulation, then native graphics/audio/touch adapters.
|
||||
4. Validate a small playable world and save/load on an actual iPhone before expanding compatibility.
|
||||
|
||||
See [porting-notes.md](porting-notes.md) for target identity and [steam-boundary.md](steam-boundary.md) for Steam scope.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Steam-free single-player target
|
||||
|
||||
User-directed scope: exclude Steam integration; retain basic local gameplay. Steam matchmaking, cloud saves, achievements, rich presence, overlay and Steam networking are outside this port's target. The installed macOS game is not modified.
|
||||
|
||||
The current translated RNG library and iOS prototype contain no Steam imports or Steam dynamic dependency. `tools/build_rng.py` rejects Steam undefined symbols; `tools/package_ios.py` checks both imports and linked libraries before packaging.
|
||||
|
||||
## Original binary observations
|
||||
|
||||
REA Mach-O import inventory observed eleven direct Steam imports, including `SteamAPI_Init`, callbacks, shutdown and SteamInternal interface acquisition. Import coverage is not exhaustive, and virtual interface calls extend beyond these named imports.
|
||||
|
||||
Independent LLVM symbol inspection found `SteamContextInterface`, `SteamContext`, and `DummySteamContext`. REA/Hopper subsequently confirmed the factory disassembly and decompilation. The interface factory is at ARM64 VA `0x100267594` (`SteamContextInterface::createContext(bool, bool)`). Its observed branches are:
|
||||
|
||||
- Nonzero second argument (`w1`) selects an object referencing `DummySteamContext`'s vtable.
|
||||
- With the second argument zero, bit zero of the first argument (`w0`) branches to a base-interface object at `0x100267640`, without entering the Steam initialization block.
|
||||
- The other branch contains calls into the Steam path and constructs `SteamContext` if initialization succeeds.
|
||||
|
||||
Parameter names and caller-level meaning have not yet been established. These observations do not prove full game startup through either optional context. The correct integration point is the factory/interface boundary, not a blanket success-returning replacement for every Steam import.
|
||||
|
||||
REA also found that the Steam initialization stub has two observed callers: the factory and `GlobalContext::init` at `0x100161584`. The latter contains an **inlined** Steam initialization block beginning at `0x100161608`. Editing only the out-of-line factory would miss it. At `0x100161604`, startup branches on `GlobalContext::headlessMode`; headless startup constructs the base context without Steam. The graphical port must select local services independently of headless mode, while preserving graphical startup. Whole-function body ranges and complete indirect-reference classification are unavailable from Hopper's public API, so these observations are not a complete reachability proof.
|
||||
|
||||
## Retained REA evidence
|
||||
|
||||
Canonical bundle: `evidence/factorio-functions-final.json`, ARM64 slice SHA-256 `2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7`.
|
||||
|
||||
- Factory dossier: `ev_4ebc9cbefaeb34d2e4f642c8cfa48d5035442aa80f6a438bf6481d215d1fe694`.
|
||||
- Steam initialization stub/callers: `ev_c2713194813cae2a2677b7d73a284f9f99b4b9214e59f58cf8ee2a0ffc159f09`.
|
||||
- Global startup dossier: `ev_7f2a2b88000ba82d85771c0f368eae9cf0a1f071beb7d73506ba6bbf3c0a22eb`.
|
||||
- Base `isSteamApiInitialized()` assembly returns zero: `ev_0b47cba381075942160840ec0d392a8f05803c3c8a2ebb672bb01c4a282f4a5b`.
|
||||
|
||||
The interface includes cloud storage, locale, networking, callbacks, on-screen keyboard, stats and achievements. Local replacements must preserve actual method contracts and object layout once lifted callers use them. Returning invented success or a fake Steam identity would hide missing behavior; the port should expose unavailable optional services and use local file storage and native text entry.
|
||||
|
||||
## Remaining work
|
||||
|
||||
Replace the inlined startup integration when that engine function is lifted, audit optional-service checks, and prove no Steam-only branch is reachable from the chosen local startup. A full translated engine does not exist yet, so Steam exclusion has only been verified for the current prototype.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Translation batch: branching map-header constructor
|
||||
|
||||
2026-10-20. The next engine primitive is implemented and passes desktop native equivalence tests. This document distinguishes constructor tests from loading a real world.
|
||||
|
||||
## Implemented
|
||||
|
||||
- `tools/record_target.py` rejects a different executable/slice and records hashes of all 8,673 installed data files (1,498,094,877 bytes). It inventories base/core metadata and tool/source identities without copying assets. Outputs: `build/target/manifest.json` and `assets.json`. Save compatibility and the gameplay configuration remain unselected.
|
||||
- `tools/a64_lift.py` now handles bounded immediate branches, loops, CBZ/CBNZ, TBZ/TBNZ, B.cond, integer ADD/SUB with NZCV, conditional selects, and selected unscaled/indexed memory operations. It also recognizes an adjacent 128-bit load/store copy idiom; this is not general SIMD support.
|
||||
- CFG exports use `run(context_offset, instruction_budget)`. Straight-line exports retain `run(context_offset)`. Status 3 aborts on budget exhaustion with partial state; it is not resumable. Unknown targets, unsupported instructions, undefined incoming flags, and entry into the middle of a copy are rejected.
|
||||
- `tools/build_map_header.py` automatically translates the original 332-byte / 83-instruction `MapHeader::MapHeader(MapDeserialiser&)` at `0x1014c6aac`. It has six direct read call sites but performs three reads per successful invocation. The native memory-stream boundary replaces `readOrThrow`; arbitrary C++ unwinding remains unimplemented.
|
||||
- `runtime/map_header_bridge.c` exposes this constructor with an explicitly supplied version and a payload already positioned at the constructor's input. It does not assume the payload follows byte nine in the save. Recovering `MapDeserialiser` initialization/stream positioning is still required before probing these fields in a real archive.
|
||||
- The constructor copies the version object and reads three MapTick-sized fields. For packed versions at or below `0x0001000200000175`, it reads three 32-bit values and widens `0xffffffff` to `UINT64_MAX`; above that cutoff, it reads three 64-bit values. The individual field meanings are not established.
|
||||
- Device reports now include run ID, timestamp, app version/build and compilation identity. Save probes stream a SHA-256 of the entire input. A fresh device report's hash matched the actual approved Mac autosave, and its run ID matched the version self-test.
|
||||
|
||||
## Checks
|
||||
|
||||
- 1,860,000 comparisons of hand-authored ARM64 branch/loop/flag/select fixtures against native execution, including edge values, preserved integer registers, memory guards, execution-budget exhaustion, and six invalid CFG cases. These fixtures are compiler validation, not recovered game code.
|
||||
- 20,000 comparisons of the original MapHeader instruction words against the generated WASM→C implementation, covering old/new version paths, boundary versions, legacy sentinels, guarded public output, unchanged input, and short-stream failure without public output mutation.
|
||||
- Existing 20,000 MapVersion comparisons and fifteen archive-prefix fixtures pass.
|
||||
- Nine unsupported-instruction/CFG/copy guards reject invalid inputs.
|
||||
- Exact REA byte provenance passes for the two RNG functions, their constants, MapVersion, and MapHeader.
|
||||
- Unsigned iOS build and Steam dependency/import audit pass. Signed build 2 was installed and launched on the iPhone 15 Pro; both modern 64-bit and legacy 32-bit/sentinel MapHeader self-tests passed. The version constructor self-test and actual-save version probe passed in the same launch, with a matching input SHA-256. `build/map-header/device-verification.json` retains these reports and the signed binary hash. These MapHeader tests use controlled payloads; the actual-save check still reads only its version.
|
||||
|
||||
Reproduce the new checks after the existing RNG/version builds:
|
||||
|
||||
```sh
|
||||
python3 tools/record_target.py '/absolute/path/to/factorio.app'
|
||||
python3 tests/check_a64_cfg.py
|
||||
python3 tests/check_a64_guards.py
|
||||
python3 tools/build_map_header.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tools/check_rea_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio' evidence/loader-cfg-final.json --include-version --include-map-header
|
||||
python3 tools/loader_inventory.py evidence/loader-cfg-final.json
|
||||
python3 tools/package_ios.py
|
||||
```
|
||||
|
||||
## New loader evidence
|
||||
|
||||
`evidence/loader-cfg-final.json` retains the complete exported records. REA was closed successfully with `build/loader-cfg-rea.snapshot.json`; the live Hopper session took roughly ten minutes to prepare this time. Snapshot replay did not eliminate native analysis for these new queries.
|
||||
|
||||
| Root | Observed direct callees | Observed basic blocks |
|
||||
| --- | ---: | ---: |
|
||||
| `Map::load`, `0x1014c93f8` | 3 | Not queried |
|
||||
| `Map::loadData`, `0x1014c9618` | 82 | 227 |
|
||||
| `Scenario::loadFactory`, `0x1000fa1d0` | 87 | 702 |
|
||||
| `MapHeader` constructor, `0x1014c6aac` | 1 | 8 |
|
||||
|
||||
The Map::load assembly allocates an object, calls the Map constructor, copies initialization state, then calls loadData. Its vector copies, allocator and constructor dependencies remain unsupported by the runnable engine prototype. Map::loadData includes indirect calls and many subsystems; its observed direct call set does not establish complete dependency closure. `build/startup/loader-frontier.json` records the immediate frontier and observed indirect call sites available in retained assembly.
|
||||
|
||||
REA evidence IDs:
|
||||
|
||||
- Map::load direct calls: `ev_9de8048a446bfa9212d8be5bd14ca915af239bde3fefe0005c4d635624fcdd35`.
|
||||
- Map::loadData direct calls: `ev_d8182d6051b428439fae8688802fd80ded2f7a4b88e4b121da8c85c2f015a3ea`.
|
||||
- Scenario::loadFactory direct calls: `ev_3272507fc8a450275baa7dcdba5330225b12cb7df300791ace8a663dd9da9833`.
|
||||
- MapHeader assembly: `ev_23fbedc8f2cdb2aa700d457c1395d090c9af691160a01e3398330fcc740031c8`.
|
||||
- Complete MapHeader bytes: `ev_edc9090e494bf4c92f5c0e5d9f4366272062c6d2d3d758442448ab720bf4e422`.
|
||||
|
||||
## Next batch
|
||||
|
||||
1. Recover MapDeserialiser construction, its version object/layout, and stream position at MapHeader entry. Establish a real-save fixture for this constructor before interpreting actual tick fields.
|
||||
2. Add general guest-to-guest calls so translated constructors compose without implementing each callee as a native bridge.
|
||||
3. Recover the Map constructor's required allocations/global references and translate its smallest reachable dependencies.
|
||||
4. Continue the runtime memory/import model and optional Steam-free startup. Full archive loading, world reconstruction, simulation, rendering and gameplay are still absent.
|
||||
|
||||
The subsequent stream-positioning batch locates MapHeader in the actual save, adds bounded direct guest calls and enters MapHeader through its original C1 tail branch. See [save-layout-progress.md](save-layout-progress.md). The original batch status above is historical.
|
||||
@@ -0,0 +1,55 @@
|
||||
# World-loading translation: first stage
|
||||
|
||||
The iOS prototype now runs an automatically translated `MapVersion::MapVersion(Deserialiser&)` constructor. It reads the save's version header. It does **not** load entities, surfaces, mods, prototypes, or a playable world.
|
||||
|
||||
## Translation and authority
|
||||
|
||||
`tools/a64_lift.py` decodes ARM64 instruction words and emits WAT for a bounded straight-line integer subset. `tools/build_version.py` compiles the original constructor at `0x10150bf28`, 192 bytes / 48 instructions, with body SHA-256 `b9254262cde89c9e79a9fdf6d700860ff9fd623d53c576efaf431600096c8a9b`. A changed body, unknown call, unsupported instruction, missing return, or extra bytes after return fails compilation.
|
||||
|
||||
Registers remain 64-bit. Guest addresses are checked before conversion to the prototype's 64 KiB WASM memory. The compiler supports only the declared execution model; it does not yet implement arbitrary branches, SIMD, native imports, global relocation, indirect calls, or exceptions. ARM encoding reference: [Arm A64 ISA](https://developer.arm.com/documentation/ddi0602/2026-03).
|
||||
|
||||
REA confirms the exact constructor bytes and instructions in `evidence/world-loading-final.json`:
|
||||
|
||||
- Constructor dossier: `ev_5b212f1169e5fff89ef9e6675af58a9df4616c5260c451a6d403a822935e36d0`.
|
||||
- Complete 192-byte read: `ev_e17c26cc53fcb5bc8e04b1342129594def2f2646cc186f462e281e394b436d45`.
|
||||
- `Deserialiser::readOrThrow` dossier: `ev_31eb669fe011e10190a6f9c1aa32ba33c2ad5b5a38cb37c46db03066d36f33b2`.
|
||||
|
||||
The constructor makes five reads of 2, 2, 2, 2 and 1 bytes. It packs four little-endian 16-bit components into a 64-bit field and stores the final byte separately. The final byte's meaning is not established. The original `readOrThrow` dispatches through a stream virtual method and throws a C++ exception on a short read. This bounded prototype replaces that host boundary with a memory stream and an explicit error status. It does not claim to preserve C++ exception unwinding for arbitrary game code.
|
||||
|
||||
## Archive adapter
|
||||
|
||||
`runtime/save_header.c` reads the ZIP central directory, locates `level.dat0` (or `level.dat`), handles stored or deflated ZIP entries, and decodes the nested zlib stream's first nine bytes. It streams bounded prefixes instead of extracting the save or loading the whole world into memory. It rejects ambiguous roots, encryption, unsupported central-directory layouts, invalid offsets and malformed compression. Each compressed/decoded prefix is limited to 1 MiB.
|
||||
|
||||
This is a **header probe**, not whole-archive validation: it does not verify the entire member CRC or the rest of the zlib/map stream. ZIP64 central directories and encrypted archives are unsupported. The generated test archive contains a header and synthetic text, not a real map.
|
||||
|
||||
## Checks completed
|
||||
|
||||
- 20,000 native comparisons against original constructor instruction words, with the same stream adapter on both sides; output bytes, five-read sequence, callee-saved registers, SP and memory guards match.
|
||||
- Short-header failures leave the public result unchanged.
|
||||
- Fifteen ZIP/zlib fixtures cover success, streaming descriptors, malformed/truncated inputs, unsupported flags, ambiguous roots and offset validation.
|
||||
- The local macOS probe reads version 2.0.77 from the installed `_autosave1.zip` without modifying it.
|
||||
- The updated iOS application builds with no Steam imports or dependency. It was signed, installed and launched on the connected iPhone 15 Pro. Its native constructor self-test passed, and it read version 2.0.77 from a generated header-only ZIP. Results are recorded under `build/version/iphone-*.json`.
|
||||
- After explicit user approval, `_autosave1.zip` was copied into the iPhone application’s `Documents/ProbeSave.zip`. The application was relaunched and returned status 0 and version 2.0.77. The actual-save result is retained in `build/version/iphone-real-save-probe.json`; `device-verification.json` records this test, and `generated-device-verification.json` retains the earlier synthetic test. This verifies the version header only; the world has not been loaded.
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
python3 tools/build_version.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
|
||||
python3 tests/check_a64_guards.py
|
||||
python3 tests/check_save_headers.py
|
||||
python3 tools/check_rea_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio' evidence/world-loading-final.json --include-version
|
||||
python3 tools/startup_inventory.py
|
||||
python3 tools/package_ios.py
|
||||
```
|
||||
|
||||
## Remaining startup and world work
|
||||
|
||||
The retained REA `GlobalContext::init` dossier observes 151 direct callees and 643 basic blocks, plus indirect calls with unresolved targets. `build/startup/frontier.json` lists this observed frontier; it is not a complete dependency graph or runnable startup implementation.
|
||||
|
||||
LLVM symbol inventory identifies the next candidate entry points (names/addresses, not recovered behavior): `Scenario::loadFactory` at `0x1000fa1d0`, `Map::load` at `0x1014c93f8`, and `Map::loadData` at `0x1014c9618`. Follow-up live REA callee queries became unavailable after the Hopper connection was lost; no graph completeness is claimed. The completed evidence was exported and a new snapshot retained in `build/world-loading-rea.snapshot.json`.
|
||||
|
||||
Next: recover those loader dependencies, add the required control-flow and memory/import support to the lifter, and replace graphical startup's inlined Steam block with a local optional-services context. Graphics and simulation remain unimplemented.
|
||||
|
||||
A subsequent REA session recovered direct callee frontiers for all three loader roots and translated the branching MapHeader constructor. See [translation-progress.md](translation-progress.md) for evidence, checks, and remaining scope.
|
||||
|
||||
The next batch adds a restricted scenario-envelope adapter and locates MapHeader at decoded offset 333 in the approved autosave. The original native constructor agrees with its translated output there. This is header decoding, not world loading; see [save-layout-progress.md](save-layout-progress.md) for profile restrictions and evidence.
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
name: FactorioPort
|
||||
options:
|
||||
deploymentTarget:
|
||||
iOS: '15.0'
|
||||
targets:
|
||||
FactorioPort:
|
||||
type: application
|
||||
platform: iOS
|
||||
sources:
|
||||
- main.m
|
||||
dependencies:
|
||||
- framework: ../../build/rng/libfactorio_rng_ios.a
|
||||
embed: false
|
||||
- framework: ../../build/version/libfactorio_version_ios.a
|
||||
embed: false
|
||||
- framework: ../../build/map-header/libfactorio_map_header_ios.a
|
||||
embed: false
|
||||
- framework: ../../build/save-layout/libfactorio_save_layout_ios.a
|
||||
embed: false
|
||||
- framework: ../../build/target-deserialiser/libfactorio_target_ios.a
|
||||
embed: false
|
||||
- framework: ../../build/deserialiser/libfactorio_deser_ios.a
|
||||
embed: false
|
||||
- sdk: UIKit.framework
|
||||
- sdk: UniformTypeIdentifiers.framework
|
||||
- sdk: libz.tbd
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: local.factorio.rng-prototype
|
||||
PRODUCT_NAME: FactorioPort
|
||||
CLANG_ENABLE_OBJC_ARC: YES
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
INFOPLIST_KEY_CFBundleDisplayName: Factorio Port Prototype
|
||||
INFOPLIST_KEY_UIFileSharingEnabled: YES
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace: YES
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
||||
HEADER_SEARCH_PATHS:
|
||||
- $(SRCROOT)/../../build/rng
|
||||
- $(SRCROOT)/../../build/version
|
||||
- $(SRCROOT)/../../build/map-header
|
||||
- $(SRCROOT)/../../runtime
|
||||
- $(SRCROOT)/../../third_party/wabt/wasm2c
|
||||
LIBRARY_SEARCH_PATHS:
|
||||
- $(SRCROOT)/../../build/rng
|
||||
- $(SRCROOT)/../../build/version
|
||||
- $(SRCROOT)/../../build/map-header
|
||||
- $(SRCROOT)/../../build/save-layout
|
||||
- $(SRCROOT)/../../build/target-deserialiser
|
||||
- $(SRCROOT)/../../build/deserialiser
|
||||
GCC_PREPROCESSOR_DEFINITIONS:
|
||||
- $(inherited)
|
||||
- WASM_RT_USE_MMAP=0
|
||||
- WASM_RT_MEMCHECK_BOUNDS_CHECK=1
|
||||
TARGETED_DEVICE_FAMILY: '1,2'
|
||||
MARKETING_VERSION: '0.1.0'
|
||||
CURRENT_PROJECT_VERSION: '5'
|
||||
@@ -0,0 +1,3 @@
|
||||
// Host executable: all behavior lives in fx_AppKit.dylib so the shim and host share one image.
|
||||
int fx_host_main(int argc, char **argv);
|
||||
int main(int argc, char **argv) { return fx_host_main(argc, argv); }
|
||||
@@ -0,0 +1,921 @@
|
||||
// AppKit subset implemented on UIKit for the retargeted macOS binary.
|
||||
// Scope: what Factorio's statically linked SDL 2.24 Cocoa backend needs to create one
|
||||
// fullscreen window with a CAMetalLayer and receive mouse-style input from touches.
|
||||
// Unknown selectors on shim classes are logged once and return zero.
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
#import "fx.h"
|
||||
#import "pngdecode.h"
|
||||
|
||||
NSUInteger fx_current_modifier_flags(void);
|
||||
|
||||
// ---- Exported data symbols -------------------------------------------------------------
|
||||
__attribute__((visibility("default"))) id NSApp = nil;
|
||||
__attribute__((visibility("default"))) const double NSAppKitVersionNumber = 2022.0;
|
||||
#define FX_STRING(name) __attribute__((visibility("default"))) NSString *const name = @#name;
|
||||
FX_STRING(NSApplicationDidBecomeActiveNotification)
|
||||
FX_STRING(NSBackingPropertyOldScaleFactorKey)
|
||||
FX_STRING(NSDeviceRGBColorSpace)
|
||||
FX_STRING(NSFilenamesPboardType)
|
||||
FX_STRING(NSWindowDidBecomeKeyNotification)
|
||||
FX_STRING(NSWindowDidChangeBackingPropertiesNotification)
|
||||
FX_STRING(NSWindowDidChangeScreenNotification)
|
||||
FX_STRING(NSWindowDidChangeScreenProfileNotification)
|
||||
FX_STRING(NSWindowDidDeminiaturizeNotification)
|
||||
FX_STRING(NSWindowDidEnterFullScreenNotification)
|
||||
FX_STRING(NSWindowDidExitFullScreenNotification)
|
||||
FX_STRING(NSWindowDidExposeNotification)
|
||||
FX_STRING(NSWindowDidMiniaturizeNotification)
|
||||
FX_STRING(NSWindowDidMoveNotification)
|
||||
FX_STRING(NSWindowDidResignKeyNotification)
|
||||
FX_STRING(NSWindowDidResizeNotification)
|
||||
FX_STRING(NSWindowWillCloseNotification)
|
||||
FX_STRING(NSWindowWillEnterFullScreenNotification)
|
||||
FX_STRING(NSWindowWillExitFullScreenNotification)
|
||||
__attribute__((visibility("default"))) NSString *const NSPasteboardTypeString = @"public.utf8-plain-text";
|
||||
|
||||
__attribute__((visibility("default"))) void NSRectFill(CGRect rect) { (void)rect; }
|
||||
|
||||
// AppKit event constants (values from the macOS SDK).
|
||||
enum {
|
||||
FXLeftMouseDown = 1, FXLeftMouseUp = 2, FXRightMouseDown = 3, FXRightMouseUp = 4,
|
||||
FXMouseMoved = 5, FXLeftMouseDragged = 6, FXRightMouseDragged = 7,
|
||||
FXKeyDown = 10, FXKeyUp = 11, FXFlagsChanged = 12, FXScrollWheel = 22,
|
||||
FXOtherMouseDown = 25, FXOtherMouseUp = 26, FXOtherMouseDragged = 27,
|
||||
};
|
||||
|
||||
// ---- Unknown-selector fallback ----------------------------------------------------------
|
||||
static id fx_zero_imp(id self, SEL _cmd, ...) { return nil; }
|
||||
|
||||
@interface FXShimObject : NSObject
|
||||
@end
|
||||
@implementation FXShimObject
|
||||
+ (BOOL)resolveInstanceMethod:(SEL)sel {
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.' || strncmp(name, "init", 4) == 0) return NO;
|
||||
fx_log("[fx] unimplemented -[%s %s] -> 0", class_getName(self), name);
|
||||
class_addMethod(self, sel, (IMP)fx_zero_imp, "@@:");
|
||||
return YES;
|
||||
}
|
||||
+ (BOOL)resolveClassMethod:(SEL)sel {
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.') return NO;
|
||||
// KVO probes these optional class methods; answering them would disable automatic
|
||||
// change notifications (SDL observes -[NSWindow visible]).
|
||||
if (!strncmp(name, "automaticallyNotifiesObserversOf", 32) || !strncmp(name, "keyPathsForValuesAffecting", 26)) return NO;
|
||||
fx_log("[fx] unimplemented +[%s %s] -> 0", class_getName(self), name);
|
||||
class_addMethod(object_getClass(self), sel, (IMP)fx_zero_imp, "@@:");
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
// ---- Screen geometry --------------------------------------------------------------------
|
||||
// The game sees an AppKit screen of "game points" with backing scale 1. By default one game
|
||||
// point is one physical pixel, so Factorio renders at the panel's native resolution (its own UI
|
||||
// scale setting then controls legibility). Documents/factorio/render-divisor.txt can hold a
|
||||
// divisor (e.g. 1.5) to render fewer pixels. UIKit views and touches convert via fx_game_per_uikit().
|
||||
static CGSize fx_landscape(CGSize s) { return s.width >= s.height ? s : CGSizeMake(s.height, s.width); }
|
||||
static CGSize fx_uikit_points(void) { return fx_landscape(UIScreen.mainScreen.bounds.size); }
|
||||
FX_EXPORT double fx_render_divisor(void) {
|
||||
static double divisor;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
|
||||
NSString *text = [NSString stringWithContentsOfFile:[docs stringByAppendingPathComponent:@"factorio/render-divisor.txt"]
|
||||
encoding:NSUTF8StringEncoding error:nil];
|
||||
divisor = text ? text.doubleValue : 0;
|
||||
if (divisor < 1 || divisor > 4) divisor = 1;
|
||||
});
|
||||
return divisor;
|
||||
}
|
||||
CGSize fx_screen_points(void) {
|
||||
CGSize px = fx_landscape(UIScreen.mainScreen.nativeBounds.size);
|
||||
double d = fx_render_divisor();
|
||||
return CGSizeMake(floor(px.width / d), floor(px.height / d));
|
||||
}
|
||||
double fx_screen_scale(void) { return 1.0; }
|
||||
FX_EXPORT double fx_game_per_uikit(void) { return fx_screen_points().width / fx_uikit_points().width; }
|
||||
|
||||
// ---- NSScreen ---------------------------------------------------------------------------
|
||||
@interface NSScreen : FXShimObject
|
||||
@end
|
||||
@implementation NSScreen
|
||||
+ (NSArray *)screens { static NSArray *s; static dispatch_once_t once; dispatch_once(&once, ^{ s = @[[NSScreen new]]; }); return s; }
|
||||
+ (NSScreen *)mainScreen { return self.screens[0]; }
|
||||
+ (NSScreen *)deepestScreen { return self.screens[0]; }
|
||||
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
- (CGRect)visibleFrame { return self.frame; }
|
||||
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
|
||||
- (NSDictionary *)deviceDescription { return @{@"NSScreenNumber": @(FX_DISPLAY_ID)}; }
|
||||
- (NSString *)localizedName { return @"iPhone"; }
|
||||
- (NSInteger)maximumFramesPerSecond { return UIScreen.mainScreen.maximumFramesPerSecond; }
|
||||
- (id)colorSpace { return nil; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
|
||||
@end
|
||||
|
||||
// ---- NSEvent ----------------------------------------------------------------------------
|
||||
static CGPoint fx_mouse_screen; // AppKit screen coordinates (bottom-left origin, points)
|
||||
static NSUInteger fx_mouse_buttons; // bitmask of pressed buttons
|
||||
|
||||
@class NSWindow;
|
||||
@interface NSEvent : FXShimObject
|
||||
@property NSUInteger type;
|
||||
@property CGPoint locationInWindow;
|
||||
@property (weak) NSWindow *window;
|
||||
@property NSUInteger modifierFlags;
|
||||
@property NSInteger buttonNumber;
|
||||
@property NSInteger clickCount;
|
||||
@property CGFloat deltaX, deltaY, scrollingDeltaX, scrollingDeltaY;
|
||||
@property NSTimeInterval timestamp;
|
||||
@property unsigned short keyCode;
|
||||
@property (copy) NSString *characters;
|
||||
@property NSInteger subtype;
|
||||
@end
|
||||
@implementation NSEvent
|
||||
+ (CGPoint)mouseLocation { return fx_mouse_screen; }
|
||||
+ (NSUInteger)pressedMouseButtons { return fx_mouse_buttons; }
|
||||
+ (NSUInteger)modifierFlags { return fx_current_modifier_flags(); }
|
||||
+ (NSTimeInterval)doubleClickInterval { return 0.3; }
|
||||
+ (instancetype)otherEventWithType:(NSUInteger)type location:(CGPoint)location modifierFlags:(NSUInteger)flags
|
||||
timestamp:(NSTimeInterval)time windowNumber:(NSInteger)wn context:(id)ctx
|
||||
subtype:(short)subtype data1:(NSInteger)d1 data2:(NSInteger)d2 {
|
||||
NSEvent *e = [NSEvent new]; e.type = type; e.locationInWindow = location; e.modifierFlags = flags;
|
||||
e.timestamp = time; e.subtype = subtype; return e;
|
||||
}
|
||||
- (BOOL)hasPreciseScrollingDeltas { return YES; }
|
||||
- (BOOL)isDirectionInvertedFromDevice { return NO; }
|
||||
- (BOOL)isARepeat { return NO; }
|
||||
- (NSString *)charactersIgnoringModifiers { return self.characters; }
|
||||
- (NSUInteger)phase { return 0; }
|
||||
- (NSUInteger)momentumPhase { return 0; }
|
||||
- (NSSet *)touchesMatchingPhase:(NSUInteger)phase inView:(id)view { return [NSSet set]; }
|
||||
- (NSInteger)windowNumber { return 1; }
|
||||
@end
|
||||
|
||||
// ---- NSResponder ------------------------------------------------------------------------
|
||||
@interface NSResponder : FXShimObject
|
||||
@property (strong) NSResponder *nextResponder;
|
||||
@end
|
||||
@implementation NSResponder
|
||||
#define FX_FORWARD(sel) - (void)sel:(NSEvent *)e { [self.nextResponder sel:e]; }
|
||||
FX_FORWARD(mouseDown) FX_FORWARD(mouseUp) FX_FORWARD(mouseDragged) FX_FORWARD(mouseMoved)
|
||||
FX_FORWARD(rightMouseDown) FX_FORWARD(rightMouseUp) FX_FORWARD(rightMouseDragged)
|
||||
FX_FORWARD(otherMouseDown) FX_FORWARD(otherMouseUp) FX_FORWARD(otherMouseDragged)
|
||||
FX_FORWARD(scrollWheel) FX_FORWARD(keyDown) FX_FORWARD(keyUp) FX_FORWARD(flagsChanged)
|
||||
FX_FORWARD(mouseEntered) FX_FORWARD(mouseExited)
|
||||
FX_FORWARD(touchesBeganWithEvent) FX_FORWARD(touchesMovedWithEvent)
|
||||
FX_FORWARD(touchesEndedWithEvent) FX_FORWARD(touchesCancelledWithEvent)
|
||||
- (BOOL)acceptsFirstResponder { return YES; }
|
||||
- (BOOL)becomeFirstResponder { return YES; }
|
||||
- (BOOL)resignFirstResponder { return YES; }
|
||||
// AppKit's text system, reduced to what SDL's field editor needs: printable characters become
|
||||
// insertText:replacementRange:, everything else is a command (ignored by SDL).
|
||||
- (void)interpretKeyEvents:(NSArray *)events {
|
||||
for (NSEvent *e in events) {
|
||||
NSString *c = e.characters;
|
||||
unichar first = c.length ? [c characterAtIndex:0] : 0;
|
||||
if (first >= 0x20 && first != 0x7f && !(first >= 0xF700 && first <= 0xF8FF)) {
|
||||
if ([self respondsToSelector:@selector(insertText:replacementRange:)])
|
||||
[(id)self insertText:c replacementRange:NSMakeRange(NSNotFound, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)doCommandBySelector:(SEL)sel { }
|
||||
- (BOOL)validateMenuItem:(id)item { return YES; }
|
||||
- (id)undoManager { return nil; }
|
||||
@end
|
||||
|
||||
// ---- NSView -----------------------------------------------------------------------------
|
||||
@interface FXBackingView : UIView
|
||||
@property (weak) id appkitView;
|
||||
@end
|
||||
|
||||
@interface NSView : NSResponder
|
||||
@property (nonatomic) CGRect frame;
|
||||
@property (nonatomic, weak) NSView *superview;
|
||||
@property (nonatomic, strong) NSMutableArray *subviewList;
|
||||
@property (nonatomic, weak) NSWindow *window;
|
||||
@property (nonatomic, strong) FXBackingView *backing;
|
||||
@property (nonatomic, strong) CALayer *layer;
|
||||
@property (nonatomic) BOOL wantsLayer;
|
||||
@property (nonatomic) NSUInteger autoresizingMask;
|
||||
@property (nonatomic, getter=isHidden) BOOL hidden;
|
||||
- (void)fx_layout;
|
||||
@end
|
||||
|
||||
@interface NSWindow : NSResponder
|
||||
@property (nonatomic, strong) NSView *contentView;
|
||||
@property (nonatomic, weak) id delegate;
|
||||
@property (nonatomic, getter=isVisible) BOOL visible;
|
||||
@property (nonatomic, getter=isKeyWindow) BOOL keyWindow;
|
||||
@property (nonatomic) NSUInteger styleMask;
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, strong) NSResponder *firstResponder;
|
||||
@property (nonatomic) BOOL releasedWhenClosed, oneShot, acceptsMouseMovedEvents, opaque, movableByWindowBackground;
|
||||
@property (nonatomic) NSInteger level;
|
||||
@property (nonatomic) NSUInteger collectionBehavior;
|
||||
@property (nonatomic) NSInteger tabbingMode;
|
||||
@property (nonatomic) CGFloat alphaValue;
|
||||
@end
|
||||
|
||||
UIView *fx_root_view(void);
|
||||
|
||||
@implementation FXBackingView
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
for (CALayer *l in self.layer.sublayers) if ([l isKindOfClass:CAMetalLayer.class]) l.frame = self.bounds;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation NSView
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if ((self = [super init])) {
|
||||
_subviewList = [NSMutableArray array];
|
||||
_frame = frame;
|
||||
fx_main_sync(^{
|
||||
self->_backing = [[FXBackingView alloc] initWithFrame:CGRectZero];
|
||||
self->_backing.appkitView = self;
|
||||
self->_backing.userInteractionEnabled = NO;
|
||||
self->_backing.backgroundColor = UIColor.clearColor;
|
||||
});
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (instancetype)init { return [self initWithFrame:CGRectZero]; }
|
||||
- (CGRect)bounds { return CGRectMake(0, 0, _frame.size.width, _frame.size.height); }
|
||||
- (void)setFrame:(CGRect)frame { _frame = frame; [self fx_layout]; for (NSView *v in _subviewList) [v fx_layout]; }
|
||||
- (void)setFrameSize:(CGSize)size { self.frame = CGRectMake(_frame.origin.x, _frame.origin.y, size.width, size.height); }
|
||||
- (void)setFrameOrigin:(CGPoint)origin { self.frame = CGRectMake(origin.x, origin.y, _frame.size.width, _frame.size.height); }
|
||||
- (void)fx_layout {
|
||||
// AppKit frames are bottom-left relative to the superview; UIKit is top-left.
|
||||
CGFloat parentHeight = _superview ? _superview.frame.size.height : _frame.size.height;
|
||||
CGFloat k = fx_game_per_uikit(); // game points -> UIKit points
|
||||
CGRect ui = CGRectMake(_frame.origin.x / k, (parentHeight - _frame.origin.y - _frame.size.height) / k,
|
||||
_frame.size.width / k, _frame.size.height / k);
|
||||
fx_main_sync(^{ self->_backing.frame = ui; [self->_backing setNeedsLayout]; [self->_backing layoutIfNeeded]; });
|
||||
}
|
||||
- (NSArray *)subviews { return [_subviewList copy]; }
|
||||
- (void)addSubview:(NSView *)view {
|
||||
[view removeFromSuperview];
|
||||
[_subviewList addObject:view];
|
||||
view.superview = self;
|
||||
view.window = self.window;
|
||||
fx_main_sync(^{ [self->_backing addSubview:view.backing]; });
|
||||
[view fx_layout];
|
||||
}
|
||||
- (void)removeFromSuperview {
|
||||
NSView *parent = _superview;
|
||||
if (!parent) return;
|
||||
[parent.subviewList removeObject:self];
|
||||
_superview = nil;
|
||||
fx_main_sync(^{ [self->_backing removeFromSuperview]; });
|
||||
}
|
||||
- (void)setWindow:(NSWindow *)window { _window = window; for (NSView *v in _subviewList) v.window = window; }
|
||||
- (id)viewWithTag:(NSInteger)tag {
|
||||
if ([self tag] == tag) return self;
|
||||
for (NSView *v in _subviewList) { id found = [v viewWithTag:tag]; if (found) return found; }
|
||||
return nil;
|
||||
}
|
||||
- (NSInteger)tag { return -1; }
|
||||
- (CALayer *)makeBackingLayer { return [CALayer layer]; }
|
||||
- (BOOL)wantsUpdateLayer { return NO; }
|
||||
- (void)updateLayer { }
|
||||
- (void)setWantsLayer:(BOOL)wants {
|
||||
_wantsLayer = wants;
|
||||
if (wants && !_layer) {
|
||||
__block CALayer *layer;
|
||||
fx_main_sync(^{
|
||||
layer = [self makeBackingLayer];
|
||||
layer.frame = self->_backing.bounds;
|
||||
[self->_backing.layer addSublayer:layer];
|
||||
});
|
||||
_layer = layer;
|
||||
fx_log("[fx] -[%s setWantsLayer:YES] -> %s", class_getName(self.class), class_getName(layer.class));
|
||||
}
|
||||
}
|
||||
- (void)setNeedsDisplay:(BOOL)flag { }
|
||||
- (void)setNeedsLayout:(BOOL)flag { }
|
||||
- (void)display { }
|
||||
- (void)drawRect:(CGRect)rect { }
|
||||
- (void)setHidden:(BOOL)hidden { _hidden = hidden; fx_main_sync(^{ self->_backing.hidden = hidden; }); }
|
||||
- (void)setWantsBestResolutionOpenGLSurface:(BOOL)flag { }
|
||||
- (void)setAcceptsTouchEvents:(BOOL)flag { }
|
||||
- (void)setAllowedTouchTypes:(NSUInteger)types { }
|
||||
- (void)registerForDraggedTypes:(NSArray *)types { }
|
||||
- (void)unregisterDraggedTypes { }
|
||||
- (void)addCursorRect:(CGRect)rect cursor:(id)cursor { }
|
||||
- (void)discardCursorRects { }
|
||||
- (void)resetCursorRects { }
|
||||
- (void)lockFocus { }
|
||||
- (void)unlockFocus { }
|
||||
- (id)hitTest:(CGPoint)point { return self; }
|
||||
- (BOOL)mouse:(CGPoint)p inRect:(CGRect)r { return CGRectContainsPoint(r, p); }
|
||||
- (CGPoint)convertPoint:(CGPoint)p fromView:(NSView *)view { return p; }
|
||||
- (CGPoint)convertPoint:(CGPoint)p toView:(NSView *)view { return p; }
|
||||
- (CGRect)convertRect:(CGRect)r toView:(NSView *)view { return r; }
|
||||
- (CGRect)convertRect:(CGRect)r fromView:(NSView *)view { return r; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
|
||||
- (CGSize)convertSizeToBacking:(CGSize)s { double k = fx_screen_scale(); return CGSizeMake(s.width*k, s.height*k); }
|
||||
- (CGPoint)convertPointToBacking:(CGPoint)p { double k = fx_screen_scale(); return CGPointMake(p.x*k, p.y*k); }
|
||||
- (CGRect)convertRectFromBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x/k, r.origin.y/k, r.size.width/k, r.size.height/k); }
|
||||
- (BOOL)isFlipped { return NO; }
|
||||
- (BOOL)mouseDownCanMoveWindow { return NO; }
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent *)e { return YES; }
|
||||
- (BOOL)inLiveResize { return NO; }
|
||||
@end
|
||||
|
||||
// ---- NSWindow ---------------------------------------------------------------------------
|
||||
static NSMutableArray *fx_windows;
|
||||
|
||||
@implementation NSWindow
|
||||
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing
|
||||
defer:(BOOL)defer screen:(id)screen {
|
||||
if ((self = [super init])) {
|
||||
_styleMask = style;
|
||||
_alphaValue = 1;
|
||||
if (!fx_windows) fx_windows = [NSMutableArray array];
|
||||
[fx_windows addObject:self];
|
||||
fx_log("[fx] NSWindow requested %.0fx%.0f style=%lu; using fullscreen %.0fx%.0f",
|
||||
rect.size.width, rect.size.height, (unsigned long)style, fx_screen_points().width, fx_screen_points().height);
|
||||
self.contentView = [[NSView alloc] initWithFrame:[self frame]];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing defer:(BOOL)defer {
|
||||
return [self initWithContentRect:rect styleMask:style backing:backing defer:defer screen:nil];
|
||||
}
|
||||
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
- (CGRect)contentRectForFrameRect:(CGRect)r { return r; }
|
||||
- (CGRect)frameRectForContentRect:(CGRect)r { return r; }
|
||||
- (CGRect)contentLayoutRect { return [self frame]; }
|
||||
- (NSScreen *)screen { return NSScreen.mainScreen; }
|
||||
- (NSInteger)windowNumber { return (NSInteger)[fx_windows indexOfObject:self] + 1; }
|
||||
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
|
||||
- (void)setContentView:(NSView *)view {
|
||||
NSView *old = _contentView;
|
||||
if (old) { old.window = nil; fx_main_sync(^{ [old.backing removeFromSuperview]; }); }
|
||||
_contentView = view;
|
||||
if (!view) return;
|
||||
view.window = self;
|
||||
view.frame = [self frame];
|
||||
fx_main_sync(^{
|
||||
UIView *root = fx_root_view();
|
||||
view.backing.frame = root.bounds;
|
||||
view.backing.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[root insertSubview:view.backing atIndex:0];
|
||||
});
|
||||
if (!_firstResponder) _firstResponder = view;
|
||||
}
|
||||
- (BOOL)makeFirstResponder:(NSResponder *)responder { _firstResponder = responder; return YES; }
|
||||
- (void)makeKeyAndOrderFront:(id)sender {
|
||||
self.visible = YES;
|
||||
if (!_keyWindow) {
|
||||
_keyWindow = YES;
|
||||
NSNotification *n = [NSNotification notificationWithName:NSWindowDidBecomeKeyNotification object:self];
|
||||
if ([_delegate respondsToSelector:@selector(windowDidBecomeKey:)]) [_delegate windowDidBecomeKey:n];
|
||||
[NSNotificationCenter.defaultCenter postNotification:n];
|
||||
}
|
||||
}
|
||||
- (void)makeKeyWindow { [self makeKeyAndOrderFront:nil]; }
|
||||
- (void)makeMainWindow { }
|
||||
- (void)orderFront:(id)sender { self.visible = YES; }
|
||||
- (void)orderFrontRegardless { self.visible = YES; }
|
||||
- (void)orderOut:(id)sender { self.visible = NO; }
|
||||
- (void)orderBack:(id)sender { }
|
||||
- (void)close { self.visible = NO; [fx_windows removeObject:self]; }
|
||||
- (void)performClose:(id)sender { [self close]; }
|
||||
- (BOOL)isMiniaturized { return NO; }
|
||||
- (BOOL)isZoomed { return YES; }
|
||||
- (BOOL)isOnActiveSpace { return YES; }
|
||||
- (BOOL)isMainWindow { return _keyWindow; }
|
||||
- (BOOL)canBecomeKeyWindow { return YES; }
|
||||
- (BOOL)canBecomeMainWindow { return YES; }
|
||||
- (BOOL)hasShadow { return NO; }
|
||||
- (void)setHasShadow:(BOOL)flag { }
|
||||
- (void)setFrame:(CGRect)frame display:(BOOL)flag { }
|
||||
- (void)setFrame:(CGRect)frame display:(BOOL)flag animate:(BOOL)animate { }
|
||||
- (void)setFrameOrigin:(CGPoint)origin { }
|
||||
- (void)setFrameTopLeftPoint:(CGPoint)point { }
|
||||
- (void)setContentSize:(CGSize)size { }
|
||||
- (void)setContentMinSize:(CGSize)size { }
|
||||
- (void)setContentMaxSize:(CGSize)size { }
|
||||
- (void)setBackgroundColor:(id)color { }
|
||||
- (void)setIgnoresMouseEvents:(BOOL)flag { }
|
||||
- (void)setMouseConfinementRect:(CGRect)rect { }
|
||||
- (void)setColorSpace:(id)space { }
|
||||
- (void)registerForDraggedTypes:(NSArray *)types { }
|
||||
- (void)unregisterDraggedTypes { }
|
||||
- (void)invalidateCursorRectsForView:(id)view { }
|
||||
- (void)disableCursorRects { }
|
||||
- (void)enableCursorRects { }
|
||||
- (void)toggleFullScreen:(id)sender { }
|
||||
- (void)miniaturize:(id)sender { }
|
||||
- (void)deminiaturize:(id)sender { }
|
||||
- (void)zoom:(id)sender { }
|
||||
- (void)performZoom:(id)sender { }
|
||||
- (void)performMiniaturize:(id)sender { }
|
||||
- (void)center { }
|
||||
- (void)setShape:(id)shape { }
|
||||
- (id)standardWindowButton:(NSUInteger)b { return nil; }
|
||||
- (void)update { }
|
||||
- (void)display { }
|
||||
- (void)setPreservesContentDuringLiveResize:(BOOL)flag { }
|
||||
- (CGRect)convertRectToScreen:(CGRect)r { return r; }
|
||||
- (CGRect)convertRectFromScreen:(CGRect)r { return r; }
|
||||
- (CGPoint)convertPointToScreen:(CGPoint)p { return p; }
|
||||
- (CGPoint)convertPointFromScreen:(CGPoint)p { return p; }
|
||||
- (CGRect)convertRectToBacking:(CGRect)r { return [self.contentView convertRectToBacking:r]; }
|
||||
- (CGPoint)mouseLocationOutsideOfEventStream { return fx_mouse_screen; }
|
||||
- (void)sendEvent:(NSEvent *)e {
|
||||
NSView *target = self.contentView;
|
||||
switch (e.type) {
|
||||
case FXLeftMouseDown: [target mouseDown:e]; break;
|
||||
case FXLeftMouseUp: [target mouseUp:e]; break;
|
||||
case FXLeftMouseDragged: [target mouseDragged:e]; break;
|
||||
case FXRightMouseDown: [target rightMouseDown:e]; break;
|
||||
case FXRightMouseUp: [target rightMouseUp:e]; break;
|
||||
case FXRightMouseDragged: [target rightMouseDragged:e]; break;
|
||||
case FXOtherMouseDown: [target otherMouseDown:e]; break;
|
||||
case FXOtherMouseUp: [target otherMouseUp:e]; break;
|
||||
case FXOtherMouseDragged: [target otherMouseDragged:e]; break;
|
||||
case FXMouseMoved: [target mouseMoved:e]; break;
|
||||
case FXScrollWheel: [target scrollWheel:e]; break;
|
||||
case FXKeyDown: [(self.firstResponder ?: target) keyDown:e]; break;
|
||||
case FXKeyUp: [(self.firstResponder ?: target) keyUp:e]; break;
|
||||
case FXFlagsChanged: [(self.firstResponder ?: target) flagsChanged:e]; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
// ---- NSApplication ----------------------------------------------------------------------
|
||||
static NSMutableArray<NSEvent *> *fx_event_queue;
|
||||
|
||||
@interface NSApplication : NSResponder
|
||||
@property (nonatomic, weak) id delegate;
|
||||
@property (nonatomic, strong) id mainMenu, windowsMenu, servicesMenu, applicationIconImage;
|
||||
@property (nonatomic, readonly) NSInteger activationPolicy;
|
||||
- (BOOL)setActivationPolicy:(NSInteger)policy;
|
||||
@end
|
||||
|
||||
@interface NSMenu : FXShimObject
|
||||
@end
|
||||
|
||||
@implementation NSApplication
|
||||
+ (instancetype)sharedApplication {
|
||||
if (!NSApp) {
|
||||
NSApp = [self new];
|
||||
fx_log("[fx] +[%s sharedApplication]", class_getName(self));
|
||||
}
|
||||
return NSApp;
|
||||
}
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
// A non-nil main menu makes SDL skip NSBundle nib loading and menu construction.
|
||||
_mainMenu = [NSMenu new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (BOOL)setActivationPolicy:(NSInteger)policy { _activationPolicy = policy; return YES; }
|
||||
- (void)finishLaunching {
|
||||
NSNotification *n = [NSNotification notificationWithName:@"NSApplicationDidFinishLaunchingNotification" object:self];
|
||||
if ([_delegate respondsToSelector:@selector(applicationDidFinishLaunching:)]) [_delegate applicationDidFinishLaunching:n];
|
||||
}
|
||||
- (void)activateIgnoringOtherApps:(BOOL)flag { }
|
||||
- (BOOL)isActive { return YES; }
|
||||
- (BOOL)isHidden { return NO; }
|
||||
- (BOOL)isRunning { return YES; }
|
||||
- (void)hide:(id)sender { }
|
||||
- (void)unhide:(id)sender { }
|
||||
- (void)unhide { }
|
||||
- (void)unhideAllApplications:(id)sender { }
|
||||
- (void)hideOtherApplications:(id)sender { }
|
||||
- (void)setAppleMenu:(id)menu { }
|
||||
- (void)setPresentationOptions:(NSUInteger)options { }
|
||||
- (NSUInteger)presentationOptions { return 0; }
|
||||
- (NSInteger)requestUserAttention:(NSUInteger)type { return 0; }
|
||||
- (void)cancelUserAttentionRequest:(NSInteger)request { }
|
||||
- (void)orderFrontStandardAboutPanel:(id)sender { }
|
||||
- (void)updateWindows { }
|
||||
- (void)stop:(id)sender { }
|
||||
- (void)run { fx_log("[fx] -[NSApplication run] called; not supported"); }
|
||||
- (void)terminate:(id)sender { exit(0); }
|
||||
- (NSArray *)windows { return [fx_windows copy] ?: @[]; }
|
||||
- (NSArray *)orderedWindows { return self.windows; }
|
||||
- (id)keyWindow { for (NSWindow *w in fx_windows) if (w.isKeyWindow) return w; return nil; }
|
||||
- (id)mainWindow { return self.keyWindow; }
|
||||
- (id)windowWithWindowNumber:(NSInteger)n { return (n >= 1 && n <= (NSInteger)fx_windows.count) ? fx_windows[n - 1] : nil; }
|
||||
- (NSEvent *)currentEvent { return nil; }
|
||||
- (void)postEvent:(NSEvent *)e atStart:(BOOL)atStart {
|
||||
@synchronized (fx_event_queue) { if (atStart) [fx_event_queue insertObject:e atIndex:0]; else [fx_event_queue addObject:e]; }
|
||||
}
|
||||
- (NSEvent *)nextEventMatchingMask:(unsigned long long)mask untilDate:(NSDate *)date inMode:(NSString *)mode dequeue:(BOOL)dequeue {
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
@synchronized (fx_event_queue) {
|
||||
if (fx_event_queue.count) {
|
||||
NSEvent *e = fx_event_queue[0];
|
||||
if (dequeue) [fx_event_queue removeObjectAtIndex:0];
|
||||
return e;
|
||||
}
|
||||
}
|
||||
if (pass == 0) {
|
||||
// Let UIKit deliver touches and run main-queue work, as SDL's UIKit backend does.
|
||||
NSTimeInterval wait = date ? MAX(0, MIN([date timeIntervalSinceNow], 0.05)) : 0;
|
||||
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource) { }
|
||||
if (wait > 0 && !fx_event_queue.count) CFRunLoopRunInMode(kCFRunLoopDefaultMode, wait, true);
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
- (void)discardEventsMatchingMask:(unsigned long long)mask beforeEvent:(NSEvent *)e { }
|
||||
- (void)sendEvent:(NSEvent *)e { [e.window sendEvent:e]; }
|
||||
- (NSInteger)runModalForWindow:(id)window { return 0; }
|
||||
- (void)stopModalWithCode:(NSInteger)code { }
|
||||
- (void)registerUserDefaults { }
|
||||
@end
|
||||
|
||||
// Queue a mouse event from UIKit (main thread).
|
||||
void fx_post_mouse(NSUInteger type, CGPoint uikit_point, NSInteger button, NSInteger clicks) {
|
||||
CGSize s = fx_screen_points();
|
||||
CGFloat k = fx_game_per_uikit();
|
||||
CGPoint p = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
|
||||
CGFloat dx = p.x - fx_mouse_screen.x, dy = -(p.y - fx_mouse_screen.y);
|
||||
fx_mouse_screen = p;
|
||||
if (type == FXLeftMouseDown || type == FXRightMouseDown || type == FXOtherMouseDown) fx_mouse_buttons |= (1u << button);
|
||||
if (type == FXLeftMouseUp || type == FXRightMouseUp || type == FXOtherMouseUp) fx_mouse_buttons &= ~(1u << button);
|
||||
NSEvent *e = [NSEvent new];
|
||||
e.type = type;
|
||||
e.locationInWindow = p;
|
||||
e.window = fx_windows.firstObject;
|
||||
e.buttonNumber = button;
|
||||
e.clickCount = clicks;
|
||||
e.modifierFlags = fx_current_modifier_flags();
|
||||
e.deltaX = dx;
|
||||
e.deltaY = dy;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags) {
|
||||
NSEvent *e = [NSEvent new];
|
||||
e.type = type;
|
||||
e.keyCode = keyCode;
|
||||
e.characters = chars ?: @"";
|
||||
e.modifierFlags = flags;
|
||||
e.locationInWindow = fx_mouse_screen;
|
||||
e.window = fx_windows.firstObject;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
void fx_post_scroll(CGPoint uikit_point, CGFloat dx, CGFloat dy) {
|
||||
NSEvent *e = [NSEvent new];
|
||||
CGSize s = fx_screen_points();
|
||||
e.type = FXScrollWheel;
|
||||
CGFloat k = fx_game_per_uikit();
|
||||
e.locationInWindow = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
|
||||
e.window = fx_windows.firstObject;
|
||||
e.deltaX = e.scrollingDeltaX = dx;
|
||||
e.deltaY = e.scrollingDeltaY = dy;
|
||||
e.timestamp = CACurrentMediaTime();
|
||||
[NSApp postEvent:e atStart:NO];
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void fx_appkit_init(void) { fx_event_queue = [NSMutableArray array]; }
|
||||
|
||||
// ---- Inert classes ----------------------------------------------------------------------
|
||||
@implementation NSMenu
|
||||
- (instancetype)initWithTitle:(NSString *)title { return [super init]; }
|
||||
- (id)addItemWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [NSClassFromString(@"NSMenuItem") new]; }
|
||||
- (void)addItem:(id)item { }
|
||||
- (id)itemWithTitle:(NSString *)t { return nil; }
|
||||
+ (void)setMenuBarVisible:(BOOL)visible { }
|
||||
+ (BOOL)menuBarVisible { return NO; }
|
||||
@end
|
||||
|
||||
@interface NSMenuItem : FXShimObject
|
||||
@end
|
||||
@implementation NSMenuItem
|
||||
+ (instancetype)separatorItem { return [self new]; }
|
||||
- (instancetype)initWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [super init]; }
|
||||
@end
|
||||
|
||||
@interface NSCursor : FXShimObject
|
||||
@end
|
||||
@implementation NSCursor
|
||||
#define FX_CURSOR(name) + (instancetype)name { static NSCursor *c; static dispatch_once_t o; dispatch_once(&o, ^{ c = [NSCursor new]; }); return c; }
|
||||
FX_CURSOR(arrowCursor) FX_CURSOR(IBeamCursor) FX_CURSOR(crosshairCursor) FX_CURSOR(closedHandCursor)
|
||||
FX_CURSOR(openHandCursor) FX_CURSOR(pointingHandCursor) FX_CURSOR(resizeLeftRightCursor)
|
||||
FX_CURSOR(resizeUpDownCursor) FX_CURSOR(operationNotAllowedCursor) FX_CURSOR(currentCursor)
|
||||
FX_CURSOR(invisibleCursor) FX_CURSOR(currentSystemCursor)
|
||||
+ (void)hide { }
|
||||
+ (void)unhide { }
|
||||
+ (void)setHiddenUntilMouseMoves:(BOOL)flag { }
|
||||
- (instancetype)initWithImage:(id)image hotSpot:(CGPoint)point { return [super init]; }
|
||||
- (void)set { }
|
||||
- (void)push { }
|
||||
- (void)pop { }
|
||||
@end
|
||||
|
||||
@interface NSImage : FXShimObject
|
||||
@end
|
||||
@implementation NSImage
|
||||
- (instancetype)initWithSize:(CGSize)size { return [super init]; }
|
||||
- (instancetype)initWithData:(NSData *)data { return [super init]; }
|
||||
- (instancetype)initWithContentsOfFile:(NSString *)path { return [super init]; }
|
||||
- (void)addRepresentation:(id)rep { }
|
||||
- (void)lockFocus { }
|
||||
- (void)unlockFocus { }
|
||||
- (void)drawInRect:(CGRect)r fromRect:(CGRect)f operation:(NSUInteger)op fraction:(CGFloat)fr { }
|
||||
@end
|
||||
|
||||
@interface NSColorSpace : FXShimObject
|
||||
+ (instancetype)sRGBColorSpace;
|
||||
@end
|
||||
|
||||
@interface NSBitmapImageRep : FXShimObject
|
||||
@end
|
||||
|
||||
// Decode statistics, logged every 500 images: how many took the direct-copy path, wall time.
|
||||
#include <stdatomic.h>
|
||||
static _Atomic long fx_decode_count, fx_decode_direct, fx_decode_pixels, fx_decode_micros;
|
||||
static void fx_log_format_once(size_t bpc, size_t bpp, uint32_t alpha, uint32_t order, int model) {
|
||||
static NSMutableSet *seen;
|
||||
NSString *key = [NSString stringWithFormat:@"bpc %zu bpp %zu alpha %u order %#x model %d", bpc, bpp, alpha, order, model];
|
||||
@synchronized (NSBitmapImageRep.class) {
|
||||
if (!seen) seen = [NSMutableSet set];
|
||||
if ([seen containsObject:key]) return;
|
||||
[seen addObject:key];
|
||||
}
|
||||
fx_log("[fx] ImageIO layout needing conversion: %s", key.UTF8String);
|
||||
}
|
||||
static void fx_decode_stats(long pixels, double seconds) {
|
||||
static double start;
|
||||
long n = atomic_fetch_add(&fx_decode_count, 1) + 1;
|
||||
atomic_fetch_add(&fx_decode_pixels, pixels);
|
||||
atomic_fetch_add(&fx_decode_micros, (long)(seconds * 1e6));
|
||||
if (n == 1) start = CACurrentMediaTime();
|
||||
if (n % 500 == 0)
|
||||
fx_log("[fx] decoded %ld images (%ld wuffs/direct), %.0f Mpx, %.1fs wall, %.1fs summed decode time", n,
|
||||
(long)fx_decode_direct, fx_decode_pixels / 1e6, CACurrentMediaTime() - start, fx_decode_micros / 1e6);
|
||||
}
|
||||
@implementation NSBitmapImageRep {
|
||||
unsigned char *_data;
|
||||
NSInteger _w, _h, _rowBytes, _bpp, _spp;
|
||||
}
|
||||
- (instancetype)initWithBitmapDataPlanes:(unsigned char **)planes pixelsWide:(NSInteger)w pixelsHigh:(NSInteger)h
|
||||
bitsPerSample:(NSInteger)bps samplesPerPixel:(NSInteger)spp hasAlpha:(BOOL)alpha
|
||||
isPlanar:(BOOL)planar colorSpaceName:(NSString *)cs bytesPerRow:(NSInteger)rowBytes
|
||||
bitsPerPixel:(NSInteger)bpp {
|
||||
if ((self = [super init])) {
|
||||
_w = w; _h = h; _spp = spp; _bpp = bpp ? bpp : bps * spp;
|
||||
_rowBytes = rowBytes ? rowBytes : (w * _bpp + 7) / 8;
|
||||
_data = calloc((size_t)(_rowBytes * h), 1);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
// Decodes like AppKit does for PNG: tightly packed, non-premultiplied, native-endian samples,
|
||||
// keeping the source channel count (gray, gray+alpha, RGB, RGBA) and 8 or 16 bits per sample.
|
||||
// Factorio's preload_image() copies and premultiplies this buffer itself.
|
||||
- (instancetype)initWithData:(NSData *)data {
|
||||
if (!(self = [super init])) return nil;
|
||||
double fx_t0 = CACurrentMediaTime();
|
||||
uint8_t *png; int pw, ph, pc;
|
||||
if (fx_png_decode(data.bytes, data.length, &png, &pw, &ph, &pc)) {
|
||||
_data = png; _w = pw; _h = ph; _spp = pc; _bpp = 8 * pc; _rowBytes = (NSInteger)pw * pc;
|
||||
fx_decode_direct++;
|
||||
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
|
||||
return self;
|
||||
}
|
||||
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data,
|
||||
(__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCache: @NO});
|
||||
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
|
||||
if (source) CFRelease(source);
|
||||
if (!image) return nil;
|
||||
BOOL ok = [self fx_unpack:image] || [self fx_render:image];
|
||||
CGImageRelease(image);
|
||||
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
|
||||
return ok ? self : nil;
|
||||
}
|
||||
+ (instancetype)imageRepWithData:(NSData *)data { return [[self alloc] initWithData:data]; }
|
||||
|
||||
- (BOOL)fx_unpack:(CGImageRef)image {
|
||||
size_t bpc = CGImageGetBitsPerComponent(image), bpp = CGImageGetBitsPerPixel(image);
|
||||
CGColorSpaceModel model = CGColorSpaceGetModel(CGImageGetColorSpace(image));
|
||||
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image);
|
||||
CGBitmapInfo info = CGImageGetBitmapInfo(image);
|
||||
if ((bpc != 8 && bpc != 16) || (info & kCGBitmapFloatComponents)) return NO;
|
||||
size_t color = model == kCGColorSpaceModelMonochrome ? 1 : model == kCGColorSpaceModelRGB ? 3 : 0;
|
||||
if (!color) return NO;
|
||||
BOOL hasAlpha = alpha == kCGImageAlphaLast || alpha == kCGImageAlphaFirst ||
|
||||
alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
|
||||
BOOL premultiplied = alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
|
||||
BOOL alphaFirst = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaNoneSkipFirst;
|
||||
BOOL skip = alpha == kCGImageAlphaNoneSkipLast || alpha == kCGImageAlphaNoneSkipFirst;
|
||||
size_t srcComponents = bpp / bpc, outComponents = color + (hasAlpha ? 1 : 0);
|
||||
if (srcComponents != color + ((hasAlpha || skip) ? 1 : 0)) return NO;
|
||||
CGBitmapInfo order = info & kCGBitmapByteOrderMask;
|
||||
BOOL reversed = (bpc == 8 && (order == kCGBitmapByteOrder32Little || order == kCGBitmapByteOrder16Little));
|
||||
BOOL littleSamples = bpc == 16 && order == kCGBitmapByteOrder16Little;
|
||||
|
||||
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
|
||||
size_t sampleBytes = bpc / 8, srcPixel = bpp / 8;
|
||||
_w = w; _h = h; _spp = outComponents; _bpp = outComponents * bpc; _rowBytes = w * outComponents * sampleBytes;
|
||||
_data = malloc(_rowBytes * h);
|
||||
// Decide from metadata before touching pixels: each pixel access below decodes the PNG.
|
||||
// Direct: decoded layout already matches (non-premultiplied R G B [A], 8-bit or 16-bit LE).
|
||||
BOOL direct = !premultiplied && !alphaFirst && !skip && !reversed && (bpc == 8 || littleSamples);
|
||||
if (!direct) {
|
||||
fx_log_format_once(bpc, bpp, alpha, order, model);
|
||||
// One vectorized vImage decode+conversion into the target layout, keeping the image's
|
||||
// colour space (no colour management changes).
|
||||
vImage_CGImageFormat target = {
|
||||
(uint32_t)bpc, (uint32_t)(outComponents * bpc), CGImageGetColorSpace(image),
|
||||
(hasAlpha ? kCGImageAlphaLast : kCGImageAlphaNone) | (bpc == 16 ? kCGBitmapByteOrder16Little : kCGBitmapByteOrderDefault),
|
||||
0, NULL, kCGRenderingIntentDefault};
|
||||
vImage_Buffer buffer = {_data, h, w, (size_t)_rowBytes};
|
||||
if (vImageBuffer_InitWithCGImage(&buffer, &target, NULL, image, kvImageNoAllocate) == kvImageNoError) return YES;
|
||||
fx_log("[fx] vImage conversion failed; using scalar path");
|
||||
}
|
||||
CFDataRef pixels = CGDataProviderCopyData(CGImageGetDataProvider(image));
|
||||
if (!pixels) return NO;
|
||||
const uint8_t *src = CFDataGetBytePtr(pixels);
|
||||
size_t srcRow = CGImageGetBytesPerRow(image);
|
||||
if (direct) {
|
||||
if (srcRow == (size_t)_rowBytes) memcpy(_data, src, _rowBytes * h);
|
||||
else for (size_t y = 0; y < h; y++) memcpy(_data + y * _rowBytes, src + y * srcRow, _rowBytes);
|
||||
fx_decode_direct++;
|
||||
CFRelease(pixels);
|
||||
return YES;
|
||||
}
|
||||
for (size_t y = 0; y < h; y++) {
|
||||
for (size_t x = 0; x < w; x++) {
|
||||
const uint8_t *p = src + y * srcRow + x * srcPixel;
|
||||
uint32_t s[4] = {0};
|
||||
for (size_t c = 0; c < srcComponents; c++) {
|
||||
size_t i = reversed ? srcComponents - 1 - c : c;
|
||||
s[c] = bpc == 8 ? p[i] : littleSamples ? (uint32_t)(p[2*i] | p[2*i+1] << 8) : (uint32_t)(p[2*i] << 8 | p[2*i+1]);
|
||||
}
|
||||
uint32_t out[4], a = 0;
|
||||
size_t first = alphaFirst ? 1 : 0;
|
||||
for (size_t c = 0; c < color; c++) out[c] = s[first + c];
|
||||
if (hasAlpha) {
|
||||
a = alphaFirst ? s[0] : s[color];
|
||||
uint32_t max = bpc == 8 ? 255 : 65535;
|
||||
if (premultiplied && a && a < max)
|
||||
for (size_t c = 0; c < color; c++) out[c] = MIN(max, (out[c] * max + a / 2) / a);
|
||||
out[color] = a;
|
||||
}
|
||||
uint8_t *d = _data + y * _rowBytes + x * outComponents * sampleBytes;
|
||||
for (size_t c = 0; c < outComponents; c++) {
|
||||
if (bpc == 8) d[c] = (uint8_t)out[c];
|
||||
else ((uint16_t *)d)[c] = (uint16_t)out[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
CFRelease(pixels);
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Fallback for indexed, CMYK and other layouts: draw into RGBA8 and un-premultiply.
|
||||
- (BOOL)fx_render:(CGImageRef)image {
|
||||
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
|
||||
free(_data);
|
||||
_w = w; _h = h; _spp = 4; _bpp = 32; _rowBytes = w * 4;
|
||||
_data = calloc(_rowBytes * h, 1);
|
||||
CGColorSpaceRef rgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef ctx = CGBitmapContextCreate(_data, w, h, 8, _rowBytes, rgb, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
||||
CGColorSpaceRelease(rgb);
|
||||
if (!ctx) return NO;
|
||||
CGContextSetBlendMode(ctx, kCGBlendModeCopy);
|
||||
CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image);
|
||||
CGContextRelease(ctx);
|
||||
for (size_t i = 0; i < w * h; i++) {
|
||||
uint8_t *p = _data + i * 4, a = p[3];
|
||||
if (a && a < 255) for (int c = 0; c < 3; c++) p[c] = (uint8_t)MIN(255, (p[c] * 255 + a / 2) / a);
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
- (void)dealloc { free(_data); }
|
||||
- (unsigned char *)bitmapData { return _data; }
|
||||
- (NSInteger)pixelsWide { return _w; }
|
||||
- (NSInteger)pixelsHigh { return _h; }
|
||||
- (NSInteger)bitsPerPixel { return _bpp; }
|
||||
- (NSInteger)samplesPerPixel { return _spp; }
|
||||
- (NSInteger)bytesPerRow { return _rowBytes; }
|
||||
- (CGSize)size { return CGSizeMake(_w, _h); }
|
||||
- (id)colorSpace { return [NSColorSpace sRGBColorSpace]; }
|
||||
- (id)bitmapImageRepByRetaggingWithColorSpace:(id)cs { return self; }
|
||||
- (id)bitmapImageRepByConvertingToColorSpace:(id)cs renderingIntent:(NSInteger)intent { return self; }
|
||||
@end
|
||||
|
||||
@interface FXNSColor : FXShimObject
|
||||
@end
|
||||
@implementation FXNSColor
|
||||
+ (instancetype)blackColor { return [self new]; }
|
||||
+ (instancetype)clearColor { return [self new]; }
|
||||
+ (instancetype)whiteColor { return [self new]; }
|
||||
+ (instancetype)colorWithRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
|
||||
+ (instancetype)colorWithSRGBRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
|
||||
- (void)set { }
|
||||
- (void)setFill { }
|
||||
- (void)setStroke { }
|
||||
@end
|
||||
|
||||
@implementation NSColorSpace
|
||||
#define FX_SPACE(name) + (instancetype)name { static id s; static dispatch_once_t o; dispatch_once(&o, ^{ s = [self new]; }); return s; }
|
||||
FX_SPACE(sRGBColorSpace) FX_SPACE(genericRGBColorSpace) FX_SPACE(deviceRGBColorSpace)
|
||||
- (NSData *)ICCProfileData { return nil; }
|
||||
- (CGColorSpaceRef)CGColorSpace { return NULL; }
|
||||
@end
|
||||
|
||||
@interface NSGraphicsContext : FXShimObject
|
||||
@end
|
||||
@implementation NSGraphicsContext
|
||||
+ (id)currentContext { return nil; }
|
||||
+ (void)setCurrentContext:(id)ctx { }
|
||||
+ (void)saveGraphicsState { }
|
||||
+ (void)restoreGraphicsState { }
|
||||
+ (id)graphicsContextWithBitmapImageRep:(id)rep { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSBezierPath : FXShimObject
|
||||
@end
|
||||
@implementation NSBezierPath
|
||||
+ (instancetype)bezierPath { return [self new]; }
|
||||
+ (instancetype)bezierPathWithRect:(CGRect)r { return [self new]; }
|
||||
- (void)appendBezierPathWithRect:(CGRect)r { }
|
||||
- (void)addClip { }
|
||||
- (void)fill { }
|
||||
@end
|
||||
|
||||
@interface NSAlert : FXShimObject
|
||||
@property (copy) NSString *messageText, *informativeText;
|
||||
@property NSUInteger alertStyle;
|
||||
@end
|
||||
@implementation NSAlert
|
||||
- (id)addButtonWithTitle:(NSString *)title { fx_log("[fx] NSAlert button: %s", title.UTF8String); return nil; }
|
||||
- (NSInteger)runModal {
|
||||
fx_log("[fx] NSAlert: %s | %s", self.messageText.UTF8String ?: "", self.informativeText.UTF8String ?: "");
|
||||
return 1000; // NSAlertFirstButtonReturn
|
||||
}
|
||||
- (void)beginSheetModalForWindow:(id)w completionHandler:(void (^)(NSInteger))handler { NSInteger r = [self runModal]; if (handler) handler(r); }
|
||||
- (id)window { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSPasteboard : FXShimObject
|
||||
@end
|
||||
@implementation NSPasteboard
|
||||
+ (instancetype)generalPasteboard { static NSPasteboard *p; static dispatch_once_t o; dispatch_once(&o, ^{ p = [NSPasteboard new]; }); return p; }
|
||||
- (NSInteger)changeCount { __block NSInteger c; fx_main_sync(^{ c = UIPasteboard.generalPasteboard.changeCount; }); return c; }
|
||||
- (NSString *)stringForType:(NSString *)type { __block NSString *s; fx_main_sync(^{ s = UIPasteboard.generalPasteboard.string; }); return s; }
|
||||
- (BOOL)setString:(NSString *)s forType:(NSString *)type { fx_main_sync(^{ UIPasteboard.generalPasteboard.string = s; }); return YES; }
|
||||
- (NSInteger)declareTypes:(NSArray *)types owner:(id)owner { return [self changeCount]; }
|
||||
- (NSInteger)clearContents { return [self changeCount]; }
|
||||
- (NSString *)availableTypeFromArray:(NSArray *)types { return types.firstObject; }
|
||||
- (id)propertyListForType:(NSString *)type { return nil; }
|
||||
- (NSData *)dataForType:(NSString *)type { return nil; }
|
||||
@end
|
||||
|
||||
@interface NSWorkspace : FXShimObject
|
||||
@end
|
||||
@implementation NSWorkspace
|
||||
+ (instancetype)sharedWorkspace { static NSWorkspace *w; static dispatch_once_t o; dispatch_once(&o, ^{ w = [NSWorkspace new]; }); return w; }
|
||||
- (BOOL)openURL:(NSURL *)url { fx_log("[fx] openURL %s", url.absoluteString.UTF8String); return NO; }
|
||||
- (void)activateFileViewerSelectingURLs:(NSArray *)urls { }
|
||||
- (id)notificationCenter { return NSNotificationCenter.defaultCenter; }
|
||||
@end
|
||||
|
||||
@interface NSRunningApplication : FXShimObject
|
||||
@end
|
||||
@implementation NSRunningApplication
|
||||
+ (instancetype)currentApplication { return [self new]; }
|
||||
+ (NSArray *)runningApplicationsWithBundleIdentifier:(NSString *)bundle { return @[]; }
|
||||
- (BOOL)activateWithOptions:(NSUInteger)options { return YES; }
|
||||
- (BOOL)isActive { return YES; }
|
||||
- (BOOL)isFinishedLaunching { return YES; }
|
||||
@end
|
||||
|
||||
@interface NSOpenGLPixelFormat : FXShimObject
|
||||
@end
|
||||
@implementation NSOpenGLPixelFormat
|
||||
- (instancetype)initWithAttributes:(const uint32_t *)attributes { fx_log("[fx] OpenGL pixel format requested; unsupported"); return nil; }
|
||||
- (void *)CGLPixelFormatObj { return NULL; }
|
||||
@end
|
||||
|
||||
@interface NSOpenGLContext : FXShimObject
|
||||
@end
|
||||
@implementation NSOpenGLContext
|
||||
- (instancetype)initWithFormat:(id)format shareContext:(id)share { fx_log("[fx] OpenGL context requested; unsupported"); return nil; }
|
||||
+ (void)clearCurrentContext { }
|
||||
+ (id)currentContext { return nil; }
|
||||
- (void)makeCurrentContext { }
|
||||
- (void)flushBuffer { }
|
||||
- (void)update { }
|
||||
- (void)setView:(id)view { }
|
||||
- (id)view { return nil; }
|
||||
- (void *)CGLContextObj { return NULL; }
|
||||
@end
|
||||
@@ -0,0 +1,97 @@
|
||||
// macOS CoreAudio device model for SDL's coreaudio backend (macOS build).
|
||||
// FX_LIB_COREAUDIO: one virtual output device (the iPhone route), answering the AudioObject
|
||||
// properties SDL 2.24 queries. Playback itself uses the real iOS AudioQueue.
|
||||
// FX_LIB_AUDIOTOOLBOX: AudioQueueSetProperty passes through, except macOS-only CurrentDevice.
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <dlfcn.h>
|
||||
#include <string.h>
|
||||
#include "fx.h"
|
||||
|
||||
#define FOURCC(a, b, c, d) ((uint32_t)(a) << 24 | (uint32_t)(b) << 16 | (uint32_t)(c) << 8 | (uint32_t)(d))
|
||||
|
||||
#ifdef FX_LIB_COREAUDIO
|
||||
typedef uint32_t AudioObjectID;
|
||||
typedef struct { uint32_t mSelector, mScope, mElement; } FXPropertyAddress;
|
||||
typedef OSStatus (*FXPropertyListener)(AudioObjectID, uint32_t, const FXPropertyAddress *, void *);
|
||||
|
||||
enum {
|
||||
kSystemObject = 1, kSpeaker = 0x100,
|
||||
kDevices = FOURCC('d', 'e', 'v', '#'), kDefaultOutput = FOURCC('d', 'O', 'u', 't'),
|
||||
kDefaultInput = FOURCC('d', 'I', 'n', ' '), kStreamConfiguration = FOURCC('s', 'l', 'a', 'y'),
|
||||
kName = FOURCC('l', 'n', 'a', 'm'), kNominalSampleRate = FOURCC('n', 's', 'r', 't'),
|
||||
kIsAlive = FOURCC('l', 'i', 'v', 'n'), kHogMode = FOURCC('o', 'i', 'n', 'k'),
|
||||
kUID = FOURCC('u', 'i', 'd', ' '), kScopeInput = FOURCC('i', 'n', 'p', 't'),
|
||||
kUnknownProperty = FOURCC('w', 'h', 'o', '?'), kBadObject = FOURCC('!', 'o', 'b', 'j'),
|
||||
};
|
||||
|
||||
// Writes the property into `out` (when non-NULL) and returns its size, or 0 if unsupported.
|
||||
static uint32_t fx_property(AudioObjectID object, const FXPropertyAddress *a, void *out, OSStatus *status) {
|
||||
*status = 0;
|
||||
if (object == kSystemObject) {
|
||||
switch (a->mSelector) {
|
||||
case kDevices: case kDefaultOutput: if (out) *(AudioObjectID *)out = kSpeaker; return 4;
|
||||
case kDefaultInput: if (out) *(AudioObjectID *)out = 0; return 4; // no capture device
|
||||
}
|
||||
} else if (object == kSpeaker) {
|
||||
int input = a->mScope == kScopeInput;
|
||||
switch (a->mSelector) {
|
||||
case kStreamConfiguration:
|
||||
if (out) {
|
||||
AudioBufferList *list = out;
|
||||
list->mNumberBuffers = input ? 0 : 1;
|
||||
list->mBuffers[0] = (AudioBuffer){2, 0, NULL};
|
||||
}
|
||||
return sizeof(AudioBufferList);
|
||||
case kName: if (out) *(CFStringRef *)out = CFStringCreateCopy(NULL, CFSTR("iPhone")); return sizeof(CFStringRef);
|
||||
case kUID: if (out) *(CFStringRef *)out = CFStringCreateCopy(NULL, CFSTR("fx-ios-output")); return sizeof(CFStringRef);
|
||||
case kNominalSampleRate: if (out) *(Float64 *)out = 48000.0; return sizeof(Float64);
|
||||
case kIsAlive: if (out) *(uint32_t *)out = !input; return 4;
|
||||
case kHogMode: if (out) *(pid_t *)out = -1; return sizeof(pid_t);
|
||||
}
|
||||
} else {
|
||||
*status = kBadObject;
|
||||
return 0;
|
||||
}
|
||||
*status = kUnknownProperty;
|
||||
fx_log("[fx] CoreAudio: unsupported property '%c%c%c%c' on object %u", a->mSelector >> 24, a->mSelector >> 16,
|
||||
a->mSelector >> 8, a->mSelector, object);
|
||||
return 0;
|
||||
}
|
||||
|
||||
FX_EXPORT Boolean AudioObjectHasProperty(AudioObjectID object, const FXPropertyAddress *a) {
|
||||
OSStatus status;
|
||||
return fx_property(object, a, NULL, &status) && !status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectGetPropertyDataSize(AudioObjectID object, const FXPropertyAddress *a, uint32_t qsize,
|
||||
const void *q, uint32_t *size) {
|
||||
OSStatus status;
|
||||
uint32_t n = fx_property(object, a, NULL, &status);
|
||||
if (!status && size) *size = n;
|
||||
return status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectGetPropertyData(AudioObjectID object, const FXPropertyAddress *a, uint32_t qsize,
|
||||
const void *q, uint32_t *size, void *data) {
|
||||
OSStatus status;
|
||||
uint32_t n = fx_property(object, a, NULL, &status);
|
||||
if (status) return status;
|
||||
if (!size || *size < n) return FOURCC('!', 's', 'i', 'z');
|
||||
fx_property(object, a, data, &status);
|
||||
*size = n;
|
||||
return status;
|
||||
}
|
||||
FX_EXPORT OSStatus AudioObjectAddPropertyListener(AudioObjectID o, const FXPropertyAddress *a, FXPropertyListener l, void *c) { return 0; }
|
||||
FX_EXPORT OSStatus AudioObjectRemovePropertyListener(AudioObjectID o, const FXPropertyAddress *a, FXPropertyListener l, void *c) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_AUDIOTOOLBOX
|
||||
FX_EXPORT OSStatus AudioQueueSetProperty(AudioQueueRef queue, AudioQueuePropertyID id, const void *data, UInt32 size) {
|
||||
static OSStatus (*real)(AudioQueueRef, AudioQueuePropertyID, const void *, UInt32);
|
||||
if (!real) {
|
||||
void *toolbox = dlopen("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", RTLD_LAZY | RTLD_NOLOAD);
|
||||
real = toolbox ? dlsym(toolbox, "AudioQueueSetProperty") : NULL;
|
||||
}
|
||||
if (id == FOURCC('a', 'q', 'c', 'd')) return 0; // kAudioQueueProperty_CurrentDevice: iOS routes itself
|
||||
return real ? real(queue, id, data, size) : -1;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,284 @@
|
||||
// iOS application host: UIKit lifecycle, touch input, and launch of the original entry point.
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
#import <dlfcn.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <mach-o/loader.h>
|
||||
#import <stdarg.h>
|
||||
#import <sys/stat.h>
|
||||
#import "fx.h"
|
||||
|
||||
extern char **environ;
|
||||
void fx_post_mouse(NSUInteger type, CGPoint point, NSInteger button, NSInteger clicks);
|
||||
void fx_post_scroll(CGPoint point, CGFloat dx, CGFloat dy);
|
||||
void fx_call_on_stack(void *stack_top, void (*fn)(void *), void *arg);
|
||||
void fx_install_crash_reporter(void);
|
||||
BOOL fx_handle_presses(NSSet<UIPress *> *presses, BOOL down);
|
||||
void fx_install_controls(UIView *root);
|
||||
void fx_controls_after_click(void);
|
||||
void fx_patch_metal(void);
|
||||
|
||||
static FILE *fx_log_file;
|
||||
static UIView *fx_root;
|
||||
static UILabel *fx_status;
|
||||
|
||||
FX_EXPORT void fx_log(const char *format, ...) {
|
||||
char buffer[2048];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(buffer, sizeof buffer, format, args);
|
||||
va_end(args);
|
||||
fprintf(stderr, "%s\n", buffer);
|
||||
if (fx_log_file) { fprintf(fx_log_file, "%s\n", buffer); fflush(fx_log_file); }
|
||||
}
|
||||
|
||||
void fx_main_sync(void (^block)(void)) {
|
||||
if (NSThread.isMainThread) block();
|
||||
else dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
|
||||
UIView *fx_root_view(void) { return fx_root; }
|
||||
|
||||
static void fx_set_status(NSString *text) {
|
||||
fx_log("[fx] status: %s", text.UTF8String);
|
||||
fx_main_sync(^{ fx_status.text = text; fx_status.hidden = text.length == 0; });
|
||||
}
|
||||
|
||||
// ---- Touch input: one finger = left mouse; two-finger pan = wheel; long press = right click.
|
||||
@interface FXTouchView : UIView <UIGestureRecognizerDelegate>
|
||||
@end
|
||||
@implementation FXTouchView {
|
||||
UITouch *_primary;
|
||||
BOOL _rightHeld;
|
||||
CGPoint _panLast;
|
||||
}
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
self.multipleTouchEnabled = YES;
|
||||
self.backgroundColor = UIColor.blackColor;
|
||||
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
|
||||
pan.minimumNumberOfTouches = 2;
|
||||
pan.cancelsTouchesInView = NO;
|
||||
pan.delegate = self;
|
||||
[self addGestureRecognizer:pan];
|
||||
UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hold:)];
|
||||
hold.minimumPressDuration = 0.6;
|
||||
hold.cancelsTouchesInView = NO;
|
||||
hold.delegate = self;
|
||||
[self addGestureRecognizer:hold];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if (_primary || event.allTouches.count > 1) return;
|
||||
_primary = touches.anyObject;
|
||||
CGPoint p = [_primary locationInView:self];
|
||||
fx_post_mouse(5, p, 0, 0); // move first so SDL knows the cursor position
|
||||
fx_post_mouse(1, p, 0, _primary.tapCount ?: 1);
|
||||
}
|
||||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if (_primary && [touches containsObject:_primary]) fx_post_mouse(6, [_primary locationInView:self], 0, 0);
|
||||
}
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if (_primary && [touches containsObject:_primary]) {
|
||||
fx_post_mouse(2, [_primary locationInView:self], 0, _primary.tapCount ?: 1);
|
||||
_primary = nil;
|
||||
fx_controls_after_click();
|
||||
}
|
||||
}
|
||||
// Overlay buttons and the stick handle their own touches.
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g shouldReceiveTouch:(UITouch *)touch { return touch.view == self; }
|
||||
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; }
|
||||
- (void)pan:(UIPanGestureRecognizer *)g {
|
||||
CGPoint t = [g translationInView:self], at = [g locationInView:self];
|
||||
if (g.state == UIGestureRecognizerStateBegan) {
|
||||
if (_primary) { fx_post_mouse(2, [_primary locationInView:self], 0, 1); _primary = nil; }
|
||||
_panLast = t;
|
||||
} else if (g.state == UIGestureRecognizerStateChanged) {
|
||||
fx_post_scroll(at, (t.x - _panLast.x) / 10.0, (t.y - _panLast.y) / 10.0);
|
||||
_panLast = t;
|
||||
}
|
||||
}
|
||||
// Long press = hold the right mouse button until the finger lifts (mining, deconstructing,
|
||||
// dragging to remove); moving while held drags with the right button.
|
||||
- (void)hold:(UILongPressGestureRecognizer *)g {
|
||||
CGPoint p = [g locationInView:self];
|
||||
switch (g.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
if (g.numberOfTouches != 1) return;
|
||||
if (_primary) { fx_post_mouse(2, p, 0, 1); _primary = nil; }
|
||||
_rightHeld = YES;
|
||||
fx_post_mouse(3, p, 1, 1);
|
||||
// Factorio refreshes the hovered entity on mouse motion; nudge it so mining starts
|
||||
// without the finger having to move.
|
||||
fx_post_mouse(7, p, 1, 0);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
||||
if (self->_rightHeld) fx_post_mouse(7, p, 1, 0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case UIGestureRecognizerStateChanged:
|
||||
if (_rightHeld) fx_post_mouse(7, p, 1, 0);
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed:
|
||||
if (_rightHeld) { _rightHeld = NO; fx_post_mouse(4, p, 1, 1); fx_controls_after_click(); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@interface FXRootViewController : UIViewController
|
||||
@end
|
||||
@implementation FXRootViewController
|
||||
- (void)loadView { self.view = [[FXTouchView alloc] initWithFrame:UIScreen.mainScreen.bounds]; }
|
||||
- (BOOL)prefersStatusBarHidden { return YES; }
|
||||
- (BOOL)prefersHomeIndicatorAutoHidden { return YES; }
|
||||
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { return UIRectEdgeAll; }
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskLandscape; }
|
||||
- (BOOL)canBecomeFirstResponder { return YES; }
|
||||
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self becomeFirstResponder]; }
|
||||
// Hardware keyboard (when the soft-keyboard input view is not first responder).
|
||||
- (void)pressesBegan:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, YES)) [super pressesBegan:p withEvent:e]; }
|
||||
- (void)pressesEnded:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesEnded:p withEvent:e]; }
|
||||
- (void)pressesCancelled:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesCancelled:p withEvent:e]; }
|
||||
@end
|
||||
|
||||
// ---- Launch ----------------------------------------------------------------------------
|
||||
typedef int (*fx_main_fn)(int, char **, char **, char **);
|
||||
static struct { fx_main_fn entry; int argc; char **argv; char **apple; int result; } fx_game;
|
||||
|
||||
static void fx_run_main(void *unused) {
|
||||
fx_game.result = fx_game.entry(fx_game.argc, fx_game.argv, environ, fx_game.apple);
|
||||
}
|
||||
|
||||
static fx_main_fn fx_find_entry(void) {
|
||||
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
|
||||
const char *name = _dyld_get_image_name(i);
|
||||
size_t n = strlen(name);
|
||||
if (n < 15 || strcmp(name + n - 15, "/Factorio.dylib")) continue;
|
||||
const struct mach_header_64 *header = (const void *)_dyld_get_image_header(i);
|
||||
const struct load_command *lc = (const void *)(header + 1);
|
||||
for (uint32_t c = 0; c < header->ncmds; c++) {
|
||||
if (lc->cmd == LC_MAIN) {
|
||||
uint64_t off = ((const struct entry_point_command *)lc)->entryoff;
|
||||
fx_log("[fx] Factorio.dylib at %p slide %#lx, entry offset %#llx", header,
|
||||
(long)_dyld_get_image_vmaddr_slide(i), off);
|
||||
return (fx_main_fn)((const char *)header + off);
|
||||
}
|
||||
lc = (const void *)((const char *)lc + lc->cmdsize);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static NSString *fx_write_config(NSString *root) {
|
||||
NSString *config = [root stringByAppendingPathComponent:@"config/config.ini"];
|
||||
NSString *data = [root stringByAppendingPathComponent:@"data"];
|
||||
NSString *write = [root stringByAppendingPathComponent:@"write"];
|
||||
[NSFileManager.defaultManager createDirectoryAtPath:config.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
[NSFileManager.defaultManager createDirectoryAtPath:write withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
NSString *text = [NSString stringWithContentsOfFile:config encoding:NSUTF8StringEncoding error:nil] ?: @"[path]\n";
|
||||
// The container path can change between installs, so rewrite both path keys every launch.
|
||||
NSMutableArray *lines = [NSMutableArray array];
|
||||
for (NSString *line in [text componentsSeparatedByString:@"\n"])
|
||||
if (![line hasPrefix:@"read-data="] && ![line hasPrefix:@"write-data="]) [lines addObject:line];
|
||||
NSUInteger at = [lines indexOfObject:@"[path]"];
|
||||
if (at == NSNotFound) { [lines insertObject:@"[path]" atIndex:0]; at = 0; }
|
||||
[lines insertObject:[@"write-data=" stringByAppendingString:write] atIndex:at + 1];
|
||||
[lines insertObject:[@"read-data=" stringByAppendingString:data] atIndex:at + 1];
|
||||
[[lines componentsJoinedByString:@"\n"] writeToFile:config atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
||||
return config;
|
||||
}
|
||||
|
||||
static void fx_launch(void) {
|
||||
NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
|
||||
NSString *root = [docs stringByAppendingPathComponent:@"factorio"];
|
||||
NSString *probe = [root stringByAppendingPathComponent:@"data/core/info.json"];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:probe]) {
|
||||
fx_set_status(@"Game data missing.\nCopy Factorio's data folder to Documents/factorio/data\n(see tools/build_native.py --push-data)");
|
||||
return;
|
||||
}
|
||||
NSString *config = fx_write_config(root);
|
||||
fx_patch_metal();
|
||||
// iOS plays nothing until the app's audio session is active. Playback + mix keeps game sound
|
||||
// audible with the silent switch on, without stopping other apps' audio.
|
||||
NSError *audioError = nil;
|
||||
AVAudioSession *session = AVAudioSession.sharedInstance;
|
||||
if (![session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&audioError] ||
|
||||
![session setActive:YES error:&audioError])
|
||||
fx_log("[fx] audio session: %s", audioError.localizedDescription.UTF8String);
|
||||
else
|
||||
fx_log("[fx] audio session active, %.0f Hz, %ld output channels", session.sampleRate, (long)session.outputNumberOfChannels);
|
||||
setenv("SDL_JOYSTICK_HIDAPI", "0", 1);
|
||||
setenv("SDL_JOYSTICK_IOKIT", "0", 1);
|
||||
setenv("SDL_MAC_BACKGROUND_APP", "0", 1);
|
||||
|
||||
NSString *dylib = [NSBundle.mainBundle.privateFrameworksPath stringByAppendingPathComponent:@"Factorio.dylib"];
|
||||
fx_set_status(@"Loading Factorio…");
|
||||
fx_log("[fx] dlopen %s", dylib.UTF8String);
|
||||
double t0 = CACurrentMediaTime();
|
||||
void *handle = dlopen(dylib.UTF8String, RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!handle) { fx_set_status([NSString stringWithFormat:@"dlopen failed:\n%s", dlerror()]); return; }
|
||||
fx_log("[fx] dlopen ok in %.2fs", CACurrentMediaTime() - t0);
|
||||
fx_game.entry = fx_find_entry();
|
||||
if (!fx_game.entry) { fx_set_status(@"LC_MAIN not found in Factorio.dylib"); return; }
|
||||
|
||||
static char *argv[8], *apple[2];
|
||||
int argc = 0;
|
||||
argv[argc++] = strdup(NSBundle.mainBundle.executablePath.UTF8String);
|
||||
argv[argc++] = "--config";
|
||||
argv[argc++] = strdup(config.UTF8String);
|
||||
argv[argc] = NULL;
|
||||
apple[0] = strdup([@"executable_path=" stringByAppendingString:NSBundle.mainBundle.executablePath].UTF8String);
|
||||
apple[1] = NULL;
|
||||
fx_game.argc = argc; fx_game.argv = argv; fx_game.apple = apple;
|
||||
|
||||
fx_set_status(@"");
|
||||
// macOS gives the main thread 8 MiB; iOS gives 1 MiB. Run the original main on a larger stack
|
||||
// while staying on the main thread, which UIKit and SDL's Cocoa code require.
|
||||
size_t size = 256u << 20;
|
||||
char *stack = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (stack == MAP_FAILED) { fx_set_status(@"stack allocation failed"); return; }
|
||||
mprotect(stack, 16384, PROT_NONE); // guard page
|
||||
fx_log("[fx] calling original main");
|
||||
fx_call_on_stack(stack + size, fx_run_main, NULL);
|
||||
fx_log("[fx] original main returned %d", fx_game.result);
|
||||
fx_set_status([NSString stringWithFormat:@"Factorio exited with code %d", fx_game.result]);
|
||||
}
|
||||
|
||||
@interface FXAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@end
|
||||
@implementation FXAppDelegate
|
||||
- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)options {
|
||||
NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
|
||||
fx_log_file = fopen([docs stringByAppendingPathComponent:@"fx-log.txt"].UTF8String, "w");
|
||||
fx_install_crash_reporter();
|
||||
fx_log("[fx] host start, screen %.0fx%.0f pt @%.2fx", fx_screen_points().width, fx_screen_points().height, fx_screen_scale());
|
||||
self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
|
||||
FXRootViewController *vc = [FXRootViewController new];
|
||||
self.window.rootViewController = vc;
|
||||
fx_root = vc.view;
|
||||
fx_status = [[UILabel alloc] initWithFrame:CGRectInset(fx_root.bounds, 40, 40)];
|
||||
fx_status.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
fx_status.textColor = UIColor.whiteColor;
|
||||
fx_status.numberOfLines = 0;
|
||||
fx_status.textAlignment = NSTextAlignmentCenter;
|
||||
[fx_root addSubview:fx_status];
|
||||
fx_install_controls(fx_root);
|
||||
[self.window makeKeyAndVisible];
|
||||
fx_set_status(@"Starting…");
|
||||
// Return from launch first; the game loop then runs inside a main-queue block and keeps
|
||||
// UIKit alive by re-entering the run loop from -[NSApplication nextEventMatchingMask:…].
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ fx_launch(); });
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
FX_EXPORT int fx_host_main(int argc, char **argv) {
|
||||
@autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass(FXAppDelegate.class)); }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// macOS-only CoreGraphics display APIs, re-exported alongside iOS CoreGraphics.
|
||||
// One display (FX_DISPLAY_ID) with the phone's landscape size; mode changes are refused.
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <string.h>
|
||||
#include "fx.h"
|
||||
|
||||
typedef uint32_t CGDirectDisplayID;
|
||||
typedef int32_t CGError;
|
||||
typedef CFDataRef CGDisplayModeRef; // CFType so CFRetain/CFEqual/CFArray callbacks work
|
||||
typedef struct { size_t w, h, pw, ph; double hz; } fx_mode;
|
||||
|
||||
static fx_mode fx_current_mode(void) {
|
||||
CGSize s = fx_screen_points();
|
||||
double k = fx_screen_scale();
|
||||
return (fx_mode){(size_t)s.width, (size_t)s.height, (size_t)(s.width * k), (size_t)(s.height * k), 60.0};
|
||||
}
|
||||
static const fx_mode *fx_m(CGDisplayModeRef m) { return (const fx_mode *)CFDataGetBytePtr(m); }
|
||||
|
||||
FX_EXPORT const CFStringRef kCGDisplayShowDuplicateLowResolutionModes = CFSTR("kCGDisplayShowDuplicateLowResolutionModes");
|
||||
|
||||
FX_EXPORT CGError CGGetOnlineDisplayList(uint32_t max, CGDirectDisplayID *displays, uint32_t *count) {
|
||||
if (displays && max) displays[0] = FX_DISPLAY_ID;
|
||||
if (count) *count = 1;
|
||||
return 0;
|
||||
}
|
||||
FX_EXPORT CGDirectDisplayID CGMainDisplayID(void) { return FX_DISPLAY_ID; }
|
||||
FX_EXPORT int CGDisplayIsMain(CGDirectDisplayID d) { return d == FX_DISPLAY_ID; }
|
||||
FX_EXPORT CGDirectDisplayID CGDisplayMirrorsDisplay(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGRect CGDisplayBounds(CGDirectDisplayID d) { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
|
||||
FX_EXPORT size_t CGDisplayPixelsWide(CGDirectDisplayID d) { return (size_t)fx_screen_points().width; }
|
||||
FX_EXPORT size_t CGDisplayPixelsHigh(CGDirectDisplayID d) { return (size_t)fx_screen_points().height; }
|
||||
FX_EXPORT CGSize CGDisplayScreenSize(CGDirectDisplayID d) {
|
||||
// Physical size in millimetres at ~460 ppi (iPhone 15 Pro class display).
|
||||
double k = fx_render_divisor(); CGSize s = fx_screen_points();
|
||||
return CGSizeMake(s.width * k / 460.0 * 25.4, s.height * k / 460.0 * 25.4);
|
||||
}
|
||||
FX_EXPORT CGDisplayModeRef CGDisplayCopyDisplayMode(CGDirectDisplayID d) {
|
||||
fx_mode m = fx_current_mode();
|
||||
return CFDataCreate(NULL, (const UInt8 *)&m, sizeof m);
|
||||
}
|
||||
FX_EXPORT CFArrayRef CGDisplayCopyAllDisplayModes(CGDirectDisplayID d, CFDictionaryRef options) {
|
||||
CGDisplayModeRef m = CGDisplayCopyDisplayMode(d);
|
||||
CFArrayRef a = CFArrayCreate(NULL, (const void **)&m, 1, &kCFTypeArrayCallBacks);
|
||||
CFRelease(m);
|
||||
return a;
|
||||
}
|
||||
FX_EXPORT void CGDisplayModeRelease(CGDisplayModeRef m) { if (m) CFRelease(m); }
|
||||
FX_EXPORT size_t CGDisplayModeGetWidth(CGDisplayModeRef m) { return fx_m(m)->w; }
|
||||
FX_EXPORT size_t CGDisplayModeGetHeight(CGDisplayModeRef m) { return fx_m(m)->h; }
|
||||
FX_EXPORT size_t CGDisplayModeGetPixelWidth(CGDisplayModeRef m) { return fx_m(m)->pw; }
|
||||
FX_EXPORT size_t CGDisplayModeGetPixelHeight(CGDisplayModeRef m) { return fx_m(m)->ph; }
|
||||
FX_EXPORT double CGDisplayModeGetRefreshRate(CGDisplayModeRef m) { return fx_m(m)->hz; }
|
||||
FX_EXPORT uint32_t CGDisplayModeGetIOFlags(CGDisplayModeRef m) { return 0x7; } // valid | safe | default
|
||||
FX_EXPORT int CGDisplayModeIsUsableForDesktopGUI(CGDisplayModeRef m) { return 1; }
|
||||
FX_EXPORT CFStringRef CGDisplayModeCopyPixelEncoding(CGDisplayModeRef m) {
|
||||
return CFStringCreateCopy(NULL, CFSTR("--------RRRRRRRRGGGGGGGGBBBBBBBB"));
|
||||
}
|
||||
FX_EXPORT CGError CGDisplaySetDisplayMode(CGDirectDisplayID d, CGDisplayModeRef m, CFDictionaryRef o) { return 1000; }
|
||||
FX_EXPORT CGError CGDisplayCapture(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGCaptureAllDisplays(void) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayRelease(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGReleaseAllDisplays(void) { return 0; }
|
||||
FX_EXPORT int32_t CGShieldingWindowLevel(void) { return 2147483630; }
|
||||
FX_EXPORT CGError CGAcquireDisplayFadeReservation(float seconds, uint32_t *token) { if (token) *token = 1; return 0; }
|
||||
FX_EXPORT CGError CGReleaseDisplayFadeReservation(uint32_t token) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayFade(uint32_t token, float d, float a, float b, float r, float g, float bl, int sync) { return 0; }
|
||||
FX_EXPORT uint32_t CGDisplayIDToOpenGLDisplayMask(CGDirectDisplayID d) { return 1; }
|
||||
FX_EXPORT uint32_t CGDisplayIOServicePort(CGDirectDisplayID d) { return 0; }
|
||||
FX_EXPORT CGError CGAssociateMouseAndMouseCursorPosition(int connected) { return 0; }
|
||||
FX_EXPORT CGError CGWarpMouseCursorPosition(CGPoint p) { return 0; }
|
||||
FX_EXPORT CGError CGDisplayMoveCursorToPoint(CGDirectDisplayID d, CGPoint p) { return 0; }
|
||||
FX_EXPORT CGError CGGetDisplayTransferByTable(CGDirectDisplayID d, uint32_t cap, float *r, float *g, float *b, uint32_t *count) {
|
||||
if (count) *count = 0;
|
||||
return 1000;
|
||||
}
|
||||
FX_EXPORT CGError CGSetDisplayTransferByTable(CGDirectDisplayID d, uint32_t n, const float *r, const float *g, const float *b) { return 1000; }
|
||||
@@ -0,0 +1,93 @@
|
||||
// Crash diagnostics: interposes sigaction so Factorio's own crash handler runs after ours.
|
||||
// Ours logs the faulting PC, LR, fault address and the frame-pointer chain as image+offset,
|
||||
// which tools/symbolize_native.py resolves against the original factorio.dSYM on the Mac.
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <sys/ucontext.h>
|
||||
#include "fx.h"
|
||||
|
||||
static struct sigaction fx_previous[NSIG];
|
||||
static int fx_reporting;
|
||||
|
||||
static void fx_describe(const char *label, uintptr_t address) {
|
||||
Dl_info info;
|
||||
if (address && dladdr((void *)address, &info) && info.dli_fname) {
|
||||
const char *base = strrchr(info.dli_fname, '/');
|
||||
fx_log("[fx-crash] %s %#lx %s+%#lx %s", label, (unsigned long)address, base ? base + 1 : info.dli_fname,
|
||||
(unsigned long)(address - (uintptr_t)info.dli_fbase), info.dli_sname ? info.dli_sname : "");
|
||||
} else {
|
||||
fx_log("[fx-crash] %s %#lx", label, (unsigned long)address);
|
||||
}
|
||||
}
|
||||
|
||||
static void fx_crash_handler(int sig, siginfo_t *si, void *context) {
|
||||
if (!fx_reporting++) {
|
||||
ucontext_t *uc = context;
|
||||
struct __darwin_arm_thread_state64 *ts = &uc->uc_mcontext->__ss;
|
||||
uintptr_t pc = (uintptr_t)__darwin_arm_thread_state64_get_pc(*ts);
|
||||
uintptr_t lr = (uintptr_t)__darwin_arm_thread_state64_get_lr(*ts);
|
||||
uintptr_t fp = (uintptr_t)__darwin_arm_thread_state64_get_fp(*ts);
|
||||
uintptr_t sp = (uintptr_t)__darwin_arm_thread_state64_get_sp(*ts);
|
||||
fx_log("[fx-crash] signal %d code %d fault address %p thread %p%s", sig, si->si_code, si->si_addr,
|
||||
(void *)pthread_self(), pthread_main_np() ? " (main)" : "");
|
||||
fx_describe("pc", pc);
|
||||
fx_describe("lr", lr);
|
||||
for (int r = 0; r < 29; r++) fx_log("[fx-crash] x%d = %#llx", r, ts->__x[r]);
|
||||
fx_log("[fx-crash] sp = %#lx fp = %#lx", (unsigned long)sp, (unsigned long)fp);
|
||||
// Frame-pointer walk, bounded to plausible stack addresses above sp.
|
||||
for (int depth = 0; depth < 64 && fp && (fp & 7) == 0 && fp >= sp && fp - sp < (512u << 20); depth++) {
|
||||
uintptr_t next = ((uintptr_t *)fp)[0], ret = ((uintptr_t *)fp)[1];
|
||||
char label[16];
|
||||
snprintf(label, sizeof label, "#%d", depth);
|
||||
fx_describe(label, ret);
|
||||
if (next <= fp) break;
|
||||
fp = next;
|
||||
}
|
||||
}
|
||||
struct sigaction *prev = &fx_previous[sig];
|
||||
if (prev->sa_flags & SA_SIGINFO) {
|
||||
if (prev->sa_sigaction) { prev->sa_sigaction(sig, si, context); return; }
|
||||
} else if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN) {
|
||||
prev->sa_handler(sig);
|
||||
return;
|
||||
}
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static int fx_is_fatal(int sig) { return sig == SIGSEGV || sig == SIGBUS || sig == SIGILL || sig == SIGFPE || sig == SIGTRAP || sig == SIGABRT; }
|
||||
|
||||
static int fx_sigaction(int sig, const struct sigaction *act, struct sigaction *old) {
|
||||
if (sig <= 0 || sig >= NSIG || !fx_is_fatal(sig) || !act) return sigaction(sig, act, old);
|
||||
if (old) *old = fx_previous[sig];
|
||||
fx_previous[sig] = *act;
|
||||
struct sigaction ours = *act;
|
||||
ours.sa_sigaction = fx_crash_handler;
|
||||
ours.sa_flags |= SA_SIGINFO;
|
||||
return sigaction(sig, &ours, NULL);
|
||||
}
|
||||
|
||||
static void (*fx_signal(int sig, void (*handler)(int)))(int) {
|
||||
if (sig <= 0 || sig >= NSIG || !fx_is_fatal(sig)) return signal(sig, handler);
|
||||
struct sigaction act = {0}, old;
|
||||
act.sa_handler = handler;
|
||||
fx_sigaction(sig, &act, &old);
|
||||
return old.sa_handler;
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_install_crash_reporter(void) {
|
||||
for (int sig = 1; sig < NSIG; sig++) {
|
||||
if (!fx_is_fatal(sig)) continue;
|
||||
struct sigaction act = {0};
|
||||
act.sa_handler = SIG_DFL;
|
||||
fx_sigaction(sig, &act, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((used, section("__DATA,__interpose"))) static const struct { const void *replacement, *original; }
|
||||
fx_interpose[] = {
|
||||
{(const void *)fx_sigaction, (const void *)sigaction},
|
||||
{(const void *)fx_signal, (const void *)signal},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Objective-C symbols missing from iOS Foundation and Metal.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "fx.h"
|
||||
|
||||
#ifdef FX_LIB_FOUNDATION
|
||||
@interface NSAppleEventManager : NSObject
|
||||
@end
|
||||
@implementation NSAppleEventManager
|
||||
+ (instancetype)sharedAppleEventManager { static NSAppleEventManager *m; static dispatch_once_t o; dispatch_once(&o, ^{ m = [self new]; }); return m; }
|
||||
- (void)setEventHandler:(id)handler andSelector:(SEL)sel forEventClass:(uint32_t)c andEventID:(uint32_t)e { }
|
||||
- (void)removeEventHandlerForEventClass:(uint32_t)c andEventID:(uint32_t)e { }
|
||||
@end
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_METAL
|
||||
#import <Metal/Metal.h>
|
||||
FX_EXPORT NSArray *MTLCopyAllDevicesWithObserver(id *observer, void (^handler)(id, NSString *)) {
|
||||
if (observer) *observer = nil;
|
||||
id device = MTLCreateSystemDefaultDevice();
|
||||
return device ? @[device] : @[];
|
||||
}
|
||||
FX_EXPORT void MTLRemoveDeviceObserver(id observer) { }
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
// Shared declarations for the macOS-API shims used by the retargeted Factorio binary.
|
||||
#pragma once
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define FX_DISPLAY_ID 1u
|
||||
#define FX_EXPORT __attribute__((visibility("default")))
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
FX_EXPORT void fx_log(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
||||
FX_EXPORT CGSize fx_screen_points(void);
|
||||
FX_EXPORT double fx_screen_scale(void);
|
||||
FX_EXPORT double fx_render_divisor(void);
|
||||
FX_EXPORT double fx_game_per_uikit(void);
|
||||
#ifdef __OBJC__
|
||||
FX_EXPORT void fx_main_sync(void (^block)(void));
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/Foundation.h>
|
||||
// Selectors sent to Factorio's own SDL classes (declared so ARC accepts the sends).
|
||||
@protocol FXAppKitCallbacks
|
||||
@optional
|
||||
- (void)mouseDown:(id)e; - (void)mouseUp:(id)e; - (void)mouseDragged:(id)e; - (void)mouseMoved:(id)e;
|
||||
- (void)rightMouseDown:(id)e; - (void)rightMouseUp:(id)e; - (void)rightMouseDragged:(id)e;
|
||||
- (void)otherMouseDown:(id)e; - (void)otherMouseUp:(id)e; - (void)otherMouseDragged:(id)e;
|
||||
- (void)scrollWheel:(id)e; - (void)keyDown:(id)e; - (void)keyUp:(id)e; - (void)flagsChanged:(id)e;
|
||||
- (void)mouseEntered:(id)e; - (void)mouseExited:(id)e;
|
||||
- (void)touchesBeganWithEvent:(id)e; - (void)touchesMovedWithEvent:(id)e;
|
||||
- (void)touchesEndedWithEvent:(id)e; - (void)touchesCancelledWithEvent:(id)e;
|
||||
- (void)windowDidBecomeKey:(NSNotification *)n; - (void)windowDidResignKey:(NSNotification *)n;
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)n;
|
||||
- (NSInteger)tag;
|
||||
- (void)insertText:(id)text replacementRange:(NSRange)range;
|
||||
@end
|
||||
@interface NSObject (FXAppKitCallbacks) <FXAppKitCallbacks>
|
||||
@end
|
||||
#endif
|
||||
@@ -0,0 +1,369 @@
|
||||
// Keyboard emulation: everything becomes AppKit key events with macOS virtual key codes,
|
||||
// which Factorio's SDL Cocoa backend translates exactly as on a Mac.
|
||||
// * hardware keyboards (UIPress HID usage -> mac key code)
|
||||
// * the iOS soft keyboard for text (insertText -> key down/up with characters)
|
||||
// * an on-screen overlay: WASD stick and common Factorio keys, sticky Shift/Ctrl
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "fx.h"
|
||||
|
||||
void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags);
|
||||
|
||||
// macOS virtual key codes.
|
||||
enum {
|
||||
MK_A = 0, MK_S = 1, MK_D = 2, MK_F = 3, MK_W = 13, MK_E = 14, MK_R = 15, MK_T = 17, MK_Q = 12, MK_M = 46,
|
||||
MK_RETURN = 36, MK_TAB = 48, MK_SPACE = 49, MK_DELETE = 51, MK_ESCAPE = 53,
|
||||
MK_RCMD = 54, MK_LCMD = 55, MK_LSHIFT = 56, MK_CAPS = 57, MK_LOPT = 58, MK_LCTRL = 59,
|
||||
MK_RSHIFT = 60, MK_ROPT = 61, MK_RCTRL = 62, MK_NONE = 0xFFFF,
|
||||
};
|
||||
|
||||
// AppKit modifier flags plus the device-dependent side bits SDL uses to tell left from right.
|
||||
#define F_CAPS (1u << 16)
|
||||
#define F_SHIFT (1u << 17)
|
||||
#define F_CTRL (1u << 18)
|
||||
#define F_OPT (1u << 19)
|
||||
#define F_CMD (1u << 20)
|
||||
static const struct { unsigned short key; NSUInteger flag, side; } fx_modifiers[] = {
|
||||
{MK_LCTRL, F_CTRL, 0x0001}, {MK_LSHIFT, F_SHIFT, 0x0002}, {MK_RSHIFT, F_SHIFT, 0x0004},
|
||||
{MK_LCMD, F_CMD, 0x0008}, {MK_RCMD, F_CMD, 0x0010}, {MK_LOPT, F_OPT, 0x0020},
|
||||
{MK_ROPT, F_OPT, 0x0040}, {MK_RCTRL, F_CTRL, 0x2000},
|
||||
};
|
||||
static NSUInteger fx_flags; // current modifier state as AppKit reports it
|
||||
|
||||
FX_EXPORT NSUInteger fx_current_modifier_flags(void) { return fx_flags; }
|
||||
|
||||
static void fx_recompute_flags(NSUInteger sides) {
|
||||
NSUInteger flags = (fx_flags & F_CAPS) | sides;
|
||||
for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++)
|
||||
if (sides & fx_modifiers[i].side) flags |= fx_modifiers[i].flag;
|
||||
fx_flags = flags;
|
||||
}
|
||||
|
||||
// Press or release one mac key. Modifier keys become flagsChanged events.
|
||||
FX_EXPORT void fx_key(unsigned short key, BOOL down, NSString *chars) {
|
||||
if (key == MK_NONE) return;
|
||||
for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++) {
|
||||
if (fx_modifiers[i].key != key) continue;
|
||||
NSUInteger sides = fx_flags & 0xFFFF;
|
||||
sides = down ? (sides | fx_modifiers[i].side) : (sides & ~fx_modifiers[i].side);
|
||||
fx_recompute_flags(sides);
|
||||
fx_post_key_event(12, key, nil, fx_flags);
|
||||
return;
|
||||
}
|
||||
if (key == MK_CAPS) {
|
||||
if (down) fx_flags ^= F_CAPS;
|
||||
fx_post_key_event(12, key, nil, fx_flags);
|
||||
return;
|
||||
}
|
||||
fx_post_key_event(down ? 10 : 11, key, chars, fx_flags);
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_tap_key(unsigned short key, NSString *chars) { fx_key(key, YES, chars); fx_key(key, NO, chars); }
|
||||
|
||||
// ---- HID usage (UIKeyboardHIDUsage) -> mac virtual key code --------------------------------
|
||||
static unsigned short fx_mac_key_for_hid(long usage) {
|
||||
static const unsigned short letters[26] = {0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6};
|
||||
static const unsigned short digits[10] = {18, 19, 20, 21, 23, 22, 26, 28, 25, 29}; // 1..9, 0
|
||||
static const unsigned short fkeys[12] = {122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111};
|
||||
static const unsigned short keypad[10] = {83, 84, 85, 86, 87, 88, 89, 91, 92, 82}; // KP1..KP9, KP0
|
||||
if (usage >= 4 && usage <= 29) return letters[usage - 4];
|
||||
if (usage >= 30 && usage <= 39) return digits[usage - 30];
|
||||
if (usage >= 58 && usage <= 69) return fkeys[usage - 58];
|
||||
if (usage >= 89 && usage <= 98) return keypad[usage - 89];
|
||||
switch (usage) {
|
||||
case 40: return MK_RETURN; case 41: return MK_ESCAPE; case 42: return MK_DELETE; case 43: return MK_TAB;
|
||||
case 44: return MK_SPACE; case 45: return 27; case 46: return 24; case 47: return 33; case 48: return 30;
|
||||
case 49: return 42; case 50: return 42; case 51: return 41; case 52: return 39; case 53: return 50;
|
||||
case 54: return 43; case 55: return 47; case 56: return 44; case 57: return MK_CAPS;
|
||||
case 73: return 114; case 74: return 115; case 75: return 116; case 76: return 117; case 77: return 119;
|
||||
case 78: return 121; case 79: return 124; case 80: return 123; case 81: return 125; case 82: return 126;
|
||||
case 83: return 71; case 84: return 75; case 85: return 67; case 86: return 78; case 87: return 69;
|
||||
case 88: return 76; case 99: return 65; case 100: return 10; case 103: return 81;
|
||||
case 104: return 105; case 105: return 107; case 106: return 113; case 107: return 106; // F13..F16
|
||||
case 224: return MK_LCTRL; case 225: return MK_LSHIFT; case 226: return MK_LOPT; case 227: return MK_LCMD;
|
||||
case 228: return MK_RCTRL; case 229: return MK_RSHIFT; case 230: return MK_ROPT; case 231: return MK_RCMD;
|
||||
default: return MK_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *fx_text_chars(NSString *chars) {
|
||||
if (chars.length == 0) return nil;
|
||||
unichar c = [chars characterAtIndex:0];
|
||||
return (c < 0x20 || c == 0x7f || (c >= 0xF700 && c <= 0xF8FF)) ? nil : chars;
|
||||
}
|
||||
|
||||
FX_EXPORT BOOL fx_handle_presses(NSSet<UIPress *> *presses, BOOL down) {
|
||||
BOOL handled = NO;
|
||||
for (UIPress *press in presses) {
|
||||
UIKey *key = press.key;
|
||||
if (!key) continue;
|
||||
unsigned short mac = fx_mac_key_for_hid(key.keyCode);
|
||||
if (mac == MK_NONE) { fx_log("[fx] unmapped HID key usage %ld", (long)key.keyCode); continue; }
|
||||
fx_key(mac, down, down ? fx_text_chars(key.characters) : nil);
|
||||
handled = YES;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Mac key code for a typed character, so soft-keyboard letters also work as hotkeys.
|
||||
static unsigned short fx_mac_key_for_char(unichar c, BOOL *shift) {
|
||||
// Index = mac key code 0..47 (US layout); '\1' marks codes that are not characters.
|
||||
static const char plain[] = "asdfhgzxcv\1bqweryt123465=97-80]ou[ip\1lj'k;\\,/nm.";
|
||||
static const char shifted[] = "ASDFHGZXCV\1BQWERYT!@#$^%+(&_*)}OU{IP\1LJ\"K:|<?NM>";
|
||||
*shift = NO;
|
||||
if (c == ' ') return MK_SPACE;
|
||||
if (c == '`') return 50;
|
||||
if (c == '~') { *shift = YES; return 50; }
|
||||
if (c >= 0x80) return MK_NONE;
|
||||
for (unsigned short i = 0; i < sizeof plain - 1; i++) {
|
||||
if (plain[i] == c) return i;
|
||||
if (shifted[i] == c) { *shift = YES; return i; }
|
||||
}
|
||||
return MK_NONE;
|
||||
}
|
||||
|
||||
static UIView *fx_make_accessory_bar(void);
|
||||
static void fx_release_sticky(void);
|
||||
|
||||
// ---- Soft keyboard -------------------------------------------------------------------------
|
||||
@interface FXTextInputView : UIView <UIKeyInput>
|
||||
@end
|
||||
@implementation FXTextInputView
|
||||
- (BOOL)canBecomeFirstResponder { return YES; }
|
||||
- (BOOL)hasText { return YES; }
|
||||
- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; }
|
||||
- (UITextAutocapitalizationType)autocapitalizationType { return UITextAutocapitalizationTypeNone; }
|
||||
- (UITextSpellCheckingType)spellCheckingType { return UITextSpellCheckingTypeNo; }
|
||||
- (UITextSmartQuotesType)smartQuotesType { return UITextSmartQuotesTypeNo; }
|
||||
- (UITextSmartDashesType)smartDashesType { return UITextSmartDashesTypeNo; }
|
||||
- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; }
|
||||
- (UIView *)inputAccessoryView {
|
||||
static UIView *bar;
|
||||
if (!bar) bar = fx_make_accessory_bar();
|
||||
return bar;
|
||||
}
|
||||
- (void)insertText:(NSString *)text {
|
||||
[text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences
|
||||
usingBlock:^(NSString *s, NSRange r, NSRange e, BOOL *stop) {
|
||||
unichar c = [s characterAtIndex:0];
|
||||
if (c == '\n') { fx_tap_key(MK_RETURN, nil); return; }
|
||||
BOOL shift = NO;
|
||||
unsigned short key = fx_mac_key_for_char(c, &shift);
|
||||
if (shift) fx_key(MK_LSHIFT, YES, nil);
|
||||
// Characters without a mac key (e.g. accented letters) still arrive as text via key 0xFFFE.
|
||||
fx_key(key == MK_NONE ? 0x7F : key, YES, s);
|
||||
fx_key(key == MK_NONE ? 0x7F : key, NO, nil);
|
||||
if (shift) fx_key(MK_LSHIFT, NO, nil);
|
||||
fx_release_sticky();
|
||||
}];
|
||||
}
|
||||
- (void)deleteBackward { fx_tap_key(MK_DELETE, nil); fx_release_sticky(); }
|
||||
// Hardware keys are handled as raw presses so they are not also delivered as text.
|
||||
- (void)pressesBegan:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, YES)) [super pressesBegan:p withEvent:e]; }
|
||||
- (void)pressesEnded:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesEnded:p withEvent:e]; }
|
||||
- (void)pressesCancelled:(NSSet<UIPress *> *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesCancelled:p withEvent:e]; }
|
||||
@end
|
||||
|
||||
// ---- Key buttons -------------------------------------------------------------------------
|
||||
// A button that presses a mac key while held; sticky buttons latch until the next click or key.
|
||||
@interface FXKeyButton : UIButton
|
||||
@property unsigned short key;
|
||||
@property (copy) NSString *chars;
|
||||
@property (nonatomic) BOOL sticky, latched;
|
||||
@end
|
||||
|
||||
static NSMutableArray<FXKeyButton *> *fx_sticky_buttons;
|
||||
|
||||
@implementation FXKeyButton
|
||||
+ (instancetype)keyButton:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame {
|
||||
FXKeyButton *b = [self buttonWithType:UIButtonTypeCustom];
|
||||
b.frame = frame;
|
||||
b.key = key;
|
||||
b.chars = chars;
|
||||
[b setTitle:title forState:UIControlStateNormal];
|
||||
b.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
|
||||
b.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
[b setTitleColor:[UIColor colorWithWhite:1 alpha:0.9] forState:UIControlStateNormal];
|
||||
b.backgroundColor = [UIColor colorWithWhite:0 alpha:0.35];
|
||||
b.layer.cornerRadius = 8;
|
||||
b.layer.borderWidth = 1;
|
||||
b.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.3].CGColor;
|
||||
[b addTarget:b action:@selector(fx_down) forControlEvents:UIControlEventTouchDown];
|
||||
[b addTarget:b action:@selector(fx_up) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel];
|
||||
return b;
|
||||
}
|
||||
- (void)setSticky:(BOOL)sticky {
|
||||
_sticky = sticky;
|
||||
if (!fx_sticky_buttons) fx_sticky_buttons = [NSMutableArray array];
|
||||
if (sticky) [fx_sticky_buttons addObject:self];
|
||||
}
|
||||
- (void)fx_set_latched:(BOOL)latched {
|
||||
if (latched == _latched) return;
|
||||
_latched = latched;
|
||||
self.backgroundColor = latched ? [UIColor colorWithRed:0.9 green:0.55 blue:0.1 alpha:0.75] : [UIColor colorWithWhite:0 alpha:0.35];
|
||||
fx_key(self.key, latched, nil);
|
||||
}
|
||||
- (void)fx_down {
|
||||
if (self.key == MK_NONE) return;
|
||||
if (self.sticky) {
|
||||
// Latching one copy of a modifier (overlay or keyboard bar) mirrors to the others.
|
||||
BOOL latch = !self.latched;
|
||||
for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key && b != self) { b->_latched = latch; b.backgroundColor = self.backgroundColor; }
|
||||
[self fx_set_latched:latch];
|
||||
for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key) b.backgroundColor = self.backgroundColor;
|
||||
return;
|
||||
}
|
||||
fx_key(self.key, YES, self.chars);
|
||||
}
|
||||
- (void)fx_up {
|
||||
if (self.key == MK_NONE || self.sticky) return;
|
||||
fx_key(self.key, NO, nil);
|
||||
fx_release_sticky();
|
||||
}
|
||||
@end
|
||||
|
||||
static void fx_release_sticky(void) {
|
||||
for (FXKeyButton *b in fx_sticky_buttons) if (b.latched) [b fx_down];
|
||||
}
|
||||
|
||||
// Extra keys shown above the iOS keyboard (which has no Tab, Esc, Ctrl, Alt, arrows or F-keys).
|
||||
static UIView *fx_make_accessory_bar(void) {
|
||||
CGFloat h = 46, s = 8;
|
||||
UIView *bar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, h)];
|
||||
bar.backgroundColor = [UIColor colorWithWhite:0.12 alpha:0.96];
|
||||
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:bar.bounds];
|
||||
scroll.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
scroll.showsHorizontalScrollIndicator = NO;
|
||||
scroll.delaysContentTouches = NO;
|
||||
[bar addSubview:scroll];
|
||||
NSArray *keys = @[
|
||||
@[@"Esc", @(MK_ESCAPE), @""], @[@"Tab", @(MK_TAB), @""], @[@"Ctrl", @(MK_LCTRL), @"sticky"],
|
||||
@[@"Alt", @(MK_LOPT), @"sticky"], @[@"⇧", @(MK_LSHIFT), @"sticky"], @[@"←", @123, @""], @[@"↑", @126, @""],
|
||||
@[@"↓", @125, @""], @[@"→", @124, @""], @[@"Enter", @(MK_RETURN), @""], @[@"Del", @117, @""],
|
||||
@[@"`", @50, @"`"], @[@"Home", @115, @""], @[@"End", @119, @""], @[@"PgUp", @116, @""], @[@"PgDn", @121, @""],
|
||||
@[@"F1", @122, @""], @[@"F2", @120, @""], @[@"F3", @99, @""], @[@"F4", @118, @""], @[@"F5", @96, @""],
|
||||
@[@"F6", @97, @""], @[@"F7", @98, @""], @[@"F8", @100, @""], @[@"F9", @101, @""], @[@"F10", @109, @""],
|
||||
@[@"F11", @103, @""], @[@"F12", @111, @""],
|
||||
];
|
||||
CGFloat x = 56; // clear of the Dynamic Island in landscape
|
||||
for (NSArray *k in keys) {
|
||||
NSString *title = k[0], *extra = k[2];
|
||||
CGFloat w = MAX(44, title.length * 11 + 22);
|
||||
BOOL sticky = [extra isEqualToString:@"sticky"];
|
||||
FXKeyButton *b = [FXKeyButton keyButton:title key:[k[1] unsignedShortValue]
|
||||
chars:(!sticky && extra.length) ? extra : nil frame:CGRectMake(x, 5, w, h - 10)];
|
||||
b.sticky = sticky;
|
||||
[scroll addSubview:b];
|
||||
x += w + s;
|
||||
}
|
||||
scroll.contentSize = CGSizeMake(x + 48, h);
|
||||
return bar;
|
||||
}
|
||||
|
||||
// ---- Overlay ---------------------------------------------------------------------------------
|
||||
@interface FXStick : UIView
|
||||
@end
|
||||
@implementation FXStick {
|
||||
UIView *_knob;
|
||||
BOOL _held[4]; // W A S D
|
||||
}
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
self.layer.cornerRadius = frame.size.width / 2;
|
||||
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.12];
|
||||
self.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.35].CGColor;
|
||||
self.layer.borderWidth = 1;
|
||||
_knob = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width * 0.42, frame.size.width * 0.42)];
|
||||
_knob.layer.cornerRadius = _knob.bounds.size.width / 2;
|
||||
_knob.backgroundColor = [UIColor colorWithWhite:1 alpha:0.4];
|
||||
_knob.userInteractionEnabled = NO;
|
||||
_knob.center = CGPointMake(frame.size.width / 2, frame.size.height / 2);
|
||||
[self addSubview:_knob];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (void)fx_update:(CGPoint)p {
|
||||
CGFloat r = self.bounds.size.width / 2;
|
||||
CGPoint c = CGPointMake(r, r);
|
||||
CGFloat dx = p.x - c.x, dy = p.y - c.y, len = hypot(dx, dy);
|
||||
if (len > r) { dx = dx / len * r; dy = dy / len * r; }
|
||||
_knob.center = CGPointMake(c.x + dx, c.y + dy);
|
||||
CGFloat dead = r * 0.3;
|
||||
BOOL want[4] = {dy < -dead, dx < -dead, dy > dead, dx > dead};
|
||||
static const unsigned short keys[4] = {MK_W, MK_A, MK_S, MK_D};
|
||||
static NSString *const chars[4] = {@"w", @"a", @"s", @"d"};
|
||||
for (int i = 0; i < 4; i++) if (want[i] != _held[i]) { _held[i] = want[i]; fx_key(keys[i], want[i], want[i] ? chars[i] : nil); }
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; }
|
||||
- (void)touchesMoved:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; }
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self fx_update:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2)]; }
|
||||
- (void)touchesCancelled:(NSSet<UITouch *> *)t withEvent:(UIEvent *)e { [self touchesEnded:t withEvent:e]; }
|
||||
@end
|
||||
|
||||
@interface FXControls : UIView
|
||||
@end
|
||||
@implementation FXControls {
|
||||
FXTextInputView *_text;
|
||||
NSMutableArray<UIView *> *_gameControls;
|
||||
}
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
UIView *hit = [super hitTest:point withEvent:event];
|
||||
return hit == self ? nil : hit; // empty overlay space belongs to the game
|
||||
}
|
||||
- (FXKeyButton *)fx_button:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame {
|
||||
FXKeyButton *b = [FXKeyButton keyButton:title key:key chars:chars frame:frame];
|
||||
[self addSubview:b];
|
||||
return b;
|
||||
}
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_gameControls = [NSMutableArray array];
|
||||
_text = [[FXTextInputView alloc] initWithFrame:CGRectZero];
|
||||
[self addSubview:_text];
|
||||
|
||||
CGFloat W = frame.size.width, H = frame.size.height, s = 40, g = 6;
|
||||
// Landscape keeps the Dynamic Island on one side, which flips with rotation: inset both.
|
||||
CGFloat left = 56, right = W - 56;
|
||||
[self fx_button:@"Esc" key:MK_ESCAPE chars:nil frame:CGRectMake(left, 8, 46, 30)];
|
||||
[self fx_button:@"Tab" key:MK_TAB chars:nil frame:CGRectMake(left + 52, 8, 46, 30)];
|
||||
FXKeyButton *kb = [self fx_button:@"⌨︎" key:MK_NONE chars:nil frame:CGRectMake(right - 46, 8, 46, 30)];
|
||||
[kb addTarget:self action:@selector(fx_toggle_keyboard) forControlEvents:UIControlEventTouchUpInside];
|
||||
FXKeyButton *hide = [self fx_button:@"◐" key:MK_NONE chars:nil frame:CGRectMake(right - 46 - g - 40, 8, 40, 30)];
|
||||
[hide addTarget:self action:@selector(fx_toggle_controls) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
FXStick *stick = [[FXStick alloc] initWithFrame:CGRectMake(left + 6, H - 128, 116, 116)];
|
||||
[self addSubview:stick];
|
||||
[_gameControls addObject:stick];
|
||||
|
||||
// Right-hand grid: common Factorio defaults.
|
||||
NSArray *grid = @[@[@"E", @(MK_E), @"e"], @[@"Q", @(MK_Q), @"q"], @[@"R", @(MK_R), @"r"], @[@"F", @(MK_F), @"f"],
|
||||
@[@"M", @(MK_M), @"m"], @[@"T", @(MK_T), @"t"], @[@"⇧", @(MK_LSHIFT), @""], @[@"Ctrl", @(MK_LCTRL), @""]];
|
||||
for (NSUInteger i = 0; i < grid.count; i++) {
|
||||
NSUInteger col = i % 4, row = i / 4;
|
||||
CGRect f = CGRectMake(right - (4 - col) * (s + g) + g, H - 12 - (2 - row) * (s + g) + g, s, s);
|
||||
FXKeyButton *b = [self fx_button:grid[i][0] key:[grid[i][1] unsignedShortValue]
|
||||
chars:[grid[i][2] length] ? grid[i][2] : nil frame:f];
|
||||
if (b.key == MK_LSHIFT || b.key == MK_LCTRL) b.sticky = YES;
|
||||
[_gameControls addObject:b];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (void)fx_toggle_keyboard {
|
||||
if (_text.isFirstResponder) [_text resignFirstResponder];
|
||||
else [_text becomeFirstResponder];
|
||||
}
|
||||
- (void)fx_toggle_controls {
|
||||
BOOL hidden = !_gameControls.firstObject.hidden;
|
||||
for (UIView *v in _gameControls) v.hidden = hidden;
|
||||
}
|
||||
@end
|
||||
|
||||
static FXControls *fx_controls;
|
||||
|
||||
FX_EXPORT void fx_install_controls(UIView *root) {
|
||||
fx_controls = [[FXControls alloc] initWithFrame:root.bounds];
|
||||
[root addSubview:fx_controls];
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_controls_after_click(void) { fx_release_sticky(); }
|
||||
@@ -0,0 +1,53 @@
|
||||
// macOS-only MTLDevice properties, added to the device's concrete class before the game starts.
|
||||
// Values describe a built-in Apple GPU. Other selectors iOS lacks are logged and answer zero.
|
||||
#import <Metal/Metal.h>
|
||||
#import <objc/runtime.h>
|
||||
#import "fx.h"
|
||||
|
||||
static id fx_zero(id self, SEL _cmd, ...) { return nil; }
|
||||
static BOOL fx_no(id self, SEL _cmd) { return NO; }
|
||||
static BOOL fx_yes(id self, SEL _cmd) { return YES; }
|
||||
static NSUInteger fx_uint_zero(id self, SEL _cmd) { return 0; }
|
||||
static NSUInteger fx_uint_one(id self, SEL _cmd) { return 1; }
|
||||
|
||||
static void fx_add(Class cls, const char *name, IMP imp, const char *types) {
|
||||
SEL sel = sel_registerName(name);
|
||||
if (!class_getInstanceMethod(cls, sel)) {
|
||||
class_addMethod(cls, sel, imp, types);
|
||||
fx_log("[fx] Metal compat: -[%s %s]", class_getName(cls), name);
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL (*fx_original_resolve)(id, SEL, SEL);
|
||||
static BOOL fx_device_resolve(id cls, SEL _cmd, SEL sel) {
|
||||
if (fx_original_resolve && fx_original_resolve(cls, _cmd, sel)) return YES;
|
||||
const char *name = sel_getName(sel);
|
||||
if (name[0] == '_' || name[0] == '.') return NO;
|
||||
fx_log("[fx] Metal compat: unknown -[%s %s] -> 0", class_getName(cls), name);
|
||||
class_addMethod(cls, sel, (IMP)fx_zero, "@@:");
|
||||
return YES;
|
||||
}
|
||||
|
||||
FX_EXPORT void fx_patch_metal(void) {
|
||||
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
||||
if (!device) { fx_log("[fx] no Metal device"); return; }
|
||||
Class cls = object_getClass(device);
|
||||
fx_log("[fx] Metal device %s (%s), unified memory %d", device.name.UTF8String, class_getName(cls), device.hasUnifiedMemory);
|
||||
fx_add(cls, "isDepth24Stencil8PixelFormatSupported", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isLowPower", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isHeadless", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "isRemovable", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "location", (IMP)fx_uint_zero, "Q@:"); // MTLDeviceLocationBuiltIn
|
||||
fx_add(cls, "locationNumber", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "maxTransferRate", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "peerGroupID", (IMP)fx_uint_zero, "Q@:");
|
||||
fx_add(cls, "peerIndex", (IMP)fx_uint_zero, "I@:");
|
||||
fx_add(cls, "peerCount", (IMP)fx_uint_one, "I@:");
|
||||
fx_add(cls, "supportsBCTextureCompression", (IMP)fx_no, "B@:");
|
||||
fx_add(cls, "areProgrammableSamplePositionsSupported", (IMP)fx_yes, "B@:");
|
||||
Method resolve = class_getClassMethod(cls, @selector(resolveInstanceMethod:));
|
||||
fx_original_resolve = (BOOL (*)(id, SEL, SEL))method_getImplementation(resolve);
|
||||
// Overrides only this concrete class; inherited NSObject behavior is kept via fx_original_resolve.
|
||||
if (!class_addMethod(object_getClass(cls), @selector(resolveInstanceMethod:), (IMP)fx_device_resolve, "B@::"))
|
||||
method_setImplementation(resolve, (IMP)fx_device_resolve);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Fast exact PNG decoding with Wuffs for the common Factorio sprite formats.
|
||||
// Produces what AppKit's NSBitmapImageRep gives on macOS: tightly packed, non-premultiplied
|
||||
// 8-bit samples with the source channel count (gray 1, gray+alpha 2, RGB 3, RGBA 4).
|
||||
// Returns 0 for anything else (palette, tRNS, 16-bit, malformed) so the caller can fall back.
|
||||
#define WUFFS_IMPLEMENTATION
|
||||
#define WUFFS_CONFIG__MODULES
|
||||
#define WUFFS_CONFIG__MODULE__BASE
|
||||
#define WUFFS_CONFIG__MODULE__ADLER32
|
||||
#define WUFFS_CONFIG__MODULE__CRC32
|
||||
#define WUFFS_CONFIG__MODULE__DEFLATE
|
||||
#define WUFFS_CONFIG__MODULE__ZLIB
|
||||
#define WUFFS_CONFIG__MODULE__PNG
|
||||
#include "../../../third_party/wuffs/wuffs-v0.3.c"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "pngdecode.h"
|
||||
|
||||
static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]; }
|
||||
|
||||
// Channel count for supported PNGs (from IHDR, no tRNS before IDAT), else 0.
|
||||
static int fx_png_channels(const uint8_t *d, size_t n) {
|
||||
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'};
|
||||
if (n < 33 || memcmp(d, sig, 8) || memcmp(d + 12, "IHDR", 4)) return 0;
|
||||
uint8_t depth = d[24], type = d[25];
|
||||
int channels = type == 0 ? 1 : type == 2 ? 3 : type == 4 ? 2 : type == 6 ? 4 : 0;
|
||||
if (depth != 8 || !channels) return 0;
|
||||
for (size_t at = 8; at + 12 <= n;) {
|
||||
uint32_t len = be32(d + at);
|
||||
const uint8_t *kind = d + at + 4;
|
||||
if (!memcmp(kind, "IDAT", 4)) break;
|
||||
if (!memcmp(kind, "tRNS", 4)) return 0;
|
||||
if ((uint64_t)at + 12 + len > n) return 0;
|
||||
at += 12 + (size_t)len;
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
int fx_png_decode(const uint8_t *data, size_t size, uint8_t **out, int *width, int *height, int *channels) {
|
||||
int ch = fx_png_channels(data, size);
|
||||
if (!ch) return 0;
|
||||
wuffs_png__decoder *dec = wuffs_png__decoder__alloc();
|
||||
if (!dec) return 0;
|
||||
// Skipping zlib/CRC verification does not change decoded pixels (verified byte-identical with
|
||||
// tests/native/png_parity.m); a corrupt file decodes to garbage instead of failing.
|
||||
wuffs_png__decoder__set_quirk_enabled(dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true);
|
||||
int ok = 0;
|
||||
uint8_t *rgba = NULL, *work = NULL;
|
||||
wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader((uint8_t *)data, size, true);
|
||||
wuffs_base__image_config ic = {0};
|
||||
if (wuffs_png__decoder__decode_image_config(dec, &ic, &src).repr) goto done;
|
||||
uint32_t w = wuffs_base__pixel_config__width(&ic.pixcfg), h = wuffs_base__pixel_config__height(&ic.pixcfg);
|
||||
if (!w || !h || (uint64_t)w * h > (1u << 28)) goto done;
|
||||
wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h);
|
||||
size_t pixels = (size_t)w * h;
|
||||
rgba = malloc(pixels * 4);
|
||||
uint64_t work_len = wuffs_png__decoder__workbuf_len(dec).max_incl;
|
||||
work = work_len ? malloc(work_len) : NULL;
|
||||
if (!rgba || (work_len && !work)) goto done;
|
||||
wuffs_base__pixel_buffer pb;
|
||||
if (wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(rgba, pixels * 4)).repr) goto done;
|
||||
if (wuffs_png__decoder__decode_frame(dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC,
|
||||
wuffs_base__make_slice_u8(work, work_len), NULL).repr) goto done;
|
||||
// Pack to the source channel count (gray sources decode with R == G == B).
|
||||
if (ch != 4) {
|
||||
uint8_t *dst = rgba;
|
||||
for (size_t i = 0; i < pixels; i++) {
|
||||
const uint8_t *p = rgba + i * 4;
|
||||
if (ch == 1) { dst[i] = p[0]; }
|
||||
else if (ch == 2) { dst[2 * i] = p[0]; dst[2 * i + 1] = p[3]; }
|
||||
else { uint8_t r = p[0], g = p[1], b = p[2]; dst[3 * i] = r; dst[3 * i + 1] = g; dst[3 * i + 2] = b; }
|
||||
}
|
||||
}
|
||||
*out = rgba; rgba = NULL;
|
||||
*width = (int)w; *height = (int)h; *channels = ch;
|
||||
ok = 1;
|
||||
done:
|
||||
free(rgba);
|
||||
free(work);
|
||||
free(dec);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Exact PNG decoding for common 8-bit formats; see pngdecode.c.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
// On success returns 1 and a malloc'd buffer (caller frees) of width*height*channels bytes.
|
||||
int fx_png_decode(const uint8_t *data, size_t size, uint8_t **out, int *width, int *height, int *channels);
|
||||
@@ -0,0 +1,118 @@
|
||||
// Small macOS-only APIs. Each FX_LIB_* section is compiled into its own shim dylib by
|
||||
// tools/build_native.py, so the original two-level-namespace bindings stay valid.
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include "fx.h"
|
||||
|
||||
#ifdef FX_LIB_COREVIDEO
|
||||
typedef struct { int64_t timeValue; int32_t timeScale; int32_t flags; } fx_cvtime;
|
||||
FX_EXPORT int32_t CVDisplayLinkCreateWithActiveCGDisplays(void **link) { *link = (void *)1; return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkCreateWithCGDisplay(uint32_t display, void **link) { *link = (void *)1; return 0; }
|
||||
FX_EXPORT fx_cvtime CVDisplayLinkGetNominalOutputVideoRefreshPeriod(void *link) { return (fx_cvtime){1, 60, 0}; }
|
||||
FX_EXPORT void CVDisplayLinkRelease(void *link) { }
|
||||
FX_EXPORT int32_t CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(void *l, void *c, void *p) { return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkSetOutputCallback(void *link, void *callback, void *context) { return 0; }
|
||||
FX_EXPORT int32_t CVDisplayLinkStart(void *link) { fx_log("[fx] CVDisplayLinkStart ignored"); return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_CARBON
|
||||
FX_EXPORT const CFStringRef kTISPropertyUnicodeKeyLayoutData = CFSTR("TISPropertyUnicodeKeyLayoutData");
|
||||
FX_EXPORT void *TISCopyCurrentKeyboardLayoutInputSource(void) { return NULL; }
|
||||
FX_EXPORT void *TISGetInputSourceProperty(void *source, CFStringRef key) { return NULL; }
|
||||
FX_EXPORT uint32_t KBGetLayoutType(int16_t keyboard) { return 0; }
|
||||
FX_EXPORT uint8_t LMGetKbdType(void) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_CORESERVICES
|
||||
FX_EXPORT int32_t LSOpenCFURLRef(CFURLRef url, CFURLRef *launched) { fx_log("[fx] LSOpenCFURLRef ignored"); return -10814; }
|
||||
FX_EXPORT int32_t UCKeyTranslate(const void *layout, uint16_t key, uint16_t action, uint32_t mods, uint32_t kbd,
|
||||
uint32_t opts, uint32_t *dead, unsigned long max, unsigned long *len, uint16_t *chars) {
|
||||
if (len) *len = 0;
|
||||
return -1;
|
||||
}
|
||||
FX_EXPORT double nan(const char *tag) { return __builtin_nan(""); }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_OPENCL
|
||||
FX_EXPORT int32_t clGetPlatformIDs(uint32_t n, void **platforms, uint32_t *count) { if (count) *count = 0; return -1001; }
|
||||
FX_EXPORT int32_t clGetDeviceIDs(void *p, uint64_t type, uint32_t n, void **devices, uint32_t *count) { if (count) *count = 0; return -1; }
|
||||
FX_EXPORT int32_t clGetDeviceInfo(void *d, uint32_t name, size_t size, void *value, size_t *ret) { return -33; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_FORCEFEEDBACK
|
||||
#define FF_UNSUPPORTED ((int32_t)0x80040202)
|
||||
FX_EXPORT int32_t FFIsForceFeedback(uint32_t service) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFCreateDevice(uint32_t service, void **device) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFReleaseDevice(void *device) { return 0; }
|
||||
FX_EXPORT int32_t FFDeviceCreateEffect(void *d, CFUUIDRef u, void *e, void **h) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceReleaseEffect(void *d, void *e) { return 0; }
|
||||
FX_EXPORT int32_t FFDeviceGetForceFeedbackCapabilities(void *d, void *c) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceGetForceFeedbackProperty(void *d, uint32_t p, void *v, uint32_t s) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceSetForceFeedbackProperty(void *d, uint32_t p, void *v) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFDeviceSendForceFeedbackCommand(void *d, uint32_t c) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectGetEffectStatus(void *e, uint32_t *s) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectSetParameters(void *e, void *p, uint32_t f) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectStart(void *e, uint32_t i, uint32_t f) { return FF_UNSUPPORTED; }
|
||||
FX_EXPORT int32_t FFEffectStop(void *e) { return FF_UNSUPPORTED; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_LDAP
|
||||
// Only reachable from libcurl's LDAP protocol handler; every entry fails.
|
||||
#define LDAP_FAIL 0x51 /* LDAP_SERVER_DOWN */
|
||||
FX_EXPORT void *ldap_init(const char *host, int port) { return NULL; }
|
||||
FX_EXPORT int ldap_set_option(void *ld, int option, const void *value) { return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_simple_bind_s(void *ld, const char *who, const char *cred) { return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_search_s(void *ld, const char *b, int s, const char *f, char **a, int o, void **r) { if (r) *r = NULL; return LDAP_FAIL; }
|
||||
FX_EXPORT int ldap_unbind_s(void *ld) { return 0; }
|
||||
FX_EXPORT char *ldap_err2string(int err) { return "LDAP unsupported on iOS port"; }
|
||||
FX_EXPORT void *ldap_first_entry(void *ld, void *r) { return NULL; }
|
||||
FX_EXPORT void *ldap_next_entry(void *ld, void *e) { return NULL; }
|
||||
FX_EXPORT char *ldap_first_attribute(void *ld, void *e, void **b) { return NULL; }
|
||||
FX_EXPORT char *ldap_next_attribute(void *ld, void *e, void *b) { return NULL; }
|
||||
FX_EXPORT char *ldap_get_dn(void *ld, void *e) { return NULL; }
|
||||
FX_EXPORT void **ldap_get_values_len(void *ld, void *e, const char *a) { return NULL; }
|
||||
FX_EXPORT void ldap_value_free_len(void **v) { }
|
||||
FX_EXPORT void ldap_memfree(void *p) { }
|
||||
FX_EXPORT int ldap_msgfree(void *m) { return 0; }
|
||||
FX_EXPORT int ldap_url_parse(const char *url, void **desc) { return LDAP_FAIL; }
|
||||
FX_EXPORT void ldap_free_urldesc(void *d) { }
|
||||
FX_EXPORT void ber_free(void *b, int f) { }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_APPLICATIONSERVICES
|
||||
FX_EXPORT int UAZoomEnabled(void) { return 0; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_SECURITY
|
||||
#define errSecUnimplemented (-4)
|
||||
FX_EXPORT CFStringRef SecCertificateCopyLongDescription(CFAllocatorRef a, void *cert, CFErrorRef *e) { return NULL; }
|
||||
FX_EXPORT int32_t SecIdentityCreateWithCertificate(CFTypeRef keychain, void *cert, void **identity) { return errSecUnimplemented; }
|
||||
FX_EXPORT int32_t SecItemImport(CFDataRef data, CFStringRef ext, void *fmt, void *type, uint32_t flags,
|
||||
const void *params, void *keychain, CFArrayRef *items) { return errSecUnimplemented; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_IOKIT
|
||||
FX_EXPORT CFDictionaryRef IODisplayCreateInfoDictionary(uint32_t framebuffer, uint32_t options) {
|
||||
return CFDictionaryCreate(NULL, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_STEAM
|
||||
// Steam is excluded from the target. Init reports failure; the game runs without Steam.
|
||||
FX_EXPORT int SteamAPI_RestartAppIfNecessary(uint32_t app) { fx_log("[fx] SteamAPI_RestartAppIfNecessary(%u) -> false", app); return 0; }
|
||||
FX_EXPORT int SteamAPI_Init(void) { fx_log("[fx] SteamAPI_Init -> false"); return 0; }
|
||||
FX_EXPORT void SteamAPI_Shutdown(void) { }
|
||||
FX_EXPORT void SteamAPI_RunCallbacks(void) { }
|
||||
FX_EXPORT int32_t SteamAPI_GetHSteamUser(void) { return 0; }
|
||||
FX_EXPORT void SteamAPI_RegisterCallback(void *cb, int id) { }
|
||||
FX_EXPORT void SteamAPI_UnregisterCallback(void *cb) { }
|
||||
FX_EXPORT void SteamAPI_RegisterCallResult(void *cb, uint64_t call) { }
|
||||
FX_EXPORT void SteamAPI_UnregisterCallResult(void *cb, uint64_t call) { }
|
||||
FX_EXPORT void *SteamInternal_ContextInit(void *context) { fx_log("[fx] SteamInternal_ContextInit -> NULL"); return NULL; }
|
||||
FX_EXPORT void *SteamInternal_FindOrCreateUserInterface(int32_t user, const char *version) { return NULL; }
|
||||
#endif
|
||||
|
||||
#ifdef FX_LIB_EMPTY
|
||||
FX_EXPORT int fx_empty_shim;
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
// void fx_call_on_stack(void *stack_top, void (*fn)(void *), void *arg)
|
||||
// Calls fn(arg) with sp moved to stack_top on the current thread, then restores sp.
|
||||
.text
|
||||
.globl _fx_call_on_stack
|
||||
.p2align 2
|
||||
_fx_call_on_stack:
|
||||
stp x29, x30, [sp, #-16]!
|
||||
mov x29, sp
|
||||
mov x9, sp
|
||||
and x0, x0, #~15
|
||||
mov sp, x0
|
||||
stp x9, xzr, [sp, #-16]!
|
||||
mov x0, x2
|
||||
blr x1
|
||||
ldp x9, xzr, [sp], #16
|
||||
mov sp, x9
|
||||
ldp x29, x30, [sp], #16
|
||||
ret
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "deserialiser_bridge.h"
|
||||
#include "deserialiser_host.h"
|
||||
#include <string.h>
|
||||
|
||||
static int range(wasm_rt_memory_t *ram,uint64_t address,uint64_t size) {
|
||||
return size<=ram->size && address<=ram->size-size;
|
||||
}
|
||||
static uint64_t get(const uint8_t *memory,uint64_t at) {
|
||||
uint64_t value;memcpy(&value,memory+at,8);return value;
|
||||
}
|
||||
u32 w2c_deser__host_allocate(struct w2c_deser__host *env,u32 context) {
|
||||
wasm_rt_memory_t *ram=w2c_factorio__deser_memory(env->module);
|
||||
if (!range(ram,context,256) || env->heap.memory!=ram->data || env->heap.memory_size!=ram->size) return 2;
|
||||
uint64_t pointer;int status=factorio_guest_heap_allocate(&env->heap,get(ram->data,context),&pointer);
|
||||
if (status) return status==1 ? 5 : 2;
|
||||
memcpy(ram->data+context,&pointer,8);return 0;
|
||||
}
|
||||
u32 w2c_deser__host_read_or_throw(struct w2c_deser__host *env,u64 source,u64 destination,u64 size) {
|
||||
wasm_rt_memory_t *ram=w2c_factorio__deser_memory(env->module);
|
||||
if (!range(ram,source,16) || !range(ram,destination,size)) return 2;
|
||||
uint64_t stream=get(ram->data,source+8);
|
||||
if (!range(ram,stream,24)) return 2;
|
||||
uint64_t data=get(ram->data,stream),length=get(ram->data,stream+8),cursor=get(ram->data,stream+16);
|
||||
if (!range(ram,data,length)) return 2;
|
||||
if (cursor>length || size>length-cursor) return 1;
|
||||
memmove(ram->data+destination,ram->data+data+cursor,(size_t)size);
|
||||
cursor+=size;memcpy(ram->data+stream+16,&cursor,8);++env->reads;return 0;
|
||||
}
|
||||
u32 w2c_deser__host_zero_memory(struct w2c_deser__host *env,u64 destination,u64 size,u64 ignored) {
|
||||
(void)ignored;wasm_rt_memory_t *ram=w2c_factorio__deser_memory(env->module);
|
||||
if (!range(ram,destination,size)) return 2;
|
||||
/* The selected bzero service may initialize live arena bytes only. */
|
||||
if (destination<env->heap.begin || destination>env->heap.cursor || size>env->heap.cursor-destination) return 2;
|
||||
memset(ram->data+destination,0,(size_t)size);++env->zero_calls;return 0;
|
||||
}
|
||||
u32 w2c_deser__host_unsupported(struct w2c_deser__host *env,u64 a,u64 b,u64 c) {
|
||||
(void)a;(void)b;(void)c;++env->unsupported_calls;return 6;
|
||||
}
|
||||
u32 w2c_deser__host_unsupported_delete(struct w2c_deser__host *env,u64 a,u64 b,u64 c) {
|
||||
return w2c_deser__host_unsupported(env,a,b,c);
|
||||
}
|
||||
u32 w2c_deser__host_unsupported_length(struct w2c_deser__host *env,u64 a,u64 b,u64 c) {
|
||||
return w2c_deser__host_unsupported(env,a,b,c);
|
||||
}
|
||||
u32 w2c_deser__host_unsupported_array_length(struct w2c_deser__host *env,u64 a,u64 b,u64 c) {
|
||||
return w2c_deser__host_unsupported(env,a,b,c);
|
||||
}
|
||||
u32 w2c_deser__host_version_error(struct w2c_deser__host *env,u64 a,u64 b,u64 c) {
|
||||
(void)a;(void)b;(void)c;++env->version_errors;return 7;
|
||||
}
|
||||
|
||||
int factorio_deserialiser_probe(const uint8_t *prefix,size_t length,FactorioDeserialiserProbe *out) {
|
||||
if (!prefix || !out) return 2;
|
||||
if (!wasm_rt_is_initialized()) wasm_rt_init();
|
||||
w2c_factorio__deser module;
|
||||
struct w2c_deser__host env={.module=&module};
|
||||
wasm2c_factorio__deser_instantiate(&module,&env);
|
||||
wasm_rt_memory_t *ram=w2c_factorio__deser_memory(&module);uint8_t *memory=ram->data;
|
||||
memset(memory,0xa5,(size_t)ram->size);
|
||||
uint64_t registers[32]={0};registers[0]=4096;registers[1]=512;registers[31]=ram->size-256;
|
||||
uint64_t stream[3]={16384,length<9 ? length:9,0};
|
||||
memcpy(memory,registers,sizeof(registers));memcpy(memory+512,stream,sizeof(stream));
|
||||
memcpy(memory+16384,prefix,(size_t)stream[1]);
|
||||
int status=factorio_guest_heap_init(&env.heap,memory,ram->size,131072,196608);
|
||||
if (!status) status=(int)w2c_factorio__deser_run(&module,0,10000);
|
||||
if (!status) {
|
||||
FactorioDeserialiserProbe result={0};uint64_t version=get(memory,4096+24),target=get(memory,4096+40);
|
||||
result.version=(FactorioMapVersion){version>>48,(version>>32)&65535,(version>>16)&65535,version&65535,memory[4096+32]};
|
||||
result.allocations=env.heap.allocations;result.reads=env.reads;
|
||||
result.heap_bytes=env.heap.cursor-env.heap.begin;result.stream_cursor=get(memory,528);
|
||||
result.owns_target=memory[4096+0x4c6];
|
||||
if (!range(ram,target,144) || target<env.heap.begin || target>env.heap.cursor-144 ||
|
||||
result.allocations!=12 || result.reads!=5 || env.zero_calls!=1 || env.unsupported_calls ||
|
||||
result.stream_cursor!=9 || result.owns_target!=1 || memory[4096+16]!=1 || get(memory,0)!=4096) status=8;
|
||||
if (!status) {
|
||||
result.target_capacity=(unsigned)get(memory,target+32);
|
||||
if (result.target_capacity!=64) status=8;
|
||||
else *out=result;
|
||||
}
|
||||
}
|
||||
wasm2c_factorio__deser_free(&module);return status;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef FACTORIO_DESERIALISER_BRIDGE_H
|
||||
#define FACTORIO_DESERIALISER_BRIDGE_H
|
||||
#include "version_bridge.h"
|
||||
typedef struct {
|
||||
FactorioMapVersion version;
|
||||
unsigned allocations, reads, target_capacity;
|
||||
uint64_t heap_bytes, stream_cursor;
|
||||
int owns_target;
|
||||
} FactorioDeserialiserProbe;
|
||||
/* Initialize a fresh original deserializer from a nine-byte version prefix.
|
||||
* 0 success; 1 short input; 2 invalid; 5 heap exhaustion; 6 unsupported service;
|
||||
* 7 original version-error branch. No output commits on failure. */
|
||||
int factorio_deserialiser_probe(const uint8_t *prefix, size_t length, FactorioDeserialiserProbe *out);
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef FACTORIO_DESERIALISER_HOST_H
|
||||
#define FACTORIO_DESERIALISER_HOST_H
|
||||
#include "factorio_deser.h"
|
||||
#include "guest_heap.h"
|
||||
struct w2c_deser__host {
|
||||
w2c_factorio__deser *module;
|
||||
FactorioGuestHeap heap;
|
||||
unsigned reads, zero_calls, unsupported_calls, version_errors;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "guest_heap.h"
|
||||
#include <limits.h>
|
||||
|
||||
int factorio_guest_heap_init(FactorioGuestHeap *heap, uint8_t *memory,
|
||||
uint64_t memory_size, uint64_t begin, uint64_t end) {
|
||||
if (!heap || !memory || !begin || begin > end || end > memory_size ||
|
||||
memory_size > SIZE_MAX || begin % 16) return 2;
|
||||
*heap = (FactorioGuestHeap){memory, memory_size, begin, begin, end, 0};
|
||||
return 0;
|
||||
}
|
||||
|
||||
int factorio_guest_heap_allocate(FactorioGuestHeap *heap, uint64_t size, uint64_t *out) {
|
||||
if (!heap || !out || !heap->memory || !heap->begin || heap->begin % 16 ||
|
||||
heap->cursor < heap->begin || heap->cursor > heap->end ||
|
||||
heap->end > heap->memory_size || heap->memory_size > SIZE_MAX ||
|
||||
heap->allocations == UINT_MAX) return 2;
|
||||
uint64_t padding = (16 - heap->cursor % 16) % 16;
|
||||
if (padding > heap->end - heap->cursor) return 1;
|
||||
uint64_t address = heap->cursor + padding;
|
||||
if (!size) size = 1; /* operator new(0) still returns a distinct allocation. */
|
||||
if (size > heap->end - address) return 1;
|
||||
heap->cursor = address + size;
|
||||
++heap->allocations;
|
||||
*out = address;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef FACTORIO_GUEST_HEAP_H
|
||||
#define FACTORIO_GUEST_HEAP_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* A bounded, per-run arena of guest offsets. No native pointers enter guest RAM.
|
||||
* Allocations are 16-byte aligned and uninitialized; reset discards the arena.
|
||||
* This does not implement C++ destruction, free, or exception unwinding. */
|
||||
typedef struct {
|
||||
uint8_t *memory;
|
||||
uint64_t memory_size, begin, cursor, end;
|
||||
unsigned allocations;
|
||||
} FactorioGuestHeap;
|
||||
|
||||
int factorio_guest_heap_init(FactorioGuestHeap *heap, uint8_t *memory,
|
||||
uint64_t memory_size, uint64_t begin, uint64_t end);
|
||||
/* 0 success, 1 exhausted, 2 invalid. Output and heap unchanged on failure. */
|
||||
int factorio_guest_heap_allocate(FactorioGuestHeap *heap, uint64_t size, uint64_t *out);
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "map_header_bridge.h"
|
||||
#include "factorio_map_header.h"
|
||||
#include <string.h>
|
||||
|
||||
struct w2c_map__host {
|
||||
w2c_factorio__map__header *module;
|
||||
unsigned reads;
|
||||
};
|
||||
|
||||
u32 w2c_map__host_read_or_throw(struct w2c_map__host *env, u64 source, u64 destination, u64 size) {
|
||||
wasm_rt_memory_t *memory = w2c_factorio__map__header_memory(env->module);
|
||||
if (memory->size < 24 || source > memory->size - 24 ||
|
||||
size > memory->size || destination > memory->size - size) return 2;
|
||||
uint64_t reader[3];
|
||||
memcpy(reader, memory->data + source, sizeof(reader));
|
||||
uint64_t data = reader[0], length = reader[1], cursor = reader[2];
|
||||
if (data > memory->size || length > memory->size - data) return 2;
|
||||
if (cursor > length || size > length - cursor) return 1;
|
||||
memmove(memory->data + destination, memory->data + data + cursor, (size_t)size);
|
||||
cursor += size;
|
||||
memcpy(memory->data + source + 16, &cursor, sizeof(cursor));
|
||||
++env->reads;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int factorio_map_header_parse(const uint8_t *payload, size_t length,
|
||||
FactorioMapVersion version, FactorioMapHeader *out) {
|
||||
if (!payload || !out) return 2;
|
||||
if (!wasm_rt_is_initialized()) wasm_rt_init();
|
||||
w2c_factorio__map__header module;
|
||||
struct w2c_map__host env = {.module = &module};
|
||||
wasm2c_factorio__map__header_instantiate(&module, &env);
|
||||
uint8_t *memory = w2c_factorio__map__header_memory(&module)->data;
|
||||
uint64_t registers[32] = {0};
|
||||
registers[0] = 2048; registers[1] = 512; registers[31] = 65280;
|
||||
uint64_t reader[5] = {4096, length < 24 ? length : 24, 0,
|
||||
(uint64_t)version.major << 48 | (uint64_t)version.minor << 32 |
|
||||
(uint64_t)version.patch << 16 | version.build, version.flag};
|
||||
memcpy(memory, registers, sizeof(registers));
|
||||
memcpy(memory + 512, reader, sizeof(reader));
|
||||
memcpy(memory + 4096, payload, (size_t)reader[1]);
|
||||
int result = (int)w2c_factorio__map__header_run(&module, 0, 1000);
|
||||
if (!result) {
|
||||
FactorioMapHeader parsed = {.version = version};
|
||||
memcpy(parsed.tick_fields, memory + 2064, sizeof(parsed.tick_fields));
|
||||
*out = parsed;
|
||||
}
|
||||
wasm2c_factorio__map__header_free(&module);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef FACTORIO_MAP_HEADER_BRIDGE_H
|
||||
#define FACTORIO_MAP_HEADER_BRIDGE_H
|
||||
#include "version_bridge.h"
|
||||
|
||||
/* Three serialized MapTick fields; their individual meanings are not recovered. */
|
||||
typedef struct {
|
||||
FactorioMapVersion version;
|
||||
uint64_t tick_fields[3];
|
||||
} FactorioMapHeader;
|
||||
|
||||
/* Input starts at the constructor's stream position, NOT at the ZIP or map start.
|
||||
* 0 success, 1 short stream, 2 invalid argument/address, 3 execution budget.
|
||||
* Only success modifies out. This is not a complete save/world parser. */
|
||||
int factorio_map_header_parse(const uint8_t *payload, size_t length,
|
||||
FactorioMapVersion version, FactorioMapHeader *out);
|
||||
#endif
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "save_header.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
|
||||
enum { PREFIX_LIMIT = 1024 * 1024, CHUNK = 4096 };
|
||||
static uint16_t u16(const unsigned char *p) { return (uint16_t)(p[0] | ((uint16_t)p[1] << 8)); }
|
||||
static uint32_t u32(const unsigned char *p) { return (uint32_t)u16(p) | ((uint32_t)u16(p + 2) << 16); }
|
||||
static int read_at(FILE *f, uint64_t position, void *out, size_t length) {
|
||||
if (position > INT64_MAX || fseeko(f, (off_t)position, SEEK_SET)) return 0;
|
||||
return fread(out, 1, length, f) == length;
|
||||
}
|
||||
static int suffix(const char *name, size_t length, const char *tail) {
|
||||
size_t count = strlen(tail);
|
||||
return length >= count && !memcmp(name + length - count, tail, count) &&
|
||||
(length == count || name[length - count - 1] == '/');
|
||||
}
|
||||
|
||||
typedef struct { uint32_t local, compressed; uint16_t method, flags, name_length; char name[4097]; } Entry;
|
||||
static int find_level(FILE *file, uint64_t file_size, Entry *selected) {
|
||||
if (file_size < 22) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
size_t count = file_size < 65557 ? (size_t)file_size : 65557;
|
||||
unsigned char *tail = malloc(count);
|
||||
if (!tail) return FACTORIO_SAVE_IO;
|
||||
if (!read_at(file, file_size - count, tail, count)) { free(tail); return FACTORIO_SAVE_IO; }
|
||||
size_t end = count;
|
||||
for (size_t i = count - 22 + 1; i-- > 0;) {
|
||||
if (u32(tail + i) == 0x06054b50 && (size_t)u16(tail + i + 20) == count - i - 22) {
|
||||
end = i; break;
|
||||
}
|
||||
}
|
||||
if (end == count) { free(tail); return FACTORIO_SAVE_INVALID_ZIP; }
|
||||
unsigned char *e = tail + end;
|
||||
uint32_t central_size = u32(e + 12), central_offset = u32(e + 16);
|
||||
uint16_t entries = u16(e + 10);
|
||||
if (u16(e + 4) || u16(e + 6) || entries != u16(e + 8) || entries == 0xffff ||
|
||||
central_size == 0xffffffff || central_offset == 0xffffffff) {
|
||||
free(tail); return FACTORIO_SAVE_UNSUPPORTED;
|
||||
}
|
||||
uint64_t central_end = (uint64_t)central_offset + central_size;
|
||||
if (central_end > file_size - count + end) { free(tail); return FACTORIO_SAVE_INVALID_ZIP; }
|
||||
free(tail);
|
||||
uint64_t cursor = central_offset;
|
||||
int priority = 0;
|
||||
for (unsigned index = 0; index < entries; ++index) {
|
||||
unsigned char header[46];
|
||||
if (cursor > central_end || central_end - cursor < sizeof(header) ||
|
||||
!read_at(file, cursor, header, sizeof(header)) || u32(header) != 0x02014b50) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
uint16_t name_length = u16(header + 28);
|
||||
uint64_t next = cursor + 46 + name_length + u16(header + 30) + u16(header + 32);
|
||||
if (next > central_end || name_length > 4096) return FACTORIO_SAVE_UNSUPPORTED;
|
||||
char name[4097];
|
||||
if (!read_at(file, cursor + 46, name, name_length)) return FACTORIO_SAVE_IO;
|
||||
if (memchr(name, 0, name_length)) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
name[name_length] = 0;
|
||||
int match = suffix(name, name_length, "level.dat0") ? 2 : suffix(name, name_length, "level.dat");
|
||||
if (match && match >= priority) {
|
||||
if (match == priority) return FACTORIO_SAVE_INVALID_ZIP; /* ambiguous roots */
|
||||
if (u16(header + 34) || u32(header + 20) == 0xffffffff || u32(header + 42) == 0xffffffff)
|
||||
return FACTORIO_SAVE_UNSUPPORTED;
|
||||
*selected = (Entry){.local = u32(header + 42), .compressed = u32(header + 20),
|
||||
.method = u16(header + 10), .flags = u16(header + 8), .name_length = name_length};
|
||||
memcpy(selected->name, name, name_length + 1);
|
||||
priority = match;
|
||||
}
|
||||
cursor = next;
|
||||
}
|
||||
if (!priority) return FACTORIO_SAVE_NO_LEVEL;
|
||||
if ((selected->flags & 0x41) || (selected->method != 0 && selected->method != 8)) return FACTORIO_SAVE_UNSUPPORTED;
|
||||
return FACTORIO_SAVE_OK;
|
||||
}
|
||||
|
||||
static int feed_inner(z_stream *inner, unsigned char *bytes, size_t length, unsigned char *header, size_t requested) {
|
||||
inner->next_in = bytes; inner->avail_in = (uInt)length;
|
||||
inner->next_out = header + inner->total_out; inner->avail_out = (uInt)(requested - inner->total_out);
|
||||
int status = inflate(inner, Z_NO_FLUSH);
|
||||
if (status != Z_OK && status != Z_STREAM_END && status != Z_BUF_ERROR) return FACTORIO_SAVE_COMPRESSION;
|
||||
if (inner->total_out == requested) return FACTORIO_SAVE_OK;
|
||||
return status == Z_STREAM_END ? FACTORIO_SAVE_SHORT_HEADER : -1;
|
||||
}
|
||||
|
||||
static int prefix(FILE *file, uint64_t file_size, const Entry *entry, unsigned char *header, size_t requested) {
|
||||
unsigned char local[30];
|
||||
if (!read_at(file, entry->local, local, sizeof(local)) || u32(local) != 0x04034b50) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
if (u16(local + 8) != entry->method || u16(local + 6) != entry->flags ||
|
||||
u16(local + 26) != entry->name_length) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
char name[4097];
|
||||
if (!read_at(file, (uint64_t)entry->local + 30, name, entry->name_length) ||
|
||||
memcmp(name, entry->name, entry->name_length)) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
uint64_t data_offset = (uint64_t)entry->local + 30 + u16(local + 26) + u16(local + 28);
|
||||
if (data_offset > file_size || entry->compressed > file_size - data_offset) return FACTORIO_SAVE_INVALID_ZIP;
|
||||
z_stream outer = {0}, inner = {0};
|
||||
if (inflateInit(&inner) != Z_OK) return FACTORIO_SAVE_COMPRESSION;
|
||||
if (entry->method == 8 && inflateInit2(&outer, -MAX_WBITS) != Z_OK) {
|
||||
inflateEnd(&inner); return FACTORIO_SAVE_COMPRESSION;
|
||||
}
|
||||
int result = FACTORIO_SAVE_SHORT_HEADER;
|
||||
unsigned char input[CHUNK], decoded[CHUNK];
|
||||
uint64_t consumed = 0, produced = 0;
|
||||
while (consumed < entry->compressed && consumed < PREFIX_LIMIT) {
|
||||
size_t amount = entry->compressed - consumed < CHUNK ? (size_t)(entry->compressed - consumed) : CHUNK;
|
||||
if (!read_at(file, data_offset + consumed, input, amount)) { result = FACTORIO_SAVE_IO; break; }
|
||||
consumed += amount;
|
||||
if (entry->method == 0) {
|
||||
result = feed_inner(&inner, input, amount, header, requested);
|
||||
if (result >= 0) goto finish;
|
||||
} else {
|
||||
outer.next_in = input; outer.avail_in = (uInt)amount;
|
||||
do {
|
||||
outer.next_out = decoded; outer.avail_out = sizeof(decoded);
|
||||
int status = inflate(&outer, Z_NO_FLUSH);
|
||||
if (status != Z_OK && status != Z_STREAM_END && status != Z_BUF_ERROR) {
|
||||
result = FACTORIO_SAVE_COMPRESSION; goto finish;
|
||||
}
|
||||
size_t bytes = sizeof(decoded) - outer.avail_out;
|
||||
produced += bytes;
|
||||
if (produced > PREFIX_LIMIT) { result = FACTORIO_SAVE_PREFIX_LIMIT; goto finish; }
|
||||
result = feed_inner(&inner, decoded, bytes, header, requested);
|
||||
if (result >= 0) goto finish;
|
||||
if (status == Z_STREAM_END) { result = FACTORIO_SAVE_SHORT_HEADER; goto finish; }
|
||||
if (!bytes && !outer.avail_in) break;
|
||||
if (!bytes && status == Z_BUF_ERROR) { result = FACTORIO_SAVE_COMPRESSION; goto finish; }
|
||||
} while (outer.avail_in || !outer.avail_out);
|
||||
}
|
||||
}
|
||||
result = consumed >= PREFIX_LIMIT ? FACTORIO_SAVE_PREFIX_LIMIT : FACTORIO_SAVE_SHORT_HEADER;
|
||||
finish:
|
||||
if (entry->method == 8) inflateEnd(&outer);
|
||||
inflateEnd(&inner);
|
||||
return result;
|
||||
}
|
||||
|
||||
int factorio_save_prefix(const char *path, uint8_t *out, size_t length) {
|
||||
if (!path || !out) return FACTORIO_SAVE_IO;
|
||||
if (!length || length > PREFIX_LIMIT) return FACTORIO_SAVE_PREFIX_LIMIT;
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (!file) return FACTORIO_SAVE_IO;
|
||||
if (fseeko(file, 0, SEEK_END)) { fclose(file); return FACTORIO_SAVE_IO; }
|
||||
off_t end = ftello(file);
|
||||
if (end < 0) { fclose(file); return FACTORIO_SAVE_IO; }
|
||||
Entry entry;
|
||||
int status = find_level(file, (uint64_t)end, &entry);
|
||||
unsigned char *temporary = NULL;
|
||||
if (!status) {
|
||||
temporary = malloc(length);
|
||||
if (!temporary) status = FACTORIO_SAVE_IO;
|
||||
else status = prefix(file, (uint64_t)end, &entry, temporary, length);
|
||||
}
|
||||
fclose(file);
|
||||
if (!status) memcpy(out, temporary, length);
|
||||
free(temporary);
|
||||
return status;
|
||||
}
|
||||
|
||||
int factorio_save_header(const char *path, FactorioMapVersion *out) {
|
||||
if (!out) return FACTORIO_SAVE_IO;
|
||||
unsigned char header[9];
|
||||
int status = factorio_save_prefix(path, header, sizeof(header));
|
||||
if (!status && factorio_version_parse(header, 9, out)) status = FACTORIO_SAVE_SHORT_HEADER;
|
||||
return status;
|
||||
}
|
||||
|
||||
const char *factorio_save_status_string(int status) {
|
||||
switch (status) {
|
||||
case FACTORIO_SAVE_OK: return "Version header read; full map is not loaded";
|
||||
case FACTORIO_SAVE_IO: return "Cannot read the selected file";
|
||||
case FACTORIO_SAVE_INVALID_ZIP: return "Invalid or ambiguous ZIP archive";
|
||||
case FACTORIO_SAVE_NO_LEVEL: return "No level.dat0 or level.dat entry";
|
||||
case FACTORIO_SAVE_UNSUPPORTED: return "Unsupported archive layout, compression or encryption";
|
||||
case FACTORIO_SAVE_COMPRESSION: return "Invalid compressed level header";
|
||||
case FACTORIO_SAVE_SHORT_HEADER: return "Incomplete map version header";
|
||||
case FACTORIO_SAVE_PREFIX_LIMIT: return "Header decompression limit reached";
|
||||
default: return "Unknown save-header error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef FACTORIO_SAVE_HEADER_H
|
||||
#define FACTORIO_SAVE_HEADER_H
|
||||
#include "version_bridge.h"
|
||||
|
||||
/* Header probe only: does not verify the whole archive or deserialize the map. */
|
||||
enum FactorioSaveStatus { FACTORIO_SAVE_OK = 0, FACTORIO_SAVE_IO,
|
||||
FACTORIO_SAVE_INVALID_ZIP, FACTORIO_SAVE_NO_LEVEL,
|
||||
FACTORIO_SAVE_UNSUPPORTED, FACTORIO_SAVE_COMPRESSION,
|
||||
FACTORIO_SAVE_SHORT_HEADER, FACTORIO_SAVE_PREFIX_LIMIT };
|
||||
int factorio_save_header(const char *path, FactorioMapVersion *out);
|
||||
/* Decode exactly length bytes of the nested map stream (1..1 MiB).
|
||||
* Writes out only on success. Does not validate the complete stream/archive. */
|
||||
int factorio_save_prefix(const char *path, uint8_t *out, size_t length);
|
||||
const char *factorio_save_status_string(int status);
|
||||
#endif
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "save_layout.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
enum { LIMIT = 1024 * 1024, NODES = 4096, DEPTH = 64 };
|
||||
typedef struct {
|
||||
const uint8_t *bytes;
|
||||
size_t length, cursor, needed;
|
||||
unsigned nodes;
|
||||
int status;
|
||||
} Reader;
|
||||
static void fail(Reader *r, int status) { if (!r->status) r->status = status; }
|
||||
static const uint8_t *take(Reader *r, size_t size) {
|
||||
if (r->status) return NULL;
|
||||
if (size > LIMIT || r->cursor > LIMIT - size) { fail(r, FACTORIO_SAVE_PREFIX_LIMIT); return NULL; }
|
||||
size_t end = r->cursor + size;
|
||||
if (end > r->length) {
|
||||
r->needed = end; fail(r, FACTORIO_SAVE_SHORT_HEADER); return NULL;
|
||||
}
|
||||
const uint8_t *result = r->bytes + r->cursor;
|
||||
r->cursor = end;
|
||||
return result;
|
||||
}
|
||||
static uint32_t number(Reader *r, unsigned size) {
|
||||
const uint8_t *p = take(r,size);
|
||||
uint32_t result = 0;
|
||||
if (p) for (unsigned i=0;i<size;++i) result |= (uint32_t)p[i] << (8*i);
|
||||
return result;
|
||||
}
|
||||
static uint32_t compact(Reader *r, unsigned extended) {
|
||||
uint32_t value = number(r,1);
|
||||
return value == 255 ? number(r,extended) : value;
|
||||
}
|
||||
static void string(Reader *r) { size_t count = compact(r,4); take(r,count); }
|
||||
static void boolean(Reader *r) { if (number(r,1)>1) fail(r,FACTORIO_SAVE_UNSUPPORTED); }
|
||||
static void version(Reader *r) { for(unsigned i=0;i<3;++i) compact(r,2); }
|
||||
static void localised(Reader *r, unsigned depth) {
|
||||
if (r->status) return;
|
||||
if (depth>=DEPTH || ++r->nodes>NODES) { fail(r,FACTORIO_SAVE_PREFIX_LIMIT); return; }
|
||||
if (number(r,1)) {
|
||||
string(r);
|
||||
unsigned children = number(r,1);
|
||||
for(unsigned i=0;i<children && !r->status;++i) localised(r,depth+1);
|
||||
}
|
||||
}
|
||||
static void ending(Reader *r) {
|
||||
localised(r,0); localised(r,0);
|
||||
uint32_t count = compact(r,4);
|
||||
if (count>NODES) { fail(r,FACTORIO_SAVE_PREFIX_LIMIT); return; }
|
||||
for(uint32_t i=0;i<count && !r->status;++i) localised(r,0);
|
||||
localised(r,0); string(r);
|
||||
}
|
||||
|
||||
int factorio_save_layout_bytes(const uint8_t *prefix, size_t length,
|
||||
FactorioSaveLayout *out, size_t *needed) {
|
||||
if (!prefix || !out || !needed) return FACTORIO_SAVE_IO;
|
||||
*needed=0;
|
||||
if (length<9) { *needed=9; return FACTORIO_SAVE_SHORT_HEADER; }
|
||||
FactorioMapVersion v;
|
||||
if (factorio_version_parse(prefix,9,&v)) return FACTORIO_SAVE_SHORT_HEADER;
|
||||
if (v.major!=2 || v.minor!=0 || v.patch!=77 || v.build || v.flag)
|
||||
return FACTORIO_SAVE_UNSUPPORTED;
|
||||
Reader r = {.bytes=prefix,.length=length,.cursor=9};
|
||||
// ScenarioLocation: three compact-length strings.
|
||||
string(&r); string(&r); string(&r);
|
||||
if(number(&r,1)>2) fail(&r,FACTORIO_SAVE_UNSUPPORTED); // Difficulty
|
||||
boolean(&r); boolean(&r); string(&r);
|
||||
for(unsigned i=0;i<4;++i) boolean(&r);
|
||||
version(&r); number(&r,4); boolean(&r); number(&r,4);
|
||||
uint32_t mods=compact(&r,4);
|
||||
if(mods>NODES) fail(&r,FACTORIO_SAVE_PREFIX_LIMIT);
|
||||
for(uint32_t i=0;i<mods && !r.status;++i) { string(&r); version(&r); number(&r,4); }
|
||||
number(&r,4);
|
||||
// PropertyTree type=5 (dictionary), any-type boolean, uint32 child count.
|
||||
// Reject other/nonempty settings instead of guessing recursive wire layouts.
|
||||
if(number(&r,1)!=5) fail(&r,FACTORIO_SAVE_UNSUPPORTED);
|
||||
boolean(&r);
|
||||
if(number(&r,4)) fail(&r,FACTORIO_SAVE_UNSUPPORTED);
|
||||
ending(&r); ending(&r); boolean(&r);
|
||||
if(r.status) { *needed=r.needed; return r.status; }
|
||||
size_t position=r.cursor;
|
||||
if (!take(&r,24)) { *needed=r.needed; return r.status; }
|
||||
FactorioSaveLayout result = {.header_offset=position,.next_offset=r.cursor,.mod_count=mods};
|
||||
if(factorio_map_header_parse(prefix+position,24,v,&result.header)) return FACTORIO_SAVE_SHORT_HEADER;
|
||||
*out=result;
|
||||
return FACTORIO_SAVE_OK;
|
||||
}
|
||||
|
||||
int factorio_save_map_header(const char *path, FactorioSaveLayout *out) {
|
||||
if(!path || !out) return FACTORIO_SAVE_IO;
|
||||
size_t requested=9;
|
||||
// Retry exactly the bytes the parser needs. Avoid assuming a fixed offset or
|
||||
// requiring a large prefix when a valid short stream contains the whole header.
|
||||
for(unsigned attempt=0;attempt<128;++attempt) {
|
||||
uint8_t *prefix=malloc(requested);
|
||||
if(!prefix) return FACTORIO_SAVE_IO;
|
||||
int status=factorio_save_prefix(path,prefix,requested);
|
||||
size_t needed=0;
|
||||
if(!status) status=factorio_save_layout_bytes(prefix,requested,out,&needed);
|
||||
free(prefix);
|
||||
if(status!=FACTORIO_SAVE_SHORT_HEADER || needed<=requested) return status;
|
||||
requested=needed;
|
||||
}
|
||||
return FACTORIO_SAVE_PREFIX_LIMIT;
|
||||
}
|
||||
|
||||
const char *factorio_save_layout_status_string(int status) {
|
||||
switch(status) {
|
||||
case FACTORIO_SAVE_OK: return "Map header read; full world is not loaded";
|
||||
case FACTORIO_SAVE_UNSUPPORTED: return "Unsupported save metadata or archive; currently supports 2.0.77 with empty startup settings";
|
||||
case FACTORIO_SAVE_SHORT_HEADER: return "Incomplete scenario metadata or map header";
|
||||
case FACTORIO_SAVE_PREFIX_LIMIT: return "Scenario prefix, recursion or read limit reached";
|
||||
default: return factorio_save_status_string(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef FACTORIO_SAVE_LAYOUT_H
|
||||
#define FACTORIO_SAVE_LAYOUT_H
|
||||
#include "save_header.h"
|
||||
#include "map_header_bridge.h"
|
||||
|
||||
/* Bounded host reconstruction of the 2.0.77 base-game scenario envelope.
|
||||
* Only empty startup PropertyTree settings are currently supported.
|
||||
* This locates MapHeader; it does not initialize a Scenario or load a world. */
|
||||
typedef struct {
|
||||
FactorioMapHeader header;
|
||||
size_t header_offset, next_offset;
|
||||
uint32_t mod_count;
|
||||
} FactorioSaveLayout;
|
||||
int factorio_save_layout_bytes(const uint8_t *prefix, size_t length,
|
||||
FactorioSaveLayout *out, size_t *needed);
|
||||
int factorio_save_map_header(const char *path, FactorioSaveLayout *out);
|
||||
const char *factorio_save_layout_status_string(int status);
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "target_bridge.h"
|
||||
#include "target_host.h"
|
||||
#include <string.h>
|
||||
|
||||
u32 w2c_target__host_allocate(struct w2c_target__host *env, u32 context) {
|
||||
wasm_rt_memory_t *memory=w2c_factorio__target_memory(env->module);
|
||||
if (memory->size<256 || context>memory->size-256 || env->heap.memory!=memory->data ||
|
||||
env->heap.memory_size!=memory->size) return 2;
|
||||
uint64_t size,address;
|
||||
memcpy(&size,memory->data+context,8);
|
||||
int status=factorio_guest_heap_allocate(&env->heap,size,&address);
|
||||
if (status) return status==1 ? 5 : 2;
|
||||
memcpy(memory->data+context,&address,8);
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 w2c_target__host_unsupported_delete(struct w2c_target__host *env,u64 a,u64 b,u64 c) {
|
||||
(void)env;(void)a;(void)b;(void)c;return 6;
|
||||
}
|
||||
u32 w2c_target__host_unsupported_length(struct w2c_target__host *env,u64 a,u64 b,u64 c) {
|
||||
return w2c_target__host_unsupported_delete(env,a,b,c);
|
||||
}
|
||||
u32 w2c_target__host_unsupported_array_length(struct w2c_target__host *env,u64 a,u64 b,u64 c) {
|
||||
return w2c_target__host_unsupported_delete(env,a,b,c);
|
||||
}
|
||||
|
||||
static uint64_t get(const uint8_t *memory,size_t offset) {
|
||||
uint64_t value;memcpy(&value,memory+offset,8);return value;
|
||||
}
|
||||
|
||||
int factorio_target_probe(FactorioTargetProbe *out) {
|
||||
if (!out) return 2;
|
||||
if (!wasm_rt_is_initialized()) wasm_rt_init();
|
||||
w2c_factorio__target module;
|
||||
struct w2c_target__host env={.module=&module};
|
||||
wasm2c_factorio__target_instantiate(&module,&env);
|
||||
wasm_rt_memory_t *ram=w2c_factorio__target_memory(&module);
|
||||
uint8_t *memory=ram->data;
|
||||
memset(memory,0xa5,(size_t)ram->size);
|
||||
int status=factorio_guest_heap_init(&env.heap,memory,ram->size,131072,196608);
|
||||
uint64_t registers[32]={0};
|
||||
registers[0]=4096;registers[31]=ram->size-256;
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
if (!status) status=(int)w2c_factorio__target_run(&module,0,10000);
|
||||
if (!status) {
|
||||
/* Fixed allocation trace is part of this pinned constructor's contract.
|
||||
* No dereference occurs until those offsets have been checked. */
|
||||
uint64_t block=131072,targets=131104,vector=133152,last=133168;
|
||||
if (env.heap.allocations!=4 || env.heap.cursor!=last+8 || get(memory,0)!=4096 ||
|
||||
memory[4096]!=0 || memory[4096+0x88]!=0 ||
|
||||
get(memory,4104)!=vector || get(memory,4112)!=vector+8 || get(memory,4120)!=vector+8 ||
|
||||
get(memory,4128)!=64 || get(memory,4136)!=0 || get(memory,4144)!=0 ||
|
||||
get(memory,4096+0x38)!=last || get(memory,4096+0x40)!=last+8 ||
|
||||
get(memory,4096+0x48)!=last+8 || get(memory,last)!=0 ||
|
||||
get(memory,vector)!=block || get(memory,block)!=targets ||
|
||||
get(memory,block+8)!=targets+2048 || get(memory,block+16)!=targets+2048) status=7;
|
||||
for (unsigned i=0;!status && i<64;++i) {
|
||||
if (get(memory,targets+i*32)!=65536+0xda8 ||
|
||||
get(memory,targets+i*32+8) || get(memory,targets+i*32+16) || get(memory,targets+i*32+24)) status=7;
|
||||
}
|
||||
for (unsigned i=0x50;!status && i<0x81;++i) if (memory[4096+i]) status=7;
|
||||
if (!status) *out=(FactorioTargetProbe){4,64,64,env.heap.cursor-env.heap.begin};
|
||||
}
|
||||
wasm2c_factorio__target_free(&module);
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef FACTORIO_TARGET_BRIDGE_H
|
||||
#define FACTORIO_TARGET_BRIDGE_H
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
unsigned allocations, pool_capacity, initialized_targets;
|
||||
uint64_t heap_bytes;
|
||||
} FactorioTargetProbe;
|
||||
/* Executes the original initialization graph in an isolated 1 MiB guest.
|
||||
* 0 verified; 5 heap exhausted; 6 unsupported service; other values are errors.
|
||||
* Result commits only after complete object-graph validation. */
|
||||
int factorio_target_probe(FactorioTargetProbe *out);
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef FACTORIO_TARGET_HOST_H
|
||||
#define FACTORIO_TARGET_HOST_H
|
||||
#include "factorio_target.h"
|
||||
#include "guest_heap.h"
|
||||
struct w2c_target__host {
|
||||
w2c_factorio__target *module;
|
||||
FactorioGuestHeap heap;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "version_bridge.h"
|
||||
#include "version_host.h"
|
||||
#include <string.h>
|
||||
|
||||
static uint64_t read64(const void *p) {
|
||||
uint64_t value;
|
||||
memcpy(&value, p, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
u32 w2c_env_read_or_throw(struct w2c_env *env, u64 source, u64 destination, u64 size) {
|
||||
wasm_rt_memory_t *memory = w2c_factorio__version_memory(env->module);
|
||||
if (memory->size < 24 || source > memory->size - 24 ||
|
||||
size > memory->size || destination > memory->size - size) return 2;
|
||||
uint64_t data = read64(memory->data + source);
|
||||
uint64_t length = read64(memory->data + source + 8);
|
||||
uint64_t cursor = read64(memory->data + source + 16);
|
||||
if (data > memory->size || length > memory->size - data) return 2;
|
||||
if (cursor > length || size > length - cursor) return 1;
|
||||
memmove(memory->data + destination, memory->data + data + cursor, (size_t)size);
|
||||
cursor += size;
|
||||
memcpy(memory->data + source + 16, &cursor, sizeof(cursor));
|
||||
++env->reads;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int factorio_version_parse(const uint8_t *bytes, size_t length, FactorioMapVersion *out) {
|
||||
if (!bytes || !out) return 2;
|
||||
if (!wasm_rt_is_initialized()) wasm_rt_init();
|
||||
w2c_factorio__version module;
|
||||
struct w2c_env env = {.module = &module};
|
||||
wasm2c_factorio__version_instantiate(&module, &env);
|
||||
uint8_t *memory = w2c_factorio__version_memory(&module)->data;
|
||||
uint64_t registers[32] = {0};
|
||||
registers[0] = 2048; /* guest output object */
|
||||
registers[1] = 512; /* opaque stream adapter */
|
||||
registers[31] = 65280;
|
||||
uint64_t reader[3] = {4096, length < 9 ? length : 9, 0};
|
||||
memcpy(memory, registers, sizeof(registers));
|
||||
memcpy(memory + 512, reader, sizeof(reader));
|
||||
memcpy(memory + 4096, bytes, (size_t)reader[1]);
|
||||
int result = (int)w2c_factorio__version_run(&module, 0);
|
||||
if (!result) {
|
||||
uint64_t packed = read64(memory + 2048);
|
||||
*out = (FactorioMapVersion){.major = (uint16_t)(packed >> 48),
|
||||
.minor = (uint16_t)(packed >> 32), .patch = (uint16_t)(packed >> 16),
|
||||
.build = (uint16_t)packed, .flag = memory[2056]};
|
||||
}
|
||||
wasm2c_factorio__version_free(&module);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef FACTORIO_VERSION_BRIDGE_H
|
||||
#define FACTORIO_VERSION_BRIDGE_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint16_t major, minor, patch, build;
|
||||
uint8_t flag; /* Stored byte; semantics have not yet been recovered. */
|
||||
} FactorioMapVersion;
|
||||
|
||||
/* 0=success; 1=short header; 2=invalid memory. Parses only the version header. */
|
||||
int factorio_version_parse(const uint8_t *bytes, size_t length, FactorioMapVersion *out);
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef FACTORIO_VERSION_HOST_H
|
||||
#define FACTORIO_VERSION_HOST_H
|
||||
#include "factorio_version.h"
|
||||
struct w2c_env {
|
||||
w2c_factorio__version *module;
|
||||
uint32_t reads;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Execute branch/loop translations against original ARM64 instruction words."""
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
from a64_lift import Compiler, UnsupportedInstruction
|
||||
|
||||
OUT = ROOT / 'build/cfg'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
# Hand-authored instruction fixtures, NOT recovered Factorio functions.
|
||||
# Original relative branch offsets are preserved in the native oracle.
|
||||
FIXTURES = {
|
||||
'countdown': [0x34000060, 0x51000400, 0x17fffffe, 0xd65f03c0],
|
||||
'bit63': [0xb6f80060, 0xd2800060, 0x14000002, 0xd28000e0, 0xd65f03c0],
|
||||
'nonzero32': [0x35000060, 0xd28000a0, 0x14000002, 0xd2800160, 0xd65f03c0],
|
||||
'nonzero64': [0xb5000060, 0xd28000a0, 0x14000002, 0xd2800160, 0xd65f03c0],
|
||||
'bit0set': [0x37000060, 0xd2800060, 0x14000002, 0xd28000e0, 0xd65f03c0],
|
||||
}
|
||||
for operation, opcode in [('cmp32', 0x6b01001f), ('cmp64', 0xeb01001f),
|
||||
('cmn32', 0x2b01001f), ('cmn64', 0xab01001f)]:
|
||||
for condition in range(16):
|
||||
FIXTURES[f'{operation}c{condition}'] = [opcode, 0x54000060 | condition,
|
||||
0xd2800060, 0x14000002, 0xd28000e0, 0xd65f03c0]
|
||||
for operation, opcode in [('addimm64', 0xb100041f), ('subimm32', 0x7100041f),
|
||||
('asr32', 0x6b817c1f), ('asr64', 0xeb81fc1f)]:
|
||||
for condition in (0, 2, 6, 10):
|
||||
FIXTURES[f'{operation}c{condition}'] = [opcode, 0x54000060 | condition,
|
||||
0xd2800060, 0x14000002, 0xd28000e0, 0xd65f03c0]
|
||||
for width in (32, 64):
|
||||
for operation in range(4):
|
||||
condition = 10 if width == 32 else 8
|
||||
select = (0x1a810000 if width == 32 else 0x9a810000) | condition << 12
|
||||
select |= (operation >> 1) << 30 | (operation & 1) << 10
|
||||
FIXTURES[f'select{width}op{operation}'] = [0x14000001,
|
||||
0x6b01001f if width == 32 else 0xeb01001f, select, 0xd65f03c0]
|
||||
|
||||
|
||||
def run(*args):
|
||||
subprocess.run(list(map(str, args)), cwd=ROOT, check=True)
|
||||
|
||||
|
||||
assembly = ['.text', '.p2align 2']
|
||||
for name, words in FIXTURES.items():
|
||||
body = struct.pack('<' + 'I' * len(words), *words)
|
||||
(OUT / f'{name}.wat').write_text(Compiler().compile(body, 0x1000))
|
||||
run(ROOT / 'build/wabt/wat2wasm', OUT / f'{name}.wat', '-o', OUT / f'{name}.wasm')
|
||||
run(ROOT / 'build/wabt/wasm2c', OUT / f'{name}.wasm', '-o', OUT / f'{name}.c', '--module-name=' + name)
|
||||
assembly += [f'.globl _native_{name}', f'_native_{name}:']
|
||||
assembly += [f'.long 0x{word:08x}' for word in words]
|
||||
(OUT / 'oracle.S').write_text('\n'.join(assembly) + '\n')
|
||||
|
||||
for words in ([0x14000002, 0xd65f03c0], # target outside body
|
||||
[0x14000002, 0xd2800000, 0xd65f03c0], # unreachable code
|
||||
[0x34000040, 0x14000002, 0xd65f03c0], # one edge leaves body
|
||||
[0x54000040, 0xd2800000, 0xd65f03c0], # flags undefined
|
||||
[0x34000040, 0xf100041f, 0x54000040, 0xd2800000, 0xd65f03c0], # flags defined on only one path
|
||||
[0x14000000]): # no RET
|
||||
try:
|
||||
Compiler().compile(struct.pack('<' + 'I' * len(words), *words), 0x1000)
|
||||
except UnsupportedInstruction:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Invalid CFG accepted')
|
||||
|
||||
includes = '\n'.join(f'#include "{name}.h"\nextern uint64_t native_{name}(uint64_t, uint64_t);' for name in FIXTURES)
|
||||
checks = []
|
||||
for name in FIXTURES:
|
||||
inputs = 'i % 201' if name == 'countdown' else 'values[i % 8] ^ (i < 64 ? 0 : random_value(&seed))'
|
||||
checks.append(f'''
|
||||
{{
|
||||
w2c_{name} module;
|
||||
wasm2c_{name}_instantiate(&module);
|
||||
uint8_t *memory = w2c_{name}_memory(&module)->data;
|
||||
for (unsigned i = 0; i < 20000; ++i) {{
|
||||
uint64_t input = {inputs};
|
||||
uint64_t registers[32];
|
||||
for (int r = 0; r < 32; ++r) registers[r] = random_value(&seed);
|
||||
registers[0] = input;
|
||||
registers[1] = i < 64 ? values[(i / 8) % 8] : registers[1];
|
||||
memset(memory, 0xa5, 65536);
|
||||
memcpy(memory, registers, sizeof(registers));
|
||||
assert(w2c_{name}_run(&module, 0, 1000) == 0);
|
||||
uint64_t output[32]; memcpy(output, memory, sizeof(output));
|
||||
assert(output[0] == native_{name}(input, registers[1]));
|
||||
assert(memcmp(output + 1, registers + 1, 31 * sizeof(uint64_t)) == 0);
|
||||
for (unsigned j = 256; j < 65536; ++j) assert(memory[j] == 0xa5);
|
||||
}}
|
||||
wasm2c_{name}_free(&module);
|
||||
}}''')
|
||||
(OUT / 'verify.c').write_text(f'''
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
{includes}
|
||||
static uint64_t random_value(uint64_t *state) {{
|
||||
*state ^= *state << 13; *state ^= *state >> 7; *state ^= *state << 17; return *state;
|
||||
}}
|
||||
int main(void) {{
|
||||
wasm_rt_init();
|
||||
uint64_t seed = 0x123456789abcdef;
|
||||
uint64_t values[] = {{0, 1, 0xffffffff, 0x100000000, 0x8000000000000000,
|
||||
0xffffffffffffffff, 0x7fffffffffffffff, 2}};
|
||||
{''.join(checks)}
|
||||
w2c_countdown loop;
|
||||
wasm2c_countdown_instantiate(&loop);
|
||||
uint64_t start = 100;
|
||||
memcpy(w2c_countdown_memory(&loop)->data, &start, 8);
|
||||
assert(w2c_countdown_run(&loop, 0, 0) == 3);
|
||||
assert(w2c_countdown_run(&loop, 0, 1) == 3);
|
||||
assert(w2c_countdown_run(&loop, 0, 1000) == 0);
|
||||
wasm2c_countdown_free(&loop);
|
||||
wasm_rt_free();
|
||||
puts("PASS: {len(FIXTURES) * 20000} native branch/loop/NZCV comparisons, preserved registers, memory guards, budget and CFG rejection");
|
||||
return 0;
|
||||
}}
|
||||
''')
|
||||
runtime = ROOT / 'third_party/wabt/wasm2c'
|
||||
run('xcrun', 'clang', '-O2', '-DWASM_RT_USE_MMAP=0', '-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1',
|
||||
'-I' + str(OUT), '-I' + str(runtime), *(OUT / f'{name}.c' for name in FIXTURES),
|
||||
OUT / 'verify.c', OUT / 'oracle.S', runtime / 'wasm-rt-impl.c', runtime / 'wasm-rt-mem-impl.c',
|
||||
'-o', OUT / 'verify')
|
||||
run(OUT / 'verify')
|
||||
(OUT / 'verification.json').write_text(json.dumps({'status': 'pass', 'native_comparisons': len(FIXTURES) * 20000,
|
||||
'fixtures': list(FIXTURES), 'source': 'hand-authored instruction fixtures; not game code',
|
||||
'cfg_invalid_cases': 6, 'budget_exhaustion_status': 3}, indent=2) + '\n')
|
||||
@@ -0,0 +1,21 @@
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / 'tools'))
|
||||
from a64_lift import Compiler, UnsupportedInstruction
|
||||
|
||||
for words, reason in [([0x14000000, 0xd65f03c0], 'loop with unreachable return'),
|
||||
([0x94000000, 0xd65f03c0], 'undeclared host call'),
|
||||
([0x91000400], 'missing return'),
|
||||
([0xd65f03c0, 0xd503201f], 'code after return'),
|
||||
([0x9a010000, 0xd65f03c0], 'unsupported carry arithmetic'),
|
||||
([0x9a810000, 0xd65f03c0], 'conditional select without defined flags'),
|
||||
([0x3cc18020, 0xd65f03c0], '128-bit load without adjacent store'),
|
||||
([0x3d800000, 0xd65f03c0], '128-bit store without adjacent load'),
|
||||
([0x14000002, 0x3cc18020, 0x3d800000, 0xd65f03c0], 'branch into middle of copy')]:
|
||||
try:
|
||||
Compiler().compile(b''.join(struct.pack('<I', word) for word in words), 0x1000)
|
||||
except UnsupportedInstruction:
|
||||
print('PASS: rejected', reason)
|
||||
else:
|
||||
raise AssertionError('Unexpected support for ' + reason)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Native comparisons for the instruction forms added for allocation/initialization."""
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'tools'))
|
||||
from a64_program import ProgramCompiler
|
||||
from a64_lift import UnsupportedInstruction
|
||||
OUT=ROOT/'build/initialization'
|
||||
OUT.mkdir(parents=True,exist_ok=True)
|
||||
def pack(words): return struct.pack('<'+'I'*len(words),*words)
|
||||
def run(*args): subprocess.run(list(map(str,args)),cwd=ROOT,check=True)
|
||||
|
||||
operations=[]
|
||||
for width in (32,64):
|
||||
for shift in range(width):
|
||||
operations.append((0x13000000 if width==32 else 0x93400000) | shift<<16 | (width-1)<<10)
|
||||
for length in range(1,6 if width==32 else 7):
|
||||
for operation in range(3):
|
||||
# Rotate repeated patterns at every supported element size.
|
||||
n=int(length==6)
|
||||
s=((((~((1 << (length+1))-1))&63) | ((1 << length)//2-1)))
|
||||
rotation=(1 << length)-1
|
||||
operations.append((0x12000000 if width==32 else 0x92000000) | operation<<29 | n<<22 | rotation<<16 | s<<10)
|
||||
words=[]
|
||||
for word in operations:
|
||||
words += [0xaa0203e0,word,0xf8008420] # mov x0,x2; op; str x0,[x1],#8
|
||||
words += [0xd65f03c0]
|
||||
compiler=ProgramCompiler(memory_pages=2)
|
||||
(OUT/'initialization.wat').write_text(compiler.compile({0x1000:pack(words)},0x1000))
|
||||
run(ROOT/'build/wabt/wat2wasm',OUT/'initialization.wat','-o',OUT/'initialization.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',OUT/'initialization.wasm','-o',OUT/'initialization.c','--module-name=initialization')
|
||||
(OUT/'oracle.S').write_text('.text\n.p2align 2\n.globl _native_initialization\n_native_initialization:\n'+
|
||||
'\n'.join(f'.long 0x{w:08x}' for w in words)+'\n')
|
||||
invalid=[
|
||||
[0x3c800000,0xd65f03c0], # uninitialized vector register
|
||||
[0xb4000040,0x6f00e400,0x3c800000,0xd65f03c0], # zero is missing on one incoming path
|
||||
[0x14000002,0x6f00e400,0x3c800000,0xd65f03c0], # branch into the idiom
|
||||
[0x6f00e400,0x3c800001,0xd65f03c0], # mismatched vector register
|
||||
[0x90000000,0xd65f03c0], # undeclared static page
|
||||
[0x9240fc00,0xd65f03c0], # reserved all-ones logical mask
|
||||
[0x93400000,0xd65f03c0], # signed bitfield beyond ASR
|
||||
]
|
||||
for fixture in invalid:
|
||||
try: ProgramCompiler().compile({0x1000:pack(fixture)},0x1000)
|
||||
except UnsupportedInstruction: pass
|
||||
else: raise AssertionError('Invalid initialization fixture accepted')
|
||||
for options in [dict(memory_pages=0),dict(memory_pages=32769),dict(returning_hosts={1}),
|
||||
dict(static_pages={0:1}),dict(hosts={1:'same',2:'same'},returning_hosts={1})]:
|
||||
try: ProgramCompiler(**options)
|
||||
except ValueError: pass
|
||||
else: raise AssertionError('Invalid program configuration accepted')
|
||||
(OUT/'verify.c').write_text(f'''
|
||||
#include "initialization.h"
|
||||
#include "wasm-rt-impl.h"
|
||||
#include "wasm-rt-exceptions.h"
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
extern uint64_t native_initialization(uint64_t,uint64_t *,uint64_t);
|
||||
int main(void) {{
|
||||
wasm_rt_init();w2c_initialization module;wasm2c_initialization_instantiate(&module);
|
||||
uint8_t *memory=w2c_initialization_memory(&module)->data;
|
||||
uint64_t seed=0xdeadbeef12345678,reference[{len(operations)}];
|
||||
for(unsigned i=0;i<512;++i) {{
|
||||
seed^=seed<<13;seed^=seed>>7;seed^=seed<<17;
|
||||
uint64_t input=i==0 ? 0 : i==1 ? UINT64_MAX : seed;
|
||||
uint64_t registers[32]={{0}};registers[1]=66048;registers[2]=input;
|
||||
memset(memory,0xa5,131072);memcpy(memory,registers,sizeof(registers));
|
||||
native_initialization(0,reference,input);
|
||||
assert(w2c_initialization_run(&module,0,10000)==0);
|
||||
assert(memcmp(memory+66048,reference,sizeof(reference))==0);
|
||||
for(unsigned j=256;j<131072;++j) {{
|
||||
if(j>=66048 && j<66048+sizeof(reference)) continue;
|
||||
assert(memory[j]==0xa5);
|
||||
}}
|
||||
}}
|
||||
const uint64_t bad[]={{131072-7,131072,UINT64_C(0x100000000)+66048,UINT64_MAX-7}};
|
||||
for(unsigned i=0;i<sizeof(bad)/sizeof(bad[0]);++i) {{
|
||||
uint64_t registers[32]={{0}};registers[1]=bad[i];
|
||||
memset(memory,0xa5,131072);memcpy(memory,registers,sizeof(registers));
|
||||
wasm_rt_trap_t trap=wasm_rt_impl_try();
|
||||
if(!trap) {{w2c_initialization_run(&module,0,10000);assert(0);}}
|
||||
assert(trap==WASM_RT_TRAP_UNREACHABLE);
|
||||
for(unsigned j=256;j<131072;++j) assert(memory[j]==0xa5);
|
||||
}}
|
||||
wasm2c_initialization_free(&module);wasm_rt_free();
|
||||
puts("PASS: {len(operations)*512} native ASR/logical-mask comparisons, extended memory, 64-bit address guards, 12 rejection cases");
|
||||
}}
|
||||
''')
|
||||
runtime=ROOT/'third_party/wabt/wasm2c'
|
||||
run('xcrun','clang','-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1',
|
||||
'-I'+str(OUT),'-I'+str(runtime),OUT/'initialization.c',OUT/'verify.c',OUT/'oracle.S',
|
||||
runtime/'wasm-rt-impl.c',runtime/'wasm-rt-mem-impl.c',runtime/'wasm-rt-exceptions-impl.c','-o',OUT/'verify')
|
||||
run(OUT/'verify')
|
||||
(OUT/'verification.json').write_text(json.dumps({'status':'pass','native_comparisons':len(operations)*512,
|
||||
'instruction_forms':len(operations),'invalid_cases':12,'source':'hand-authored instruction fixtures'},indent=2)+'\n')
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Compare direct guest calls, nested LR saves, recursion, tails and memory with ARM64."""
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'tools'))
|
||||
from a64_program import ProgramCompiler
|
||||
from a64_lift import UnsupportedInstruction
|
||||
|
||||
OUT = ROOT/'build/program'
|
||||
OUT.mkdir(parents=True,exist_ok=True)
|
||||
|
||||
|
||||
def bl(site,target): return 0x94000000 | (((target-site)//4)&0x3ffffff)
|
||||
def body(words): return struct.pack('<'+'I'*len(words),*words)
|
||||
def run(*args): subprocess.run(list(map(str,args)),cwd=ROOT,check=True)
|
||||
|
||||
|
||||
fixtures = {
|
||||
'nested': (0x1000, {
|
||||
0x1000:[0xa9bf7bf3,0xaa0003f3,bl(0x1008,0x1100),bl(0x100c,0x1200),0x8b130000,0xa8c17bf3,0xd65f03c0],
|
||||
0x1100:[0x91000400,0xd65f03c0],
|
||||
0x1200:[0xa9bf7bf4,bl(0x1204,0x1100),0xa8c17bf4,0x91000800,0xd65f03c0]}),
|
||||
'recursive': (0x2000, {0x2000:[0xa9bf7bf3,0xb4000080,0xd1000400,
|
||||
bl(0x200c,0x2000),0x91000400,0xa8c17bf3,0xd65f03c0]}),
|
||||
'tail': (0x3000, {0x3000:[0x14000040],0x3100:[0x91001c00,0xd65f03c0]}),
|
||||
'memory': (0x4000, {
|
||||
0x4000:[0xa9bf7bf3,0xaa0003f3,bl(0x4008,0x4100),0xaa1303e0,bl(0x4010,0x4100),0xa8c17bf3,0xd65f03c0],
|
||||
0x4100:[0xf9400008,0x91000508,0xf9000008,0xaa0803e0,0xd65f03c0]})}
|
||||
|
||||
assembly = ['.text','.p2align 2']
|
||||
cursor = 0x1000
|
||||
for name,(entry,functions) in fixtures.items():
|
||||
compiler = ProgramCompiler()
|
||||
wat = compiler.compile({address:body(words) for address,words in functions.items()},entry)
|
||||
(OUT/f'{name}.wat').write_text(wat)
|
||||
run(ROOT/'build/wabt/wat2wasm',OUT/f'{name}.wat','-o',OUT/f'{name}.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',OUT/f'{name}.wasm','-o',OUT/f'{name}.c','--module-name='+name)
|
||||
for address,words in sorted(functions.items()):
|
||||
assert address>=cursor
|
||||
assembly.append(f'.space {address-cursor}')
|
||||
if address==entry: assembly += [f'.globl _native_{name}',f'_native_{name}:']
|
||||
assembly += [f'.long 0x{word:08x}' for word in words]
|
||||
cursor=address+4*len(words)
|
||||
(OUT/'oracle.S').write_text('\n'.join(assembly)+'\n')
|
||||
bad_return = ProgramCompiler().compile({0x5000:body([0xd282469e,0xd65f03c0])},0x5000)
|
||||
(OUT/'badreturn.wat').write_text(bad_return)
|
||||
run(ROOT/'build/wabt/wat2wasm',OUT/'badreturn.wat','-o',OUT/'badreturn.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',OUT/'badreturn.wasm','-o',OUT/'badreturn.c','--module-name=badreturn')
|
||||
for functions,entry in [({0x1000:body([bl(0x1000,0x1100),0xd65f03c0])},0x1000),
|
||||
({0x1000:body([0xd65f03c0]),0x1100:body([0xd65f03c0])},0x1000),
|
||||
({0x1000:body([0x14000000])},0x1000),
|
||||
({0x1000:body([0x54000040,0xd2800000,0xd65f03c0])},0x1000)]:
|
||||
try: ProgramCompiler().compile(functions,entry)
|
||||
except UnsupportedInstruction: pass
|
||||
else: raise AssertionError('Invalid guest program accepted')
|
||||
|
||||
includes = '\n'.join(f'#include "{name}.h"\nextern uint64_t native_{name}(uint64_t);' for name in fixtures)
|
||||
checks=[]
|
||||
for name in fixtures:
|
||||
argument = 'i%51' if name=='recursive' else 'next(&seed)'
|
||||
prepare = 'memcpy(memory+2048,&expected,8); registers[0]=2048;' if name=='memory' else ''
|
||||
callarg = '(uint64_t)&expected' if name=='memory' else 'input'
|
||||
memory_check = 'assert(memcmp(memory+2048,&expected,8)==0);' if name=='memory' else ''
|
||||
checks.append(f'''
|
||||
{{
|
||||
w2c_{name} module; wasm2c_{name}_instantiate(&module);
|
||||
uint8_t *memory=w2c_{name}_memory(&module)->data;
|
||||
for(unsigned i=0;i<20000;++i) {{
|
||||
uint64_t input={argument}, expected=input, registers[32], after[32];
|
||||
for(unsigned r=0;r<32;++r) registers[r]=next(&seed);
|
||||
registers[0]=input; registers[30]=0; registers[31]=65280;
|
||||
memset(memory,0xa5,65536);
|
||||
{prepare}
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
uint64_t native=native_{name}({callarg});
|
||||
assert(w2c_{name}_run(&module,0,1000)==0);
|
||||
memcpy(after,memory,sizeof(after));
|
||||
assert(after[0]==native);
|
||||
for(unsigned r=19;r<32;++r) assert(after[r]==registers[r]);
|
||||
{memory_check}
|
||||
for(unsigned a=256;a<65536;++a) {{
|
||||
if(a>=64464 && a<65280) continue;
|
||||
if(a>=2048 && a<2056) continue;
|
||||
assert(memory[a]==0xa5);
|
||||
}}
|
||||
}}
|
||||
uint64_t start[32]={{0}}; start[0]=20; start[31]=65280;
|
||||
memcpy(memory,start,sizeof(start));
|
||||
assert(w2c_{name}_run(&module,0,0)==3);
|
||||
memcpy(memory,start,sizeof(start));
|
||||
assert(w2c_{name}_run(&module,0,1)==3);
|
||||
memcpy(memory,start,sizeof(start));
|
||||
start[30]={fixtures[name][0]}; memcpy(memory,start,sizeof(start));
|
||||
assert(w2c_{name}_run(&module,0,1000)==2);
|
||||
wasm2c_{name}_free(&module);
|
||||
}}
|
||||
''')
|
||||
(OUT/'verify.c').write_text(f'''
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
{includes}
|
||||
#include "badreturn.h"
|
||||
static uint64_t next(uint64_t *s) {{ *s^=*s<<13; *s^=*s>>7; *s^=*s<<17; return *s; }}
|
||||
int main(void) {{
|
||||
wasm_rt_init(); uint64_t seed=0x123456789abcdef;
|
||||
{''.join(checks)}
|
||||
w2c_nested bounded; wasm2c_nested_instantiate(&bounded);
|
||||
uint64_t registers[32]={{0}}; registers[0]=3; registers[31]=65280;
|
||||
memcpy(w2c_nested_memory(&bounded)->data,registers,sizeof(registers));
|
||||
assert(w2c_nested_run(&bounded,0,15)==3);
|
||||
memcpy(w2c_nested_memory(&bounded)->data,registers,sizeof(registers));
|
||||
assert(w2c_nested_run(&bounded,0,16)==0);
|
||||
memcpy(registers,w2c_nested_memory(&bounded)->data,sizeof(registers));
|
||||
assert(registers[0]==10);
|
||||
wasm2c_nested_free(&bounded);
|
||||
w2c_badreturn bad; wasm2c_badreturn_instantiate(&bad);
|
||||
assert(w2c_badreturn_run(&bad,0,10)==4);
|
||||
wasm2c_badreturn_free(&bad);
|
||||
wasm_rt_free();
|
||||
puts("PASS: 80000 native guest-program comparisons, nested calls, recursion, tails, shared memory, guards and failure statuses");
|
||||
return 0;
|
||||
}}
|
||||
''')
|
||||
runtime=ROOT/'third_party/wabt/wasm2c'
|
||||
run('xcrun','clang','-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1',
|
||||
'-I'+str(OUT),'-I'+str(runtime),*(OUT/f'{name}.c' for name in [*fixtures,'badreturn']),
|
||||
OUT/'verify.c',OUT/'oracle.S',runtime/'wasm-rt-impl.c',runtime/'wasm-rt-mem-impl.c','-o',OUT/'verify')
|
||||
run(OUT/'verify')
|
||||
(OUT/'verification.json').write_text(json.dumps({'status':'pass','native_comparisons':80000,
|
||||
'source':'hand-authored instruction fixtures, not recovered game code','fixtures':list(fixtures),
|
||||
'scope':'bounded direct guest calls, known returns, shared registers/memory'},indent=2)+'\n')
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Compare zero-vector CFG facts and store addressing with native ARM64 fixtures."""
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT/'tools'))
|
||||
from a64_program import ProgramCompiler
|
||||
from a64_lift import UnsupportedInstruction
|
||||
OUT=ROOT/'build/zero-vectors';OUT.mkdir(parents=True,exist_ok=True)
|
||||
def body(words):return struct.pack('<'+'I'*len(words),*words)
|
||||
def run(*args):subprocess.run(list(map(str,args)),cwd=ROOT,check=True)
|
||||
def zero(register):return 0x6f00e400|register
|
||||
def single(register,offset,mode=0):return 0x3c800000|((offset&511)<<12)|(mode<<10)|register
|
||||
def pair(first,second,offset,mode=2):return 0xac000000|(mode<<23)|(((offset//16)&127)<<15)|(second<<10)|first
|
||||
fixtures={
|
||||
'unscaled':[zero(31),single(31,-256),single(31,255),0xd65f03c0],
|
||||
'scaled':[zero(1),0x3d800000|(63<<10)|1,0xd65f03c0],
|
||||
'prepost':[zero(0),single(0,-32,3),single(0,64,1),0xd65f03c0],
|
||||
'pair':[zero(0),zero(31),pair(0,31,-1024),pair(31,0,1008),0xd65f03c0],
|
||||
'pairprepost':[zero(1),zero(31),pair(1,31,-32,3),pair(31,1,64,1),0xd65f03c0],
|
||||
'branch':[zero(0),0x14000001,0x91000408,single(0,0),0xd65f03c0],
|
||||
'merge':[0xb4000061,zero(0),0x14000002,zero(0),single(0,0),0xd65f03c0],
|
||||
'loop':[zero(0),single(0,0),0x91000421,0xf100103f,0x54ffffa3,0xd65f03c0]}
|
||||
assembly=['.text','.p2align 2']
|
||||
for name,words in fixtures.items():
|
||||
(OUT/f'{name}.wat').write_text(ProgramCompiler().compile({0x1000:body(words)},0x1000))
|
||||
run(ROOT/'build/wabt/wat2wasm',OUT/f'{name}.wat','-o',OUT/f'{name}.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',OUT/f'{name}.wasm','-o',OUT/f'{name}.c','--module-name='+name)
|
||||
assembly += [f'.globl _native_{name}',f'_native_{name}:',*[f'.long 0x{word:08x}' for word in words]]
|
||||
(OUT/'oracle.S').write_text('\n'.join(assembly)+'\n')
|
||||
invalid=[([zero(0),pair(0,1,0),0xd65f03c0],{}),
|
||||
([zero(0),0x9400003f,single(0,0),0xd65f03c0],{0x1100:'clobber'}),
|
||||
([zero(0),pair(0,0,0,0),0xd65f03c0],{}),
|
||||
([zero(0),single(0,0,2),0xd65f03c0],{})]
|
||||
for words,hosts in invalid:
|
||||
try:ProgramCompiler(hosts).compile({0x1000:body(words)},0x1000)
|
||||
except UnsupportedInstruction:pass
|
||||
else:raise AssertionError('Invalid vector facts/access accepted')
|
||||
includes='\n'.join(f'#include "{name}.h"\nextern uint8_t *native_{name}(uint8_t *,uint64_t);' for name in fixtures)
|
||||
checks=[]
|
||||
for name in fixtures:
|
||||
checks.append(f'''
|
||||
{{ w2c_{name} module;wasm2c_{name}_instantiate(&module);
|
||||
uint8_t *memory=w2c_{name}_memory(&module)->data;
|
||||
for(unsigned i=0;i<512;++i) {{
|
||||
uint8_t expected[4096];memset(expected,i&255,sizeof(expected));
|
||||
memset(memory,0xa5,65536);memset(memory+16384,i&255,sizeof(expected));
|
||||
uint64_t registers[32]={{0}};registers[0]=16384+1024;registers[1]=i%2;
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
uint8_t *result=native_{name}(expected+1024,i%2);
|
||||
assert(w2c_{name}_run(&module,0,1000)==0);
|
||||
uint64_t returned;memcpy(&returned,memory,8);
|
||||
assert(returned==16384+(uint64_t)(result-expected));
|
||||
assert(memcmp(memory+16384,expected,sizeof(expected))==0);
|
||||
for(unsigned j=256;j<65536;++j) {{if(j>=16384 && j<20480)continue;assert(memory[j]==0xa5);}}
|
||||
}}
|
||||
wasm2c_{name}_free(&module); }}''')
|
||||
(OUT/'verify.c').write_text(f'''
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
{includes}
|
||||
int main(void) {{wasm_rt_init();{''.join(checks)}wasm_rt_free();
|
||||
puts("PASS: 4096 native zero-vector store comparisons, pairs, writeback, negative offsets, branches, merges, loops and 4 rejection cases");}}
|
||||
''')
|
||||
runtime=ROOT/'third_party/wabt/wasm2c'
|
||||
run('xcrun','clang','-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1',
|
||||
'-I'+str(OUT),'-I'+str(runtime),*[OUT/f'{name}.c' for name in fixtures],OUT/'verify.c',OUT/'oracle.S',
|
||||
runtime/'wasm-rt-impl.c',runtime/'wasm-rt-mem-impl.c','-o',OUT/'verify')
|
||||
run(OUT/'verify')
|
||||
(OUT/'verification.json').write_text(json.dumps({'status':'pass','native_comparisons':4096,
|
||||
'fixtures':list(fixtures),'invalid_cases':4,'scope':'hand-authored zero-vector fixtures'},indent=2)+'\n')
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Check refusal of changed game code/constants without writing modified binaries."""
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("lift_rng", Path(__file__).parents[1] / "tools/lift_rng.py")
|
||||
lift = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lift)
|
||||
source = Path(sys.argv[1])
|
||||
original = source.read_bytes()
|
||||
image, slice_offset = lift.arm64_image(original)
|
||||
ranges = [("function", lift.FUNCTION_VA, lift.BODY_SIZE),
|
||||
("seed constructor", lift.SEED_VA, lift.SEED_SIZE),
|
||||
("constants", lift.CONSTANT_VA, 32)]
|
||||
for label, address, length in ranges:
|
||||
changed = bytearray(original)
|
||||
changed[slice_offset + lift.file_range(image, address, length)] ^= 1
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
out = Path(folder) / "generated"
|
||||
with patch.object(Path, "read_bytes", return_value=bytes(changed)):
|
||||
try:
|
||||
lift.generate(source, out)
|
||||
except ValueError:
|
||||
assert not out.exists(), "Rejected input produced output artifacts"
|
||||
print(f"PASS: changed {label} rejected before output")
|
||||
else:
|
||||
raise AssertionError(f"Changed {label} accepted")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Exercise native ZIP/zlib header reading, including malformed archives."""
|
||||
import io
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
HEADER = struct.pack('<4HB', 2, 0, 77, 0, 0)
|
||||
|
||||
|
||||
def archive(entries, method=zipfile.ZIP_DEFLATED, stream=False):
|
||||
class NonSeeking(io.BytesIO):
|
||||
def seekable(self): return False
|
||||
def seek(self, *args): raise OSError('streaming archive')
|
||||
buffer = NonSeeking() if stream else io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w', compression=method) as output:
|
||||
for name, contents in entries:
|
||||
output.writestr(name, contents)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def probe(contents, status, label):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'save.zip'
|
||||
path.write_bytes(contents)
|
||||
result = subprocess.run([str(ROOT / 'build/version/save_probe'), str(path)], capture_output=True, text=True)
|
||||
actual = json.loads(result.stdout)
|
||||
assert actual['status'] == status, (label, actual, status, result.stderr)
|
||||
assert result.returncode == (1 if status else 0), (label, result.returncode)
|
||||
if not status:
|
||||
assert [actual[k] for k in ('major', 'minor', 'patch', 'build', 'flag')] == [2, 0, 77, 0, 0]
|
||||
print('PASS:', label)
|
||||
|
||||
|
||||
payload = zlib.compress(HEADER + bytes(10000))
|
||||
for method in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
|
||||
probe(archive([('world/level.dat0', payload)], method), 0, f'outer method {method}, nested zlib')
|
||||
probe(archive([('world/level.dat0', payload)], stream=True), 0, 'ZIP data descriptor')
|
||||
probe(archive([('world/level.dat', payload)]), 0, 'legacy level.dat entry name')
|
||||
probe(archive([('world/level.dat', b'invalid'), ('world/level.dat0', payload)]), 0, 'level.dat0 preference')
|
||||
probe(b'not a zip', 2, 'invalid ZIP')
|
||||
probe(archive([('description.json', b'{}')]), 3, 'no level entry')
|
||||
probe(archive([('a/level.dat0', payload), ('b/level.dat0', payload)]), 2, 'ambiguous roots')
|
||||
probe(archive([('world/level.dat0', b'not zlib')]), 5, 'bad inner compression')
|
||||
probe(archive([('world/level.dat0', zlib.compress(HEADER[:8]))]), 6, 'short decompressed header')
|
||||
valid = archive([('world/level.dat0', payload)])
|
||||
probe(valid[:-1], 2, 'truncated end record')
|
||||
changed = bytearray(valid)
|
||||
central = changed.index(b'PK\x01\x02')
|
||||
struct.pack_into('<H', changed, 6, 1)
|
||||
struct.pack_into('<H', changed, central + 8, 1)
|
||||
probe(changed, 4, 'encrypted flag refused')
|
||||
changed = bytearray(valid)
|
||||
changed[30] ^= 1
|
||||
probe(changed, 2, 'local/central name mismatch')
|
||||
changed = bytearray(valid)
|
||||
struct.pack_into('<I', changed, central + 42, 0xfffffffe)
|
||||
probe(changed, 2, 'out-of-file local offset')
|
||||
changed = bytearray(valid)
|
||||
end = changed.rindex(b'PK\x05\x06')
|
||||
struct.pack_into('<H', changed, end + 10, 0xffff)
|
||||
struct.pack_into('<H', changed, end + 8, 0xffff)
|
||||
probe(changed, 4, 'ZIP64 sentinel refused')
|
||||
print('PASS: 15 archive cases; only the bounded version prefix is validated')
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Verify the bounded 2.0.77 scenario envelope, offsets and unsupported inputs."""
|
||||
import io
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
PROBE=ROOT/'build/save-layout/probe'
|
||||
TICKS=(720001,42,0xffffffffffffffff)
|
||||
|
||||
|
||||
def compact(value,size=4):
|
||||
return bytes([value]) if value<255 else b'\xff'+value.to_bytes(size,'little')
|
||||
|
||||
|
||||
def string(value): return compact(len(value))+value
|
||||
def version(values=(2,0,77)): return b''.join(compact(v,2) for v in values)
|
||||
def localised(text=None,children=(),kind=1):
|
||||
return b'\0' if text is None else bytes([kind])+string(text)+bytes([len(children)])+b''.join(children)
|
||||
def ending(title=None,children=()):
|
||||
return localised(title,children)+localised()+compact(0)+localised()+string(b'')
|
||||
|
||||
|
||||
def fixture(long_string=b'freeplay',mod_count=1,extended_version=False,nested=False):
|
||||
result=bytearray(struct.pack('<4HB',2,0,77,0,0))
|
||||
positions={}
|
||||
result+=string(b'')+string(long_string)+string(b'base')
|
||||
positions['difficulty']=len(result);result+=b'\1'
|
||||
positions['boolean']=len(result);result+=b'\0\0'+string(b'')+b'\0\0\0\1'
|
||||
result+=version((65535,255,254) if extended_version else (2,0,77))
|
||||
result+=struct.pack('<I',1234)+b'\1'+struct.pack('<I',0xa00000)
|
||||
positions['mod_count']=len(result);result+=compact(mod_count)
|
||||
for i in range(mod_count):
|
||||
result+=string(b'base')+version((255,256,65535) if extended_version else (2,0,77))+struct.pack('<I',i)
|
||||
result+=struct.pack('<I',0)
|
||||
positions['property_type']=len(result);result+=b'\5\0'
|
||||
positions['property_count']=len(result);result+=struct.pack('<I',0)
|
||||
positions['ending']=len(result)
|
||||
children=(localised(b'child',(localised(),)),) if nested else ()
|
||||
result+=ending(b'victory',children)+ending(b'defeat')+b'\0'
|
||||
offset=len(result)
|
||||
result+=struct.pack('<3Q',*TICKS)
|
||||
return bytes(result),offset,positions
|
||||
|
||||
|
||||
checks=0
|
||||
def probe(data,status=0,mode='bytes',offset=None,mods=1):
|
||||
global checks
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
p=Path(directory)/'fixture';p.write_bytes(data)
|
||||
result=subprocess.run([str(PROBE),mode,str(p)],capture_output=True,text=True)
|
||||
assert result.returncode==(1 if status else 0),(result.returncode,result.stderr)
|
||||
report=json.loads(result.stdout)
|
||||
assert report['status']==status,report
|
||||
if not status:
|
||||
assert report['header_offset']==offset,report
|
||||
assert report['next_offset']==offset+24,report
|
||||
assert report['mod_count']==mods,report
|
||||
assert report['tick_fields_hex']==[f'{v:016x}' for v in TICKS],report
|
||||
elif status==6 and mode=='bytes':
|
||||
assert report['needed']>len(data),report
|
||||
checks+=1
|
||||
|
||||
|
||||
for length in (0,1,254,255,256,4097):
|
||||
data,offset,_=fixture(long_string=b'x'*length,extended_version=True,nested=True)
|
||||
probe(data,offset=offset)
|
||||
for count in (0,1,254,255):
|
||||
data,offset,_=fixture(mod_count=count)
|
||||
probe(data,offset=offset,mods=count)
|
||||
data,offset,positions=fixture()
|
||||
for length in range(len(data)):
|
||||
probe(data[:length],6)
|
||||
for index,value in [(4,78),(8,1),(positions['difficulty'],3),(positions['boolean'],2),
|
||||
(positions['property_type'],4),(positions['property_count'],1)]:
|
||||
changed=bytearray(data);changed[index]=value;probe(changed,4)
|
||||
probe(data[:9]+b'\xff'+struct.pack('<I',1024*1024+1),7)
|
||||
probe(data[:positions['mod_count']]+b'\xff'+struct.pack('<I',4097),7)
|
||||
deep=localised()
|
||||
for i in range(65): deep=localised(b'',(deep,))
|
||||
probe(data[:positions['ending']]+deep+data[positions['ending']:],7)
|
||||
|
||||
for method in (zipfile.ZIP_STORED,zipfile.ZIP_DEFLATED):
|
||||
output=io.BytesIO()
|
||||
with zipfile.ZipFile(output,'w',compression=method) as z:
|
||||
z.writestr('fixture/level.dat0',zlib.compress(data))
|
||||
probe(output.getvalue(),mode='zip',offset=offset)
|
||||
print(f'PASS: {checks} scenario-envelope cases, variable offsets, sentinel lengths, truncation, limits and failure atomicity')
|
||||
(ROOT/'build/save-layout/verification.json').write_text(json.dumps({'status':'pass','cases':checks,
|
||||
'scope':'2.0.77 envelope with empty startup settings; not world loading'},indent=2)+'\n')
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Exercise arbitrary bounded prefix lengths, outer/nested streaming and guards."""
|
||||
import io
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'build/save-prefix'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
runtime = ROOT / 'third_party/wabt/wasm2c'
|
||||
subprocess.run(['xcrun', 'clang', '-O2', '-DWASM_RT_USE_MMAP=0', '-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1',
|
||||
'-I'+str(ROOT/'runtime'), '-I'+str(ROOT/'build/version'), '-I'+str(runtime),
|
||||
str(ROOT/'tests/save_prefix_probe.c'), str(ROOT/'runtime/save_header.c'),
|
||||
str(ROOT/'runtime/version_bridge.c'), str(ROOT/'build/version/factorio_version.c'),
|
||||
str(runtime/'wasm-rt-impl.c'), str(runtime/'wasm-rt-mem-impl.c'), '-lz', '-o', str(OUT/'probe')], check=True)
|
||||
|
||||
|
||||
def archive(payload, method, streaming=False):
|
||||
class NonSeeking(io.BytesIO):
|
||||
def seek(self, *args): raise OSError('streaming')
|
||||
output = NonSeeking() if streaming else io.BytesIO()
|
||||
with zipfile.ZipFile(output, 'w', compression=method) as z:
|
||||
z.writestr('fixture/level.dat0', zlib.compress(payload))
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
checks = 0
|
||||
def probe(data, length, expected, status=0):
|
||||
global checks
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'fixture.zip'
|
||||
path.write_bytes(data)
|
||||
result = subprocess.run([str(OUT/'probe'),str(path),str(length)],capture_output=True,text=True)
|
||||
assert result.returncode == (1 if status else 0), result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report['status'] == status, report
|
||||
assert bytes.fromhex(report['prefix_hex']) == expected
|
||||
checks += 1
|
||||
|
||||
|
||||
payload = struct.pack('<4HB',2,0,77,0,0) + bytes(range(256))*300
|
||||
for method in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
|
||||
for streaming in (False, True):
|
||||
data = archive(payload,method,streaming)
|
||||
for length in (1,8,9,10,33,4095,4096,4097,65536,len(payload)):
|
||||
probe(data,length,payload[:length])
|
||||
probe(data,len(payload)+1,b'',6)
|
||||
probe(data,0,b'',7)
|
||||
probe(data,1024*1024+1,b'',7)
|
||||
probe(b'invalid',33,b'',2)
|
||||
incompressible = struct.pack('<4HB',2,0,77,0,0)
|
||||
incompressible += b''.join(hashlib.sha256(i.to_bytes(4,'little')).digest() for i in range(2048))
|
||||
for method in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
|
||||
for streaming in (False,True):
|
||||
data=archive(incompressible,method,streaming)
|
||||
for length in (4097,len(incompressible)):
|
||||
probe(data,length,incompressible[:length])
|
||||
over_budget=b''.join(hashlib.sha256(i.to_bytes(4,'little')).digest() for i in range(32768))
|
||||
probe(archive(over_budget,zipfile.ZIP_STORED),1024*1024,b'',7)
|
||||
print(f'PASS: {checks} bounded prefix cases, exact bytes, output guards and failure atomicity')
|
||||
(OUT/'verification.json').write_text(json.dumps({'status':'pass','cases':checks,
|
||||
'scope':'synthetic archives; bounded prefix only'},indent=2)+'\n')
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "deserialiser_bridge.h"
|
||||
#include "deserialiser_host.h"
|
||||
#include <assert.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void *factorio_deser_original(void *,void *,void *,unsigned);
|
||||
extern const uint8_t native_deser_vtable[];
|
||||
static _Alignas(16) uint8_t native_heap[65536],native_object[1224],native_external[144];
|
||||
static size_t native_cursor,native_limit;
|
||||
static unsigned native_allocations,native_reads,native_zero_calls,native_version_errors;
|
||||
static jmp_buf abort_target;
|
||||
typedef struct {const uint8_t *data;size_t length,cursor;} NativeReader;
|
||||
static NativeReader native_reader;
|
||||
void *native_deser_allocate(size_t size) {
|
||||
size_t address=(native_cursor+15)&~(size_t)15;
|
||||
if(address>native_limit || size>native_limit-address) longjmp(abort_target,5);
|
||||
native_cursor=address+size;++native_allocations;return native_heap+address;
|
||||
}
|
||||
static uint64_t get(const uint8_t *data,size_t at) {uint64_t x;memcpy(&x,data+at,8);return x;}
|
||||
static void put(uint8_t *data,size_t at,uint64_t x) {memcpy(data+at,&x,8);}
|
||||
void native_deser_read_or_throw(const uint8_t *source,void *destination,size_t size) {
|
||||
NativeReader *reader=(NativeReader *)(uintptr_t)get(source,8);
|
||||
assert(reader==&native_reader);
|
||||
if(reader->cursor>reader->length || size>reader->length-reader->cursor) longjmp(abort_target,1);
|
||||
memcpy(destination,reader->data+reader->cursor,size);reader->cursor+=size;++native_reads;
|
||||
}
|
||||
void native_deser_zero_memory(void *destination,size_t size) {memset(destination,0,size);++native_zero_calls;}
|
||||
void native_deser_version_error(void) {++native_version_errors;longjmp(abort_target,7);}
|
||||
void native_deser_unsupported(void) {abort();}
|
||||
void native_deser_unsupported_delete(void) {abort();}
|
||||
void native_deser_unsupported_length(void) {abort();}
|
||||
void native_deser_unsupported_array_length(void) {abort();}
|
||||
|
||||
static uint64_t canonical(uint64_t value,uint64_t heap,uint64_t object) {
|
||||
if(value>=(uintptr_t)native_heap && value<=(uintptr_t)native_heap+native_cursor)
|
||||
return heap+value-(uintptr_t)native_heap;
|
||||
if(value>=(uintptr_t)native_object && value<=(uintptr_t)native_object+sizeof(native_object))
|
||||
return object+value-(uintptr_t)native_object;
|
||||
if(value>=(uintptr_t)native_external && value<=(uintptr_t)native_external+sizeof(native_external))
|
||||
return 8192+value-(uintptr_t)native_external;
|
||||
if(value==(uintptr_t)&native_reader) return 512;
|
||||
if(value==(uintptr_t)native_deser_vtable+16) return 65536+0xda8;
|
||||
return value;
|
||||
}
|
||||
static void normalize(uint8_t *data,size_t length,uint64_t heap,uint64_t object) {
|
||||
for(size_t i=0;i+8<=length;i+=8) put(data,i,canonical(get(data,i),heap,object));
|
||||
}
|
||||
static uint8_t payload[9];
|
||||
static void version(unsigned major,unsigned minor,unsigned patch,unsigned build,unsigned flag) {
|
||||
const uint16_t fields[]={(uint16_t)major,(uint16_t)minor,(uint16_t)patch,(uint16_t)build};
|
||||
memcpy(payload,fields,8);payload[8]=(uint8_t)flag;
|
||||
}
|
||||
|
||||
static void compare(w2c_factorio__deser *module,struct w2c_deser__host *env,unsigned iteration,
|
||||
size_t length,size_t limit,int external,unsigned owns,int expected) {
|
||||
uint8_t *memory=w2c_factorio__deser_memory(module)->data;
|
||||
uint64_t begin=131072+(iteration%32)*16,object=4096+(iteration%8)*256;
|
||||
memset(memory,0xa5,1048576);memset(native_heap,0xa5,sizeof(native_heap));
|
||||
memset(native_object,0xa5,sizeof(native_object));memset(native_external,0xa5,sizeof(native_external));
|
||||
native_cursor=0;native_allocations=0;native_reads=0;native_zero_calls=0;native_version_errors=0;
|
||||
native_limit=limit;native_reader=(NativeReader){payload,length,0};
|
||||
int native_status=setjmp(abort_target);
|
||||
if(!native_status) assert(factorio_deser_original(native_object,&native_reader,external?native_external:NULL,owns)==native_object);
|
||||
assert(native_status==expected);
|
||||
*env=(struct w2c_deser__host){.module=module};
|
||||
assert(factorio_guest_heap_init(&env->heap,memory,1048576,begin,begin+limit)==0);
|
||||
uint64_t registers[32]={0};registers[0]=object;registers[1]=512;
|
||||
registers[2]=external?8192:0;registers[3]=owns;registers[31]=1048576-256;
|
||||
for(unsigned i=19;i<30;++i) registers[i]=UINT64_C(0x123456789abc0000)+iteration+i;
|
||||
uint64_t stream[]={16384,length,0};
|
||||
memcpy(memory,registers,sizeof(registers));memcpy(memory+512,stream,sizeof(stream));
|
||||
memcpy(memory+16384,payload,length);
|
||||
unsigned status=w2c_factorio__deser_run(module,0,10000);
|
||||
assert(status==(unsigned)native_status);
|
||||
assert(env->heap.allocations==native_allocations && env->heap.cursor-begin==native_cursor);
|
||||
assert(env->reads==native_reads && env->zero_calls==native_zero_calls && env->version_errors==native_version_errors);
|
||||
assert(get(memory,528)==native_reader.cursor && !env->unsupported_calls);
|
||||
if(!status) {
|
||||
normalize(native_object,sizeof(native_object),begin,object);
|
||||
normalize(native_heap,native_cursor,begin,object);
|
||||
assert(memcmp(memory+object,native_object,sizeof(native_object))==0);
|
||||
assert(memcmp(memory+begin,native_heap,native_cursor)==0);
|
||||
assert(get(memory,0)==object);
|
||||
for(unsigned i=19;i<32;++i) assert(get(memory,i*8)==registers[i]);
|
||||
}
|
||||
for(size_t i=256;i<1048576;++i) {
|
||||
if(i>=object && i<object+sizeof(native_object)) continue;
|
||||
if(i>=begin && i<begin+native_cursor) continue;
|
||||
if(i>=512 && i<536) continue;
|
||||
if(i>=16384 && i<16384+length) continue;
|
||||
if(i>=1048576-256-368 && i<1048576-256) continue;
|
||||
assert(memory[i]==0xa5);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
wasm_rt_init();w2c_factorio__deser module;
|
||||
struct w2c_deser__host env={.module=&module};wasm2c_factorio__deser_instantiate(&module,&env);
|
||||
for(unsigned i=0;i<512;++i) {
|
||||
if(i%2) version(2,0,i%78,i%78==77?0:i,0);
|
||||
else version(1,i%65500,i*13,i*29,0);
|
||||
compare(&module,&env,i,9,65536,i%3==0,i%2,0);
|
||||
}
|
||||
for(unsigned length=0;length<9;++length) {
|
||||
version(2,0,77,0,0);compare(&module,&env,length,length,65536,0,0,1);
|
||||
}
|
||||
const unsigned invalid[][5]={{0,0,0,0,0},{2,0,77,1,0},{2,0,78,0,0},{2,1,0,0,0},
|
||||
{3,0,0,0,0},{2,0,77,0,1},{1,2,0,0,0},{1,2,0,27,0}};
|
||||
for(unsigned i=0;i<sizeof(invalid)/sizeof(invalid[0]);++i) {
|
||||
version(invalid[i][0],invalid[i][1],invalid[i][2],invalid[i][3],invalid[i][4]);
|
||||
compare(&module,&env,i,9,65536,0,0,7);
|
||||
}
|
||||
version(1,2,0,28,0);compare(&module,&env,0,9,65536,0,0,0);
|
||||
version(2,0,77,0,0);compare(&module,&env,0,9,65536,0,0,0);
|
||||
uint64_t required=env.heap.cursor-env.heap.begin;
|
||||
/* Find all allocation boundaries from the original constructor's trace. */
|
||||
const size_t sizes[]={48,8,56,8192,24,48,56,144,24,2048,8,8};
|
||||
size_t cursor=0;
|
||||
for(unsigned i=0;i<sizeof(sizes)/sizeof(sizes[0]);++i) {
|
||||
cursor=(cursor+15)&~(size_t)15;
|
||||
compare(&module,&env,i,9,cursor+sizes[i]-1,0,0,5);
|
||||
cursor+=sizes[i];
|
||||
}
|
||||
assert(cursor==required);
|
||||
compare(&module,&env,0,9,required,0,0,0);
|
||||
FactorioDeserialiserProbe report;
|
||||
assert(factorio_deserialiser_probe(payload,9,&report)==0);
|
||||
assert(report.version.major==2 && report.version.patch==77 && report.allocations==12 &&
|
||||
report.reads==5 && report.stream_cursor==9 && report.target_capacity==64 && report.owns_target==1 && report.heap_bytes==required);
|
||||
FactorioDeserialiserProbe untouched;memset(&untouched,0xa5,sizeof(untouched));
|
||||
for(unsigned n=0;n<9;++n) {
|
||||
report=untouched;assert(factorio_deserialiser_probe(payload,n,&report)==1);
|
||||
assert(memcmp(&report,&untouched,sizeof(report))==0);
|
||||
}
|
||||
version(2,0,78,0,0);report=untouched;
|
||||
assert(factorio_deserialiser_probe(payload,9,&report)==7 && memcmp(&report,&untouched,sizeof(report))==0);
|
||||
assert(factorio_deserialiser_probe(NULL,9,&report)==2);
|
||||
assert(factorio_deserialiser_probe(payload,9,NULL)==2);
|
||||
wasm2c_factorio__deser_free(&module);wasm_rt_free();
|
||||
printf("PASS: 512 original MapDeserialiser graphs, supplied/owned targets, 9 truncations, 8 version errors, 12 allocation failures; heap %llu bytes\n",(unsigned long long)required);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "map_header_bridge.h"
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *bytes;
|
||||
size_t length, cursor;
|
||||
uint64_t version[2];
|
||||
unsigned reads;
|
||||
size_t sizes[3];
|
||||
} NativeReader;
|
||||
_Static_assert(offsetof(NativeReader, version) == 24, "MapDeserialiser version offset");
|
||||
extern void *factorio_map_header_original(void *, NativeReader *);
|
||||
void native_map_header_read(NativeReader *reader, void *out, size_t length) {
|
||||
assert(reader->reads < 3 && (length == 4 || length == 8));
|
||||
assert(reader->cursor + length <= reader->length);
|
||||
memcpy(out, reader->bytes + reader->cursor, length);
|
||||
reader->cursor += length;
|
||||
reader->sizes[reader->reads++] = length;
|
||||
}
|
||||
static uint64_t random_value(uint64_t *state) {
|
||||
*state ^= *state << 13; *state ^= *state >> 7; *state ^= *state << 17; return *state;
|
||||
}
|
||||
int main(void) {
|
||||
uint64_t seed = 0x123456789abcdef;
|
||||
const uint64_t cutoff = UINT64_C(0x1000200000175);
|
||||
for (unsigned test = 0; test < 20000; ++test) {
|
||||
uint64_t packed = test < 4 ? cutoff + test - 1 : random_value(&seed);
|
||||
if (test % 4 == 0) packed &= UINT64_C(0xffffffffffff);
|
||||
uint64_t payload[3] = {random_value(&seed), random_value(&seed), random_value(&seed)};
|
||||
if (test < 8) memset(payload, test & 1 ? 0xff : 0, sizeof(payload));
|
||||
uint64_t original_payload[3]; memcpy(original_payload, payload, sizeof(payload));
|
||||
uint8_t flag = (uint8_t)test;
|
||||
NativeReader reader = {.bytes=(const uint8_t *)payload, .length=sizeof(payload), .version={packed,flag}};
|
||||
uint8_t native[48]; memset(native, 0xa5, sizeof(native));
|
||||
assert(factorio_map_header_original(native, &reader) == native);
|
||||
assert(reader.reads == 3);
|
||||
size_t field_size = packed > cutoff ? 8 : 4;
|
||||
assert(reader.cursor == 3 * field_size);
|
||||
for (unsigned i=0;i<3;++i) assert(reader.sizes[i] == field_size);
|
||||
for (unsigned i=40;i<48;++i) assert(native[i] == 0xa5);
|
||||
FactorioMapVersion version = {.major=packed>>48, .minor=packed>>32,
|
||||
.patch=packed>>16, .build=packed, .flag=flag};
|
||||
struct { uint64_t before; FactorioMapHeader header; uint64_t after; } output;
|
||||
memset(&output, 0xa5, sizeof(output));
|
||||
assert(factorio_map_header_parse((const uint8_t *)payload,sizeof(payload),version,&output.header)==0);
|
||||
assert(output.before == UINT64_C(0xa5a5a5a5a5a5a5a5) && output.after == output.before);
|
||||
assert(memcmp(output.header.tick_fields,native+16,24)==0);
|
||||
assert(output.header.version.major==version.major && output.header.version.flag==flag);
|
||||
assert(memcmp(payload,original_payload,sizeof(payload))==0);
|
||||
if (test < 8) for (size_t length=0;length<3*field_size;++length) {
|
||||
FactorioMapHeader before; memset(&before,0xa5,sizeof(before));
|
||||
FactorioMapHeader result = before;
|
||||
assert(factorio_map_header_parse((const uint8_t *)payload,length,version,&result)==1);
|
||||
assert(memcmp(&before,&result,sizeof(result))==0);
|
||||
}
|
||||
}
|
||||
puts("PASS: 20000 original ARM64 MapHeader comparisons, version branches, legacy sentinels, guards and short-stream failures");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Decodes every PNG under a directory with pngdecode.c and with macOS ImageIO (which yields
|
||||
// exact non-premultiplied samples on macOS) and requires byte-identical results.
|
||||
// Usage: png_parity DATA_DIR
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#include "pngdecode.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@autoreleasepool {
|
||||
NSString *root = @(argv[1]);
|
||||
long checked = 0, fallback = 0, mismatched = 0, skipped = 0;
|
||||
double tw = 0, ti = 0; long px = 0;
|
||||
for (NSString *rel in [NSFileManager.defaultManager enumeratorAtPath:root]) {
|
||||
if (![rel hasSuffix:@".png"]) continue;
|
||||
NSData *data = [NSData dataWithContentsOfFile:[root stringByAppendingPathComponent:rel]];
|
||||
uint8_t *mine; int w, h, ch;
|
||||
double t0 = CACurrentMediaTime();
|
||||
int ok = fx_png_decode(data.bytes, data.length, &mine, &w, &h, &ch);
|
||||
double t1 = CACurrentMediaTime();
|
||||
if (!ok) { fallback++; continue; }
|
||||
CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
||||
CGImageRef img = CGImageSourceCreateImageAtIndex(src, 0, NULL);
|
||||
CFDataRef ref = CGDataProviderCopyData(CGImageGetDataProvider(img));
|
||||
double t2 = CACurrentMediaTime();
|
||||
tw += t1 - t0; ti += t2 - t1; px += (long)w * h;
|
||||
size_t bpp = CGImageGetBitsPerPixel(img) / 8, row = CGImageGetBytesPerRow(img);
|
||||
CGImageAlphaInfo a = CGImageGetAlphaInfo(img);
|
||||
if (CGImageGetBitsPerComponent(img) != 8 || (CGImageGetBitmapInfo(img) & kCGBitmapByteOrderMask) ||
|
||||
a == kCGImageAlphaPremultipliedLast || a == kCGImageAlphaPremultipliedFirst || a == kCGImageAlphaFirst ||
|
||||
a == kCGImageAlphaNoneSkipFirst) { skipped++; goto next; }
|
||||
// Compare the channels AppKit would expose (drop ImageIO's padding byte for RGBX).
|
||||
for (int y = 0; y < h && !mismatched; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
const uint8_t *r = CFDataGetBytePtr(ref) + y * row + x * bpp, *m = mine + ((size_t)y * w + x) * ch;
|
||||
if (memcmp(r, m, ch)) {
|
||||
NSLog(@"MISMATCH %@ at %d,%d ch=%d", rel, x, y, ch);
|
||||
mismatched++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
checked++;
|
||||
next:
|
||||
CFRelease(ref); CGImageRelease(img); CFRelease(src); free(mine);
|
||||
}
|
||||
NSLog(@"checked %ld identical-format images, %ld mismatched, %ld skipped (ImageIO layout), %ld fallback to ImageIO",
|
||||
checked, mismatched, skipped, fallback);
|
||||
NSLog(@"%.0f Mpx: wuffs %.2fs (%.0f Mpx/s), ImageIO %.2fs (%.0f Mpx/s)", px / 1e6, tw, px / 1e6 / tw, ti, px / 1e6 / ti);
|
||||
return mismatched ? 1 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "factorio_rng.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
extern uint32_t factorio_rng_original(uint32_t state[3]);
|
||||
extern void factorio_rng_seed_original(uint32_t state[3], uint32_t seed);
|
||||
|
||||
static uint32_t next_case(uint32_t *seed) {
|
||||
*seed ^= *seed << 13;
|
||||
*seed ^= *seed >> 17;
|
||||
*seed ^= *seed << 5;
|
||||
return *seed;
|
||||
}
|
||||
|
||||
// Returns zero only after comparing outputs AND mutated state with original
|
||||
// ARM64 instruction words. No generated code is mapped executable at runtime.
|
||||
int factorio_rng_verify(void) {
|
||||
const uint32_t edges[] = {0, 1, 2, 7, 8, 15, 16, 127, 128,
|
||||
0x7fffffff, 0x80000000, 0xffffffff};
|
||||
const uint32_t offsets[] = {0, 1, 15, 1024, 65524};
|
||||
uint32_t random = 0x375adc91;
|
||||
uint64_t calls = 0;
|
||||
w2c_factorio__rng instance;
|
||||
wasm_rt_init();
|
||||
wasm2c_factorio__rng_instantiate(&instance);
|
||||
wasm_rt_memory_t *memory = w2c_factorio__rng_memory(&instance);
|
||||
for (uint32_t example = 0; example < 6848; ++example) {
|
||||
uint32_t state[3];
|
||||
if (example < 1728) {
|
||||
state[0] = edges[example % 12];
|
||||
state[1] = edges[(example / 12) % 12];
|
||||
state[2] = edges[example / 144];
|
||||
} else if (example < 5824) {
|
||||
for (int i = 0; i < 3; ++i) state[i] = next_case(&random);
|
||||
}
|
||||
uint32_t offset = offsets[example % 5];
|
||||
memset(memory->data, 0xa5, 65536);
|
||||
if (example >= 5824) {
|
||||
uint32_t seed = example < 6336 ? example - 5824 : next_case(&random);
|
||||
factorio_rng_seed_original(state, seed);
|
||||
w2c_factorio__rng_seed(&instance, offset, seed);
|
||||
if (memcmp(memory->data + offset, state, 12)) {
|
||||
fprintf(stderr, "Constructor mismatch at seed %u\n", seed);
|
||||
wasm2c_factorio__rng_free(&instance);
|
||||
wasm_rt_free();
|
||||
return 3;
|
||||
}
|
||||
} else {
|
||||
memcpy(memory->data + offset, state, 12);
|
||||
}
|
||||
for (int step = 0; step < 128; ++step) {
|
||||
uint32_t expected = factorio_rng_original(state);
|
||||
uint32_t actual = w2c_factorio__rng_get_int(&instance, offset);
|
||||
++calls;
|
||||
if (actual != expected || memcmp(memory->data + offset, state, 12)) {
|
||||
fprintf(stderr, "Mismatch: case %u step %d, expected %08x actual %08x\n",
|
||||
example, step, expected, actual);
|
||||
wasm2c_factorio__rng_free(&instance);
|
||||
wasm_rt_free();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Catch writes outside the 12-byte state, including unaligned addresses.
|
||||
for (uint32_t i = 0; i < 65536; ++i) {
|
||||
if ((i < offset || i >= offset + 12) && memory->data[i] != 0xa5) {
|
||||
fprintf(stderr, "Out-of-state write: case %u offset %u\n", example, i);
|
||||
wasm2c_factorio__rng_free(&instance);
|
||||
wasm_rt_free();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
wasm2c_factorio__rng_free(&instance);
|
||||
wasm_rt_free();
|
||||
printf("PASS: %llu RNG calls and 1024 constructor calls, 6848 starting states, five memory alignments; outputs, state and guards match\n",
|
||||
(unsigned long long)calls);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef FACTORIO_RNG_NO_MAIN
|
||||
int main(void) { return factorio_rng_verify(); }
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "save_layout.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *bytes;
|
||||
size_t length,cursor;
|
||||
uint64_t version[2];
|
||||
unsigned reads;
|
||||
} NativeReader;
|
||||
extern void *factorio_map_header_original(void *,NativeReader *);
|
||||
void native_map_header_read(NativeReader *r,void *out,size_t size) {
|
||||
assert(size==8 && r->reads<3 && r->cursor+size<=r->length);
|
||||
memcpy(out,r->bytes+r->cursor,size);r->cursor+=size;++r->reads;
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=2) return 2;
|
||||
FactorioSaveLayout layout;
|
||||
assert(factorio_save_map_header(argv[1],&layout)==0);
|
||||
uint8_t *prefix=malloc(layout.next_offset);assert(prefix);
|
||||
assert(factorio_save_prefix(argv[1],prefix,layout.next_offset)==0);
|
||||
FactorioMapVersion v=layout.header.version;
|
||||
NativeReader reader={.bytes=prefix,.length=layout.next_offset,.cursor=layout.header_offset,
|
||||
.version={(uint64_t)v.major<<48|(uint64_t)v.minor<<32|(uint64_t)v.patch<<16|v.build,v.flag}};
|
||||
uint8_t native[48];memset(native,0xa5,sizeof(native));
|
||||
assert(factorio_map_header_original(native,&reader)==native);
|
||||
assert(reader.reads==3 && reader.cursor==layout.next_offset);
|
||||
assert(memcmp(native+16,layout.header.tick_fields,24)==0);
|
||||
for(unsigned i=40;i<48;++i) assert(native[i]==0xa5);
|
||||
free(prefix);
|
||||
printf("{\"status\":\"pass\",\"header_offset\":%zu,\"native_reads\":%u,"
|
||||
"\"constructor_values_match\":true,\"offset_authority\":\"recovered metadata layout; not full-engine execution\"}\n",
|
||||
layout.header_offset,reader.reads);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "save_layout.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=3) return 2;
|
||||
FactorioSaveLayout result;
|
||||
memset(&result,0xa5,sizeof(result));
|
||||
FactorioSaveLayout before=result;
|
||||
int status;
|
||||
size_t needed=0;
|
||||
if(!strcmp(argv[1],"zip")) status=factorio_save_map_header(argv[2],&result);
|
||||
else if(!strcmp(argv[1],"bytes")) {
|
||||
FILE *file=fopen(argv[2],"rb"); if(!file) return 2;
|
||||
uint8_t *bytes=malloc(1024*1024); if(!bytes) return 2;
|
||||
size_t count=fread(bytes,1,1024*1024,file); fclose(file);
|
||||
status=factorio_save_layout_bytes(bytes,count,&result,&needed); free(bytes);
|
||||
} else return 2;
|
||||
if(status && memcmp(&before,&result,sizeof(result))) return 3;
|
||||
printf("{\"status\":%d,\"needed\":%zu",status,needed);
|
||||
if(!status) printf(",\"header_offset\":%zu,\"next_offset\":%zu,\"mod_count\":%u,"
|
||||
"\"tick_fields_hex\":[\"%016" PRIx64 "\",\"%016" PRIx64 "\",\"%016" PRIx64 "\"]",
|
||||
result.header_offset,result.next_offset,result.mod_count,
|
||||
result.header.tick_fields[0],result.header.tick_fields[1],result.header.tick_fields[2]);
|
||||
puts("}");
|
||||
return status ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "save_header.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
char *end = NULL;
|
||||
unsigned long requested = strtoul(argv[2], &end, 10);
|
||||
if (!end || *end || requested > 1024 * 1024 + 1) return 2;
|
||||
uint8_t *guarded = malloc(requested + 16);
|
||||
if (!guarded) return 2;
|
||||
memset(guarded, 0xa5, requested + 16);
|
||||
int status = factorio_save_prefix(argv[1], guarded + 8, requested);
|
||||
for (unsigned i=0;i<8;++i) {
|
||||
if (guarded[i] != 0xa5 || guarded[requested+8+i] != 0xa5) return 3;
|
||||
}
|
||||
if (status) for (unsigned long i=0;i<requested;++i) if (guarded[i+8]!=0xa5) return 4;
|
||||
printf("{\"status\":%d,\"prefix_hex\":\"", status);
|
||||
if (!status) for (unsigned long i=0;i<requested;++i) printf("%02x",guarded[i+8]);
|
||||
puts("\"}");
|
||||
free(guarded);
|
||||
return status ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "save_header.h"
|
||||
#include <stdio.h>
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) return 64;
|
||||
FactorioMapVersion version;
|
||||
int status = factorio_save_header(argv[1], &version);
|
||||
if (!status) printf("{\"status\":0,\"major\":%u,\"minor\":%u,\"patch\":%u,\"build\":%u,\"flag\":%u}\n",
|
||||
version.major, version.minor, version.patch, version.build, version.flag);
|
||||
else printf("{\"status\":%d}\n", status);
|
||||
return status ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "target_bridge.h"
|
||||
#include "target_host.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void *factorio_target_original(void *);
|
||||
extern const uint8_t native_target_vtable[];
|
||||
static _Alignas(16) uint8_t native_heap[65536];
|
||||
static size_t native_cursor;
|
||||
static unsigned native_allocations;
|
||||
void *native_target_allocate(size_t size) {
|
||||
native_cursor=(native_cursor+15)&~(size_t)15;
|
||||
assert(size<=sizeof(native_heap)-native_cursor);
|
||||
void *result=native_heap+native_cursor;
|
||||
native_cursor+=size; ++native_allocations;
|
||||
return result;
|
||||
}
|
||||
void native_target_unsupported_delete(void *p) {(void)p;abort();}
|
||||
void native_target_unsupported_length(void *p) {(void)p;abort();}
|
||||
void native_target_unsupported_array_length(void *p) {(void)p;abort();}
|
||||
static uint64_t get(const uint8_t *data,size_t at) {uint64_t x;memcpy(&x,data+at,8);return x;}
|
||||
static void put(uint8_t *data,size_t at,uint64_t x) {memcpy(data+at,&x,8);}
|
||||
static uint64_t relocate(uint64_t pointer,uint64_t begin) {
|
||||
assert(pointer>=(uintptr_t)native_heap && pointer<=(uintptr_t)native_heap+sizeof(native_heap));
|
||||
return pointer-(uintptr_t)native_heap+begin;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
wasm_rt_init();
|
||||
w2c_factorio__target module;
|
||||
struct w2c_target__host env={.module=&module};
|
||||
wasm2c_factorio__target_instantiate(&module,&env);
|
||||
wasm_rt_memory_t *ram=w2c_factorio__target_memory(&module);
|
||||
assert(ram->size==1048576);
|
||||
uint8_t *memory=ram->data;
|
||||
for (unsigned iteration=0;iteration<512;++iteration) {
|
||||
uint64_t begin=131072+iteration*16,object=4096+(iteration%16)*256;
|
||||
memset(memory,0xa5,(size_t)ram->size);
|
||||
memset(native_heap,0xa5,sizeof(native_heap));
|
||||
uint8_t original[144];memset(original,0xa5,sizeof(original));
|
||||
native_cursor=0;native_allocations=0;
|
||||
assert(factorio_target_original(original)==original);
|
||||
assert(native_allocations==4 && native_cursor==2104);
|
||||
assert(factorio_guest_heap_init(&env.heap,memory,ram->size,begin,begin+65536)==0);
|
||||
uint64_t registers[32]={0};
|
||||
for(unsigned i=19;i<30;++i) registers[i]=0x1234567800000000ull+iteration+i;
|
||||
registers[0]=object;registers[31]=ram->size-256;
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
assert(w2c_factorio__target_run(&module,0,10000)==0);
|
||||
assert(env.heap.allocations==4 && env.heap.cursor-begin==native_cursor);
|
||||
assert(get(memory,0)==object);
|
||||
for(unsigned i=19;i<32;++i) assert(get(memory,i*8)==registers[i]);
|
||||
const unsigned fields[]={8,16,24,56,64,72};
|
||||
for(unsigned i=0;i<sizeof(fields)/sizeof(fields[0]);++i)
|
||||
put(original,fields[i],relocate(get(original,fields[i]),begin));
|
||||
assert(memcmp(memory+object,original,sizeof(original))==0);
|
||||
/* Normalize every native pointer in the allocated object graph. */
|
||||
for(unsigned i=0;i<3;++i) put(native_heap,i*8,relocate(get(native_heap,i*8),begin));
|
||||
put(native_heap,2080,relocate(get(native_heap,2080),begin));
|
||||
for(unsigned i=0;i<64;++i) {
|
||||
assert(get(native_heap,32+i*32)==(uintptr_t)native_target_vtable+16);
|
||||
put(native_heap,32+i*32,65536+0xda8);
|
||||
}
|
||||
assert(memcmp(memory+begin,native_heap,native_cursor)==0);
|
||||
for(size_t i=256;i<ram->size;++i) {
|
||||
if(i>=object && i<object+144) continue;
|
||||
if(i>=begin && i<begin+native_cursor) continue;
|
||||
if(i>=ram->size-368 && i<ram->size-256) continue;
|
||||
assert(memory[i]==0xa5);
|
||||
}
|
||||
}
|
||||
/* Each original allocation can fail without returning a forged pointer. */
|
||||
const uint64_t lengths[]={0,23,24,2079,2080,2087,2088,2103,2104};
|
||||
for(unsigned i=0;i<sizeof(lengths)/sizeof(lengths[0]);++i) {
|
||||
memset(memory,0xa5,(size_t)ram->size);
|
||||
assert(factorio_guest_heap_init(&env.heap,memory,ram->size,131072,131072+lengths[i])==0);
|
||||
uint64_t registers[32]={0};registers[0]=4096;registers[31]=ram->size-256;
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
assert(w2c_factorio__target_run(&module,0,10000)==(lengths[i]<2104 ? 5u:0u));
|
||||
for(uint64_t j=131072+lengths[i];j<196608;++j) assert(memory[j]==0xa5);
|
||||
}
|
||||
assert(w2c_factorio__target_run(&module,1048576-255,10000)==2);
|
||||
assert(w2c_factorio__target_run(&module,UINT32_MAX,10000)==2);
|
||||
uint64_t registers[32]={0};registers[0]=4096;registers[31]=ram->size-256;
|
||||
memcpy(memory,registers,sizeof(registers));
|
||||
assert(w2c_factorio__target_run(&module,0,0)==3);
|
||||
/* Heap input arithmetic is checked before any addition or narrowing. */
|
||||
FactorioGuestHeap heap;
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,131072,196608)==0);
|
||||
FactorioGuestHeap before=heap;uint64_t address=123;
|
||||
assert(factorio_guest_heap_allocate(&heap,UINT64_MAX,&address)==1);
|
||||
assert(address==123 && memcmp(&before,&heap,sizeof(heap))==0);
|
||||
assert(factorio_guest_heap_allocate(&heap,0,&address)==0 && address==131072);
|
||||
assert(factorio_guest_heap_allocate(&heap,0,&address)==0 && address==131088);
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,0,196608)==2);
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,131073,196608)==2);
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,131072,UINT64_MAX)==2);
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,131072,131071)==2);
|
||||
assert(factorio_guest_heap_init(&heap,memory,ram->size,131072,196608)==0);
|
||||
heap.cursor=heap.end+1;address=123;
|
||||
assert(factorio_guest_heap_allocate(&heap,8,&address)==2 && address==123);
|
||||
heap.cursor=131072;heap.allocations=UINT32_MAX;
|
||||
assert(factorio_guest_heap_allocate(&heap,8,&address)==2 && address==123);
|
||||
FactorioTargetProbe report={0};
|
||||
assert(factorio_target_probe(&report)==0);
|
||||
assert(report.allocations==4 && report.initialized_targets==64 && report.heap_bytes==2104);
|
||||
assert(factorio_target_probe(NULL)==2);
|
||||
wasm2c_factorio__target_free(&module);wasm_rt_free();
|
||||
puts("PASS: 512 original ARM64 object graphs, 64 targets each, relocated pointers, heap exhaustion, output/register/memory guards");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "version_bridge.h"
|
||||
#include "version_host.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { uint8_t bytes[9]; size_t cursor; unsigned reads; } NativeReader;
|
||||
extern void *factorio_version_original(void *out, NativeReader *reader);
|
||||
void native_version_read(NativeReader *reader, void *out, size_t length) {
|
||||
assert(length == (reader->reads == 4 ? 1 : 2));
|
||||
assert(reader->cursor + length <= 9);
|
||||
memcpy(out, reader->bytes + reader->cursor, length);
|
||||
reader->cursor += length;
|
||||
++reader->reads;
|
||||
}
|
||||
static uint32_t random_word(uint32_t *state) {
|
||||
*state ^= *state << 13; *state ^= *state >> 17; *state ^= *state << 5;
|
||||
return *state;
|
||||
}
|
||||
int main(void) {
|
||||
wasm_rt_init();
|
||||
w2c_factorio__version module;
|
||||
struct w2c_env env = {.module = &module};
|
||||
wasm2c_factorio__version_instantiate(&module, &env);
|
||||
uint8_t *memory = w2c_factorio__version_memory(&module)->data;
|
||||
uint32_t seed = 0xc46b21f3;
|
||||
for (unsigned example = 0; example < 20000; ++example) {
|
||||
NativeReader reader = {0};
|
||||
for (int i = 0; i < 9; ++i) reader.bytes[i] = (uint8_t)random_word(&seed);
|
||||
if (example < 2) memset(reader.bytes, example ? 0xff : 0, 9);
|
||||
uint8_t expected[16];
|
||||
memset(expected, 0xa5, sizeof(expected));
|
||||
assert(factorio_version_original(expected, &reader) == expected);
|
||||
assert(reader.cursor == 9 && reader.reads == 5);
|
||||
memset(memory, 0xa5, 65536);
|
||||
uint64_t registers[32];
|
||||
for (int i = 0; i < 32; ++i) registers[i] = random_word(&seed);
|
||||
registers[0] = 2048; registers[1] = 512; registers[31] = 65280;
|
||||
uint64_t stream[] = {4096, 9, 0};
|
||||
memcpy(memory, registers, sizeof(registers));
|
||||
memcpy(memory + 512, stream, sizeof(stream));
|
||||
memcpy(memory + 4096, reader.bytes, 9);
|
||||
env.reads = 0;
|
||||
assert(w2c_factorio__version_run(&module, 0) == 0);
|
||||
assert(env.reads == 5);
|
||||
assert(memcmp(memory + 2048, expected, sizeof(expected)) == 0);
|
||||
uint64_t after[32]; memcpy(after, memory, sizeof(after));
|
||||
assert(after[0] == 2048 && after[31] == registers[31]);
|
||||
for (int i = 19; i <= 30; ++i) assert(after[i] == registers[i]);
|
||||
for (int i = 256; i < 65536; ++i) {
|
||||
if ((i >= 512 && i < 536) || (i >= 2048 && i < 2057) ||
|
||||
(i >= 4096 && i < 4105) || (i >= 65200 && i < 65280)) continue;
|
||||
assert(memory[i] == 0xa5);
|
||||
}
|
||||
}
|
||||
uint8_t sample[9] = {2, 0, 0, 0, 77, 0, 0, 0, 0};
|
||||
FactorioMapVersion output;
|
||||
assert(factorio_version_parse(sample, sizeof(sample), &output) == 0);
|
||||
assert(output.major == 2 && output.minor == 0 && output.patch == 77 && output.build == 0 && output.flag == 0);
|
||||
for (size_t length = 0; length < 9; ++length) {
|
||||
memset(&output, 0xa5, sizeof(output));
|
||||
FactorioMapVersion before = output;
|
||||
assert(factorio_version_parse(sample, length, &output) == 1);
|
||||
assert(memcmp(&output, &before, sizeof(output)) == 0);
|
||||
}
|
||||
wasm2c_factorio__version_free(&module);
|
||||
wasm_rt_free();
|
||||
puts("PASS: 20000 original ARM64 constructor comparisons, five-read protocol, preserved registers, memory guards, and short-header errors");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
"""Bounded A64 integer-to-WAT compiler with validated immediate control flow.
|
||||
|
||||
Handles selected integer operations, NZCV arithmetic, branches, and declared
|
||||
host calls. It is not a whole-program lifter. Guest registers remain 64-bit;
|
||||
memory addresses are checked before narrowing to the 64 KiB prototype memory.
|
||||
Straight-line exports use run(ctx); CFG exports use run(ctx, instruction_budget).
|
||||
Budget exhaustion returns status 3 with partial register/memory effects; it is
|
||||
an abort, not a resumable checkpoint. Incoming NZCV is not part of this ABI.
|
||||
"""
|
||||
import struct
|
||||
|
||||
|
||||
class UnsupportedInstruction(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def signed(value, bits):
|
||||
return value - (1 << bits) if value & (1 << (bits - 1)) else value
|
||||
|
||||
|
||||
def const(value):
|
||||
return f"(i64.const {signed(value & ((1 << 64) - 1), 64)})"
|
||||
|
||||
|
||||
class Compiler:
|
||||
def __init__(self, hosts=None):
|
||||
self.hosts = hosts or {}
|
||||
self.instructions = []
|
||||
self.calls = []
|
||||
|
||||
def reg(self, index, width=64, sp=False):
|
||||
value = f"(local.get ${'sp' if index == 31 else 'x' + str(index)})" if index != 31 or sp else const(0)
|
||||
return value if width == 64 else f"(i64.and {value} {const(0xffffffff)})"
|
||||
|
||||
def put(self, index, value, width=64, sp=False):
|
||||
if index == 31 and not sp:
|
||||
return f"(drop {value})"
|
||||
if width == 32:
|
||||
value = f"(i64.and {value} {const(0xffffffff)})"
|
||||
return f"(local.set ${'sp' if index == 31 else 'x' + str(index)} {value})"
|
||||
|
||||
def address(self, base, offset):
|
||||
return f"(i64.add {self.reg(base, sp=True)} {const(offset)})"
|
||||
|
||||
def load(self, size, address):
|
||||
op = {1: 'i64.load8_u', 2: 'i64.load16_u', 4: 'i64.load32_u', 8: 'i64.load'}[size]
|
||||
return f"({op} (call $addr {address} {const(size)}))"
|
||||
|
||||
def store(self, size, address, value):
|
||||
op = {1: 'i64.store8', 2: 'i64.store16', 4: 'i64.store32', 8: 'i64.store'}[size]
|
||||
return f"({op} (call $addr {address} {const(size)}) {value})"
|
||||
|
||||
def arithmetic(self, rd, left, right, width, subtract, flags, sp=False):
|
||||
operation = 'sub' if subtract else 'add'
|
||||
mask = const((1 << width) - 1)
|
||||
result = [f'(local.set $a {left})', f'(local.set $b {right})',
|
||||
f'(local.set $result (i64.and (i64.{operation} (local.get $a) (local.get $b)) {mask}))']
|
||||
if flags:
|
||||
a, b, r = '(local.get $a)', '(local.get $b)', '(local.get $result)'
|
||||
overflow = f'(i64.and (i64.xor {a} {b}) (i64.xor {a} {r}))' if subtract else \
|
||||
f'(i64.and (i64.xor {a} {r}) (i64.xor {b} {r}))'
|
||||
result += [f'(local.set $n (i32.wrap_i64 (i64.shr_u {r} {const(width-1)})))',
|
||||
f'(local.set $z (i64.eqz {r}))',
|
||||
f'(local.set $c (i64.{"ge_u" if subtract else "lt_u"} {a if subtract else r} {b if subtract else a}))',
|
||||
f'(local.set $v (i32.wrap_i64 (i64.shr_u {overflow} {const(width-1)})))']
|
||||
result.append(self.put(rd, '(local.get $result)', width, sp=sp and not flags))
|
||||
return result
|
||||
|
||||
def condition(self, cond):
|
||||
n, z, c, v = ['(local.get $' + name + ')' for name in ('n', 'z', 'c', 'v')]
|
||||
conditions = [z, c, n, v, f'(i32.and {c} (i32.eqz {z}))',
|
||||
f'(i32.eq {n} {v})', f'(i32.and (i32.eq {n} {v}) (i32.eqz {z}))', '(i32.const 1)']
|
||||
result = conditions[cond >> 1]
|
||||
return f'(i32.eqz {result})' if cond & 1 and cond < 14 else result
|
||||
|
||||
def decode(self, word, pc):
|
||||
rd, rn = word & 31, (word >> 5) & 31
|
||||
width = 64 if word >> 31 else 32
|
||||
if word == 0xd65f03c0:
|
||||
return ["(br $done)"], 'ret'
|
||||
# Narrow 128-bit load/store copy idiom; compile() validates the adjacent
|
||||
# matching pair and forbids control-flow entry into its store half.
|
||||
if word & 0xffe00c00 == 0x3cc00000:
|
||||
address = self.address(rn, signed((word >> 12) & 511, 9))
|
||||
return [f'(local.set $base {address})',
|
||||
f'(local.set $qlo {self.load(8, "(local.get $base)")})',
|
||||
f'(local.set $qhi {self.load(8, "(i64.add (local.get $base) (i64.const 8))")})'], 'copy128-load'
|
||||
if word & 0xffc00000 == 0x3d800000:
|
||||
address = self.address(rn, ((word >> 10) & 4095) * 16)
|
||||
return [f'(local.set $base {address})',
|
||||
self.store(8, '(local.get $base)', '(local.get $qlo)'),
|
||||
self.store(8, '(i64.add (local.get $base) (i64.const 8))', '(local.get $qhi)')], 'copy128-store'
|
||||
# Conditional select family (CSEL/CSINC/CSINV/CSNEG), including CSET.
|
||||
if word & 0x3fe00800 == 0x1a800000:
|
||||
rm, cond = (word >> 16) & 31, (word >> 12) & 15
|
||||
alternative = self.reg(rm, width)
|
||||
invert, increment = bool(word & (1 << 30)), bool(word & (1 << 10))
|
||||
if invert:
|
||||
alternative = f'(i64.xor {alternative} {const((1 << width)-1)})'
|
||||
if increment:
|
||||
alternative = f'(i64.add {alternative} (i64.const 1))'
|
||||
return [self.put(rd, f'(select {self.reg(rn, width)} {alternative} {self.condition(cond)})', width)], 'select-flags'
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target = pc + signed(word & 0x3ffffff, 26) * 4
|
||||
if target not in self.hosts:
|
||||
raise UnsupportedInstruction(f"undeclared call to {target:#x}")
|
||||
host = self.hosts[target]
|
||||
self.calls.append({'site': hex(pc), 'target': hex(target), 'host': host})
|
||||
return [self.put(30, const(pc + 4)),
|
||||
f"(local.set $status (call ${host} {self.reg(0)} {self.reg(1)} {self.reg(2)}))",
|
||||
"(br_if $done (local.get $status))"], 'bl-host'
|
||||
# ADD/SUB immediate, including CMP/CMN aliases.
|
||||
if word & 0x1f000000 == 0x11000000:
|
||||
if word & (1 << 23):
|
||||
raise UnsupportedInstruction('reserved ADD/SUB immediate')
|
||||
immediate = ((word >> 10) & 4095) << (12 if word & (1 << 22) else 0)
|
||||
flags = bool(word & (1 << 29))
|
||||
return self.arithmetic(rd, self.reg(rn, width, sp=True), const(immediate), width,
|
||||
bool(word & (1 << 30)), flags, sp=True), 'arithmetic' + ('-flags' if flags else '')
|
||||
if word & 0x1f200000 == 0x0b000000:
|
||||
shift, amount = (word >> 22) & 3, (word >> 10) & 63
|
||||
if shift == 3 or amount >= width:
|
||||
raise UnsupportedInstruction('reserved arithmetic shift')
|
||||
source = self.reg((word >> 16) & 31, width)
|
||||
if shift == 2 and width == 32:
|
||||
source = f'(i64.extend_i32_u (i32.shr_s (i32.wrap_i64 {source}) (i32.const {amount})))'
|
||||
elif amount:
|
||||
source = f'(i64.{["shl", "shr_u", "shr_s"][shift]} {source} {const(amount)})'
|
||||
source = f'(i64.and {source} {const((1 << width)-1)})'
|
||||
flags = bool(word & (1 << 29))
|
||||
return self.arithmetic(rd, self.reg(rn, width), source, width,
|
||||
bool(word & (1 << 30)), flags), 'arithmetic' + ('-flags' if flags else '')
|
||||
# Move-wide immediate (MOVZ/MOVN/MOVK).
|
||||
if word & 0x1f800000 == 0x12800000:
|
||||
operation, shift = (word >> 29) & 3, ((word >> 21) & 3) * 16
|
||||
if operation == 1 or shift >= width:
|
||||
raise UnsupportedInstruction('reserved move-wide encoding')
|
||||
value = ((word >> 5) & 65535) << shift
|
||||
if operation == 0:
|
||||
expression = const((~value) & ((1 << width) - 1))
|
||||
elif operation == 2:
|
||||
expression = const(value)
|
||||
else:
|
||||
mask = (~(65535 << shift)) & ((1 << width) - 1)
|
||||
expression = f"(i64.or (i64.and {self.reg(rd, width)} {const(mask)}) {const(value)})"
|
||||
return [self.put(rd, expression, width)], 'move-wide'
|
||||
# Logical shifted register, including the MOV register alias.
|
||||
if word & 0x1f000000 == 0x0a000000:
|
||||
op, shift_kind = (word >> 29) & 3, (word >> 22) & 3
|
||||
amount, rm = (word >> 10) & 63, (word >> 16) & 31
|
||||
if op == 3 or shift_kind == 2 or amount >= width:
|
||||
raise UnsupportedInstruction('flag-setting, arithmetic shift, or reserved logical encoding')
|
||||
source = self.reg(rm, width)
|
||||
if amount:
|
||||
if shift_kind == 3 and width == 32:
|
||||
source = f"(i64.extend_i32_u (i32.rotr (i32.wrap_i64 {source}) (i32.const {amount})))"
|
||||
else:
|
||||
shift = {0: 'shl', 1: 'shr_u', 3: 'rotr'}[shift_kind]
|
||||
source = f"(i64.{shift} {source} {const(amount)})"
|
||||
if word & (1 << 21):
|
||||
source = f"(i64.xor {source} {const((1 << width) - 1)})"
|
||||
operation = {0: 'and', 1: 'or', 2: 'xor'}[op]
|
||||
return [self.put(rd, f"(i64.{operation} {self.reg(rn, width)} {source})", width)], 'logical-register'
|
||||
# Unsigned bitfield. Covers UBFX, UBFIZ and immediate logical shifts.
|
||||
if word & 0x7f800000 == 0x53000000:
|
||||
if ((word >> 22) & 1) != (width == 64):
|
||||
raise UnsupportedInstruction('reserved UBFM width')
|
||||
r, s = (word >> 16) & 63, (word >> 10) & 63
|
||||
if max(r, s) >= width:
|
||||
raise UnsupportedInstruction('reserved UBFM range')
|
||||
value = self.reg(rn, width)
|
||||
if s >= r:
|
||||
value = f"(i64.and (i64.shr_u {value} {const(r)}) {const((1 << (s - r + 1)) - 1)})"
|
||||
else:
|
||||
value = f"(i64.shl (i64.and {value} {const((1 << (s + 1)) - 1)}) {const(width - r)})"
|
||||
return [self.put(rd, value, width)], 'unsigned-bitfield'
|
||||
# Integer load/store with unsigned scaled immediate.
|
||||
if word & 0x3b000000 == 0x39000000 and not word & (1 << 26):
|
||||
size, op = 1 << (word >> 30), (word >> 22) & 3
|
||||
if op not in (0, 1):
|
||||
raise UnsupportedInstruction('signed/prefetch load variant')
|
||||
address = self.address(rn, ((word >> 10) & 4095) * size)
|
||||
code = self.put(rd, self.load(size, address)) if op else self.store(size, address, self.reg(rd))
|
||||
return [code], ('load' if op else 'store') + str(size * 8)
|
||||
# Integer unscaled or pre/post-indexed immediate accesses.
|
||||
if word & 0x3b200000 == 0x38000000 and not word & (1 << 26):
|
||||
size, op, mode = 1 << (word >> 30), (word >> 22) & 3, (word >> 10) & 3
|
||||
if op not in (0, 1) or mode == 2:
|
||||
raise UnsupportedInstruction('signed/prefetch/unprivileged unscaled access')
|
||||
if mode in (1, 3) and rn != 31 and rn == rd:
|
||||
raise UnsupportedInstruction('unscaled access with unpredictable writeback overlap')
|
||||
offset = signed((word >> 12) & 511, 9)
|
||||
result = []
|
||||
if mode == 3:
|
||||
result.append(self.put(rn, self.address(rn, offset), sp=True))
|
||||
result.append(f'(local.set $base {self.address(rn, offset if mode == 0 else 0)})')
|
||||
address = '(local.get $base)'
|
||||
result.append(self.put(rd, self.load(size, address)) if op else self.store(size, address, self.reg(rd)))
|
||||
if mode == 1:
|
||||
result.append(self.put(rn, self.address(rn, offset), sp=True))
|
||||
return result, 'unscaled-access'
|
||||
# Integer pairs, including SP pre/post-indexed stack saves.
|
||||
if word & 0x3a000000 == 0x28000000 and not word & (1 << 26):
|
||||
opc, mode, rt2 = word >> 30, (word >> 23) & 3, (word >> 10) & 31
|
||||
if opc not in (0, 2) or mode == 0:
|
||||
raise UnsupportedInstruction('non-temporal/signed/reserved pair')
|
||||
if mode in (1, 3) and rn != 31 and rn in (rd, rt2):
|
||||
raise UnsupportedInstruction('pair with constrained-unpredictable writeback overlap')
|
||||
size = 8 if opc == 2 else 4
|
||||
offset = signed((word >> 15) & 127, 7) * size
|
||||
result = []
|
||||
if mode == 3:
|
||||
result.append(self.put(rn, self.address(rn, offset), sp=True))
|
||||
effective_offset = offset if mode == 2 else 0
|
||||
# Snapshot the base so register destinations cannot alter later addresses.
|
||||
result.append(f"(local.set $base {self.address(rn, effective_offset)})")
|
||||
for index, register in enumerate((rd, rt2)):
|
||||
address = f"(i64.add (local.get $base) {const(index * size)})"
|
||||
if word & (1 << 22):
|
||||
result.append(self.put(register, self.load(size, address)))
|
||||
else:
|
||||
result.append(self.store(size, address, self.reg(register)))
|
||||
if mode == 1:
|
||||
result.append(self.put(rn, self.address(rn, offset), sp=True))
|
||||
return result, ('load' if word & (1 << 22) else 'store') + '-pair'
|
||||
raise UnsupportedInstruction(f"unsupported opcode 0x{word:08x}")
|
||||
|
||||
def compile(self, body, address):
|
||||
self.instructions = []
|
||||
self.calls = []
|
||||
if not body or len(body) % 4:
|
||||
raise ValueError('A64 input must contain complete instructions')
|
||||
words = list(struct.unpack('<' + 'I' * (len(body) // 4), body))
|
||||
for i, word in enumerate(words):
|
||||
if word & 0xffe00c00 == 0x3cc00000:
|
||||
if i + 1 >= len(words) or words[i+1] & 0xffc0001f != 0x3d800000 | (word & 31):
|
||||
raise UnsupportedInstruction('128-bit load requires adjacent matching store')
|
||||
if word & 0xffc00000 == 0x3d800000:
|
||||
if not i or words[i-1] & 0xffe00c1f != 0x3cc00000 | (word & 31):
|
||||
raise UnsupportedInstruction('128-bit store requires adjacent matching load')
|
||||
for j, candidate in enumerate(words):
|
||||
branch = self.branch(candidate, address+j*4)
|
||||
if branch and branch[0] == address+i*4:
|
||||
raise UnsupportedInstruction('control flow enters 128-bit copy halfway')
|
||||
if any(self.branch(word, address + i * 4) is not None for i, word in enumerate(words)):
|
||||
return self.compile_cfg(words, address)
|
||||
code = []
|
||||
saw_ret = False
|
||||
defined_flags = False
|
||||
for index, (word,) in enumerate(struct.iter_unpack('<I', body)):
|
||||
pc = address + index * 4
|
||||
if saw_ret:
|
||||
raise UnsupportedInstruction('bytes after terminal RET require a control-flow model')
|
||||
try:
|
||||
emitted, kind = self.decode(word, pc)
|
||||
except UnsupportedInstruction as error:
|
||||
raise UnsupportedInstruction(f'{pc:#x}: {error}') from error
|
||||
if kind == 'select-flags' and ((word >> 12) & 15) < 14 and not defined_flags:
|
||||
raise UnsupportedInstruction(f'conditional select reads undefined NZCV at {pc:#x}')
|
||||
if kind == 'arithmetic-flags':
|
||||
defined_flags = True
|
||||
elif kind == 'bl-host':
|
||||
defined_flags = False
|
||||
self.instructions.append({'address': hex(pc), 'word': f'{word:08x}', 'kind': kind})
|
||||
code += [f';; {pc:#x}: {word:08x} {kind}', *emitted]
|
||||
saw_ret = kind == 'ret'
|
||||
if not saw_ret:
|
||||
raise UnsupportedInstruction('missing terminal RET')
|
||||
registers = [f'x{i}' for i in range(31)] + ['sp']
|
||||
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param i64 i64 i64) (result i32)))'
|
||||
for name in sorted(set(self.hosts.values())))
|
||||
declarations = '\n'.join(f'(local ${name} i64)' for name in registers)
|
||||
loads = '\n'.join(f'(local.set ${name} (i64.load offset={i * 8} (local.get $ctx)))'
|
||||
for i, name in enumerate(registers))
|
||||
stores = '\n'.join(f'(i64.store offset={i * 8} (local.get $ctx) (local.get ${name}))'
|
||||
for i, name in enumerate(registers))
|
||||
return f'''(module
|
||||
{imports}
|
||||
(memory (export "memory") 1 1)
|
||||
(func $addr (param $value i64) (param $size i64) (result i32)
|
||||
(if (i64.gt_u (local.get $value) (i64.sub (i64.const 65536) (local.get $size)))
|
||||
(then unreachable))
|
||||
(i32.wrap_i64 (local.get $value)))
|
||||
(func (export "run") (param $ctx i32) (result i32)
|
||||
{declarations}
|
||||
(local $base i64) (local $status i32)
|
||||
(local $a i64) (local $b i64) (local $result i64) (local $qlo i64) (local $qhi i64)
|
||||
(local $n i32) (local $z i32) (local $c i32) (local $v i32)
|
||||
{loads}
|
||||
(block $done
|
||||
{chr(10).join(code)}
|
||||
)
|
||||
{stores}
|
||||
(local.get $status)))
|
||||
'''
|
||||
|
||||
def branch(self, word, pc):
|
||||
"""Return (target, condition) for supported immediate branches."""
|
||||
if word & 0xfc000000 == 0x14000000:
|
||||
return pc + signed(word & 0x3ffffff, 26) * 4, None
|
||||
if word & 0xff000010 == 0x54000000:
|
||||
cond = word & 15
|
||||
return pc + signed((word >> 5) & 0x7ffff, 19) * 4, self.condition(cond)
|
||||
if word & 0x7e000000 == 0x34000000:
|
||||
width = 64 if word >> 31 else 32
|
||||
zero = f'(i64.eqz {self.reg(word & 31, width)})'
|
||||
condition = f'(i32.eqz {zero})' if word & (1 << 24) else zero
|
||||
return pc + signed((word >> 5) & 0x7ffff, 19) * 4, condition
|
||||
if word & 0x7e000000 == 0x36000000:
|
||||
bit = ((word >> 31) << 5) | ((word >> 19) & 31)
|
||||
zero = f'(i64.eqz (i64.and {self.reg(word & 31)} {const(1 << bit)}))'
|
||||
condition = f'(i32.eqz {zero})' if word & (1 << 24) else zero
|
||||
return pc + signed((word >> 5) & 0x3fff, 14) * 4, condition
|
||||
return None
|
||||
|
||||
def compile_cfg(self, words, address):
|
||||
"""Validated instruction-block dispatcher; guest loops consume a caller budget.
|
||||
|
||||
Each instruction is currently a block. This deliberately simple CFG is
|
||||
correct before block coalescing/structured-loop optimization is added.
|
||||
"""
|
||||
end = address + len(words) * 4
|
||||
nodes = []
|
||||
reachable = set()
|
||||
successors = {}
|
||||
pending = [address]
|
||||
while pending:
|
||||
pc = pending.pop()
|
||||
if pc in reachable:
|
||||
continue
|
||||
if not address <= pc < end or (pc - address) % 4:
|
||||
raise UnsupportedInstruction(f'control flow outside declared body: {pc:#x}')
|
||||
reachable.add(pc)
|
||||
word = words[(pc - address) // 4]
|
||||
branch = self.branch(word, pc)
|
||||
if branch:
|
||||
edges = [branch[0]]
|
||||
if branch[1] is not None:
|
||||
edges.append(pc + 4)
|
||||
elif word != 0xd65f03c0:
|
||||
edges = [pc + 4]
|
||||
else:
|
||||
edges = []
|
||||
successors[pc] = edges
|
||||
pending.extend(edges)
|
||||
if len(reachable) != len(words):
|
||||
raise UnsupportedInstruction('unreachable bytes require explicit code/data classification')
|
||||
if not any(word == 0xd65f03c0 for word in words):
|
||||
raise UnsupportedInstruction('missing RET in control-flow body')
|
||||
for i, word in enumerate(words):
|
||||
pc = address + i * 4
|
||||
branch = self.branch(word, pc)
|
||||
if branch:
|
||||
target, condition = branch
|
||||
next_pc = f'(i32.const {(target-address)//4})'
|
||||
if condition is not None:
|
||||
next_pc = f'(select {next_pc} (i32.const {i+1}) {condition})'
|
||||
emitted = [f'(local.set $pc {next_pc})', '(br $dispatch)']
|
||||
kind = 'branch-immediate'
|
||||
else:
|
||||
emitted, kind = self.decode(word, pc)
|
||||
emitted += [f'(local.set $pc (i32.const {i+1}))', '(br $dispatch)']
|
||||
self.instructions.append({'address': hex(pc), 'word': f'{word:08x}', 'kind': kind})
|
||||
nodes.append(f'(if (i32.eq (local.get $pc) (i32.const {i})) (then\n' + '\n'.join(emitted) + '\n))')
|
||||
# Require flags to be defined on every path before a conditional read.
|
||||
# Host calls invalidate flags: their ARM64 ABI does not preserve NZCV.
|
||||
incoming = {pc: pc != address for pc in reachable}
|
||||
outgoing = dict(incoming)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for i, instruction in enumerate(self.instructions):
|
||||
pc = address + i * 4
|
||||
predecessors = [p for p, edges in successors.items() if pc in edges]
|
||||
value = pc != address and all(outgoing[p] for p in predecessors)
|
||||
after = True if instruction['kind'] == 'arithmetic-flags' else \
|
||||
False if instruction['kind'] == 'bl-host' else value
|
||||
if (incoming[pc], outgoing[pc]) != (value, after):
|
||||
incoming[pc], outgoing[pc] = value, after
|
||||
changed = True
|
||||
for i, word in enumerate(words):
|
||||
cond = (word & 15) if word & 0xff000010 == 0x54000000 else \
|
||||
((word >> 12) & 15) if self.instructions[i]['kind'] == 'select-flags' else 14
|
||||
if cond < 14 and not incoming[address+i*4]:
|
||||
raise UnsupportedInstruction(f'conditional operation reads undefined NZCV at {address+i*4:#x}')
|
||||
registers = [f'x{i}' for i in range(31)] + ['sp']
|
||||
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param i64 i64 i64) (result i32)))'
|
||||
for name in sorted(set(self.hosts.values())))
|
||||
declarations = '\n'.join(f'(local ${name} i64)' for name in registers)
|
||||
loads = '\n'.join(f'(local.set ${name} (i64.load offset={i * 8} (local.get $ctx)))'
|
||||
for i, name in enumerate(registers))
|
||||
stores = '\n'.join(f'(i64.store offset={i * 8} (local.get $ctx) (local.get ${name}))'
|
||||
for i, name in enumerate(registers))
|
||||
return f'''(module
|
||||
{imports}
|
||||
(memory (export "memory") 1 1)
|
||||
(func $addr (param $value i64) (param $size i64) (result i32)
|
||||
(if (i64.gt_u (local.get $value) (i64.sub (i64.const 65536) (local.get $size))) (then unreachable))
|
||||
(i32.wrap_i64 (local.get $value)))
|
||||
(func (export "run") (param $ctx i32) (param $budget i32) (result i32)
|
||||
{declarations}
|
||||
(local $base i64) (local $status i32) (local $pc i32)
|
||||
(local $a i64) (local $b i64) (local $result i64) (local $qlo i64) (local $qhi i64)
|
||||
(local $n i32) (local $z i32) (local $c i32) (local $v i32)
|
||||
{loads}
|
||||
(block $done
|
||||
(loop $dispatch
|
||||
(if (i32.eqz (local.get $budget)) (then (local.set $status (i32.const 3)) (br $done)))
|
||||
(local.set $budget (i32.sub (local.get $budget) (i32.const 1)))
|
||||
{chr(10).join(nodes)}
|
||||
unreachable
|
||||
))
|
||||
{stores}
|
||||
(local.get $status)))
|
||||
'''
|
||||
@@ -0,0 +1,276 @@
|
||||
"""Bounded multi-function A64 dispatcher with shared registers and guest memory.
|
||||
|
||||
Direct BL enters a declared guest function, B can tail-call declared code, and
|
||||
RET dispatches through the guest link register. No native recursion or JIT is
|
||||
used. Guest stack instructions retain their original semantics. All guest
|
||||
instructions share one budget. Indirect calls and arbitrary computed returns
|
||||
are unsupported. Host calls use an explicit status ABI, with an opt-in context
|
||||
adapter that can return x0. Static ADRP pages must be explicitly mapped. The
|
||||
zero-vector stores require a MOVI-zero definition on every incoming path.
|
||||
"""
|
||||
import struct
|
||||
from a64_lift import Compiler, UnsupportedInstruction, const, signed
|
||||
|
||||
|
||||
class ProgramCompiler(Compiler):
|
||||
def __init__(self, hosts=None, *, returning_hosts=(), memory_pages=1, static_pages=None):
|
||||
super().__init__(hosts)
|
||||
if type(memory_pages) is not int or not 1 <= memory_pages <= 32768:
|
||||
raise ValueError('Guest memory must contain 1..32768 fixed WASM pages')
|
||||
self.memory_pages = memory_pages
|
||||
self.returning_hosts = set(returning_hosts)
|
||||
if not self.returning_hosts <= set(self.hosts):
|
||||
raise ValueError('Returning hosts must be declared host targets')
|
||||
self.static_pages = dict(static_pages or {})
|
||||
self.zero_stores = {}
|
||||
for original, guest in self.static_pages.items():
|
||||
if original < 0 or original >= 1 << 64 or original % 4096 or guest < 0 or guest % 4096 or guest + 4096 > memory_pages*65536:
|
||||
raise ValueError('Static pages require aligned original and bounded guest addresses')
|
||||
signatures = {}
|
||||
for target, name in self.hosts.items():
|
||||
signature = target in self.returning_hosts
|
||||
if name in signatures and signatures[name] != signature:
|
||||
raise ValueError('A host import cannot have conflicting ABIs')
|
||||
signatures[name] = signature
|
||||
|
||||
def decode(self, word, pc):
|
||||
if pc in self.zero_stores:
|
||||
rn = (word >> 5) & 31
|
||||
mode, count = 2, 1
|
||||
if word & 0xffc00000 == 0x3d800000:
|
||||
offset = ((word >> 10) & 4095)*16
|
||||
elif word & 0xffe00000 == 0x3c800000:
|
||||
mode = (word >> 10)&3
|
||||
if mode == 2: raise UnsupportedInstruction('Unprivileged vector store')
|
||||
offset = signed((word >> 12)&511,9)
|
||||
else:
|
||||
mode, count = (word >> 23)&3, 2
|
||||
if mode == 0: raise UnsupportedInstruction('Non-temporal vector pair')
|
||||
offset = signed((word >> 15)&127,7)*16
|
||||
code = []
|
||||
if mode == 3: code.append(self.put(rn,self.address(rn,offset),sp=True))
|
||||
code.append(f'(local.set $base {self.address(rn,offset if mode in (0,2) else 0)})')
|
||||
code += [self.store(8,f'(i64.add (local.get $base) {const(i*8)})',const(0)) for i in range(count*2)]
|
||||
if mode == 1: code.append(self.put(rn,self.address(rn,offset),sp=True))
|
||||
return code, 'zero128-store'
|
||||
if word & 0xffffffe0 == 0x6f00e400:
|
||||
return ['(nop)'], 'zero128-immediate'
|
||||
if word & 0x9f000000 == 0x90000000:
|
||||
immediate = ((word >> 5) & 0x7ffff) << 2 | ((word >> 29) & 3)
|
||||
page = ((pc & ~4095) + signed(immediate, 21)*4096) & ((1 << 64)-1)
|
||||
if page not in self.static_pages:
|
||||
raise UnsupportedInstruction(f'Unmapped ADRP page {page:#x}')
|
||||
return [self.put(word & 31, const(self.static_pages[page]))], 'mapped-static-page'
|
||||
if word & 0x1f800000 == 0x12000000:
|
||||
width = 64 if word >> 31 else 32
|
||||
n, r, s, operation = (word >> 22)&1, (word >> 16)&63, (word >> 10)&63, (word >> 29)&3
|
||||
length = ((n << 6) | ((~s)&63)).bit_length()-1
|
||||
if operation == 3 or length < 1 or (width == 32 and n):
|
||||
raise UnsupportedInstruction('Unsupported logical immediate encoding')
|
||||
levels = (1 << length)-1
|
||||
if s & levels == levels:
|
||||
raise UnsupportedInstruction('Reserved logical immediate mask')
|
||||
element_size, rotation = 1 << length, r & levels
|
||||
mask = (1 << element_size)-1
|
||||
element = (1 << ((s & levels)+1))-1
|
||||
element = ((element >> rotation) | (element << (element_size-rotation))) & mask
|
||||
immediate = sum(element << shift for shift in range(0,width,element_size))
|
||||
expression = f'(i64.{["and","or","xor"][operation]} {self.reg((word >> 5)&31,width)} {const(immediate)})'
|
||||
return [self.put(word&31,expression,width)], 'logical-immediate'
|
||||
# ASR immediate only; other signed bitfield forms remain unsupported.
|
||||
if word & 0x7f800000 == 0x13000000:
|
||||
width = 64 if word >> 31 else 32
|
||||
r, s = (word >> 16) & 63, (word >> 10) & 63
|
||||
if ((word >> 22) & 1) != (width == 64) or s != width-1 or r >= width:
|
||||
raise UnsupportedInstruction('Unsupported signed bitfield form')
|
||||
value = self.reg((word >> 5) & 31, width)
|
||||
expression = f'(i64.shr_s {value} {const(r)})' if width == 64 else \
|
||||
f'(i64.extend_i32_u (i32.shr_s (i32.wrap_i64 {value}) (i32.const {r})))'
|
||||
return [self.put(word & 31, expression, width)], 'arithmetic-shift'
|
||||
if word & 0xffe0001f == 0xd4200000:
|
||||
return ['unreachable'], 'break-trap'
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target = pc + signed(word & 0x3ffffff, 26)*4
|
||||
if target in self.returning_hosts:
|
||||
host = self.hosts[target]
|
||||
self.calls.append({'site':hex(pc), 'target':hex(target), 'host':host, 'abi':'context-x0-result'})
|
||||
registers = [f'x{i}' for i in range(31)]+['sp']
|
||||
# Snapshot the complete register file; the adapter may return x0
|
||||
# only. All other registers remain in their guest locals.
|
||||
return [self.put(30, const(pc+4)),
|
||||
*[f'(i64.store offset={i*8} (local.get $ctx) (local.get ${name}))' for i,name in enumerate(registers)],
|
||||
f'(local.set $status (call ${host} (local.get $ctx)))',
|
||||
'(br_if $done (local.get $status))',
|
||||
'(local.set $x0 (i64.load (local.get $ctx)))'], 'bl-host'
|
||||
return super().decode(word, pc)
|
||||
|
||||
def compile(self, functions, entry):
|
||||
self.instructions, self.calls = [], []
|
||||
if entry not in functions or not functions:
|
||||
raise ValueError('Entry must name a declared guest function')
|
||||
sites, pairs = {}, set()
|
||||
self.zero_stores = {}
|
||||
for address, body in sorted(functions.items()):
|
||||
if address < 0 or address % 4 or not body or len(body) % 4 or address + len(body) > 1 << 64:
|
||||
raise ValueError('Guest functions require aligned, nonempty 64-bit instruction ranges')
|
||||
words = struct.unpack('<'+'I'*(len(body)//4),body)
|
||||
# Identify stores first; validate definitely-zero source registers
|
||||
# against the complete control-flow graph below. Other SIMD remains
|
||||
# unsupported. The existing adjacent load/copy idiom stays separate.
|
||||
for i, word in enumerate(words):
|
||||
if word & 0xffe00000 == 0x3c800000 or word & 0xffc00000 == 0x3d800000:
|
||||
is_copy = (word & 0xffc00000 == 0x3d800000 and i and
|
||||
words[i-1] & 0xffe00c1f == 0x3cc00000 | (word&31))
|
||||
if not is_copy: self.zero_stores[address+i*4] = [word&31]
|
||||
elif word & 0x3a000000 == 0x28000000 and word & (1<<26) and word >> 30 == 2 and not word & (1<<22):
|
||||
self.zero_stores[address+i*4] = [word&31,(word>>10)&31]
|
||||
for i, word in enumerate(words):
|
||||
pc = address+i*4
|
||||
if pc in sites:
|
||||
raise ValueError('Overlapping guest functions')
|
||||
sites[pc] = word
|
||||
if word & 0xffe00c00 == 0x3cc00000:
|
||||
if i+1 >= len(words) or words[i+1] & 0xffc0001f != 0x3d800000 | (word & 31):
|
||||
raise UnsupportedInstruction('128-bit load requires adjacent matching store')
|
||||
if word & 0xffc00000 == 0x3d800000 and pc not in self.zero_stores:
|
||||
if not i or words[i-1] & 0xffe00c1f != 0x3cc00000 | (word & 31):
|
||||
raise UnsupportedInstruction('128-bit store requires adjacent matching load')
|
||||
pairs.add(pc)
|
||||
if set(sites) & set(self.hosts):
|
||||
raise ValueError('Guest and host entry points overlap')
|
||||
addresses = sorted(sites)
|
||||
indices = {pc:i for i,pc in enumerate(addresses)}
|
||||
guest_calls = {}
|
||||
for pc, word in sites.items():
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target = pc + ((word & 0x3ffffff) - (0x4000000 if word & 0x2000000 else 0))*4
|
||||
if target not in self.hosts:
|
||||
if target not in functions:
|
||||
raise UnsupportedInstruction(f'undeclared guest BL target {target:#x}')
|
||||
if pc+4 not in sites:
|
||||
raise UnsupportedInstruction('Guest call has no declared continuation')
|
||||
guest_calls[pc] = target
|
||||
returns = sorted(pc+4 for pc in guest_calls)
|
||||
successors, kinds, nodes = {}, {}, []
|
||||
for pc in addresses:
|
||||
word = sites[pc]
|
||||
branch = self.branch(word, pc)
|
||||
if branch:
|
||||
target, condition = branch
|
||||
edges = [target] + ([pc+4] if condition is not None else [])
|
||||
next_pc = f'(i32.const {indices.get(target, -1)})'
|
||||
if condition is not None:
|
||||
next_pc = f'(select {next_pc} (i32.const {indices.get(pc+4, -1)}) {condition})'
|
||||
emitted = [f'(local.set $pc {next_pc})','(br $dispatch)']
|
||||
kind = 'branch-immediate'
|
||||
if target in pairs:
|
||||
raise UnsupportedInstruction('Control flow enters a 128-bit copy halfway')
|
||||
elif pc in guest_calls:
|
||||
target = guest_calls[pc]
|
||||
emitted = [self.put(30,const(pc+4)),
|
||||
f'(local.set $pc (i32.const {indices[target]}))','(br $dispatch)']
|
||||
edges, kind = [target], 'bl-guest'
|
||||
self.calls.append({'site':hex(pc),'target':hex(target),'kind':'guest'})
|
||||
elif word == 0xd65f03c0:
|
||||
emitted = ['(br_if $done (i64.eq (local.get $x30) (local.get $exit_lr)))']
|
||||
emitted += [f'(if (i64.eq (local.get $x30) {const(target)}) (then '
|
||||
f'(local.set $pc (i32.const {indices[target]})) (br $dispatch)))' for target in returns]
|
||||
emitted += ['(local.set $status (i32.const 4))','(br $done)']
|
||||
edges, kind = returns, 'ret'
|
||||
else:
|
||||
try:
|
||||
emitted, kind = self.decode(word,pc)
|
||||
except UnsupportedInstruction as error:
|
||||
raise UnsupportedInstruction(f'{pc:#x}: {error}') from error
|
||||
emitted += [f'(local.set $pc (i32.const {indices.get(pc+4,-1)}))','(br $dispatch)']
|
||||
edges = [] if kind == 'break-trap' else [pc+4]
|
||||
for target in edges:
|
||||
if target not in sites:
|
||||
raise UnsupportedInstruction(f'Control flow outside declared code: {target:#x}')
|
||||
successors[pc], kinds[pc] = edges, kind
|
||||
self.instructions.append({'address':hex(pc),'word':f'{word:08x}','kind':kind})
|
||||
nodes.append(f'(if (i32.eq (local.get $pc) (i32.const {indices[pc]})) (then\n'+ '\n'.join(emitted)+'\n))')
|
||||
reachable, pending = set(), [entry]
|
||||
while pending:
|
||||
pc = pending.pop()
|
||||
if pc not in reachable:
|
||||
reachable.add(pc)
|
||||
pending.extend(successors[pc])
|
||||
if reachable != set(sites):
|
||||
raise UnsupportedInstruction('Unreachable declared code requires explicit classification')
|
||||
if not any(kind=='ret' for kind in kinds.values()):
|
||||
raise UnsupportedInstruction('Program has no RET')
|
||||
# Conservative flag analysis includes every possible declared return edge.
|
||||
predecessors = {pc:[] for pc in sites}
|
||||
for pc, edges in successors.items():
|
||||
for target in edges: predecessors[target].append(pc)
|
||||
incoming = {pc: pc!=entry for pc in sites}
|
||||
outgoing = dict(incoming)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pc in addresses:
|
||||
value = pc!=entry and all(outgoing[p] for p in predecessors[pc])
|
||||
after = True if kinds[pc]=='arithmetic-flags' else False if kinds[pc]=='bl-host' else value
|
||||
if (incoming[pc],outgoing[pc])!=(value,after):
|
||||
incoming[pc],outgoing[pc],changed = value,after,True
|
||||
for pc,word in sites.items():
|
||||
condition = word & 15 if word & 0xff000010 == 0x54000000 else \
|
||||
(word>>12)&15 if kinds[pc]=='select-flags' else 14
|
||||
if condition<14 and not incoming[pc]:
|
||||
raise UnsupportedInstruction(f'Conditional operation reads undefined NZCV at {pc:#x}')
|
||||
# Definitely-zero facts are intersected at every merge. Calls clear all
|
||||
# facts (the ABI preserves no complete 128-bit vector register). An
|
||||
# adjacent copy load also overwrites its destination vector register.
|
||||
zero_in = {pc:0 if pc==entry else 0xffffffff for pc in sites}
|
||||
zero_out = dict(zero_in)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pc in addresses:
|
||||
before = 0 if pc==entry else 0xffffffff
|
||||
for parent in predecessors[pc]: before &= zero_out[parent]
|
||||
after = before
|
||||
if kinds[pc] in ('bl-host','bl-guest'): after = 0
|
||||
elif kinds[pc]=='zero128-immediate': after |= 1 << (sites[pc]&31)
|
||||
elif kinds[pc]=='copy128-load': after &= ~(1 << (sites[pc]&31))
|
||||
if (zero_in[pc],zero_out[pc])!=(before,after):
|
||||
zero_in[pc],zero_out[pc],changed = before,after,True
|
||||
for pc, registers in self.zero_stores.items():
|
||||
if any(not zero_in[pc] & (1 << register) for register in registers):
|
||||
raise UnsupportedInstruction(f'Vector store reads undefined/nonzero Q register at {pc:#x}')
|
||||
registers = [f'x{i}' for i in range(31)]+['sp']
|
||||
declarations = '\n'.join(f'(local ${name} i64)' for name in registers)
|
||||
loads = '\n'.join(f'(local.set ${name} (i64.load offset={i*8} (local.get $ctx)))' for i,name in enumerate(registers))
|
||||
stores = '\n'.join(f'(i64.store offset={i*8} (local.get $ctx) (local.get ${name}))' for i,name in enumerate(registers))
|
||||
returning_names = {self.hosts[target] for target in self.returning_hosts}
|
||||
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param {"i32" if name in returning_names else "i64 i64 i64"}) (result i32)))'
|
||||
for name in sorted(set(self.hosts.values())))
|
||||
# The external return sentinel must not alias any declared instruction.
|
||||
sentinel_checks = '\n'.join(f'(if (i64.eq (local.get $exit_lr) {const(pc)}) (then (return (i32.const 2))))'
|
||||
for pc in addresses)
|
||||
return f'''(module
|
||||
{imports}
|
||||
(memory (export "memory") {self.memory_pages} {self.memory_pages})
|
||||
(func $addr (param $value i64) (param $size i64) (result i32)
|
||||
(if (i64.gt_u (local.get $value) (i64.sub (i64.const {self.memory_pages*65536}) (local.get $size))) (then unreachable))
|
||||
(i32.wrap_i64 (local.get $value)))
|
||||
(func (export "run") (param $ctx i32) (param $budget i32) (result i32)
|
||||
{declarations}
|
||||
(local $base i64) (local $status i32) (local $pc i32) (local $exit_lr i64)
|
||||
(local $a i64) (local $b i64) (local $result i64) (local $qlo i64) (local $qhi i64)
|
||||
(local $n i32) (local $z i32) (local $c i32) (local $v i32)
|
||||
(if (i32.gt_u (local.get $ctx) (i32.const {self.memory_pages*65536-256})) (then (return (i32.const 2))))
|
||||
{loads}
|
||||
(local.set $exit_lr (local.get $x30))
|
||||
{sentinel_checks}
|
||||
(local.set $pc (i32.const {indices[entry]}))
|
||||
(block $done (loop $dispatch
|
||||
(if (i32.eqz (local.get $budget)) (then (local.set $status (i32.const 3)) (br $done)))
|
||||
(local.set $budget (i32.sub (local.get $budget) (i32.const 1)))
|
||||
{chr(10).join(nodes)}
|
||||
unreachable
|
||||
))
|
||||
{stores}
|
||||
(local.get $status)))
|
||||
'''
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lift original MapDeserialiser initialization and its actual C1 target dependency."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from a64_program import ProgramCompiler
|
||||
from lift_rng import arm64_image, file_range
|
||||
from build_target_deserialiser import RANGES as TARGET_RANGES, HOSTS as TARGET_HOSTS
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
ENTRY=0x1014DEF40
|
||||
RANGES=[(ENTRY,1272,'afef8906673fbfdab8f47b5663112ce9c4666a757bd1c80ab7e670e121f383ea'),
|
||||
(0x1012C1044,116,'c607983877925865ddb27bca5841f45a88f90896250f36864deac97462687d08'),
|
||||
*TARGET_RANGES[1:]]
|
||||
HOSTS={**TARGET_HOSTS,0x10035E264:'read_or_throw',0x1027D3918:'zero_memory',
|
||||
0x1000EA874:'version_error',0x101D5C248:'unsupported',0x1000EABE4:'unsupported',
|
||||
0x102226250:'unsupported',0x102226290:'unsupported',0x101B89938:'unsupported'}
|
||||
STATIC_PAGES={0x102FC2000:65536,0x102D55000:69632,0x1014DF000:73728}
|
||||
|
||||
def run(*args): subprocess.run(list(map(str,args)),cwd=ROOT,check=True)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('binary',type=Path)
|
||||
args=parser.parse_args();image,_=arm64_image(args.binary.read_bytes())
|
||||
source_hash=hashlib.sha256(image).hexdigest()
|
||||
if source_hash!='2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7':
|
||||
raise ValueError('Unrecognized ARM64 image')
|
||||
bodies={}
|
||||
for address,size,expected in RANGES:
|
||||
offset=file_range(image,address,size);data=image[offset:offset+size]
|
||||
if hashlib.sha256(data).hexdigest()!=expected: raise ValueError('Unrecognized constructor input')
|
||||
bodies[address]=data
|
||||
functions={address:bodies[address] for address,_,_ in RANGES[:-1]}
|
||||
compiler=ProgramCompiler(HOSTS,returning_hosts={0x1027D36E4},memory_pages=16,static_pages=STATIC_PAGES)
|
||||
wat=compiler.compile(functions,ENTRY).replace('(import "env"','(import "deser_host"')
|
||||
out=ROOT/'build/deserialiser';out.mkdir(parents=True,exist_ok=True)
|
||||
(out/'factorio_deser.wat').write_text(wat)
|
||||
run(ROOT/'build/wabt/wat2wasm',out/'factorio_deser.wat','-o',out/'factorio_deser.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',out/'factorio_deser.wasm','-o',out/'factorio_deser.c','--module-name=factorio_deser')
|
||||
assembly=['.text','.p2align 2','.globl _factorio_deser_original']
|
||||
for address,data in functions.items():
|
||||
if address==ENTRY: assembly.append('_factorio_deser_original:')
|
||||
for i,(word,) in enumerate(struct.iter_unpack('<I',data)):
|
||||
pc=address+i*4;assembly.append(f'Ldeser_{pc:x}:')
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target=pc+((word&0x3ffffff)-(0x4000000 if word&0x2000000 else 0))*4
|
||||
assembly.append('bl '+(f'Ldeser_{target:x}' if target in functions else '_native_deser_'+HOSTS[target]))
|
||||
elif pc==0x10244A50C: assembly.append('adrp x9, _native_deser_vtable@PAGE')
|
||||
elif pc==0x10244A510: assembly.append('add x9, x9, _native_deser_vtable@PAGEOFF')
|
||||
else: assembly.append(f'.long 0x{word:08x}')
|
||||
assembly += ['.section __DATA,__const','.p2align 3','.globl _native_deser_vtable',
|
||||
'_native_deser_vtable:','.quad 0,0,0,0']
|
||||
(out/'oracle.S').write_text('\n'.join(assembly)+'\n')
|
||||
runtime=ROOT/'third_party/wabt/wasm2c'
|
||||
includes=['-I'+str(p) for p in (out,ROOT/'runtime',runtime)]
|
||||
flags=['-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1']
|
||||
sources=[out/'factorio_deser.c',ROOT/'runtime/deserialiser_bridge.c',ROOT/'runtime/guest_heap.c']
|
||||
run('xcrun','clang',*flags,*includes,*sources,runtime/'wasm-rt-impl.c',runtime/'wasm-rt-mem-impl.c',
|
||||
out/'oracle.S',ROOT/'tests/deserialiser_equivalence.c','-o',out/'verify_deserialiser')
|
||||
run(out/'verify_deserialiser')
|
||||
sdk=subprocess.check_output(['xcrun','--sdk','iphoneos','--show-sdk-path'],text=True).strip()
|
||||
objects=[]
|
||||
for source in sources:
|
||||
obj=out/(source.stem+'.ios.o')
|
||||
run('xcrun','--sdk','iphoneos','clang','-target','arm64-apple-ios15.0','-isysroot',sdk,
|
||||
*flags,*includes,'-c',source,'-o',obj);objects.append(obj)
|
||||
run('xcrun','libtool','-static','-o',out/'libfactorio_deser_ios.a',*objects)
|
||||
manifest={'arm64_sha256':source_hash,'ranges':[{'address':hex(a),'bytes':n,'sha256':h} for a,n,h in RANGES],
|
||||
'instructions':compiler.instructions,'calls':compiler.calls,'memory_bytes':1048576,
|
||||
'scope':'original constructor initialization and version decisions; host reads/allocation/bzero; no world loading or unwinding',
|
||||
'exceptions':'abort status 7 at cxa_allocate_exception; original exception objects are not constructed',
|
||||
'static_pages':{hex(a):b for a,b in STATIC_PAGES.items()}}
|
||||
(out/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
(out/'verification.json').write_text(json.dumps({'status':'pass','native_object_graph_comparisons':512,
|
||||
'normal_boundary_cases':3,'truncated_headers':9,'version_errors':8,'allocation_failures':12,
|
||||
'instructions':len(compiler.instructions),'owned_target_allocations':12,'owned_target_heap_bytes':10712,
|
||||
'scope':'constructor initialization and explicit abort paths; not world loading or C++ unwinding'},indent=2)+'\n')
|
||||
print('PASS: original MapDeserialiser initialization graph, desktop comparisons and iOS library')
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Byte-guarded automatic CFG lift of Factorio's original MapHeader constructor."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from a64_program import ProgramCompiler
|
||||
from lift_rng import arm64_image, file_range
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ADDRESS, SIZE = 0x1014C6AAC, 332
|
||||
EXPECTED = '4f940fe2feb23b8301d080d3678634fa33891d4d53de87c844d70c1319282ff6'
|
||||
READ = 0x10035E264
|
||||
ENTRY, ENTRY_SIZE = 0x10010582C, 4
|
||||
ENTRY_HASH = 'cb0ac2c0409e9c0c1a8de8acf7ce68abc43ca0ab4fb20c2a97c27daab156df81'
|
||||
|
||||
|
||||
def run(*args):
|
||||
subprocess.run(list(map(str, args)), cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('binary', type=Path)
|
||||
args = parser.parse_args()
|
||||
image, slice_offset = arm64_image(args.binary.read_bytes())
|
||||
offset = file_range(image, ADDRESS, SIZE)
|
||||
body = image[offset:offset+SIZE]
|
||||
if hashlib.sha256(body).hexdigest() != EXPECTED:
|
||||
raise ValueError('Unrecognized MapHeader constructor bytes')
|
||||
entry_offset = file_range(image, ENTRY, ENTRY_SIZE)
|
||||
entry_body = image[entry_offset:entry_offset+ENTRY_SIZE]
|
||||
if hashlib.sha256(entry_body).hexdigest() != ENTRY_HASH:
|
||||
raise ValueError('Unrecognized MapHeader public constructor entry')
|
||||
compiler = ProgramCompiler({READ: 'read_or_throw'})
|
||||
wat = compiler.compile({ENTRY:entry_body,ADDRESS:body}, ENTRY).replace('(import "env"', '(import "map_host"')
|
||||
out = ROOT / 'build/map-header'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / 'factorio_map_header.wat').write_text(wat)
|
||||
manifest = {'symbol': 'MapHeader::MapHeader(MapDeserialiser&)', 'address': hex(ADDRESS),
|
||||
'bytes': SIZE, 'sha256': EXPECTED, 'source_file_offset': slice_offset+offset,
|
||||
'instructions': compiler.instructions, 'host_calls': compiler.calls,
|
||||
'rea_bytes_evidence': 'ev_edc9090e494bf4c92f5c0e5d9f4366272062c6d2d3d758442448ab720bf4e422',
|
||||
'public_entry': {'address':hex(ENTRY),'bytes':ENTRY_SIZE,'sha256':ENTRY_HASH,
|
||||
'rea_bytes_evidence':'ev_525aeb761646ca31af2fbacfc51ec786e6036b6259fd09505063144d0a5feba1'},
|
||||
'scope': 'version-dependent header constructor; declared memory-stream adapter; no C++ unwinding'}
|
||||
(out / 'manifest.json').write_text(json.dumps(manifest, indent=2)+'\n')
|
||||
assembly = ['.text', '.p2align 2', '.globl _factorio_map_header_original', '_factorio_map_header_original:',
|
||||
'b Lmap_header_c2', 'Lmap_header_c2:'] # Relocate the C1 tail B to the copied C2 body.
|
||||
for (word,) in struct.iter_unpack('<I', body):
|
||||
assembly.append('bl _native_map_header_read' if word & 0xfc000000 == 0x94000000 else f'.long 0x{word:08x}')
|
||||
(out / 'oracle.S').write_text('\n'.join(assembly)+'\n')
|
||||
run(ROOT / 'build/wabt/wat2wasm', out / 'factorio_map_header.wat', '-o', out / 'factorio_map_header.wasm')
|
||||
run(ROOT / 'build/wabt/wasm2c', out / 'factorio_map_header.wasm', '-o', out / 'factorio_map_header.c', '--module-name=factorio_map_header')
|
||||
runtime = ROOT / 'third_party/wabt/wasm2c'
|
||||
includes = ['-I'+str(p) for p in (out, ROOT/'runtime', runtime)]
|
||||
flags = ['-O2', '-DWASM_RT_USE_MMAP=0', '-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1']
|
||||
sources = [out/'factorio_map_header.c', ROOT/'runtime/map_header_bridge.c']
|
||||
run('xcrun', 'clang', *flags, *includes, *sources, runtime/'wasm-rt-impl.c', runtime/'wasm-rt-mem-impl.c',
|
||||
out/'oracle.S', ROOT/'tests/map_header_equivalence.c', '-o', out/'verify_map_header')
|
||||
run(out/'verify_map_header')
|
||||
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
|
||||
objects = []
|
||||
for source in sources:
|
||||
obj = out/(source.stem+'.ios.o')
|
||||
run('xcrun', '--sdk', 'iphoneos', 'clang', '-target', 'arm64-apple-ios15.0', '-isysroot', sdk,
|
||||
*flags, *includes, '-c', source, '-o', obj)
|
||||
objects.append(obj)
|
||||
run('xcrun', 'libtool', '-static', '-o', out/'libfactorio_map_header_ios.a', *objects)
|
||||
(out/'verification.json').write_text(json.dumps({'status':'pass', 'native_comparisons':20000,
|
||||
'instructions':len(compiler.instructions), 'host_call_sites':len(compiler.calls),
|
||||
'whole_map_loading':False, 'scope':'controlled payload at constructor stream position'}, indent=2)+'\n')
|
||||
print('PASS: original MapHeader CFG lift, native oracle comparisons, and iOS static library')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build, sign and optionally install the native iOS host for the retargeted Factorio binary.
|
||||
|
||||
Usage:
|
||||
python3 tools/build_native.py [--install] [--push-data] [--launch] [options]
|
||||
|
||||
--install install build/native/Factorio.app on the connected device
|
||||
--push-data copy the installed game's data/ folder into the app's Documents (once, ~1.5 GB)
|
||||
--launch launch with the console attached; output is also saved to build/native/console.log
|
||||
--no-build skip building (install/launch the existing bundle)
|
||||
|
||||
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)
|
||||
--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
|
||||
tools/retarget.py). Shims supply the macOS APIs iOS lacks. No game data is put in the bundle.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
STEAM_GAME = Path.home() / "Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents"
|
||||
OUT = ROOT / "build/native"
|
||||
APP = OUT / "Factorio.app"
|
||||
SRC = ROOT / "platform/native"
|
||||
MIN_IOS = "17.0"
|
||||
# Set from command-line options / environment in main().
|
||||
GAME = STEAM_GAME
|
||||
BUNDLE_ID = "local.factorio.native"
|
||||
TEAM = None
|
||||
|
||||
SLICE = ROOT / "build/factorio.arm64"
|
||||
WUFFS = ROOT / "third_party/wuffs/wuffs-v0.3.c"
|
||||
WUFFS_URL = "https://raw.githubusercontent.com/google/wuffs/v0.3.4/release/c/wuffs-v0.3.c"
|
||||
WUFFS_SHA256 = "a3db4bd979663423de00309d1ba07d7fa8576845223d3e02764181bd6da23f90"
|
||||
|
||||
SDK = subprocess.check_output(["xcrun", "--sdk", "iphoneos", "--show-sdk-path"], text=True).strip()
|
||||
CC = ["xcrun", "--sdk", "iphoneos", "clang", "-target", f"arm64-apple-ios{MIN_IOS}", "-isysroot", SDK,
|
||||
"-O2", "-g", "-Wall", "-Wno-unused-function", "-Wno-objc-property-no-attribute",
|
||||
f"-I{SRC / 'shim'}"]
|
||||
|
||||
# shim name -> (sources, defines, re-exported iOS framework or None, links fx_AppKit)
|
||||
SHIMS = {
|
||||
"fx_CoreGraphics": (["coregraphics.c"], [], "CoreGraphics", True),
|
||||
"fx_CoreVideo": (["small.c"], ["FX_LIB_COREVIDEO"], "CoreVideo", True),
|
||||
"fx_CoreServices": (["small.c"], ["FX_LIB_CORESERVICES"], "CoreServices", True),
|
||||
"fx_Security": (["small.c"], ["FX_LIB_SECURITY"], "Security", True),
|
||||
"fx_IOKit": (["small.c"], ["FX_LIB_IOKIT"], "IOKit", True),
|
||||
"fx_Foundation": (["frameworks.m"], ["FX_LIB_FOUNDATION"], "Foundation", True),
|
||||
"fx_Metal": (["frameworks.m"], ["FX_LIB_METAL"], "Metal", True),
|
||||
"fx_CoreAudio": (["audio.c"], ["FX_LIB_COREAUDIO"], "CoreAudio", True),
|
||||
"fx_AudioToolbox": (["audio.c"], ["FX_LIB_AUDIOTOOLBOX"], "AudioToolbox", True),
|
||||
"fx_Carbon": (["small.c"], ["FX_LIB_CARBON"], None, True),
|
||||
"fx_OpenCL": (["small.c"], ["FX_LIB_OPENCL"], None, True),
|
||||
"fx_ForceFeedback": (["small.c"], ["FX_LIB_FORCEFEEDBACK"], None, True),
|
||||
"fx_LDAP": (["small.c"], ["FX_LIB_LDAP"], None, True),
|
||||
"fx_ApplicationServices": (["small.c"], ["FX_LIB_APPLICATIONSERVICES"], None, True),
|
||||
"fx_Cocoa": (["small.c"], ["FX_LIB_EMPTY"], None, False),
|
||||
"fx_OpenGL": (["small.c"], ["FX_LIB_EMPTY"], None, False),
|
||||
"libsteam_api": (["small.c"], ["FX_LIB_STEAM"], None, True),
|
||||
}
|
||||
|
||||
|
||||
def run(cmd, **kw):
|
||||
print("+", " ".join(str(c) for c in cmd), flush=True)
|
||||
return subprocess.run([str(c) for c in cmd], check=True, **kw)
|
||||
|
||||
|
||||
def dylib(name, sources, defines=(), reexport=None, link_appkit=True, extra=()):
|
||||
out = APP / "Frameworks" / f"{name}.dylib"
|
||||
cmd = CC + ["-dynamiclib", "-o", out, "-install_name", f"@rpath/{name}.dylib",
|
||||
"-compatibility_version", "1000", "-current_version", "1000",
|
||||
"-Wl,-rpath,@loader_path", *[f"-D{d}" for d in defines],
|
||||
*[SRC / "shim" / s for s in sources], "-framework", "CoreFoundation", *extra]
|
||||
if any(s.endswith(".m") for s in sources):
|
||||
cmd += ["-fobjc-arc", "-framework", "Foundation"]
|
||||
if reexport:
|
||||
cmd += [f"-Wl,-reexport_framework,{reexport}"]
|
||||
if link_appkit:
|
||||
cmd += [APP / "Frameworks/fx_AppKit.dylib"]
|
||||
run(cmd)
|
||||
|
||||
|
||||
def build_icon():
|
||||
"""Compile an asset catalog with the app icon, made from the game's own icon; returns plist keys."""
|
||||
work = OUT / "icon"
|
||||
iconset = work / "Assets.xcassets/AppIcon.appiconset"
|
||||
iconset.mkdir(parents=True, exist_ok=True)
|
||||
tool = work / "make_icon"
|
||||
run(["xcrun", "clang", "-O2", "-fobjc-arc", ROOT / "tools/make_icon.m", "-framework", "Foundation",
|
||||
"-framework", "CoreGraphics", "-framework", "ImageIO", "-o", tool])
|
||||
run([tool, GAME / "data/core/graphics/[email protected]", iconset / "AppIcon-1024.png"])
|
||||
(iconset / "Contents.json").write_text(json.dumps({"images": [{"filename": "AppIcon-1024.png", "idiom": "universal",
|
||||
"platform": "ios", "size": "1024x1024"}], "info": {"author": "xcode", "version": 1}}))
|
||||
(work / "Assets.xcassets/Contents.json").write_text(json.dumps({"info": {"author": "xcode", "version": 1}}))
|
||||
partial = work / "partial-info.plist"
|
||||
run(["xcrun", "actool", work / "Assets.xcassets", "--compile", APP, "--platform", "iphoneos",
|
||||
"--minimum-deployment-target", MIN_IOS, "--app-icon", "AppIcon", "--target-device", "iphone",
|
||||
"--target-device", "ipad", "--output-partial-info-plist", partial], stdout=subprocess.DEVNULL)
|
||||
with open(partial, "rb") as f:
|
||||
return plistlib.load(f)
|
||||
|
||||
|
||||
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])
|
||||
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)
|
||||
data = urllib.request.urlopen(WUFFS_URL, timeout=120).read()
|
||||
if hashlib.sha256(data).hexdigest() != WUFFS_SHA256:
|
||||
raise SystemExit("downloaded Wuffs source does not match the pinned SHA-256")
|
||||
WUFFS.write_bytes(data)
|
||||
|
||||
|
||||
def build():
|
||||
prepare_inputs()
|
||||
run([sys.executable, ROOT / "tools/retarget.py", SLICE, OUT / "Factorio.dylib"])
|
||||
(APP / "Frameworks").mkdir(parents=True, exist_ok=True)
|
||||
dylib("fx_AppKit", ["appkit.m", "boot.m", "crash.c", "keyboard.m", "metal_compat.m", "pngdecode.c", "stack.s"], link_appkit=False,
|
||||
extra=["-fobjc-arc", "-framework", "Foundation", "-framework", "UIKit", "-framework", "QuartzCore",
|
||||
"-framework", "CoreGraphics", "-framework", "Metal", "-framework", "ImageIO", "-framework", "AVFAudio", "-framework", "Accelerate",
|
||||
# UIKitCore already has a private NSColor; export ours under the AppKit symbol name.
|
||||
"-Wl,-alias,_OBJC_CLASS_$_FXNSColor,_OBJC_CLASS_$_NSColor"])
|
||||
for name, (sources, defines, reexport, link_appkit) in SHIMS.items():
|
||||
extra = ["-framework", "Metal"] if name == "fx_Metal" else []
|
||||
dylib(name, sources, defines, reexport, link_appkit, extra)
|
||||
run(["cp", OUT / "Factorio.dylib", APP / "Frameworks/Factorio.dylib"])
|
||||
run(CC + ["-o", APP / "FactorioNative", SRC / "host/main.c", APP / "Frameworks/fx_AppKit.dylib",
|
||||
"-Wl,-rpath,@executable_path/Frameworks"])
|
||||
info = {
|
||||
"CFBundleExecutable": "FactorioNative", "CFBundleIdentifier": BUNDLE_ID, "CFBundleName": "Factorio",
|
||||
"CFBundleDisplayName": "Factorio", "CFBundlePackageType": "APPL", "CFBundleVersion": "1",
|
||||
"CFBundleShortVersionString": "2.0.77", "CFBundleInfoDictionaryVersion": "6.0",
|
||||
"CFBundleSupportedPlatforms": ["iPhoneOS"], "MinimumOSVersion": MIN_IOS, "UIDeviceFamily": [1, 2],
|
||||
"DTPlatformName": "iphoneos", "UIRequiresFullScreen": True, "UIStatusBarHidden": True,
|
||||
"UISupportedInterfaceOrientations": ["UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight"],
|
||||
"UILaunchScreen": {"UIColorName": ""}, "UIFileSharingEnabled": True,
|
||||
"LSSupportsOpeningDocumentsInPlace": True, "GCSupportsControllerUserInteraction": True,
|
||||
}
|
||||
info.update(build_icon())
|
||||
with open(APP / "Info.plist", "wb") as f:
|
||||
plistlib.dump(info, f, fmt=plistlib.FMT_BINARY)
|
||||
symbols = OUT / "dSYM"
|
||||
run(["rm", "-rf", symbols])
|
||||
symbols.mkdir()
|
||||
for d in list(APP.rglob("*.dSYM")):
|
||||
d.rename(symbols / d.name)
|
||||
sign()
|
||||
|
||||
|
||||
def find_profile():
|
||||
"""A development profile covering BUNDLE_ID (explicit or wildcard), optionally for TEAM."""
|
||||
folder = Path.home() / "Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||||
candidates = []
|
||||
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]
|
||||
|
||||
|
||||
def signing_identity(profile):
|
||||
"""The keychain identity whose certificate is embedded in the profile."""
|
||||
allowed = {hashlib.sha1(cert).hexdigest().upper() for cert in profile.get("DeveloperCertificates", [])}
|
||||
listing = subprocess.check_output(["security", "find-identity", "-v", "-p", "codesigning"], text=True)
|
||||
for line in listing.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) > 2 and parts[1] in allowed:
|
||||
return parts[1]
|
||||
raise SystemExit("no signing identity in the keychain matches the provisioning profile")
|
||||
|
||||
|
||||
def sign():
|
||||
profile, plist, team = find_profile()
|
||||
run(["cp", profile, APP / "embedded.mobileprovision"])
|
||||
identity = signing_identity(plist)
|
||||
entitlements = {"application-identifier": f"{team}.{BUNDLE_ID}", "com.apple.developer.team-identifier": team,
|
||||
"get-task-allow": True}
|
||||
if plist["Entitlements"].get("com.apple.developer.kernel.increased-memory-limit"):
|
||||
entitlements["com.apple.developer.kernel.increased-memory-limit"] = True
|
||||
ent = OUT / "entitlements.plist"
|
||||
with open(ent, "wb") as f:
|
||||
plistlib.dump(entitlements, f)
|
||||
for lib in sorted((APP / "Frameworks").glob("*.dylib")):
|
||||
run(["codesign", "-f", "-s", identity, "--timestamp=none", lib], stdout=subprocess.DEVNULL)
|
||||
run(["codesign", "-f", "-s", identity, "--timestamp=none", "--entitlements", ent, APP])
|
||||
print(json.dumps({"app": str(APP), "profile": plist["Name"], "entitlements": entitlements}, indent=2))
|
||||
|
||||
|
||||
def device(requested=None):
|
||||
if requested:
|
||||
return requested
|
||||
listing = OUT / "devices.json"
|
||||
subprocess.run(["xcrun", "devicectl", "list", "devices", "--json-output", listing],
|
||||
check=True, stdout=subprocess.DEVNULL)
|
||||
data = json.loads(listing.read_text())
|
||||
for d in data["result"]["devices"]:
|
||||
if d.get("connectionProperties", {}).get("pairingState") == "paired":
|
||||
return d["identifier"]
|
||||
raise SystemExit("no paired device")
|
||||
|
||||
|
||||
def main():
|
||||
global GAME, BUNDLE_ID, TEAM
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--install", action="store_true")
|
||||
parser.add_argument("--push-data", action="store_true")
|
||||
parser.add_argument("--launch", action="store_true")
|
||||
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("--device", default=os.environ.get("FX_DEVICE"))
|
||||
args = parser.parse_args()
|
||||
GAME, BUNDLE_ID, TEAM = Path(args.game), args.bundle_id, args.team
|
||||
if not args.no_build:
|
||||
build()
|
||||
if args.install or args.push_data or args.launch:
|
||||
dev = device(args.device)
|
||||
if args.install:
|
||||
run(["xcrun", "devicectl", "device", "install", "app", "--device", dev, APP])
|
||||
if args.push_data:
|
||||
run(["xcrun", "devicectl", "device", "copy", "to", "--device", dev, "--domain-type", "appDataContainer",
|
||||
"--domain-identifier", BUNDLE_ID, "--source", GAME / "data", "--destination", "Documents/factorio/data"])
|
||||
if args.launch:
|
||||
with open(OUT / "console.log", "w") as log:
|
||||
proc = subprocess.Popen(["xcrun", "devicectl", "device", "process", "launch", "--device", dev,
|
||||
"--terminate-existing", "--console", BUNDLE_ID],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
for line in proc.stdout:
|
||||
sys.stdout.write(line)
|
||||
log.write(line)
|
||||
log.flush()
|
||||
proc.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and validate the bounded Factorio RNG no-JIT prototype on Apple Silicon."""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from lift_rng import generate
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run(*command):
|
||||
print("+", " ".join(map(str, command)), flush=True)
|
||||
subprocess.run(list(map(str, command)), cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("binary", type=Path)
|
||||
args = parser.parse_args()
|
||||
wabt = ROOT / "third_party/wabt"
|
||||
if not (wabt / "CMakeLists.txt").exists():
|
||||
parser.error("Fetch official WABT tag 1.0.42 into third_party/wabt first")
|
||||
revision = subprocess.check_output(["git", "-C", str(wabt), "rev-parse", "HEAD"], text=True).strip()
|
||||
if revision != "ff0ef7e0009402740c805a9744c09b05be063e48":
|
||||
parser.error("WABT checkout must match the pinned 1.0.42 commit")
|
||||
run("cmake", "-S", wabt, "-B", ROOT / "build/wabt", "-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DBUILD_TESTS=OFF", "-DBUILD_LIBWASM=OFF", "-DUSE_INTERNAL_SHA256=ON",
|
||||
"-DHAVE_OPENSSL_SHA_H=OFF")
|
||||
run("cmake", "--build", ROOT / "build/wabt", "--target", "wat2wasm", "wasm2c", "--parallel", "6")
|
||||
out = ROOT / "build/rng"
|
||||
generate(args.binary.resolve(), out)
|
||||
run(ROOT / "build/wabt/wat2wasm", out / "factorio_rng.wat", "-o", out / "factorio_rng.wasm")
|
||||
run(ROOT / "build/wabt/wasm2c", out / "factorio_rng.wasm", "-o", out / "factorio_rng.c", "--module-name=factorio_rng")
|
||||
sources = [out / "factorio_rng.c", wabt / "wasm2c/wasm-rt-impl.c", wabt / "wasm2c/wasm-rt-mem-impl.c"]
|
||||
includes = ["-I" + str(out), "-I" + str(wabt / "wasm2c")]
|
||||
flags = ["-O2", "-DWASM_RT_USE_MMAP=0", "-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1"]
|
||||
run("xcrun", "clang", *flags, *includes, *sources, out / "oracle.S", ROOT / "tests/rng_equivalence.c",
|
||||
"-o", out / "verify_rng")
|
||||
run(out / "verify_rng")
|
||||
# This archive contains the translated function and wasm2c runtime only.
|
||||
# The native oracle is a desktop verification input, never iOS runtime code.
|
||||
objects = []
|
||||
sdk = subprocess.check_output(["xcrun", "--sdk", "iphoneos", "--show-sdk-path"], text=True).strip()
|
||||
for source in sources:
|
||||
obj = out / (source.stem + ".ios.o")
|
||||
run("xcrun", "--sdk", "iphoneos", "clang", "-target", "arm64-apple-ios15.0", "-isysroot", sdk,
|
||||
*flags, *includes, "-c", source, "-o", obj)
|
||||
objects.append(obj)
|
||||
run("xcrun", "libtool", "-static", "-o", out / "libfactorio_rng_ios.a", *objects)
|
||||
undefined = subprocess.check_output(["xcrun", "nm", "-u", str(out / "libfactorio_rng_ios.a")], text=True)
|
||||
if "steam" in undefined.lower():
|
||||
raise RuntimeError("Steam import found in the single-player port library")
|
||||
report = {"status": "pass", "native_comparisons": 876544, "constructor_comparisons": 1024, "starting_states": 6848,
|
||||
"steps_per_state": 128, "ios_artifact": "libfactorio_rng_ios.a",
|
||||
"ios_device_execution": "not run", "full_game": "not implemented",
|
||||
"wabt_commit": revision, "runtime_jit": False,
|
||||
"steam_import_audit": "pass: no Steam undefined symbols"}
|
||||
(out / "verification.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the bounded 2.0.77 envelope adapter and real map-header probe."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
OUT=ROOT/'build/save-layout'
|
||||
OUT.mkdir(parents=True,exist_ok=True)
|
||||
RUNTIME=ROOT/'third_party/wabt/wasm2c'
|
||||
includes=['-I'+str(p) for p in (ROOT/'runtime',ROOT/'build/version',ROOT/'build/map-header',RUNTIME)]
|
||||
flags=['-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1']
|
||||
sources=[ROOT/'runtime/save_layout.c',ROOT/'runtime/save_header.c',ROOT/'runtime/version_bridge.c',
|
||||
ROOT/'runtime/map_header_bridge.c',ROOT/'build/version/factorio_version.c',
|
||||
ROOT/'build/map-header/factorio_map_header.c',RUNTIME/'wasm-rt-impl.c',RUNTIME/'wasm-rt-mem-impl.c',
|
||||
ROOT/'tests/save_layout_probe.c']
|
||||
subprocess.run(['xcrun','clang',*flags,*includes,*map(str,sources),'-lz','-o',str(OUT/'probe')],cwd=ROOT,check=True)
|
||||
subprocess.run(['xcrun','clang',*flags,*includes,*map(str,sources[:-1]),
|
||||
str(ROOT/'tests/save_layout_native.c'),str(ROOT/'build/map-header/oracle.S'),
|
||||
'-lz','-o',str(OUT/'verify_native')],cwd=ROOT,check=True)
|
||||
sdk=subprocess.check_output(['xcrun','--sdk','iphoneos','--show-sdk-path'],text=True).strip()
|
||||
obj=OUT/'save_layout.ios.o'
|
||||
subprocess.run(['xcrun','--sdk','iphoneos','clang','-target','arm64-apple-ios15.0','-isysroot',sdk,
|
||||
*flags,*includes,'-c',str(ROOT/'runtime/save_layout.c'),'-o',str(obj)],check=True)
|
||||
subprocess.run(['xcrun','libtool','-static','-o',str(OUT/'libfactorio_save_layout_ios.a'),str(obj)],check=True)
|
||||
print('Built bounded scenario-envelope adapter and linked AOT constructor probe')
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Translate the pinned TargetDeserialiser initialization graph, without unwinding."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from a64_program import ProgramCompiler
|
||||
from lift_rng import arm64_image, file_range
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RANGES = [
|
||||
(0x101AB7C74, 108, '2ada8a5d403d8d30f9bc053f3cad0e74d08103d6c96bcdbf4db44d481ba6d4d0'),
|
||||
(0x10244A4BC, 1260, '0e450e3ad613dded236f6627d78a07894d48b46e845baa40877d6929043b171a'),
|
||||
(0x102FC2D98, 32, 'c991e960829a78707ab67384d3dbf5d296aaa8fc9f33878ae7c1e7571f779dce')]
|
||||
HOSTS = {0x1027D36E4:'allocate', 0x1027D36B4:'unsupported_delete',
|
||||
0x10244A9D4:'unsupported_length', 0x100035550:'unsupported_array_length'}
|
||||
|
||||
def run(*args):
|
||||
subprocess.run(list(map(str,args)),cwd=ROOT,check=True)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('binary',type=Path)
|
||||
args=parser.parse_args()
|
||||
image,slice_offset=arm64_image(args.binary.read_bytes())
|
||||
slice_hash=hashlib.sha256(image).hexdigest()
|
||||
if slice_hash!='2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7':
|
||||
raise ValueError('Unrecognized Factorio ARM64 image')
|
||||
bodies={}
|
||||
for address,size,expected in RANGES:
|
||||
offset=file_range(image,address,size)
|
||||
data=image[offset:offset+size]
|
||||
if hashlib.sha256(data).hexdigest()!=expected:
|
||||
raise ValueError(f'Unrecognized target bytes at {address:#x}')
|
||||
bodies[address]=data
|
||||
compiler=ProgramCompiler(HOSTS,returning_hosts={0x1027D36E4}, memory_pages=16,
|
||||
static_pages={0x102FC2000:65536})
|
||||
functions={address:bodies[address] for address,_,_ in RANGES[:2]}
|
||||
wat=compiler.compile(functions,RANGES[0][0]).replace('(import "env"','(import "target_host"')
|
||||
out=ROOT/'build/target-deserialiser'
|
||||
out.mkdir(parents=True,exist_ok=True)
|
||||
(out/'factorio_target.wat').write_text(wat)
|
||||
run(ROOT/'build/wabt/wat2wasm',out/'factorio_target.wat','-o',out/'factorio_target.wasm')
|
||||
run(ROOT/'build/wabt/wasm2c',out/'factorio_target.wasm','-o',out/'factorio_target.c','--module-name=factorio_target')
|
||||
# Only the desktop oracle executes copied ARM64 instructions. Relocate
|
||||
# direct calls, immediate branches and the one ADRP+ADD vtable reference.
|
||||
assembly=['.text','.p2align 2','.globl _factorio_target_original']
|
||||
labels={address:f'Ltarget_{address:x}' for address in functions}
|
||||
for address,data in functions.items():
|
||||
if address==RANGES[0][0]: assembly.append('_factorio_target_original:')
|
||||
for i,(word,) in enumerate(struct.iter_unpack('<I',data)):
|
||||
pc=address+4*i
|
||||
assembly.append(f'Ltarget_{pc:x}:')
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target=pc+((word&0x3ffffff)-(0x4000000 if word&0x2000000 else 0))*4
|
||||
symbol=labels.get(target) or '_native_target_'+HOSTS[target]
|
||||
assembly.append('bl '+symbol)
|
||||
elif pc==0x10244A50C:
|
||||
assembly.append('adrp x9, _native_target_vtable@PAGE')
|
||||
elif pc==0x10244A510:
|
||||
assembly.append('add x9, x9, _native_target_vtable@PAGEOFF')
|
||||
else:
|
||||
assembly.append(f'.long 0x{word:08x}')
|
||||
assembly += ['.section __DATA,__const','.p2align 3','.globl _native_target_vtable',
|
||||
'_native_target_vtable:', '.quad 0, 0, 0, 0']
|
||||
(out/'oracle.S').write_text('\n'.join(assembly)+'\n')
|
||||
runtime=ROOT/'third_party/wabt/wasm2c'
|
||||
includes=['-I'+str(p) for p in (out,ROOT/'runtime',runtime)]
|
||||
flags=['-O2','-DWASM_RT_USE_MMAP=0','-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1']
|
||||
sources=[out/'factorio_target.c',ROOT/'runtime/target_bridge.c',ROOT/'runtime/guest_heap.c']
|
||||
run('xcrun','clang',*flags,*includes,*sources,runtime/'wasm-rt-impl.c',runtime/'wasm-rt-mem-impl.c',
|
||||
out/'oracle.S',ROOT/'tests/target_equivalence.c','-o',out/'verify_target')
|
||||
run(out/'verify_target')
|
||||
sdk=subprocess.check_output(['xcrun','--sdk','iphoneos','--show-sdk-path'],text=True).strip()
|
||||
objects=[]
|
||||
for source in sources:
|
||||
obj=out/(source.stem+'.ios.o')
|
||||
run('xcrun','--sdk','iphoneos','clang','-target','arm64-apple-ios15.0','-isysroot',sdk,
|
||||
*flags,*includes,'-c',source,'-o',obj)
|
||||
objects.append(obj)
|
||||
run('xcrun','libtool','-static','-o',out/'libfactorio_target_ios.a',*objects)
|
||||
manifest={'arm64_sha256':slice_hash,'ranges':[{'address':hex(a),'bytes':s,'sha256':h} for a,s,h in RANGES],
|
||||
'instructions':compiler.instructions,'calls':compiler.calls,'memory_bytes':1048576,
|
||||
'static_page':{'original':'0x102fc2000','guest':65536},
|
||||
'scope':'constructor initialization only; static vtable identity is opaque; no virtual calls, free or C++ unwinding',
|
||||
'rea_assembly_evidence':['ev_1a00dc8f1f23ce6c1e54f8d189d163fbdd012621890e0288643b8f83a8b11611',
|
||||
'ev_b08fa6c3b275200505c3e78c71046526444b5c0ec603ef09d18d5db53dbf0eab'],
|
||||
'rea_exact_byte_records':'validated separately by tools/check_initialization_rea.py; see rea-provenance.json',
|
||||
'excluded_unwind_ranges':['0x101ab7ce0..0x101ab7d0c','0x10244a9a8..0x10244a9d4']}
|
||||
(out/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
(out/'verification.json').write_text(json.dumps({'status':'pass','native_object_graph_comparisons':512,
|
||||
'targets_per_graph':64,'allocations_per_graph':4,'heap_bytes_per_graph':2104,
|
||||
'allocation_failure_cases':8,'instructions':len(compiler.instructions),
|
||||
'scope':'normal constructor initialization and allocation aborts; no C++ unwinding'},indent=2)+'\n')
|
||||
print('PASS: original TargetDeserialiser graph, bounded heap, desktop oracle and iOS library')
|
||||
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Statically compile the original MapVersion constructor and its declared I/O adapter."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from a64_lift import Compiler
|
||||
from lift_rng import arm64_image, file_range
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ADDRESS, SIZE = 0x10150BF28, 192
|
||||
EXPECTED = 'b9254262cde89c9e79a9fdf6d700860ff9fd623d53c576efaf431600096c8a9b'
|
||||
READ = 0x10035E264
|
||||
|
||||
|
||||
def run(*args):
|
||||
print('+', ' '.join(map(str, args)), flush=True)
|
||||
subprocess.run(list(map(str, args)), cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('binary', type=Path)
|
||||
args = parser.parse_args()
|
||||
image, slice_offset = arm64_image(args.binary.read_bytes())
|
||||
offset = file_range(image, ADDRESS, SIZE)
|
||||
body = image[offset:offset + SIZE]
|
||||
if hashlib.sha256(body).hexdigest() != EXPECTED:
|
||||
raise ValueError('Unrecognized MapVersion constructor bytes')
|
||||
compiler = Compiler({READ: 'read_or_throw'})
|
||||
wat = compiler.compile(body, ADDRESS)
|
||||
out = ROOT / 'build/version'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / 'factorio_version.wat').write_text(wat)
|
||||
manifest = {'symbol': 'MapVersion::MapVersion(Deserialiser&)', 'address': hex(ADDRESS),
|
||||
'source_file_offset': slice_offset + offset, 'bytes': SIZE, 'sha256': EXPECTED,
|
||||
'instructions': compiler.instructions, 'host_calls': compiler.calls,
|
||||
'scope': 'automatic straight-line integer lift; host read errors return status rather than C++ exceptions'}
|
||||
(out / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
assembly = ['.text', '.p2align 2', '.globl _factorio_version_original', '_factorio_version_original:']
|
||||
for (word,) in struct.iter_unpack('<I', body):
|
||||
assembly.append(' bl _native_version_read' if word & 0xfc000000 == 0x94000000 else f' .long 0x{word:08x}')
|
||||
(out / 'oracle.S').write_text('\n'.join(assembly) + '\n')
|
||||
run(ROOT / 'build/wabt/wat2wasm', out / 'factorio_version.wat', '-o', out / 'factorio_version.wasm')
|
||||
run(ROOT / 'build/wabt/wasm2c', out / 'factorio_version.wasm', '-o', out / 'factorio_version.c', '--module-name=factorio_version')
|
||||
includes = ['-I' + str(out), '-I' + str(ROOT / 'runtime'), '-I' + str(ROOT / 'third_party/wabt/wasm2c')]
|
||||
flags = ['-O2', '-DWASM_RT_USE_MMAP=0', '-DWASM_RT_MEMCHECK_BOUNDS_CHECK=1']
|
||||
sources = [out / 'factorio_version.c', ROOT / 'runtime/version_bridge.c', ROOT / 'runtime/save_header.c']
|
||||
runtime = [ROOT / 'third_party/wabt/wasm2c' / x for x in ['wasm-rt-impl.c', 'wasm-rt-mem-impl.c']]
|
||||
run('xcrun', 'clang', *flags, *includes, *sources, *runtime, out / 'oracle.S',
|
||||
ROOT / 'tests/version_equivalence.c', '-lz', '-o', out / 'verify_version')
|
||||
run(out / 'verify_version')
|
||||
run('xcrun', 'clang', *flags, *includes, *sources, *runtime, ROOT / 'tests/save_probe.c',
|
||||
'-lz', '-o', out / 'save_probe')
|
||||
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
|
||||
objects = []
|
||||
for source in sources:
|
||||
obj = out / (source.stem + '.ios.o')
|
||||
run('xcrun', '--sdk', 'iphoneos', 'clang', '-target', 'arm64-apple-ios15.0', '-isysroot', sdk,
|
||||
*flags, *includes, '-c', source, '-o', obj)
|
||||
objects.append(obj)
|
||||
run('xcrun', 'libtool', '-static', '-o', out / 'libfactorio_version_ios.a', *objects)
|
||||
(out / 'verification.json').write_text(json.dumps({'status': 'pass', 'native_comparisons': 20000,
|
||||
'instruction_count': len(compiler.instructions), 'host_calls': len(compiler.calls),
|
||||
'scope': 'MapVersion constructor success path; stream errors use a host status adapter',
|
||||
'whole_map_loading': False}, indent=2) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check build-5 constructor, actual-save and regression reports from the iPhone."""
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'build/deserialiser'
|
||||
names=('deserialiser','save-initialization','target','layout','map-header','version','version-probe')
|
||||
reports={name:json.loads((OUT/('iphone-'+name+'-result.json')).read_text()) for name in names}
|
||||
assert all(r['app_build']=='5' and r['app_version']=='0.1.0' for r in reports.values())
|
||||
assert len({r['run_id'] for r in reports.values()})==1 and len({r['build_id'] for r in reports.values()})==1
|
||||
times=[datetime.fromisoformat(r['recorded_at'].replace('Z','+00:00')) for r in reports.values()]
|
||||
assert (max(times)-min(times)).total_seconds()<30
|
||||
constructor=reports['deserialiser'];actual=reports['save-initialization']
|
||||
assert constructor['status']=='pass' and constructor['code']==0 and constructor['newer_version_status']==7
|
||||
assert actual['status']==0 and actual['version']==[2,0,77,0,0]
|
||||
for report in (constructor,actual):
|
||||
for key,value in {'allocations':12,'reads':5,'target_capacity':64,'heap_bytes':10712,
|
||||
'stream_cursor':9,'owns_target':1}.items():assert report[key]==value
|
||||
assert reports['target']['status']=='pass' and reports['target']['initialized_targets']==64
|
||||
assert reports['version']['status']=='pass' and reports['version-probe']['status']==0
|
||||
assert reports['map-header']['status']=='pass' and reports['map-header']['modern_64_bit'] and reports['map-header']['legacy_32_bit_sentinel']
|
||||
desktop=json.loads((ROOT/'build/save-layout/desktop-real-save.json').read_text())
|
||||
for report in (actual,reports['layout'],reports['version-probe']):
|
||||
assert report['status']==0 and report['input_sha256']==desktop['input_sha256']
|
||||
assert len({reports[name]['probe_id'] for name in ('save-initialization','layout','version-probe')})==1
|
||||
for key in ('header_offset','next_offset','mod_count','tick_fields_hex'):
|
||||
assert reports['layout'][key]==desktop['decoded_layout'][key]
|
||||
binary=ROOT/'build/ios-device/Build/Products/Release-iphoneos/FactorioPort.app/FactorioPort'
|
||||
assert constructor['build_id'].encode() in binary.read_bytes()
|
||||
dependencies=subprocess.check_output(['xcrun','otool','-L',str(binary)],text=True)
|
||||
imports=subprocess.check_output(['xcrun','nm','-u',str(binary)],text=True)
|
||||
symbols=subprocess.check_output(['xcrun','nm',str(binary)],text=True)
|
||||
assert 'steam' not in (dependencies+imports).lower()
|
||||
for symbol in ('_factorio_rng_original','_factorio_version_original','_factorio_map_header_original',
|
||||
'_factorio_target_original','_factorio_deser_original','_native_deser_allocate'):
|
||||
assert symbol not in symbols
|
||||
result={'status':'pass','app_build':'5','run_id':constructor['run_id'],'build_id':constructor['build_id'],
|
||||
'device':'iPhone 15 Pro','constructor':constructor,'actual_save_initialization':actual,
|
||||
'same_launch_regressions':'pass','steam_dependency_or_import':False,'desktop_oracle_symbols':False,
|
||||
'signed_binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||
'scope':'original constructor initialized from actual-save prefix; no loaded world or gameplay'}
|
||||
(OUT/'device-verification.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print('PASS: build 5 MapDeserialiser, actual-save prefix, same-launch regressions and signed binary audit')
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify initialization inputs against complete, source-bound REA byte records."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from lift_rng import arm64_image,file_range
|
||||
from build_target_deserialiser import RANGES as TARGET_RANGES
|
||||
from build_deserialiser import RANGES as DESER_RANGES
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('binary',type=Path);parser.add_argument('bundle',type=Path)
|
||||
args=parser.parse_args();image,_=arm64_image(args.binary.read_bytes())
|
||||
source_hash=hashlib.sha256(image).hexdigest()
|
||||
bundle=json.loads(args.bundle.read_text())
|
||||
for name,ranges in [('target-deserialiser',TARGET_RANGES),('deserialiser',DESER_RANGES)]:
|
||||
checks=[]
|
||||
for address,size,expected in ranges:
|
||||
offset=file_range(image,address,size);data=image[offset:offset+size]
|
||||
assert hashlib.sha256(data).hexdigest()==expected
|
||||
matches=[]
|
||||
for record in bundle['records']:
|
||||
if record.get('operation')!='read_bytes' or record.get('confidence')!='observed' or record.get('authority')!='shipped-artifact':continue
|
||||
if record.get('subject',{}).get('digest',{}).get('sha256')!=source_hash:continue
|
||||
value=record['normalized_result']
|
||||
if value['address']==hex(address) and value['complete'] and value['returned_bytes']==size and value['requested_bytes']==size:
|
||||
if bytes.fromhex(value['bytes_hex'])!=data:raise ValueError('REA bytes contradict the pinned local input')
|
||||
matches.append(record['evidence_id'])
|
||||
if not matches:raise ValueError(f'Missing complete REA byte record at {address:#x} ({size} bytes)')
|
||||
checks.append({'address':hex(address),'bytes':size,'sha256':expected,'evidence_ids':matches})
|
||||
report={'status':'pass','arm64_sha256':source_hash,'checks':checks,
|
||||
'scope':'exact translation input bytes; not whole-engine behavioral equivalence'}
|
||||
(ROOT/'build'/name/'rea-provenance.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
print(f'PASS: {name}, {len(checks)} pinned input ranges match complete REA observations')
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bind local RNG translation inputs to complete, exported REA byte evidence."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from lift_rng import arm64_image, file_range, FUNCTION_VA, BODY_SIZE, SEED_VA, SEED_SIZE, CONSTANT_VA
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("binary", type=Path)
|
||||
parser.add_argument("bundle", type=Path)
|
||||
parser.add_argument("--include-version", action="store_true")
|
||||
parser.add_argument("--include-map-header", action="store_true")
|
||||
parser.add_argument("--include-map-header-entry", action="store_true")
|
||||
args = parser.parse_args()
|
||||
data = args.binary.read_bytes()
|
||||
image, _ = arm64_image(data)
|
||||
slice_hash = hashlib.sha256(image).hexdigest()
|
||||
bundle = json.loads(args.bundle.read_text())
|
||||
checks = []
|
||||
ranges = [(FUNCTION_VA, BODY_SIZE), (SEED_VA, SEED_SIZE), (CONSTANT_VA, 32)]
|
||||
if args.include_version:
|
||||
ranges.append((0x10150bf28, 192))
|
||||
if args.include_map_header:
|
||||
ranges.append((0x1014c6aac, 332))
|
||||
if args.include_map_header_entry:
|
||||
ranges.append((0x10010582c, 4))
|
||||
for address, length in ranges:
|
||||
matches = []
|
||||
offset = file_range(image, address, length)
|
||||
expected = image[offset:offset + length]
|
||||
for record in bundle["records"]:
|
||||
if record.get("operation") != "read_bytes":
|
||||
continue
|
||||
if record.get("authority") != "shipped-artifact" or record.get("confidence") != "observed":
|
||||
continue
|
||||
if record.get("subject", {}).get("digest", {}).get("sha256") != slice_hash:
|
||||
continue
|
||||
value = record["normalized_result"]
|
||||
if (value["address"] == hex(address) and value["complete"]
|
||||
and value["returned_bytes"] == length and value["requested_bytes"] == length):
|
||||
if bytes.fromhex(value["bytes_hex"]) != expected:
|
||||
raise ValueError("REA byte evidence contradicts the local input")
|
||||
matches.append(record["evidence_id"])
|
||||
if not matches:
|
||||
raise ValueError(f"No complete matching REA byte evidence for {hex(address)}")
|
||||
checks.append({"address": hex(address), "bytes": length, "evidence_ids": matches,
|
||||
"sha256": hashlib.sha256(expected).hexdigest()})
|
||||
report = {"status": "pass", "scope": "translation input bytes, not full behavioral equivalence",
|
||||
"artifact_sha256": hashlib.sha256(data).hexdigest(), "arm64_slice_sha256": slice_hash,
|
||||
"checks": checks}
|
||||
destination = "build/map-header/rea-provenance.json" if args.include_map_header else \
|
||||
"build/version/rea-provenance.json" if args.include_version else "build/rng/rea-provenance.json"
|
||||
Path(destination).write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate downloaded build-4 reports and audit the signed iOS executable."""
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
OUT=ROOT/'build/target-deserialiser'
|
||||
reports={name:json.loads((OUT/('iphone-'+name+'-result.json')).read_text())
|
||||
for name in ('target','layout','map-header','version')}
|
||||
target=reports['target']
|
||||
assert all(r['app_build']=='4' and r['app_version']=='0.1.0' for r in reports.values())
|
||||
assert len({r['run_id'] for r in reports.values()})==1
|
||||
assert len({r['build_id'] for r in reports.values()})==1
|
||||
times=[datetime.fromisoformat(r['recorded_at'].replace('Z','+00:00')) for r in reports.values()]
|
||||
assert (max(times)-min(times)).total_seconds()<30
|
||||
assert target['status']=='pass' and target['code']==0
|
||||
assert target['allocations']==4 and target['pool_capacity']==64 and target['initialized_targets']==64
|
||||
assert target['heap_bytes']==2104
|
||||
assert reports['version']['status']=='pass'
|
||||
assert reports['map-header']['status']=='pass'
|
||||
assert reports['map-header']['modern_64_bit'] and reports['map-header']['legacy_32_bit_sentinel']
|
||||
layout=reports['layout']
|
||||
desktop=json.loads((ROOT/'build/save-layout/desktop-real-save.json').read_text())
|
||||
assert layout['status']==0 and layout['input_sha256']==desktop['input_sha256']
|
||||
for key in ('header_offset','next_offset','mod_count','tick_fields_hex'):
|
||||
assert layout[key]==desktop['decoded_layout'][key]
|
||||
binary=ROOT/'build/ios-device/Build/Products/Release-iphoneos/FactorioPort.app/FactorioPort'
|
||||
assert target['build_id'].encode() in binary.read_bytes()
|
||||
dependencies=subprocess.check_output(['xcrun','otool','-L',str(binary)],text=True)
|
||||
imports=subprocess.check_output(['xcrun','nm','-u',str(binary)],text=True)
|
||||
symbols=subprocess.check_output(['xcrun','nm',str(binary)],text=True)
|
||||
assert 'steam' not in (dependencies+imports).lower()
|
||||
for symbol in ('_factorio_rng_original','_factorio_version_original','_factorio_map_header_original',
|
||||
'_factorio_target_original','_native_target_allocate'):
|
||||
assert symbol not in symbols
|
||||
report={'status':'pass','app_build':'4','run_id':target['run_id'],'build_id':target['build_id'],
|
||||
'device':'iPhone 15 Pro','target_constructor':target,
|
||||
'same_launch_version_map_header_and_actual_save':'pass',
|
||||
'input_sha256':layout['input_sha256'],'steam_dependency_or_import':False,
|
||||
'desktop_oracle_symbols':False,'signed_binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||
'scope':'original initialization graph and existing header probes; no loaded world or gameplay'}
|
||||
(OUT/'device-verification.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
print('PASS: build 4 same-launch device reports, actual-save regression, signed binary dependency/oracle audit')
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Specialized, byte-guarded static lift of one Factorio 2.0.77 function.
|
||||
|
||||
This is NOT a general ARM64 lifter or a translated game engine. Original code
|
||||
and constants are read locally; no game binary is stored in this source file.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
FUNCTION_VA = 0x1004DFD14
|
||||
CONSTANT_VA = 0x10296B4E8
|
||||
SEED_VA = 0x101AA1848
|
||||
SEED_SIZE = 24
|
||||
EXPECTED_SEED = "b1d9c0b86c5590e9c62c7e433ef7b8635d963dd7db98939ebda558004260c7ef"
|
||||
BODY_SIZE = 104
|
||||
EXPECTED_BODY = "4fd978c1f02f6c828413991ba37e9d84f5a7805d4c75603ff61f7ef9759db9d3"
|
||||
|
||||
|
||||
def arm64_image(data):
|
||||
if data[:4] == bytes.fromhex("cffaedfe"):
|
||||
if struct.unpack_from("<I", data, 4)[0] != 0x100000C:
|
||||
raise ValueError("Expected ARM64 Mach-O")
|
||||
return data, 0
|
||||
if data[:4] != bytes.fromhex("cafebabe"):
|
||||
raise ValueError("Expected FAT32 or thin ARM64 Mach-O")
|
||||
count = struct.unpack_from(">I", data, 4)[0]
|
||||
if count > 32 or 8 + count * 20 > len(data):
|
||||
raise ValueError("Invalid FAT table")
|
||||
for i in range(count):
|
||||
cpu, _, offset, size, _ = struct.unpack_from(">IIIII", data, 8 + i * 20)
|
||||
if cpu == 0x100000C:
|
||||
if offset + size > len(data):
|
||||
raise ValueError("Truncated ARM64 slice")
|
||||
return data[offset:offset + size], offset
|
||||
raise ValueError("No ARM64 slice")
|
||||
|
||||
|
||||
def file_range(image, address, size):
|
||||
cursor = 32
|
||||
for _ in range(struct.unpack_from("<I", image, 16)[0]):
|
||||
command, length = struct.unpack_from("<II", image, cursor)
|
||||
if length < 8 or cursor + length > len(image):
|
||||
raise ValueError("Invalid Mach-O load command")
|
||||
if command == 0x19:
|
||||
va, _, offset, file_size = struct.unpack_from("<QQQQ", image, cursor + 24)
|
||||
if va <= address and address + size <= va + file_size:
|
||||
result = offset + address - va
|
||||
if result + size > len(image):
|
||||
raise ValueError("Truncated segment")
|
||||
return result
|
||||
cursor += length
|
||||
raise ValueError("Requested range is not backed by one file segment")
|
||||
|
||||
|
||||
def generate(binary, output):
|
||||
data = binary.read_bytes()
|
||||
image, slice_offset = arm64_image(data)
|
||||
body_offset = file_range(image, FUNCTION_VA, BODY_SIZE)
|
||||
body = image[body_offset:body_offset + BODY_SIZE]
|
||||
if hashlib.sha256(body).hexdigest() != EXPECTED_BODY:
|
||||
raise ValueError("Unrecognized function bytes; refusing to translate another build")
|
||||
seed_offset = file_range(image, SEED_VA, SEED_SIZE)
|
||||
seed_body = image[seed_offset:seed_offset + SEED_SIZE]
|
||||
if hashlib.sha256(seed_body).hexdigest() != EXPECTED_SEED:
|
||||
raise ValueError("Unrecognized seed constructor bytes")
|
||||
constants_offset = file_range(image, CONSTANT_VA, 32)
|
||||
constants = image[constants_offset:constants_offset + 32]
|
||||
shifts = struct.unpack("<6i2I", constants)
|
||||
if shifts != (13, 2, -19, -25, 12, 4, 0xFFFFE000, 0xFFFFFF80):
|
||||
raise ValueError("Unrecognized SIMD shift/mask constants")
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
# Scalarize two NEON lanes; i32 operations preserve modulo-2^32 arithmetic.
|
||||
# bfxil w8,w9,#11,#21 replaces the low 21 bits of (c << 17).
|
||||
wat = '''(module
|
||||
(memory (export "memory") 1)
|
||||
(func (export "seed") (param $state i32) (param $seed i32)
|
||||
(local $value i32)
|
||||
(local.set $value (select (local.get $seed) (i32.const 341)
|
||||
(i32.gt_u (local.get $seed) (i32.const 341))))
|
||||
(i32.store (local.get $state) (local.get $value))
|
||||
(i32.store offset=4 (local.get $state) (local.get $value))
|
||||
(i32.store offset=8 (local.get $state) (local.get $value)))
|
||||
(func (export "get_int") (param $state i32) (result i32)
|
||||
(local $a i32) (local $b i32) (local $c i32)
|
||||
(local.set $a (i32.load (local.get $state)))
|
||||
(local.set $b (i32.load offset=4 (local.get $state)))
|
||||
(local.set $c (i32.load offset=8 (local.get $state)))
|
||||
(local.set $a
|
||||
(i32.or
|
||||
(i32.shr_u (i32.xor (i32.shl (local.get $a) (i32.const 13)) (local.get $a)) (i32.const 19))
|
||||
(i32.and (i32.shl (local.get $a) (i32.const 12)) (i32.const -8192))))
|
||||
(local.set $b
|
||||
(i32.or
|
||||
(i32.shr_u (i32.xor (i32.shl (local.get $b) (i32.const 2)) (local.get $b)) (i32.const 25))
|
||||
(i32.and (i32.shl (local.get $b) (i32.const 4)) (i32.const -128))))
|
||||
(local.set $c
|
||||
(i32.or
|
||||
(i32.and (i32.shl (local.get $c) (i32.const 17)) (i32.const -2097152))
|
||||
(i32.shr_u (i32.xor (local.get $c) (i32.shl (local.get $c) (i32.const 3))) (i32.const 11))))
|
||||
(i32.store (local.get $state) (local.get $a))
|
||||
(i32.store offset=4 (local.get $state) (local.get $b))
|
||||
(i32.store offset=8 (local.get $state) (local.get $c))
|
||||
(i32.xor (i32.xor (local.get $a) (local.get $b)) (local.get $c))))
|
||||
'''
|
||||
(output / "factorio_rng.wat").write_text(wat)
|
||||
# Native oracle: retain all source instructions except the four ADRP page
|
||||
# references, relocated to the same extracted constants in this test image.
|
||||
assembly = [".text", ".p2align 2", ".globl _factorio_rng_original", "_factorio_rng_original:"]
|
||||
for offset in range(0, len(body), 4):
|
||||
word = struct.unpack_from("<I", body, offset)[0]
|
||||
if offset in (4, 20, 32, 44):
|
||||
assembly.append(" adrp x8, Lrng_constants_page@PAGE")
|
||||
else:
|
||||
assembly.append(f" .long 0x{word:08x}")
|
||||
assembly += [".p2align 2", ".globl _factorio_rng_seed_original", "_factorio_rng_seed_original:"]
|
||||
assembly += [f" .long 0x{struct.unpack_from('<I', seed_body, i)[0]:08x}" for i in range(0, SEED_SIZE, 4)]
|
||||
assembly += [".section __TEXT,__const", ".p2align 12", "Lrng_constants_page:",
|
||||
" .space 0x4e8", " .byte " + ",".join(str(x) for x in constants)]
|
||||
(output / "oracle.S").write_text("\n".join(assembly) + "\n")
|
||||
manifest = {
|
||||
"status": "two-function-specialized-lift", "symbol": "RandomGenerator::getInt()",
|
||||
"artifact_sha256": hashlib.sha256(data).hexdigest(), "slice_offset": slice_offset,
|
||||
"function_va": hex(FUNCTION_VA), "function_size": BODY_SIZE,
|
||||
"function_file_offset": slice_offset + body_offset,
|
||||
"function_sha256": EXPECTED_BODY, "constants_va": hex(CONSTANT_VA),
|
||||
"seed_constructor": {"symbol": "RandomGenerator::RandomGenerator(unsigned int)",
|
||||
"va": hex(SEED_VA), "size": SEED_SIZE,
|
||||
"file_offset": slice_offset + seed_offset, "sha256": EXPECTED_SEED},
|
||||
"constants_file_offset": slice_offset + constants_offset,
|
||||
"constants_sha256": hashlib.sha256(constants).hexdigest(),
|
||||
"translation": "Manually reviewed NEON scalarization, not general automatic lifting",
|
||||
"abi": "seed(i32 state_offset, i32 seed); get_int(i32 state_offset) -> i32; 12 state bytes",
|
||||
"oracle": "Original instruction words with four ADRP relocations; no game startup or platform calls",
|
||||
}
|
||||
(output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
print(json.dumps(manifest, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("binary", type=Path)
|
||||
parser.add_argument("--output", type=Path, default=Path("build/rng"))
|
||||
args = parser.parse_args()
|
||||
generate(args.binary, args.output)
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize observed REA loader frontiers without inferring graph closure."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOTS = ['0x1014c93f8', '0x1014c9618', '0x1000fa1d0', '0x1014c6aac']
|
||||
TARGET = '2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7'
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('bundle', type=Path)
|
||||
args = parser.parse_args()
|
||||
records = json.loads(args.bundle.read_text())['records']
|
||||
records = [r for r in records if r.get('authority') == 'shipped-artifact'
|
||||
and r.get('confidence') == 'observed'
|
||||
and r.get('subject', {}).get('digest', {}).get('sha256') == TARGET]
|
||||
roots = []
|
||||
for address in ROOTS:
|
||||
selected = [r for r in records if r.get('parameters', {}).get('procedure') == address]
|
||||
calls = [r for r in selected if r['operation'] == 'procedure_callees']
|
||||
if not calls:
|
||||
raise ValueError('Missing observed call frontier for ' + address)
|
||||
record = calls[-1]
|
||||
info = [r['normalized_result'] for r in selected if r['operation'] == 'procedure_info']
|
||||
assembly = [r for r in selected if r['operation'] == 'procedure_assembly']
|
||||
roots.append({'address': address, 'direct_callees': record['normalized_result'],
|
||||
'direct_callee_count': len(record['normalized_result']),
|
||||
'evidence_id': record['evidence_id'], 'relationship_coverage': record.get('relationship_coverage'),
|
||||
'procedure': info[-1] if info else None,
|
||||
'unresolved_indirect_sites': [line for line in assembly[-1]['normalized_result'].splitlines()
|
||||
if ': blr ' in line or ': br ' in line] if assembly else None,
|
||||
'scope': 'observed immediate frontier; not transitive dependency closure'})
|
||||
output = Path('build/startup/loader-frontier.json')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps({'status': 'not-runnable', 'arm64_sha256': TARGET,
|
||||
'roots': roots}, indent=2) + '\n')
|
||||
print('PASS: observed direct call frontiers:', ', '.join(str(r['direct_callee_count']) for r in roots))
|
||||
@@ -0,0 +1,35 @@
|
||||
// Builds the 1024x1024 iOS app icon from the game's own 128 px icon (read from the local install).
|
||||
// The gear is scaled with high-quality interpolation onto an opaque dark background
|
||||
// (iOS app icons must be opaque).
|
||||
// Usage: make_icon SOURCE_PNG OUTPUT_PNG
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@autoreleasepool {
|
||||
if (argc != 3) { fprintf(stderr, "usage: make_icon SOURCE_PNG OUTPUT_PNG\n"); return 2; }
|
||||
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:@(argv[1])], NULL);
|
||||
CGImageRef gear = src ? CGImageSourceCreateImageAtIndex(src, 0, NULL) : NULL;
|
||||
if (!gear) { fprintf(stderr, "cannot read %s\n", argv[1]); return 1; }
|
||||
const size_t size = 1024;
|
||||
CGColorSpaceRef rgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef ctx = CGBitmapContextCreate(NULL, size, size, 8, 0, rgb, (CGBitmapInfo)kCGImageAlphaNoneSkipLast);
|
||||
// Dark warm gradient, close to Factorio's menu backdrop.
|
||||
CGFloat stops[] = {0.20, 0.17, 0.14, 1, 0.07, 0.06, 0.05, 1};
|
||||
CGGradientRef gradient = CGGradientCreateWithColorComponents(rgb, stops, NULL, 2);
|
||||
CGContextDrawRadialGradient(ctx, gradient, CGPointMake(size / 2.0, size * 0.55), 0,
|
||||
CGPointMake(size / 2.0, size / 2.0), size * 0.75, kCGGradientDrawsAfterEndLocation);
|
||||
CGContextSetInterpolationQuality(ctx, kCGInterpolationHigh);
|
||||
CGFloat inset = size * 0.12;
|
||||
CGContextDrawImage(ctx, CGRectMake(inset, inset, size - 2 * inset, size - 2 * inset), gear);
|
||||
CGImageRef icon = CGBitmapContextCreateImage(ctx);
|
||||
CGImageDestinationRef dst = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:@(argv[2])],
|
||||
CFSTR("public.png"), 1, NULL);
|
||||
CGImageDestinationAddImage(dst, icon, NULL);
|
||||
BOOL ok = CGImageDestinationFinalize(dst);
|
||||
CFRelease(dst); CGImageRelease(icon); CGGradientRelease(gradient); CGContextRelease(ctx);
|
||||
CGColorSpaceRelease(rgb); CGImageRelease(gear); CFRelease(src);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve every import of the macOS ARM64 slice against the iOS SDK's text stubs.
|
||||
|
||||
Usage: python3 tools/native_audit.py build/factorio.arm64 [--json out.json]
|
||||
|
||||
For each LC_LOAD_DYLIB the import is looked up in the iOS library with the same
|
||||
framework name, following re-exported libraries. Imports that do not resolve
|
||||
must be supplied by a shim. Nothing on the device is consulted, so this is a
|
||||
link-level audit only; Objective-C selectors are not checked.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SDK = Path(subprocess.check_output(["xcrun", "--sdk", "iphoneos", "--show-sdk-path"], text=True).strip())
|
||||
|
||||
|
||||
def tbd_documents():
|
||||
"""Yield (install_name, symbols, reexports) for every tbd document in the SDK."""
|
||||
files = list((SDK / "usr/lib").rglob("*.tbd")) + list((SDK / "System/Library").rglob("*.tbd"))
|
||||
for path in files:
|
||||
text = path.read_text(errors="ignore")
|
||||
for doc in re.split(r"^--- !tapi-tbd", text, flags=re.M):
|
||||
name = re.search(r"^install-name:\s*'?([^'\n]+)'?", doc, re.M)
|
||||
if not name:
|
||||
continue
|
||||
exports = doc.split("\nreexports:")[0].split("\nundefineds:")[0]
|
||||
symbols = set()
|
||||
for kind, prefix in (("symbols", ""), ("weak-symbols", ""), ("thread-local-symbols", ""),
|
||||
("objc-classes", "class"), ("objc-eh-types", "eh"), ("objc-ivars", "ivar")):
|
||||
for block in re.findall(rf"\b{kind}:\s*\[([^\]]*)\]", exports):
|
||||
for item in block.split(","):
|
||||
item = item.strip().strip("'\"")
|
||||
if not item:
|
||||
continue
|
||||
if prefix == "class":
|
||||
symbols.update({f"_OBJC_CLASS_$_{item}", f"_OBJC_METACLASS_$_{item}"})
|
||||
elif prefix == "eh":
|
||||
symbols.add(f"_OBJC_EHTYPE_$_{item}")
|
||||
elif prefix == "ivar":
|
||||
symbols.add(f"_OBJC_IVAR_$_{item}")
|
||||
else:
|
||||
symbols.add(item)
|
||||
reexports = set()
|
||||
for block in re.findall(r"reexported-libraries:.*?libraries:\s*\[([^\]]*)\]", doc, re.S):
|
||||
reexports.update(x.strip().strip("'\"") for x in block.split(",") if x.strip())
|
||||
yield name.group(1), symbols, reexports
|
||||
|
||||
|
||||
def main():
|
||||
binary = sys.argv[1]
|
||||
libs = {}
|
||||
for name, symbols, reexports in tbd_documents():
|
||||
entry = libs.setdefault(name, [set(), set()])
|
||||
entry[0] |= symbols
|
||||
entry[1] |= reexports
|
||||
|
||||
def resolves(install, symbol, seen=()):
|
||||
if install not in libs or install in seen:
|
||||
return False
|
||||
symbols, reexports = libs[install]
|
||||
return symbol in symbols or any(resolves(r, symbol, seen + (install,)) for r in reexports)
|
||||
|
||||
def ios_path(mac_path):
|
||||
if mac_path in libs: # e.g. IOKit keeps its Versions/A install name on iOS
|
||||
return mac_path
|
||||
m = re.match(r"/System/Library/Frameworks/(\w+)\.framework/", mac_path)
|
||||
if m:
|
||||
return f"/System/Library/Frameworks/{m.group(1)}.framework/{m.group(1)}"
|
||||
return mac_path
|
||||
|
||||
loads = re.findall(r"cmd LC_LOAD(?:_WEAK)?_DYLIB\n\s+cmdsize \d+\n\s+name (\S+)",
|
||||
subprocess.check_output(["otool", "-l", binary], text=True))
|
||||
short = {Path(p).name.replace(".dylib", "").split(".")[0]: p for p in loads}
|
||||
undef = subprocess.check_output(["nm", "-m", "-u", binary], text=True)
|
||||
report = {}
|
||||
for line in undef.splitlines():
|
||||
m = re.search(r"external (\S+) \(from (\S+)\)", line)
|
||||
if not m:
|
||||
continue
|
||||
symbol, lib = m.groups()
|
||||
mac = short.get(lib)
|
||||
target = ios_path(mac) if mac else None
|
||||
status = "resolved" if target and resolves(target, symbol) else "missing"
|
||||
lib_report = report.setdefault(lib, {"mac_path": mac, "ios_path": target,
|
||||
"ios_library_exists": target in libs,
|
||||
"resolved": 0, "missing": []})
|
||||
if status == "resolved":
|
||||
lib_report["resolved"] += 1
|
||||
else:
|
||||
lib_report["missing"].append(symbol)
|
||||
for lib, r in sorted(report.items(), key=lambda kv: -len(kv[1]["missing"])):
|
||||
print(f"{lib:20} exists={r['ios_library_exists']!s:5} resolved={r['resolved']:4} missing={len(r['missing']):3} "
|
||||
+ " ".join(r["missing"][:12]) + (" ..." if len(r["missing"]) > 12 else ""))
|
||||
if "--json" in sys.argv:
|
||||
Path(sys.argv[sys.argv.index("--json") + 1]).write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and audit an unsigned iOS prototype; does not sign or install it."""
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
subprocess.run(["xcodegen", "generate", "--spec", "platform/ios/project.yml"], cwd=root, check=True)
|
||||
build = subprocess.run([
|
||||
"xcodebuild", "-quiet", "-project", "platform/ios/FactorioPort.xcodeproj",
|
||||
"-scheme", "FactorioPort", "-sdk", "iphoneos", "-destination", "generic/platform=iOS",
|
||||
"-configuration", "Release", "-derivedDataPath", "build/ios", "CODE_SIGNING_ALLOWED=NO", "build"
|
||||
], cwd=root, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(root / "build/ios-build.log").write_text(build.stdout)
|
||||
if build.returncode:
|
||||
print(build.stdout[-12000:])
|
||||
raise SystemExit(build.returncode)
|
||||
app = root / "build/ios/Build/Products/Release-iphoneos/FactorioPort.app"
|
||||
binary = app / "FactorioPort"
|
||||
dependencies = subprocess.check_output(["xcrun", "otool", "-L", str(binary)], text=True)
|
||||
imports = subprocess.check_output(["xcrun", "nm", "-u", str(binary)], text=True)
|
||||
if "steam" in (dependencies + imports).lower():
|
||||
raise RuntimeError("Steam dependency/import found in local single-player target")
|
||||
ipa = root / "build/FactorioRNG-unsigned.ipa"
|
||||
with zipfile.ZipFile(ipa, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for path in sorted(app.rglob("*")):
|
||||
if path.is_file():
|
||||
archive.write(path, Path("Payload") / app.name / path.relative_to(app))
|
||||
report = {"status": "pass", "ios_build": "unsigned device build", "device_execution": "not run",
|
||||
"steam_dependency_or_import": False,
|
||||
"binary_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||
"dynamic_dependencies": dependencies.splitlines()[1:], "ipa": str(ipa)}
|
||||
(root / "build/ios-verification.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,51 @@
|
||||
// A temporary MCP client; does not modify any saved MCP client or REA configuration.
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { createInterface } from 'node:readline';
|
||||
|
||||
const [packageRoot, hopperLauncher] = process.argv.slice(2);
|
||||
if (!packageRoot || !hopperLauncher) {
|
||||
throw new Error('Usage: node tools/rea-session.mjs REA_PACKAGE_ROOT HOPPER_LAUNCHER (the vendor hopper helper)');
|
||||
}
|
||||
const dependencies = resolve(packageRoot, '..', '@modelcontextprotocol');
|
||||
const { Client } = await import(pathToFileURL(join(dependencies, 'client/dist/index.mjs')));
|
||||
const { StdioClientTransport } = await import(pathToFileURL(join(dependencies, 'client/dist/stdio.mjs')));
|
||||
const transport = new StdioClientTransport({
|
||||
command: join(packageRoot, '..', '.bin', 'rea'), args: ['--mcp'],
|
||||
env: { ...process.env, HOPPER_LAUNCHER_PATH: hopperLauncher,
|
||||
REA_HOPPER_STARTUP_TIMEOUT_MS: '600000' },
|
||||
stderr: 'pipe',
|
||||
});
|
||||
// Consume diagnostics without printing inherited environment or bridge credentials.
|
||||
transport.stderr?.on('data', () => {});
|
||||
const client = new Client({ name: 'factorio-port-investigation', version: '0.1.0' });
|
||||
const allowed = new Set(['open_binary', 'binary_session', 'list_documents',
|
||||
'search_procedures', 'procedure_assembly', 'procedure_info', 'read_bytes',
|
||||
'search_strings', 'read_function_instructions', 'procedure_callers', 'procedure_callees',
|
||||
'procedure_pseudo_code', 'analyze_function', 'export_evidence_bundle', 'close_binary']);
|
||||
await mkdir('evidence', { recursive: true });
|
||||
await client.connect(transport);
|
||||
console.log('REA MCP connected. Enter one JSON tool request per line; quit closes the target.');
|
||||
const input = createInterface({ input: process.stdin, terminal: false });
|
||||
let sequence = 0;
|
||||
try {
|
||||
for await (const line of input) {
|
||||
if (line.trim() === 'quit') break;
|
||||
try {
|
||||
const request = JSON.parse(line);
|
||||
if (!allowed.has(request.name)) throw new Error('Tool outside investigation allowlist');
|
||||
console.log(`Calling ${request.name}`);
|
||||
const result = await client.callTool(request, { timeout: 660000 });
|
||||
const file = `evidence/${Date.now()}-${++sequence}-${request.name}.json`;
|
||||
await writeFile(file, JSON.stringify(result, null, 2), { mode: 0o600 });
|
||||
const data = result.structuredContent ?? result.content?.filter(x => x.type === 'text').map(x => {
|
||||
try { return JSON.parse(x.text); } catch { return x.text; }
|
||||
});
|
||||
console.log(JSON.stringify({ file, isError: result.isError ?? false, data }).slice(0, 14000));
|
||||
} catch (error) { console.log(JSON.stringify({ error: error.message })); }
|
||||
}
|
||||
} finally {
|
||||
try { await client.callTool({ name: 'close_binary', arguments: {} }); }
|
||||
finally { await client.close(); }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record exact local executable and installed data identities, without copying assets."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import plistlib
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from lift_rng import arm64_image
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
EXPECTED_BINARY = '00b014d78df07e05ab1cee853904c8f7b3814ca652c043461a9d49fbad46cdf6'
|
||||
EXPECTED_SLICE = '2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7'
|
||||
|
||||
|
||||
def digest(path):
|
||||
result = hashlib.sha256()
|
||||
with path.open('rb') as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b''):
|
||||
result.update(block)
|
||||
return result.hexdigest()
|
||||
|
||||
|
||||
def command(*args):
|
||||
return subprocess.check_output(args, cwd=ROOT, text=True).strip()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('app', type=Path)
|
||||
args = parser.parse_args()
|
||||
app = args.app.resolve(strict=True)
|
||||
binary = app / 'Contents/MacOS/factorio'
|
||||
source = binary.read_bytes()
|
||||
image, offset = arm64_image(source)
|
||||
binary_hash, slice_hash = hashlib.sha256(source).hexdigest(), hashlib.sha256(image).hexdigest()
|
||||
if binary_hash != EXPECTED_BINARY or slice_hash != EXPECTED_SLICE:
|
||||
raise ValueError('Target differs from the inspected Factorio 2.0.77 build')
|
||||
info = plistlib.loads((app / 'Contents/Info.plist').read_bytes())
|
||||
assets = app / 'Contents/data'
|
||||
entries = []
|
||||
for path in sorted(assets.rglob('*')):
|
||||
if path.is_symlink():
|
||||
raise ValueError(f'Asset symlink requires explicit inventory policy: {path}')
|
||||
if path.is_file():
|
||||
entries.append({'path': path.relative_to(assets).as_posix(),
|
||||
'bytes': path.stat().st_size, 'sha256': digest(path)})
|
||||
if not entries:
|
||||
raise ValueError('No installed data files found')
|
||||
packages = []
|
||||
for path in sorted(assets.glob('*/info.json')):
|
||||
metadata = json.loads(path.read_text())
|
||||
packages.append({'directory': path.parent.name, 'name': metadata.get('name'),
|
||||
'version': metadata.get('version'), 'dependencies': metadata.get('dependencies')})
|
||||
asset_manifest = {'scope': 'all regular files under installed Contents/data; no user mods or saves',
|
||||
'files': entries, 'file_count': len(entries),
|
||||
'total_bytes': sum(e['bytes'] for e in entries), 'packages': packages,
|
||||
'gameplay_configuration': 'not selected; inventory does not prove save compatibility'}
|
||||
out = ROOT / 'build/target'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
asset_path = out / 'assets.json'
|
||||
asset_path.write_text(json.dumps(asset_manifest, indent=2) + '\n')
|
||||
report = {'recorded_at': datetime.now(timezone.utc).isoformat(), 'app': str(app),
|
||||
'version': info.get('CFBundleShortVersionString'), 'binary_sha256': binary_hash,
|
||||
'arm64_sha256': slice_hash, 'arm64_slice_offset': offset, 'arm64_bytes': len(image),
|
||||
'binary_uuids': command('xcrun', 'dwarfdump', '--uuid', str(binary)).splitlines(),
|
||||
'xcode': command('xcodebuild', '-version'),
|
||||
'iphoneos_sdk': command('xcrun', '--sdk', 'iphoneos', '--show-sdk-version'),
|
||||
'wabt_revision': command('git', '-C', str(ROOT / 'third_party/wabt'), 'rev-parse', 'HEAD'),
|
||||
'asset_manifest_sha256': digest(asset_path), 'asset_file_count': len(entries),
|
||||
'source_files': {str(p.relative_to(ROOT)): digest(p) for base in ('tools', 'runtime', 'platform/ios', 'tests')
|
||||
for p in sorted((ROOT / base).glob('*'))
|
||||
if p.is_file() and p.suffix in ('.py', '.mjs', '.c', '.h', '.m', '.yml')}}
|
||||
(out / 'manifest.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(f'PASS: pinned executable and ARM64 slice; recorded {len(entries)} asset files ({asset_manifest["total_bytes"]} bytes)')
|
||||
print(out / 'manifest.json')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retarget the unmodified macOS ARM64 slice into an iOS-loadable dylib.
|
||||
|
||||
Usage: python3 tools/retarget.py build/factorio.arm64 build/native/Factorio.dylib
|
||||
|
||||
Only Mach-O metadata changes; every instruction and data byte stays the same:
|
||||
* MH_EXECUTE -> MH_DYLIB, plus an LC_ID_DYLIB in the header padding.
|
||||
* __PAGEZERO shrinks to one page just below __TEXT, so dyld can slide the image.
|
||||
Segment indices stay unchanged, so rebase/bind opcodes remain valid.
|
||||
* LC_BUILD_VERSION platform macOS -> iOS.
|
||||
* macOS framework paths -> iOS paths, or to @loader_path shims for frameworks
|
||||
(or individual symbols) that iOS lacks. Shims re-export the real framework
|
||||
when one exists.
|
||||
The host app reads LC_MAIN at runtime to find the original entry point.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
EXPECTED_SHA256 = "2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7"
|
||||
MH_MAGIC_64, MH_EXECUTE, MH_DYLIB = 0xFEEDFACF, 2, 6
|
||||
MH_NO_REEXPORTED_DYLIBS = 0x100000
|
||||
LC_SEGMENT_64, LC_ID_DYLIB, LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB = 0x19, 0xD, 0xC, 0x80000018
|
||||
LC_BUILD_VERSION, LC_MAIN = 0x32, 0x80000028
|
||||
PLATFORM_IOS = 2
|
||||
MIN_IOS = (17 << 16)
|
||||
SDK_IOS = (26 << 16) | (5 << 8)
|
||||
INSTALL_NAME = "@rpath/Factorio.dylib"
|
||||
|
||||
# Framework name -> replacement install name. Absent: use the iOS path of the same framework.
|
||||
SHIMS = {
|
||||
"AppKit": "@loader_path/fx_AppKit.dylib",
|
||||
"Cocoa": "@loader_path/fx_Cocoa.dylib",
|
||||
"Carbon": "@loader_path/fx_Carbon.dylib",
|
||||
"OpenGL": "@loader_path/fx_OpenGL.dylib",
|
||||
"OpenCL": "@loader_path/fx_OpenCL.dylib",
|
||||
"ForceFeedback": "@loader_path/fx_ForceFeedback.dylib",
|
||||
"LDAP": "@loader_path/fx_LDAP.dylib",
|
||||
"ApplicationServices": "@loader_path/fx_ApplicationServices.dylib",
|
||||
# Present on iOS but missing some imported symbols: shim re-exports the real framework.
|
||||
"CoreGraphics": "@loader_path/fx_CoreGraphics.dylib",
|
||||
"CoreVideo": "@loader_path/fx_CoreVideo.dylib",
|
||||
"CoreServices": "@loader_path/fx_CoreServices.dylib",
|
||||
"Security": "@loader_path/fx_Security.dylib",
|
||||
"Metal": "@loader_path/fx_Metal.dylib",
|
||||
"Foundation": "@loader_path/fx_Foundation.dylib",
|
||||
"IOKit": "@loader_path/fx_IOKit.dylib",
|
||||
"CoreAudio": "@loader_path/fx_CoreAudio.dylib",
|
||||
"AudioToolbox": "@loader_path/fx_AudioToolbox.dylib",
|
||||
}
|
||||
|
||||
|
||||
def cstr(data, offset):
|
||||
return data[offset:data.index(b"\0", offset)].decode()
|
||||
|
||||
|
||||
def main():
|
||||
source, output = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
data = bytearray(source.read_bytes())
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if digest != EXPECTED_SHA256:
|
||||
raise SystemExit(f"unexpected input {digest}; only the inspected 2.0.77 ARM64 slice is supported")
|
||||
magic, _cpu, _sub, filetype, ncmds, sizeofcmds, flags, _ = struct.unpack_from("<IiiIIIII", data, 0)
|
||||
if magic != MH_MAGIC_64 or filetype != MH_EXECUTE:
|
||||
raise SystemExit("input is not a 64-bit Mach-O executable")
|
||||
|
||||
report = {"input_sha256": digest, "dylibs": [], "changes": []}
|
||||
offset, first_section = 32, None
|
||||
text_vmaddr = None
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, offset)
|
||||
if cmd == LC_SEGMENT_64:
|
||||
segname = data[offset + 8:offset + 24].rstrip(b"\0").decode()
|
||||
vmaddr, vmsize, fileoff, filesize = struct.unpack_from("<QQQQ", data, offset + 24)
|
||||
nsects = struct.unpack_from("<I", data, offset + 64)[0]
|
||||
if segname == "__TEXT":
|
||||
text_vmaddr = vmaddr
|
||||
for s in range(nsects):
|
||||
sect_offset = struct.unpack_from("<I", data, offset + 72 + s * 80 + 48)[0]
|
||||
if sect_offset and (first_section is None or sect_offset < first_section):
|
||||
first_section = sect_offset
|
||||
if segname == "__PAGEZERO":
|
||||
pagezero = offset
|
||||
elif cmd == LC_BUILD_VERSION:
|
||||
platform, minos, sdk = struct.unpack_from("<III", data, offset + 8)
|
||||
struct.pack_into("<III", data, offset + 8, PLATFORM_IOS, MIN_IOS, SDK_IOS)
|
||||
report["changes"].append({"build_version": [platform, hex(minos), hex(sdk)],
|
||||
"to": [PLATFORM_IOS, hex(MIN_IOS), hex(SDK_IOS)]})
|
||||
elif cmd == LC_MAIN:
|
||||
report["entryoff"] = struct.unpack_from("<Q", data, offset + 8)[0]
|
||||
elif cmd in (LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB):
|
||||
name_offset = struct.unpack_from("<I", data, offset + 8)[0]
|
||||
old = cstr(data, offset + name_offset)
|
||||
framework = None
|
||||
if old.startswith("/System/Library/Frameworks/"):
|
||||
framework = old.split("/")[4].removesuffix(".framework")
|
||||
if framework in SHIMS:
|
||||
new = SHIMS[framework]
|
||||
elif framework == "IOKit":
|
||||
new = old
|
||||
elif framework:
|
||||
new = f"/System/Library/Frameworks/{framework}.framework/{framework}"
|
||||
else:
|
||||
new = old # libSystem, libobjc, libresolv, @loader_path/libsteam_api.dylib
|
||||
room = cmdsize - name_offset
|
||||
encoded = new.encode() + b"\0"
|
||||
if len(encoded) > room:
|
||||
raise SystemExit(f"{new} does not fit in {room} bytes")
|
||||
data[offset + name_offset:offset + cmdsize] = encoded.ljust(room, b"\0")
|
||||
report["dylibs"].append({"from": old, "to": new, "weak": cmd == LC_LOAD_WEAK_DYLIB})
|
||||
offset += cmdsize
|
||||
|
||||
if text_vmaddr is None or first_section is None:
|
||||
raise SystemExit("__TEXT segment not found")
|
||||
# Shrink __PAGEZERO to a single 16 KiB page directly below __TEXT.
|
||||
struct.pack_into("<QQ", data, pagezero + 24, text_vmaddr - 0x4000, 0x4000)
|
||||
report["changes"].append({"pagezero": [hex(text_vmaddr - 0x4000), "0x4000"]})
|
||||
|
||||
# Append LC_ID_DYLIB in the padding before the first section.
|
||||
name = INSTALL_NAME.encode() + b"\0"
|
||||
id_size = (24 + len(name) + 7) & ~7
|
||||
end = 32 + sizeofcmds
|
||||
if end + id_size > first_section:
|
||||
raise SystemExit("no header padding for LC_ID_DYLIB")
|
||||
if any(data[end:end + id_size]):
|
||||
raise SystemExit("header padding is not empty")
|
||||
struct.pack_into("<IIIIII", data, end, LC_ID_DYLIB, id_size, 24, 2, 0x10000, 0x10000)
|
||||
data[end + 24:end + 24 + len(name)] = name
|
||||
struct.pack_into("<IIIII", data, 12, MH_DYLIB, ncmds + 1, sizeofcmds + id_size,
|
||||
flags | MH_NO_REEXPORTED_DYLIBS, 0)
|
||||
report["changes"].append({"filetype": "MH_EXECUTE->MH_DYLIB", "install_name": INSTALL_NAME})
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(data)
|
||||
report["output_sha256"] = hashlib.sha256(data).hexdigest()
|
||||
changed = sum(1 for a, b in zip(data, source.read_bytes()) if a != b)
|
||||
report["changed_bytes"] = changed
|
||||
report["changed_bytes_outside_header"] = sum(
|
||||
1 for i, (a, b) in enumerate(zip(data[first_section:], source.read_bytes()[first_section:])) if a != b)
|
||||
if report["changed_bytes_outside_header"]:
|
||||
raise SystemExit("retarget changed bytes outside the Mach-O header")
|
||||
(output.parent / "retarget.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in report.items() if k != "dylibs"}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Export the observed startup frontier; never equates discovery with runnable code."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
bundle = json.loads(Path('evidence/factorio-functions-final.json').read_text())
|
||||
record = next(r for r in bundle['records'] if r['operation'] == 'analyze_function'
|
||||
and r['parameters']['procedure'] == '0x100161584')
|
||||
native = record['normalized_result']
|
||||
callees = []
|
||||
for callee in native['callees']:
|
||||
symbol = callee['name']
|
||||
route = 'exclude-optional-steam' if 'Steam' in symbol else 'host-abi-adapter' if symbol.startswith('imp___stubs') else 'pending-lift'
|
||||
callees.append({'address': callee['address'], 'symbol': symbol,
|
||||
'planned_route': route, 'route_authority': 'analyst planning from symbol identity'})
|
||||
indirect = [line for line in native['assembly'] if re.search(r': blr\s', line)]
|
||||
result = {'status': 'not-runnable', 'root': native['procedure']['name'],
|
||||
'root_address': '0x100161584', 'evidence_id': record['evidence_id'],
|
||||
'observed_direct_callees': len(callees), 'observed_basic_blocks': len(native['basic_blocks']),
|
||||
'observed_indirect_call_sites': indirect, 'callees': callees,
|
||||
'limitations': ['This is one observed function frontier, not a transitive closure.',
|
||||
'Hopper does not expose complete body ranges or all indirect targets.',
|
||||
'The straight-line lifter cannot yet compile this startup function.',
|
||||
'Graphical startup includes inlined Steam initialization; changing the factory alone is insufficient.']}
|
||||
out = Path('build/startup'); out.mkdir(parents=True, exist_ok=True)
|
||||
(out / 'frontier.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps({key: result[key] for key in ['status', 'root_address', 'evidence_id',
|
||||
'observed_direct_callees', 'observed_basic_blocks']}, indent=2))
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Symbolize [fx-crash] lines from build/native/console.log against the original factorio.dSYM.
|
||||
|
||||
Usage: python3 tools/symbolize_native.py [console.log]
|
||||
Factorio.dylib offsets map to the original executable's addresses as 0x100000000 + offset.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DSYM = Path.home() / ("Library/Application Support/Steam/steamapps/common/Factorio/"
|
||||
"factorio.app/Contents/MacOS/factorio.dSYM/Contents/Resources/DWARF/factorio")
|
||||
log = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "build/native/console.log"
|
||||
lines = [l.rstrip() for l in log.read_text(errors="replace").splitlines() if "[fx-crash]" in l]
|
||||
addrs = []
|
||||
for line in lines:
|
||||
m = re.search(r"Factorio\.dylib\+(0x[0-9a-f]+)", line)
|
||||
if m:
|
||||
addrs.append(hex(0x100000000 + int(m.group(1), 16)))
|
||||
symbols = iter(subprocess.check_output(["atos", "-o", str(DSYM), "-arch", "arm64", "-l", "0x100000000", *addrs],
|
||||
text=True).splitlines()) if addrs else iter(())
|
||||
for line in lines:
|
||||
if "Factorio.dylib+" in line:
|
||||
print(line.split("[fx-crash] ", 1)[1], "=>", next(symbols))
|
||||
elif not re.search(r"\] x\d+ =", line):
|
||||
print(line.split("[fx-crash] ", 1)[1])
|
||||
Reference in new issue
Block a user