sh23ds

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.exe and data/ 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

FilesWhat they do
main.cMain loop, room changes, rendering
room.c, stagemdl.c, collide.cRoom geometry and textures, stage models, collision
gamecam.c, game/view/sh2cam.cThe PC camera module and its roads
envlight.c, chrlight.c, fog.c, shadow.cRoom and character lighting, the fog layer, flashlight and drop shadows
chr.c, move.c, controls.cJames’s animation, movement, button layouts
events.c, prog*.cStage events and the stage programs, one file per part of the game
enemy.c, combat.c, maria.c, blood.cEnemies and bosses, weapons, Maria, blood
touchui.c, map.c, itemview.cThe touch screen: map, items, memos
titlemenu.c, optmenu.c, savescreen.c, gameover.cTitle, options, save files, game over
demo.c, movie.c, noise.cIn-engine cutscenes, FMV playback, the film grain
audio.c, bgm.c, sfx.cMusic, per-area music, sound effects
stream.c, profile.c, perf.cWorker-thread streaming, the New and Old 3DS profiles, frame timing
*.v.picaVertex 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.

Terminal
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.

VariableDefault and use
SH23DS_BUILD_JOBS4, parallel make jobs
SH23DS_BUILD_IMAGEThe pinned devkitpro/devkitarm image
SH23DS_CIA_IMAGEsh23ds-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.

  1. Bump VERSION, write what changed in .github/releases/v<version>.md and commit.
  2. Push a matching tag, such as v0.2.0.
  3. CI checks the tag against VERSION, verifies the checksums and publishes the release with those notes. The packer workflow pushes ghcr.io/ankushkun/sh23ds-packer with the same version and latest.

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
LineDoes
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)
wishBorn 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 1Start a New Game without input
debughud 1The readout on the bottom screen (SELECT toggles it)
dumpframe <n>Save frame n as frame_n.bmp with its stats
perflog 1, memlog 1Log 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
ConverterWrites
convert.pyRoom geometry and ETC1 textures with mipmaps, camera roads
collision.pyRoom collision
chr.pyCharacters, weapons and their motions
events.py, events_pack.pyStage events and English messages
enemies_pack.py, moths.pyEnemy lists, Mary’s moths
lighting.py, fog.pyRoom lighting records with the EE’s fixes, the fog layer
ui.py, pics.py, maps.pyThe PC font and title art, stills and puzzle overlays, maps and memos
sound.pySound effects, footstep materials, per-stage music tables
movies.pyThe Bink movies as MPEG-1 and MP2 (needs ffmpeg)
demos.pyCutscene 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

  1. 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.
  2. Write the C in port/source with the PC address in a comment.
  3. If it needs data, extend a converter, then rebuild only that part of the pack.
  4. Check it with a spawn line in the debug file and a dumpframe.
  5. 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.mdWhat works, and the gaps in order
TESTING.mdThe smoke run and the other test runs
PROGRAMS.mdStage programs, puzzles, endings, results
ENEMIES.mdEnemies, bosses and combat
CUTSCENES.mdThe scene player and its data
MOVIES.mdFMV conversion and playback
SOUND.mdMusic, sound effects and voices
EE_FIXES.mdEvery Enhanced Edition option and its state
PERFORMANCE.mdFrame budget, streaming and memory
CONTROLS.mdBoth button layouts and the remap page
EVENTS.mdThe event data format
RESEARCH.mdHow the exe was mapped, key addresses