Docs
How the port fits together, how to build it, and how to port the next piece. Everything builds in Docker.
Architecture
Two halves. The converters in tools/sh2conv read the PC game’s files once, on the player’s computer, and write the pack. The engine in port/ reads the pack from the SD card and runs the game, with logic ported from the Director’s Cut exe function by function and the PC address of each function in a comment next to it.
- The PC game:
sh2pc.exeanddata/from the player’s copy. - The converters (Python, run by the packer) write the pack.
- The pack goes to
sdmc:/3ds/sh23ds/, about 1.3 GB. - The engine (C with libctru and citro3d) reads it on the console.
The engine
| Files | What they do |
|---|---|
| main.c | Main loop, room changes, rendering |
| room.c, stagemdl.c, collide.c | Room geometry and textures, stage models, collision |
| gamecam.c, game/view/sh2cam.c | The PC camera module and its roads |
| envlight.c, chrlight.c, fog.c, shadow.c | Room and character lighting, the fog layer, flashlight and drop shadows |
| chr.c, move.c, controls.c | James’s animation, movement, button layouts |
| events.c, prog*.c | Stage events and the stage programs, one file per part of the game |
| enemy.c, combat.c, maria.c, blood.c | Enemies and bosses, weapons, Maria, blood |
| touchui.c, map.c, itemview.c | The touch screen: map, items, memos |
| titlemenu.c, optmenu.c, savescreen.c, gameover.c | Title, options, save files, game over |
| demo.c, movie.c, noise.c | In-engine cutscenes, FMV playback, the film grain |
| audio.c, bgm.c, sfx.c | Music, per-area music, sound effects |
| stream.c, profile.c, perf.c | Worker-thread streaming, the New and Old 3DS profiles, frame timing |
| *.v.pica | Vertex shaders |
Repository
port/ the 3DS engine: source/, meta/, build.sh, package.sh tools/sh2conv/ converters from the PC formats to the pack; pack.py is the step list tools/make_pack.sh runs pack.py natively tools/packer/ the packer image (Dockerfile, entry point) tools/packer-web/ the browser packer's tests (the page is site/packer.html) tools/texenc/ tex3ds's texture encoders, built to WebAssembly sh2-3ds-port-kit/ reverse-engineering kit: labels, Ghidra scripts docs/RESEARCH.md how the exe was mapped, key addresses, the work log site/ this website, deployed by GitHub Pages
Build from source
You need Docker: Docker Desktop, or the Docker CLI with Colima on macOS. The build runs in a devkitARM image pinned by digest, and the same commit always builds the same sh23ds.3dsx, byte for byte.
git clone https://github.com/ankushKun/sh23ds cd sh23ds ./port/build.sh ./port/package.sh
build.sh writes port/sh23ds.3dsx and sh23ds.elf, and passes its arguments to make, so ./port/build.sh clean works. package.sh builds the CIA with bannertool and makerom and copies everything to dist/ with SHA-256 sums.
| Variable | Default and use |
|---|---|
| SH23DS_BUILD_JOBS | 4, parallel make jobs |
| SH23DS_BUILD_IMAGE | The pinned devkitpro/devkitarm image |
| SH23DS_CIA_IMAGE | sh23ds-cia-tools:1.0.0, built from port/cia-tools.Dockerfile on first use |
Releases
The CIA asks for the 124 MB memory mode, the 804 MHz clock and the L2 cache (port/meta/template.rsf). The 3DSX gets whatever the Homebrew Launcher runs with. GitHub Actions builds both on every push and pull request.
- Bump
VERSION, write what changed in.github/releases/v<version>.mdand commit. - Push a matching tag, such as
v0.2.0. - CI checks the tag against
VERSION, verifies the checksums and publishes the release with those notes. The packer workflow pushesghcr.io/ankushkun/sh23ds-packerwith the same version andlatest.
The QR codes on the install guide point at releases/latest/download/, so they stay valid for every release. tools/make_site_qr.py regenerates them if the repository or file names change.
The debug file
Lines in sdmc:/3ds/sh23ds/debug.txt change how the game starts. Delete the file to play normally. A common setup puts James in a room with a flag set and the readout on:
debughud 1 flag 32 1 spawn er01 -19250 0 21250 90 2
| Line | Does |
|---|---|
| spawn <map> <x> <y> <z> <yaw> <mode> | Start in a room. Mode 2 plays as James; other modes open the room viewer |
| stage <n> | The stage for the spawn, otherwise taken from the room |
| flag <n> <0|1> | Set a game flag before the spawn |
| item <id> <count> | Give an item before the spawn |
| hp <n> | Start with this much health |
| action <0..3>, riddle <0..3> | Action and riddle level |
| enemy <kind> <x> <z> <yaw> | Place an extra enemy, kind in hex (202 is a creeper) |
| wish | Born From a Wish flags, with stage 53 for its spawn |
| demo <name> <seconds> | Play a cutscene from this point |
| movie <name> | Play a movie |
| newgame 1 | Start a New Game without input |
| debughud 1 | The readout on the bottom screen (SELECT toggles it) |
| dumpframe <n> | Save frame n as frame_n.bmp with its stats |
| perflog 1, memlog 1 | Log frame times and memory use |
The converters
tools/make_pack.sh runs them in order and stops early unless sh2pc.exe has the Director’s Cut MD5, because several converters read tables at fixed addresses in that exe. To rebuild one part of the pack, run its converter on its own:
python3 -m venv .venv && .venv/bin/pip install numpy pillow pefile .venv/bin/python -I tools/sh2conv/pics.py "Silent Hill 2/data" pack
| Converter | Writes |
|---|---|
| convert.py | Room geometry and ETC1 textures with mipmaps, camera roads |
| collision.py | Room collision |
| chr.py | Characters, weapons and their motions |
| events.py, events_pack.py | Stage events and English messages |
| enemies_pack.py, moths.py | Enemy lists, Mary’s moths |
| lighting.py, fog.py | Room lighting records with the EE’s fixes, the fog layer |
| ui.py, pics.py, maps.py | The PC font and title art, stills and puzzle overlays, maps and memos |
| sound.py | Sound effects, footstep materials, per-stage music tables |
| movies.py | The Bink movies as MPEG-1 and MP2 (needs ffmpeg) |
| demos.py | Cutscene scripts, motions, their characters, voices |
Pack layout
bg/<area>/<room>.s3m room geometry bg/<area>/<room>.s3l collision bg/<area>/*.cam camera roads, with <area>GB.cam for open areas bg/<area>/tex/<id>.t3x textures chr/<folder>/<model>.s3c characters, with .s3a motions and tex/ events/ map/ pic/ ui/ events, maps, stills, font and title demo/ cutscenes movie/ FMV sound/ ADX music, effects, voices, tables lighting.txt chrlight.txt room and character lighting
The converters also write PNG previews and decoded events as JSON. The packer deletes them unless it runs with -e KEEP_EXTRAS=1.
Porting a feature
- Find the PC function in the Director’s Cut exe. The Ghidra project gets its names from the PS2 beta’s debug information and the Enhanced Edition source, as RESEARCH.md describes.
- Write the C in
port/sourcewith the PC address in a comment. - If it needs data, extend a converter, then rebuild only that part of the pack.
- Check it with a
spawnline in the debug file and adumpframe. - Run the smoke run from TESTING.md and update the notes for that part of the game.
Rules
Never commit anything that comes from the game: exe bytes, extracted assets, a converted pack, Ghidra databases or decompiler output. .gitignore covers the usual places; check git status before every commit anyway.
Pull requests should build in CI, pass the smoke run and keep the notes in port/*.md in step with the code. Code that ports an Enhanced Edition fix names the EE option and file in a comment.
Reference notes
| STATUS.md | What works, and the gaps in order |
| TESTING.md | The smoke run and the other test runs |
| PROGRAMS.md | Stage programs, puzzles, endings, results |
| ENEMIES.md | Enemies, bosses and combat |
| CUTSCENES.md | The scene player and its data |
| MOVIES.md | FMV conversion and playback |
| SOUND.md | Music, sound effects and voices |
| EE_FIXES.md | Every Enhanced Edition option and its state |
| PERFORMANCE.md | Frame budget, streaming and memory |
| CONTROLS.md | Both button layouts and the remap page |
| EVENTS.md | The event data format |
| RESEARCH.md | How the exe was mapped, key addresses |