About

A native port of the engine behind the four .hack games for the PlayStation 2 (CyberConnect2 / Bandai, 2002-2003): .hack//Infection, .hack//Mutation, .hack//Outbreak and .hack//Quarantine. It is written in Rust from a reverse engineering of the games, and it plays your own discs.

Status: work in progress.

partdiscwhere it stands
.hack//InfectionSLUS_202.67Playable from power-on to the ending: the whole story, the staff roll and the save after it
.hack//MutationSLUS_205.62The whole story, all 16 events, plays through under the test autopilot from the new game to the ending; not yet played through by hand
.hack//OutbreakSLUS_205.63The whole story, all 19 events, plays through under the test autopilot from the new game to the ending and staff roll; not yet played through by hand
.hack//QuarantineSLUS_205.64Boots: the title, the opening cut scenes with their sound, the desktop, the Root Towns; its story comes after Outbreak's

What is still missing is listed in GAPS.md, and the known bugs in BUGS.md.

Not an emulator

An emulator runs the PlayStation 2's code: it imitates the console's processors and the game runs as it did on the hardware. This project runs none of that code.

  • What runs is the engine rewritten in Rust, part by part: the game loop and its tasks, the event-script interpreter, the battle rules and the AI, the Chaos Gate's area-word generator, the fields, dungeons and Root Towns, the menus, the ALTIMIT desktop, the in-engine cut scenes, the sound driver and synthesizer, and the movie decoder. The picture is drawn with wgpu from the port's own draw lists, and the sound is mixed by the port.
  • How we know it matches. Each part is checked against the game's own code: the original functions are run one instruction at a time in a small interpreter written for this project, and the port has to give the same results. More than 850 tests hold it to that.
  • What it reads is your discs, once. piney-build reads them into a build and generates the tables the engine needs from the discs' own executables. During play nothing from the PlayStation 2 runs, and no BIOS is needed.

What owning the engine gives you

The games' systems become ordinary source code, so you can read them, change them and build on them. You do not have to patch a binary.

