Section 5.5 narrowed the search to 21 functions but could not name one, because the scan assumed the copy destination was an r1-relative stack local. It is not. Reading the known word-A writer instead of guessing its shape shows a frame register: bl singleton, addi r3,r31,96 (dest), addi r4,r11,80 (src = obj+80), bl copier, stw r10,96(r31). That one wrong assumption made the first scan return 0 for all 21, the known-good included - the third filter this week killed by its own control. With the real shape - addi r3,rF,K before the copier, then stores at K+d on rF - word B sits at d = 1956-80 = 1876, and exactly one of the 21 hits: sub_8219F2E8, copy dest r31+560, storing 2436(r31), and 560+1876 = 2436. Control: the word-A writer 0x821C1630 stores at relative +0, i.e. word A. The proof is the bias. Word B's writer computes addi r11,r11,-24 then 1 << (index-24) before or-ing into the copy; word A's writer shifts by the raw stage number with no bias. That reproduces section 5.1's read-side rule - n >= 24 means bit (n-24) of word B - on the write side, so word B's bits 0-5 are challenge stages 24-29, the same six that 5.4's MISSION_ID names. The two writers are exact complements. It has exactly one real caller, 0x821A0B1C inside 0x821A0AD0, whose strings are BASE_INFO and DEBRIEFING - so clearing a challenge mission is recorded from the debriefing screen. A second bitfield falls out of the same function: it reads the copy at +1880 (2440(r31) = obj+1960, the word after B), re-reads obj+1960 after the setter, xors old against new, and loops the 32 result bits calling 0x82175C20 with bit + 64. So obj+1960 is a second 32-bit progress bitfield with ids 64-95, whose newly-set bits are announced one at a time - an unlock-notification loop. Against 5.3's own prediction: neither sub_8219F2E8 nor its caller references Time or Points, 0 for both. 5.3 expected word B's writer to also store the stage's Time/Points record. It does not - this is the bit-setter only, and the record store is still unlocated. All seventeen artefacts byte-identical.
Project Sylpheed: Arc of Deception — Reborn
A clean-room, open-source reimplementation of Project Sylpheed: Arc of Deception (Xbox 360, 2006).
Built with Rust and the Bevy game engine. Runs natively on Windows, macOS, and Linux, and in the browser via WebAssembly.
Legal note: This project contains no original game code or assets. You must own a legitimate copy of Project Sylpheed to use this engine. Assets remain the intellectual property of SETA Corporation / Square Enix.
Current Status: Milestone 1 — Asset Explorer
- XISO disc image reading via
xdvdfs - Xbox 360 texture de-tiling (Morton / Z-order)
- XPR2 texture container parsing
- Bevy custom
AssetLoaderfor.xprtextures - Orbit camera viewer
- CLI tools: extract, list, sniff, texture info, texture export
- GitHub Actions CI (Windows + macOS + Linux + WASM)
- WASM / web build target
- Mesh format (reverse engineering in progress)
- Audio format (XMA → PCM pipeline)
- Mission data format
Repository Structure
sylpheed-reborn/
└── sylpheed-viewer/ ← Milestone 1: asset explorer (Rust/Bevy workspace)
├── Cargo.toml ← Workspace root
├── crates/
│ ├── sylpheed-formats/ # Format parsers — no Bevy dependency
│ │ ├── xiso.rs # XISO / XDVDFS disc image reader
│ │ ├── texture.rs # XPR2 container + DXT de-tiling (Morton)
│ │ ├── vfs.rs # Virtual filesystem + magic-byte sniffer
│ │ ├── mesh.rs # Mesh parser (stub — RE in progress)
│ │ └── audio.rs # Audio parser (stub — XMA TODO)
│ │
│ ├── sylpheed-viewer/ # Bevy application
│ │ ├── lib.rs # App setup + WASM entry point
│ │ ├── main.rs # Native binary entry point
│ │ ├── asset_loader.rs # Custom Bevy AssetLoaders
│ │ ├── camera.rs # Orbit camera (LMB orbit, RMB pan, scroll zoom)
│ │ └── ui.rs # egui file browser + RE notes panel
│ │
│ └── sylpheed-cli/ # Command-line tools
│ └── main.rs # extract / list / sniff / texture info commands
│
├── assets/ ← Extracted game files (gitignored)
├── .github/workflows/
│ └── ci.yml # CI: Windows + macOS + Linux + WASM
├── Trunk.toml # WASM build configuration
└── justfile # All build recipes
Getting Started
Prerequisites
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install just (task runner)
cargo install just
# For web builds only
cargo install trunk
rustup target add wasm32-unknown-unknown
Linux — Bevy system dependencies
sudo apt-get install -y \
libasound2-dev libudev-dev libwayland-dev \
libxkbcommon-dev libx11-dev libxi-dev pkg-config
Step 1 — Extract your disc
cd sylpheed-viewer
# Extract using the CLI tool
just extract /path/to/project_sylpheed.iso
# Or manually with xdvdfs
cargo install xdvdfs-cli
xdvdfs unpack project_sylpheed.iso ./assets/
Step 2 — Run the viewer
cd sylpheed-viewer
just run # Native viewer
just web # WASM dev server → http://localhost:8080
The asset viewer opens an orbit camera scene. Use the left panel to browse extracted game files. Click a .xpr file to preview it as a texture.
Build Commands
All commands run from the sylpheed-viewer/ directory.
just run # Run the native viewer (debug)
just run-dev # Run with hot-reloading
just build-native # Build native release binary
just web # WASM dev server at localhost:8080
just build-web # WASM release build → ./dist/
just extract game.iso # Extract an ISO to ./assets/
just sniff # Identify file formats in ./assets/
just sniff-unknown # Show only unrecognised formats (RE focus)
just test # Run all tests
just ci # Full CI: fmt + lint + test + WASM check
Technology Stack
| Layer | Choice | Reason |
|---|---|---|
| Language | Rust | Memory safety, performance, cross-platform |
| Game engine | Bevy 0.15 | ECS-first, WASM-native, data-driven |
| XISO reading | xdvdfs | Pure Rust, reads Xbox 360 disc images |
| Binary parsing | binrw | Derive-macro based, ideal for RE work |
| Web bundler | Trunk | Bevy's standard WASM build tool |
| CLI | clap | Asset extraction and inspection tools |
| Debug UI | bevy_egui | In-viewer asset browser and RE notes panel |
Key Architectural Decisions
sylpheed-formats has zero Bevy dependency. All binary format parsers live here and are testable with plain cargo test. Bevy integration is a thin layer on top in sylpheed-viewer.
WASM is a first-class target. XISO reading is gated behind #[cfg(not(target_arch = "wasm32"))] since the browser can't read local files. On the web, assets must be pre-extracted and served over HTTP.
Modding is designed in from the start. The virtual filesystem (vfs.rs) is the single choke point for all asset reads — a mod loader only needs to intercept that one layer to override any file.
Reverse Engineering Notes
Known file formats
| Path pattern | Format | Status | Notes |
|---|---|---|---|
*.XPR, *.XPR2 |
XPR2 texture | ✅ Parsing | DXT1/3/5, DXN, Morton de-tiling done |
DEFAULT.XEX |
Xbox EXE 2 | 🔬 Study | Main executable — load in Ghidra (PPC BE) |
*.XWB, *.XSB |
XACT audio | ⏳ TODO | Wave/sound banks; audio is XMA codec |
*.pak, *.p00 |
SETA archive | ⏳ Unknown | Paired header/data format |
| mesh files | Unknown | ⏳ TODO | Run just sniff-unknown to find candidates |
RE workflow
# 1. Extract and map the disc
just extract game.iso
just sniff-unknown # shows hex magic bytes of unknown files
# 2. Hex-inspect candidates
# Groups of 12 bytes at offsets: likely f32 XYZ vertices
# Groups of 6 bytes: likely u16 triangle indices
# 3. Cross-reference in Ghidra
# Load DEFAULT.XEX with PowerPC Big-Endian processor
# Search string refs to file extensions → find load functions
# 4. Implement parser
# Add binrw #[derive(BinRead)] struct in sylpheed-formats/src/mesh.rs
# cargo test -p sylpheed-formats
Next steps (Milestone 2)
- Mesh format — fingerprint with
sniff-unknown, implementmesh.rs - Audio — parse XWB headers, batch-convert XMA to WAV via
ffmpeg - Mission data — format unknown; starts after mesh/texture loading works
- Flight model — document by playing the original; implement as Bevy system
Constraints
- Never copy decompiled or disassembled game code — reimplement behavior through observation only.
- Assets stay gitignored —
assets/and*.isoare excluded. Never commit game files. - Keep
sylpheed-formatsBevy-free — parsers must be testable without a GPU. - WASM must always compile — CI enforces
cargo check --target wasm32-unknown-unknown. justfileis the source of truth for build commands — add new recipes there.
Milestone Roadmap
| Milestone | Goal | Status |
|---|---|---|
| 1 | Asset Explorer | 🚧 In Progress |
| 2 | Flying Tech Demo | ⏳ Planned |
| 3 | Combat Prototype | ⏳ Planned |
| 4 | Mission 1 Playable | ⏳ Planned |
| 5 | Full Game (all 16 missions) | ⏳ Planned |
| 6 | Mod SDK | ⏳ Planned |
Useful References
- xdvdfs — XISO disc image reader (used in this project)
- Xenia emulator — GPU/API reference (cloned at
../../xenia-canary/) - xboxdevwiki — Xbox 360 hardware documentation
- binrw docs — binary format parsing framework
- Free60 Project — open Xbox 360 hardware docs