Today:

  • One program for all four parts. One build holds your discs and one memory card serves all four parts. It opens on the four-part selector that Outbreak's disc carries but never uses.

  • Every Root Town from any disc. The console's town N reaches Mac Anu, Dun Loireag, Carmina Gadelica, Fort Ouph and Lia Fail from any part.

  • A console (F1 or `) into the running game. It has god mode, items and gold, warps to any town, and any character into the party whatever the story says. It also sets event flags and the bracelet's infection, and restarts the game at any story event (help lists them all).

  • A scriptable, repeatable game. The same input gives the same game.

    • --pad-log FILE records your pad from power-on, or type pad_log in the console at any time to write the run so far and go on recording; --replay FILE plays either back.
    • --press scripts button presses and --console scripts console commands.
    • --mode story:N starts at story event N.
    • --shot and --webp capture frames and video without a window.

    The test suite plays the story this way and checks what the game draws and plays.

  • A viewer (piney-viewer) for every scene on the discs: models, animations, towns as they are assembled, and any dungeon or field from its seed.

  • Unused content, reachable. Outbreak's unused cut scenes play with --mode loose:STREAM/STRT.BIN:N. The earlier builds of the later towns left in Infection's executable are documented.

  • Documentation for everyone.

    • docs/ is the reference for the formats and the engine.
    • WORKLOG.md records how each piece was worked out.
    • piney-build --export-symbols DIR writes the function and data names of all four executables for other reverse engineers.

Possible because of it (not done yet):

  • Changing the rules in code: the battle, the AI, items and skills, the area words, the story's events. The event scripts already have a readable text form of their own.
  • Fixing the originals' bugs, or keeping them. Either way it is a choice made in code.
  • Drawing beyond the PS2, such as other resolutions and aspect ratios, since the picture is the port's own.
  • Running on any platform that Rust, wgpu and cpal reach.

Getting started

The game is built from source on your own computer, then made from your own discs. It takes four steps the first time; after that, updating is two commands.

What you need

  • Your own disc images (.iso) of the North American releases: Infection SLUS_202.67, Mutation SLUS_205.62, Outbreak SLUS_205.63, Quarantine SLUS_205.64. One disc is enough to play that part. Other regions are not supported.
  • A graphics card with Vulkan, Metal or DirectX 12: most from the last ten years, with current drivers.
  • Disk space: about 2 GB per disc for the game, and a few GB more for compiling.

1. Install the tools (once)

You need Git and Rust (the current stable version).

  • Windows: install Git for Windows and run rustup-init.exe from rustup.rs. When it asks, let it install the Visual Studio C++ build tools. Use the commands below in PowerShell or the "Developer PowerShell".
  • Linux: install Git, a C compiler, pkg-config, and the ALSA (sound) and udev (gamepads) headers, then Rust from rustup.rs:
    • Debian, Ubuntu, Mint: sudo apt install git build-essential pkg-config libasound2-dev libudev-dev
    • Fedora: sudo dnf install git gcc pkgconf-pkg-config alsa-lib-devel systemd-devel
    • Arch: sudo pacman -S git base-devel alsa-lib systemd-libs
    • then: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • macOS: run xcode-select --install, then install Rust from rustup.rs.

2. Download and compile (once)

git clone https://github.com/Toyz/piney_apples.git
cd piney_apples
cargo build --release

The first compile takes a while. The programs end up in target/release/ (.exe on Windows).

3. Make the game from your discs (once)

Give piney-build your disc images, or a folder holding them:

# Linux and macOS
target/release/piney-build ~/isos/infection.iso ~/isos/mutation.iso

# Windows (PowerShell)
target\release\piney-build.exe "D:\isos\infection.iso" "D:\isos\mutation.iso"

It tells the discs apart by their contents, checks every file it reads, and prints where the game went:

  • Linux: ~/.local/share/piney/game
  • Windows: %APPDATA%\piney\game
  • macOS: ~/Library/Application Support/piney/game

Got another disc later? Run it again with just that disc: the ones already in are kept. Your disc images are not needed after this step.

4. Play

target/release/piney-game           # Windows: target\release\piney-game.exe

With more than one part made, it opens on the four-part selector; --volume 1 (2, 3, 4) starts a part directly. You can also play one image without step 3: piney-game --iso path/to/infection.iso.

On a console the game softens the picture for an interlaced TV, mixing each line with the one above. The port shows it sharp, as PCSX2 does by default. --deflicker (or deflicker on in the console) shows it as the console did.

The picture can be drawn sharper than the PS2's own resolution: --render-scale N (or render_scale N in the console) draws at N times it, 1 to 8. --no-vsync (vsync off) stops waiting for the display's refresh, and --fps-cap N (fps_cap N, 0 for none) limits the pictures a second. The game itself runs at its own rate whatever these are.

--hud-scale N (hud_scale N in the console, 0.5 to 1) draws the HUD smaller: the party panels toward the bottom left corner, the map toward the top right, the target's window toward the top left. Render scale does not do this: it adds detail, while the picture, HUD included, always fills the window as the PS2's did.

Updating

git pull
cargo build --release

If the game then says no port data of version N ... run piney-build again, run target/release/piney-build with no arguments. It remakes the game from the discs already in it; your saves are not touched.

If something goes wrong

  • It will not start, or says no graphics adapter: update your graphics drivers. On Linux, make sure the Vulkan driver for your card is installed (mesa-vulkan-drivers on Debian and Ubuntu, vulkan-radeon or vulkan-intel on Arch).
  • No sound: the game plays on your system's default output device. Check it there, and that the in-game Option menu's volumes are up.
  • A gamepad does nothing: plug it in before starting the game, then press a button on it (the game reads the pad last used).
  • Messages: the game's warnings and errors go to the terminal it was started from, and also to the console (F1). PINEY_LOG picks how much is shown: PINEY_LOG=debug piney-game for everything, or one part, e.g. PINEY_LOG=piney_world=debug,warn. The default is warn,piney=info.
  • Anything else: see Reporting a bug below.

Controls

PS2 padkeyboardgamepad
D-padarrow keysD-pad
Cross / CircleZ / Xsouth / east button
Square / TriangleA / Swest / north button
L1 / R1Q / Wshoulders
L2 / R21 / 2triggers
L3 / R3C / Vstick clicks
Start / SelectEnter / BackspaceOptions / Create (or Start / Back)
left stick(none)left stick
right stick (camera)I / J / K / Lright stick

Any gamepad that gilrs knows works, with its buttons where a DualShock 2 has them. With more than one connected, the one that last sent input is read. Escape asks to quit, and F1 or ` opens the console. Its history scrolls with Page Up / Page Down, the mouse wheel or the scroll bar on its right; Tab completes a command.

Reporting a bug

Bugs go to the issues page: say which part (Infection, Mutation...), where you were, what you did, and what you expected. Give the build too: it is in brackets after "Ported by helba" on the disc menu, the first line piney-game prints, and the console's version (a 7-digit commit such as fc82e63; "unknown" for a tree downloaded as a ZIP, so clone with git if you can). A screenshot helps; a recording helps most:

Open the console (F1 or `) and type pad_log. The game has been keeping every frame's pad since power-on, so this writes the whole run so far and keeps recording. It prints where the file went: padlogs/ in the build's folder, next to a .card folder with your memory card as it was at power-on. Play until the bug shows, then type pad_log stop. Attach the log and the .card folder (zipped) to the issue. piney-game --replay FILE plays the run back exactly, console commands included.

Saves and settings

Saves go to one memory card in the build's folder (memcard/slot1), shared by all four parts. The options (volumes, voice language, screen position, vibration, the message window) are kept in settings.toml in the same folder. Whatever any part's Option menu sets, every part starts with.

Saves from PCSX2 can be brought over. Point the port at PCSX2's memory card file (Mcd001.ps2 in PCSX2's memcards folder), at a folder memory card, or at one exported save folder such as BASLUS-20267DOTHACK:

piney-game --import-card path/to/Mcd001.ps2

or type import_card path/to/Mcd001.ps2 in the console (F1 or `). The four parts' save folders are copied onto the port's card. A save it replaces is kept in a backup-... folder beside the card. Only the North American discs' saves are recognised.

A single slot file (dhdata01 to dhdata12, or a folder of them) can be imported the same way. A slot shows as used only through its entry in its folder's index file, so copying the file in by hand leaves it "unused"; the import writes that entry for it. A later part's slot file needs the part named (--volume 2, or the console of a running game).

This repository does not include the games. Everything the port plays comes from your discs.

For developers

The port is a Cargo workspace under crates/:

cratewhat
piney-datathe disc: ISO 9660, the DATA.BIN gzip archive, CCSF scene files - textures, palettes, models, clumps, animation playback - and the engine's generators (towns' placement tables, dungeons, fields)
piney-viewerevery DATA.BIN scene textured, posed and playing, towns as their placement tables assemble them, generated dungeons and fields, with mouse, keyboard or a gamepad; --shot renders one to PNG without a window
piney-inputthe pad as ccPad::Read reads it: held, pressed, released, repeat, the stick as a direction
piney-drawone frame as the game hands it to the GS: primitives, model draws, run-time textures, blend and test state
piney-gsdraws those frames as the GS does, on wgpu, in the game's 512 x 448 frame buffer
piney-gamethe game from power-on: the title with its movies and intro stream, New Game, the desktop with the game's own event scripts, the Root Towns, fields and dungeons, their cut scenes, voices and sound; modes on the game's own frame clock, pad or keyboard in, GS frames out
piney-worldThe World: the Root Towns, fields and dungeons as their tasks run them, the camera, the characters, the party and its AI
piney-battlethe battle's rules: stats, hits, skills, items, conditions, the enemies' and members' AI, Data Drain
piney-effectthe effects: skills, spells, transfers, the particles
piney-fielduithe field's menus and HUD
piney-audiothe game's sound: sound effects and the sequenced music through ports of the IOP's MIDI sequencer and hardware synthesizer, an SPU2 model mixing at 48 kHz, BGM.BIN's streams; cpal output or headless WAV
piney-desktopthe ALTIMIT desktop (DESKTOP.PRG) as a state machine: the opening, the main screen, the mailer, News, Accessory, Audio and Data (saving to a memory card), the event scripts' message windows and the START menu
piney-demothe title screen (DEMO.PRG) as a state machine: the memory-card check, the logo movies and intro stream, the menu (New Game, Load, Option), and New Game's hand-off to the desktop
piney-toppageTHE WORLD's top page: the board, the news and the log in
piney-mpegthe PSS movies: our own MPEG-2 decoder and the IPU's colour conversion, every picture exact against ffmpeg, and the movies' SPU2-format sound
piney-eventthe event scripts: our own instruction set and its text form, the scripts and messages read from the disc and converted without loss, and the interpreter checked against the game's own code
piney-streamthe in-engine cut scenes (ccRequestLoadStream): a stream's files read from STREAM/*.BIN, its scenes built and played frame by frame as the game's InitScene / DecodeFrameSection do them, pad in, draw list and PCM out
piney-buildone game from your discs: the disc images' files cut into chunks, each kept once and zstd-compressed, into a build piney-game finds by itself; the port's data generated from the discs into it
piney-genthe table generator: each volume's engine tables read out of its executable through Infection's DWARF types, into the build; the carry that finds Infection's globals in the later volumes; and the symbol transfer that names the stripped executables
piney-eemuthe EE interpreter the checks run the game's own code in
cargo run --release -p piney-viewer -- town01
cargo run --release -p piney-viewer -- --dungeon 1234,0,5
cargo run --release -p piney-viewer -- --field 777,2
cargo run --release -p piney-game -- --mode story:11      # the game at story event 11
cargo run --release -p piney-audio --example render -- desktop 50 out.wav
cargo run --release -p piney-stream --example stream_shot -- --stream 2 --frame 600 --out stream.png
cargo test --workspace      # the checks; the disc tests need the images under work/

For the checks and the tools, put your disc images in originals/ and extract them under work/ (both are ignored by git). docs/ is the reference: what the formats are and how the engine works, with an honesty status on every page. WORKLOG.md is how each piece was worked out.

Tools

The reverse engineering started on a machine with no tools for it, so they are written here as the work needs them, under tools/, in Python with nothing outside the standard library:

toolwhat it does
tools/iso.pylist, extract and cat files from an ISO 9660 DVD image; map an LBA back to a file
tools/elf.pythe EE executable: headers, sections, symbols, reads by VA
tools/mips.pyR5900 instruction decoder: base MIPS, EE extensions, MMI, FPU, VU0 macro mode
tools/image.pythe program image: main plus one overlay, symbols and relocations; a stripped volume's .syms sidecar
tools/disasm.pydisassemble a function or range, exact xrefs from the relocations
tools/fdtbl.pythe DATA.BIN index compiled into the executable; check verifies it against the archive
tools/gzarc.pylist, cat and extract the sector-aligned gzip archives (DATA.BIN, STREAM/*.BIN)
tools/ccs.pyCCSF scene files: header, name table, objects, chunk walk
tools/dwarf1.pythe DWARF 1 debug information: functions, types, globals, lines, per-unit headers
tools/demangle.pyCodeWarrior C++ symbol demangler, works like c++filt
tools/ccstex.pytextures and palettes out of CCSF files, as PNG
tools/ccsmodel.pymodels out of CCSF files as Wavefront OBJ, optionally posed from an animation or a cutscene frame
tools/areas.pythe Chaos Gate keywords, the story areas, and the area generator reproduced exactly
tools/tables.pyany global table decoded through its DWARF type: enemies, skills, items, equipment, bosses
tools/sound.pysound banks, voice lines, streamed music and cutscene audio, through the game's own indexes
tools/scei.pySony's SCEI .hd / .sq sound-bank formats
tools/adpcm.pyPS-ADPCM decoder
tools/midi.py, tools/hsyn.pymodels of the IOP's MIDI sequencer and hardware synthesizer, checked against the modules
tools/irx.pyIOP module (.IRX) disassembler: imports, exports, functions
tools/iopemu.pyIOP modules run in eemu.py, the synthesizer's register writes recorded
tools/sound_ee.pythe game's EE sound code run in eemu.py, what it sends to the IOP recorded
tools/wav.pyWAV writer
tools/vu.pyVU0/VU1 microcode: labels, disassembly, and the game's model packets built in the interpreter
tools/vif.pyVIF code streams, DMA tags and GIFtags
tools/evscript.pyevent scripts: list, disassemble with dialogue and voice inlined, survey, check against the game code
tools/dungeon.pydungeon layouts from the area seed, reproduced exactly, as ASCII maps
tools/battle.pythe battle rules reproduced: effective stats, hit and damage, protect break, status, experience, Data Drain
tools/field.pyfield terrain and objects from the area seed, with PNG height maps
tools/anim.pyAnime chunks evaluated at any time as the game plays them: poses, texture offsets, morphs
tools/png.pyminimal PNG writer
tools/eemu.pya small EE interpreter: runs static initialisers, and game functions to check reimplementations against
tools/font.pytext rendering: glyph tables, measuring, and strings drawn to PNG as the game draws them
tools/text.pythe desktop's mail, bulletin board and news text, English or parody
tools/portdata.pythe port's data files (work/data, the build's PINEY/TABLES) read back for the other tools
tools/save.pythe memory-card save as each volume's own code writes it, and the carry-over into the next volume
tools/xfer.pycode compared across volumes: a body with its address fields masked, the addresses it forms, its opcode shape
tools/docs.pyregenerate and check the reference index

License

The code in this repository is licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

The license covers this repository's own code and writing only. The games themselves, their data, text, art, sound and code, belong to their owners and are not covered by it. .hack is a trademark of its owners; this project is not affiliated with or endorsed by CyberConnect2 or Bandai Namco.