- C 96.1%
- Python 3.2%
- Makefile 0.4%
- Java 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| include | ||
| re@b1b24b642b | ||
| src | ||
| test | ||
| tools | ||
| .gitignore | ||
| .gitmodules | ||
| DECOMP.md | ||
| Makefile | ||
| README.md | ||
de-hmbtn
Functional decompilation of Harvest Moon: Back to Nature (USA, SLUS-01115) for native macOS/Linux builds, with no PS1 hardware emulation (the game's C runs on a native runtime).
The game began as a complete static recompilation from re-hmbtn
(submodule re/, the native runtime). Decompiled C replaced the recompiled functions one by
one, and the game stayed playable at every step. Each decompiled function behaves exactly like
the original; byte matching is not a goal. make difftest checks equivalence against the
recompiled original.
Status:
- Functions: all 2675 game functions (executable and every overlay) and all 677 Sony PsyQ
SDK functions are decompiled C.
make coverage ROUTE=day1reports no recompiled game code on the route. About 92 % of the functions have real names; the rest arefunc_<address>. - Checks:
make difftestcompares 2952 functions against the recompiled original (0 failures; 465 are only weakly exercised).make checkalso runs 12 whole-game routes (lockstep: RAM and CPU time identical to the original every game frame; statestep: game state identical, as objects) at 4:3, at 16:9 and at 50 % zoom. - Native libraries: the pad, CD, memory card, sound (SPU) and GPU libraries and the basic system libraries are native C on the host. LIBSND, LIBGS, LIBGTE and LIBMCX are still decompiled C on PS1-style guest memory.
- Game state: moved from emulated PS1 RAM into host-owned C structs (player, NPCs, animals, inventory, clock, field, menus, overlays, sound; DECOMP.md "Host-owned state"). Draw packets, loaded assets, text buffers, save buffers and the two developer tools stay in PS1-format memory on purpose.
- Data: the game reads named files from
assets/(no disc needed after the first start), and all 255 areas are data files inmaps/that the in-game map editor changes. - PC features (not in the original): widescreen and zoom, HD rendering, 60 fps / display-rate frame interpolation, Enhanced audio with master/music/SFX volume, no-slowdown timing, text speed, mouse in menus, typing names, save files, and the map editor (see the sections below).
- Still to do: names for the remaining
func_/D_symbols andunkfields, a few PS1-memory leftovers (DECOMP.md), and playing on a real Windows PC and on stock SteamOS (Linux x86_64 on a Steam Deck and a Windows cross-build are checked, see below).
Requirements
- A disc image of SLUS-01115 (USA), for the first build. Redump SHA-1 of the raw image:
cb26381fa1336bdd15f07469e59f1143ccf37b03. No game data is in this repository. - clang or gcc, GNU make, Python 3, pkg-config, SDL3 (built and checked with 3.4.18), and a
re-hmbtn checkout:
git submodule update --init. For the HD renderer a Vulkan driver (Linux, Windows) or Metal (macOS); without one the game falls back to the software renderer. - macOS: Xcode command line tools and
brew install sdl3. Linux: see "Building on Linux and the Steam Deck". Windows: see "Building on Windows".
Build
make DISC=/path/to/HARVESTMOON.bin # first build: extracts the game's files into assets/
make # later builds
build/hmbtn-de # play: reads assets/, no disc needed
build/hmbtn-de /path/to/HARVESTMOON.bin # with setting data=disc: play from the disc image
The first build runs re/tools/extract.py (the recompiler needs the executable and the
overlays from assets/) and records the disc image in .disc; make assets extracts again.
A checkout that still has the former extracted/ converts with one command, make (it
extracts from the disc recorded in .disc, or make DISC=...); extracted/ can then be
deleted.
Players need neither Python nor make: the game binary extracts the assets itself.
hmbtn-de game.bin (or .cue) with no assets/ around shows "Preparing game files... N%",
writes assets/ next to the binary and starts the game; later starts need no disc.
hmbtn-de --extract game.bin [--assets DIR] only extracts. Both check the disc: another
region's disc or a damaged image is refused with a message.
Building on Linux and the Steam Deck
Checked on a Steam Deck (x86_64, AMD VanGogh GPU, Vulkan/RADV, Arch-based CachyOS "deckify"
kernel 7.2, gcc 16 and clang 23, SDL3 3.4.18): the build is warning-free apart from a few
gcc string-truncation notes, make difftest (2952 functions, 0 FAIL, 465 weakly exercised),
make lockstep-all DATA=files (12 routes), make statestep-all (12 routes) and make coverage ROUTE=day1 give the same results as on macOS. The Original-audio WAV of the first
cutscene route (--audio original --wav) and the HD renderer's picture (Vulkan, SPIR-V
shaders, --renderer gpu --gpu-screenshot) are byte-identical to the macOS build's (Metal).
Not checked: the game in a window on the Deck (Gaming Mode, fullscreen), audio through the
Deck's device, stock SteamOS.
Packages (Arch, CachyOS, SteamOS inside a container; the names are Arch's):
sudo pacman -S --needed base-devel git clang python sdl3 pkgconf vulkan-icd-loader vulkan-radeon
# Intel: vulkan-intel, NVIDIA: nvidia-utils; no GPU driver: vulkan-swrast (software Vulkan, slow)
Other distributions: the same tools, and SDL3's development files (libsdl3-dev on
Debian 13 / Ubuntu 25.04 and later; older ones need SDL3 built from source).
git clone --recurse-submodules <this repository> hmbtn && cd hmbtn
make DISC=/path/to/HARVESTMOON.bin -j6 # the compiler is cc (gcc on most systems); CC=clang for clang
build/hmbtn-de # from the checkout directory (see below)
CC=clang make (the environment, not make CC=clang) also reaches the test scripts, which call
make again. A build takes about a minute on the Deck (-j6). The whole suite on the Deck (6 jobs):
difftest 15 min, lockstep-all 12 min, statestep-all 3.5 min, coverage 3 min.
Where the game keeps its files. All relative to the working directory, so start the game
from the folder that holds it: assets/ (also found next to the binary or one or two levels
above it, so build/hmbtn-de finds the checkout's), maps/ beside it, hmbtn_settings.ini,
saves/ and the memory card images hmbtn_slot1.mcd, hmbtn_slot2.mcd in the working
directory. In your home directory all of it is writable; nothing goes to /usr, /etc or the
XDG directories. As a Steam shortcut ("Add a Non-Steam Game"): target build/hmbtn-de
inside the checkout and "Start In" the checkout directory, or the saves and settings end up in
whatever directory Steam starts it in. The first start with a disc image and no assets/
extracts the game files next to the binary (3 s on the Deck; --extract game.bin --assets DIR
only extracts; DIR's parent must exist).
Display. The Deck's screen is 1280×800 (16:10): in the settings (Graphics page) choose
Aspect 16:10 (a 384×240 field fills the screen; 16:9 and 4:3 leave bars) and Display
Fullscreen, or write aspect=16:10 and display=fullscreen into hmbtn_settings.ini before
the first start. Gaming Mode's gamescope gives a window the whole screen anyway. The picture
always keeps its shape, with black bars as needed.
Controller. SDL3 lists the Deck's own controls as a gamepad ("Steam Deck Controller"); the game takes the first two gamepads as players 1 and 2 and maps them by position as on the PS1 (Controls). Untested: with Steam Input in Gaming Mode the Deck might show up twice (the raw pad and Steam's virtual pad), which would make player 2 mirror player 1; if it does, use Steam's "Gamepad" template or turn "Enable Steam Input" off for the shortcut.
Building on a Deck with the read-only SteamOS (the root file system is read-only and a
system update removes what pacman installs): build and run in a container, which keeps your
Deck as it is. distrobox (needs podman, which SteamOS includes;
install it into your home with curl -s https://raw.githubusercontent.com/89luca89/distrobox/main/install | sh -s -- --prefix ~/.local).
This container recipe is untested (the Deck above runs CachyOS, not SteamOS):
distrobox create --name hmbtn --image archlinux:latest # once
distrobox enter hmbtn # a shell in the container; your home is shared
sudo pacman -Syu --needed base-devel git clang python sdl3 pkgconf vulkan-icd-loader vulkan-radeon pipewire libpulse
git clone --recurse-submodules <this repository> ~/hmbtn && cd ~/hmbtn
make DISC=/path/to/HARVESTMOON.bin -j6
build/hmbtn-de # window, GPU and audio come from the Deck
To start it from Gaming Mode add a Non-Steam Game whose target is ~/.local/bin/distrobox-enter
with the arguments -n hmbtn -- sh -c 'cd ~/hmbtn && build/hmbtn-de'. The binary links the
container's SDL3, so run it inside the container, not on the host.
Building on Windows
Untested at runtime on a real Windows PC. What was checked: the whole game cross-compiles
and links for x86_64-w64-mingw32 (MinGW-w64 gcc 16 against the SDL3 3.4.18 MinGW
development package) to one hmbtn-de.exe that needs only SDL3.dll and Windows' own DLLs, and
under Wine 8 (headless) that exe boots, extracts the disc into assets byte-identical to the
macOS ones (including with \ paths), and plays the first cutscene route with an
Original-audio WAV byte-identical to the macOS and Linux builds'. Not checked: a window,
the HD renderer (SDL_GPU's Vulkan driver takes the SPIR-V shaders; without a Vulkan driver the
game falls back to the software renderer), audio devices, gamepads, make itself on
Windows. The test suite (make check and the scripts behind it) is for macOS and Linux only; on
Windows build and play.
MSYS2 (https://www.msys2.org), in the MSYS2 UCRT64 shell:
pacman -S --needed git make mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-sdl3 \
mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-python
git clone --recurse-submodules <this repository> hmbtn && cd hmbtn
make CC=gcc DISC=/c/Games/HARVESTMOON.bin -j6 # the disc image as an MSYS2 path (/c/... is C:\...)
build/hmbtn-de.exe # from the UCRT64 shell: SDL3.dll is found on its PATH
MSYS2 sets OS=Windows_NT, which the Makefile takes for a Windows build (WINDOWS=1): .exe,
a console program (its log shows in the window), a manifest that makes file names UTF-8
(tools/win/hmbtn.rc, needs windres from the gcc package, Windows 10 1903 or later), and
PYTHONUTF8 for the generators. clang (mingw-w64-ucrt-x86_64-clang) works the same with
CC=clang. To start the game by double-click, put SDL3.dll (from /ucrt64/bin) next to
hmbtn-de.exe; it keeps assets/, maps/, hmbtn_settings.ini and saves/ in its folder
(the working directory) as everywhere else.
Cross-compiling from macOS or Linux (this is what was checked), with MinGW-w64
(brew install mingw-w64, or the distribution's mingw-w64-gcc) and the SDL3 MinGW package
(https://github.com/libsdl-org/SDL/releases, SDL3-devel-3.x.y-mingw.tar.gz):
make build/gen/funcs.h assets/manifest.json DISC=/path/to/HARVESTMOON.bin # the generated C and the assets, on the host
SDL=/path/to/SDL3-3.x.y/x86_64-w64-mingw32
PKG_CONFIG_PATH=$SDL/lib/pkgconfig make WINDOWS=1 CC=x86_64-w64-mingw32-gcc \
WINDRES=x86_64-w64-mingw32-windres O=build/obj-win -j8 build/hmbtn-de.exe
cp $SDL/bin/SDL3.dll build/ # next to the exe
Game data
The game reads its data from assets/, named files with the original bytes
(re/recomp/runtime/rt_assets.h, list in assets/manifest.json): the executable
SLUS_011.15 (the one SYSTEM.CNF boots), the XA audio STR/01..04.XA, the disc's ISO 9660
sectors (disc/), and the archive A_FILE.BIN cut into named regions: overlays
(overlay/ovl11.bin), the 255 areas (area/00_farm_spring/{header,textures,schedule, resident}.bin), cutscenes, sound banks and sequences, message banks, portraits, icons, ...
The game's loaders ask for them by kind and index (include/asset.h, DECOMP.md). Or it
reads a disc image. Either way the game's CD library (LIBCD, native: re/recomp/runtime/psyq/ libcd.c) reads them through the same drive model at their disc sectors, so the game finds,
reads and streams the same data. Music and voices stream from the XA sectors at the disc's
rate into the SPU's CD input.
The assets directory is --assets DIR, else assets/ in the working directory, next to the
binary, or one or two directories above it (so build/hmbtn-de finds the checkout's
assets/). A disc image argument is then optional; one that does not exist is ignored.
With neither, the game stops with a message saying how to provide the disc.
The maps come from maps/ beside assets/ (or --maps DIR): index.json (the area table,
places, destination rules) and a directory per area (map.json: exits and triggers, camera
limits, props, people, music, flags; model.bin, textures.bin, schedule.bin,
resident.bin). The game writes them from the assets on a start without them; make maps (or
hmbtn-de --export-maps) writes them anew. Edits to them change the game; as written they
give the original game byte for byte (DECOMP.md "Map data").
Setting data (files or disc, in hmbtn_settings.ini) chooses the source. Headless runs
without --settings-menu (the tests) use the disc image when given one, else the assets
(make lockstep DATA=files plays the decompiled build from assets/). The source can be
changed in the settings menu (Game page) while playing; the drive reads its next sector from
the new source, also in the middle of a load or an XA track. Without a disc image (or without the assets) the setting cannot be changed:
the menu says No disc image given.
Setting loading (fast or original, Game page) decides how long reads take. original
keeps the drive's timing (the timing the tests check, to the emulated cycle: with
loading=original a route played from the files leaves the same game RAM at every vblank as
from the disc image). fast (the default with a window or --settings-menu) completes every
read at once, so loading takes only the game's own fades; headless test runs never use it.
Loads the game polls once per frame then take fewer frames (title intro, area changes,
portraits), as the former files mode did; game logic waits for the data either way.
Settings
Settings are in the game: SETTINGS on the title screen (under NEW GAME and CONTINUE), Select on the Start status screen in the field (M on the keyboard), or Settings in the pause menu (Esc in the field). The window has pages, Graphics, Controls and Game: Left/Right on the page names at the top (or L1/R1, Q/R on the keyboard) switch pages. The window is centred, in widescreen too, over the screen it was opened from, which keeps running underneath (the status screen keeps its page; it sees no buttons) and is dimmed like the game's own dims, so that screen's windows recede behind it.
| Setting | Values |
|---|---|
| Graphics | HD (GPU renderer, Metal or Vulkan), Original (the exact 320×240 picture) |
| Resolution | 1x–8x internal resolution in HD |
| Aspect | 4:3, 16:9, 16:10, 21:9: widescreen field in HD |
| Zoom | 100% to 50% in HD at every aspect (keys - and =): more of the field, HUD unchanged |
| Text | k8x12 (the game's letters redrawn with the free k8x12 pixel font), Pixel, Original; HD at internal scale ≥ 2 |
| Filter | Smooth, Sharp |
| Display | Window, Fullscreen |
| VSync | Off, On |
| Data | Files (assets/), Disc (the image given on the command line); switches at once |
| Saves | Files (save files with a load list, mid-day saves), Memory card (the game's own card screens) |
| Loading | Fast (reads complete at once), Original (the CD drive's pace, as the tests play) |
| Timing | Original (the PS1's CPU time: a frame whose work runs over its 2 vblanks makes a lag frame), No slowdown (every game frame within its 2 vblanks); switches at once |
| Text speed | Original (the game's pace, what the tests play), Fast (2 letters a frame), Faster (4), Instant (a whole box at once; a longer text scrolls up a line at a time); switches at once |
| Audio | Enhanced (the same music and sounds rendered at the device's rate, 48 kHz usually), Original (the PS1's own 44.1 kHz mix, exact); switches at once |
| Master vol., Music vol., SFX vol. | 0% to 100% in steps of 10 (Left/Right), default 100%; the volume of everything, of the music, of the sound effects and voices |
| Geometry | Precise (3D world without vertex wobble, perspective-correct textures), Original (as the PS1); HD |
| Frame rate | Display (the field's motion interpolated at the screen's refresh rate; VSync On recommended), 60 fps, 30 fps (the game's own); HD |
Changes apply immediately and are saved to hmbtn_settings.ini in the working directory.
The picture keeps its shape in any window or fullscreen, with black bars as needed.
Setting audio (Game page) chooses what is heard; the game itself never notices. The PS1
sound chip's voices always run exactly as before (envelopes, key status, sound RAM: what the
game reads back), and their own mix is original, the PCM the tests check. enhanced (the
default with a window or --settings-menu) renders the same voices again from the same SEQ,
VAB and XA data at the output device's rate in floating point: band-limited (windowed-sinc)
resampling of every voice instead of the chip's linear interpolation, the PS1 envelopes
evaluated per output sample (no steps), smoothed volume and pan changes, a note the game
restarts keeps fading out on a spare voice instead of being cut, the game's reverb (type and
depth) computed at the full rate without 16-bit clamping, the XA streams resampled from their
own rate, and a soft limiter only at the end. Headless test runs without --settings-menu
always hear original; --audio original|enhanced sets it for one run (and --wav records
whichever is in effect, at its rate).
The volumes (settings volume_master, volume_music, volume_sfx, Game page: Master vol., Music
vol., SFX vol.; --master-volume N, --music-volume N, --sfx-volume N, 0-100, for one run)
scale the heard output only, in both renderers: the sound chip's voices, the game's own volumes,
the map's musicVolume and the reverb input stay exactly as the game left them. The game's sound
driver (LIBSND, _SsVmKeyOnNow / vmNoiseOn) tells the runtime at each key on whether the voice is
a sound effect (SsUtKeyOn*: Snd_PlaySe, the menu sounds) or a note of a song's
sequence; that group is kept with the voice until its next key on. Music is the songs (every
sequence voice), SFX every sound-effect voice and the XA streams (the game plays only spoken
lines and animal cries there). Master scales both. A voice's reverb is scaled
with its group (the Original mix runs a private copy of the reverb per group while a volume is
below 100%, the Enhanced renderer a second reverb), and a changed volume takes ~10 ms to arrive
(no click). With all three at 100% the output is bit for bit the unscaled one, so headless test
runs (which keep 100%, whatever the settings file says) are unchanged; --export-music always
writes the songs at full volume.
Setting timing (original or smooth, Game page: Original / No slowdown) decides what
happens when a game frame's work does not fit its 2 vblanks. The runtime still charges the
PS1's CPU time for the game's loops, and the vblank, the CD drive, the sound chip and the
timers run on that clock; original (the default, the timing the tests check) lets such a
frame run into the next vblank, so the game loses a frame there, as the console does. With
smooth the frame's work stops taking emulated time once the frame has used its budget, so
its VSync(2) still comes after exactly 2 vblanks; waits on hardware (CD reads at
loading=original, the memory card, DMA, the sound driver's loads) take their time as
before, and sound, XA streams, the frame pacer and frame interpolation run as always. Only
frames that would have lagged behave differently. On the routes such frames are rare and are
at loading points (the title screen loading, the first frame after a save is loaded, the
start of a day, the save screens): in the field, festivals and the town a game frame's work
takes 0.4–0.6 vblanks (median; at most 1.4 in 99.9 % of frames), so the game does not slow
down there with either setting. The frames that take longer in play are synchronous loads:
each use of a tool reads its animation from the CD in the middle of a frame (6 vblanks; 5
with loading=fast, the game's own VSync(3) before the read), which timing leaves alone.
Headless test runs without --settings-menu always play original; --timing original|smooth sets it for one run.
Setting textspeed (original, fast, faster or instant, Game page: Text speed) speeds
up the dialog text. The game types one letter every other game frame (every frame while ✕ is
held); with Fast, Faster and Instant every frame reveals 2, 4 or as many letters as fit the
box (up to its fourth line), and the scroll of a long text takes 7, 4 or 1 frames a line instead of 14. Every code of
the text that the game reacts to by position (a wait for ✕, the end of a page, a choice, a
portrait or event code) stops the frame's run of letters, so waits, questions and scenes
work as before; the game simply waits for the text to end sooner. The typing sound plays once
per frame's run. Headless test runs without --settings-menu always play original;
--textspeed original|fast|faster|instant sets it for one run.
Widescreen shows more of the field (farm, village, houses) to the left and right. The
date window moves to the right edge; message boxes and menus stay in the middle; the
title, menus and other 2D screens stay 4:3. Zoom draws the field's world (ordering tables
named with rt_gpu_world_lists in Gfx_DrawFrame) smaller around the screen centre; the
HUD and message boxes keep their size. The game code involved widens its off-screen
culling (Tmd_*OnScreen, View_SetFrustum, Screen_IsNear, Props_Draw, the crop
tile range, weather particles) by rt_view_margin_x/y(), and also culls vertically and
guards the packet buffer then; all of this is 0 / off in headless test runs.
Outdoors, a widened or zoomed-out view would show the black past the map's edge where the
original camera limits only keep the 320x240 view on the map (src/ovl11/render_camera.c).
The field is drawn from the camera target moved a little away from that edge (at most a
quarter of the half screen, a smooth function of the target), and what still lies past the
edge is filled with a skirt of ground: tiles continuing the nearest flat ground polygon of
the map's edge (its texture, mirrored tile to tile, and colour), linked at the back of the
ordering table so the whole map draws over them. The game's own camera target is unchanged;
indoors (black around the rooms, as in the original) and at 100 % / 4:3 nothing changes.
Precise geometry draws the 3D world (maps, buildings, props: every polygon Tmd_SortObject
sorts) at the exact projected positions instead of the PS1's whole pixels, so edges stay
still and straight while the camera moves, and textures are mapped with perspective instead
of the PS1's affine warp. Prim_Sort stores each SXY as the original does, then hands the
packet word to the runtime (psx_gte_store_sxy), which keeps the vertex's unrounded
position and depth outside game RAM for the GPU renderer (see re-hmbtn's README). On route
day1 that covers 99.4% of the world OTs' polygon vertices; the rest are the sprites
(characters, animals, items: Anim_DrawPolys POLY_FT4s placed at a projected point),
which keep their whole-pixel positions. Measured on the farm (walking down for 60 game
frames, 4x, native pixels): the frame-to-frame scroll judders by 0.163 px RMS with
original geometry and 0.042 px with precise; each 32-pixel patch of the picture by 0.212
px and 0.028 px. Vertices sit 0.42 px RMS (at most 0.87 px) from their exact projection
with original geometry, 0 with precise; the affine texture error removed is 0.16 texel
on average and up to 1.2 texels (2.3% of textured triangles over 0.5 texel, day1).
Frame rate Display or 60 shows the field's motion smoothly: the game draws a new frame only
every other vblank (30 per second), and the GPU renderer shows pictures in between, with
each moving thing part of the way from its place in one game frame to its place in the next
(re-hmbtn's README: frame interpolation). What a vertex belongs to comes from the draw code:
Prim_Sort names the model vertex each SXY was projected from (psx_gte_store_sxy_key), and
the sprite drawers Anim_DrawSprites, Anim_DrawPolys and Anim_DrawStreamed (characters,
animals, items, shadows) tag their corners with the animation and the point the sprite is
placed at (psx_gte_tag_sprite), all outside game RAM. The HUD, text, message boxes, menus
and fades are shown as in the newest game frame; area changes, camera cuts and teleports
snap. The pictures are one game frame (about 33 ms) behind the game, at Display about 25 ms
more. Measured headless on the farm (walking down, 2x, present_seq; motion of the world
per picture, native pixels):
| Presented at | Step per picture | Spread of the steps (std) |
|---|---|---|
| 30 fps (60 Hz display) | 3.15 and 0, alternating (mean 1.59) | 1.565 |
| 60 Hz, interpolated | 1.568 | 0.023 |
| 120 Hz, interpolated | 0.785 | 0.015 |
| 144 Hz, interpolated | 0.649 | 0.015 |
Every 32-pixel patch of the picture moves evenly (21:9, zoom 80 %: per-patch step spread
0.024 px median, 30 fps 1.22 px); an animal walking past moves 0.32 px each picture instead
of 0.64 px every other one. In a window at Display, on a 100 Hz screen: 96–104 pictures per
second, the presentation times within 1.2–1.6 ms of an even line (--perf), Metal and Vulkan
alike.
The window and its code (src/ui/settings_menu.c: pages of table rows, one entry per
setting) are not part of the original game; the menu is offered only with a window (or
--settings-menu), so headless test runs see the original title menu. Command-line
options are listed in re-hmbtn's README.
Music export
build/hmbtn-de [disc] --export-music DIR [--audio original|enhanced] [--song N] writes every
background music song (BGM_TABLE, 1-36; --song N only that one) as DIR/song_NN.wav,
Enhanced at 48 kHz unless --audio original (44.1 kHz). It boots the game without a window
to its title menu, starts each song as the game does (its bank, sequence and its own reverb
type and depth), and records it through the selected renderer. The loop is the sequence's
own (LIBSND's loop markers): each file holds the song up to the end of one loop, then up to
10 s of the repeat fading out, and a RIFF smpl chunk with the loop's first and last sample,
so loop-aware players and engines repeat it without a seam. DIR/export.log lists each song's
loop (samples and seconds), the seam check, peak and RMS. Takes about 40 minutes for all 36.
Editing textures
build/hmbtn-de --map-tool export-textures maps/00_farm_spring (or maps for every map)
writes the map's textures as PNG files, textures/*.png, plus textures.json (VRAM place,
depth, palette and coding of each). Edit them in any image editor: an image as a palette PNG
with its own colours, or recolour a palette strip (*_clut_*.png) to recolour everything drawn
with it. On its next load of that map the game uses the PNGs when they are newer than
textures.bin; --map-tool import-textures DIR writes textures.bin from them and reports
palette changes (new colours go into free palette entries; a palette only these images use is
rebuilt; otherwise the nearest colour). PS1 colours have 5 bits per channel; alpha 0 is
transparent. Unchanged files give the original bytes (--map-tool check-textures maps).
--map-tool export-model DIR [out.glb] writes the map's model as glTF 2.0 (one node per object,
vertex colours, UVs on the texture pages, textures embedded) for any viewer;
--map-tool import-model DIR in.glb takes back moved vertices, UVs and colours from such a file
as long as nothing else changed. Details: DECOMP.md "Editing textures", "Exporting models".
Map editor
The game has a map editor for the files in maps/: exits and triggers, camera limits, props,
people, the map's own values, the farm's starting tiles and ground textures. Open it with
build/hmbtn-de --editor [MAP] (MAP: a directory of maps/, e.g. 38_beach_spring, or an
area index; default the farm in spring): it starts a new game, skips its introduction and
opens the map. Or press F2 in the field of your own game, where the player can walk. It needs
the GPU renderer (Graphics HD). Its text is readable at every Resolution: at 1x it uses a
pixel font of its own, at 2x and up the game's font. While it is open the game waits as under
its menus.
The area is drawn by the game from a free camera, with the editor's overlays: exits green
(with their destination), other triggers orange, camera limits yellow, props blue, people
magenta (their walking steps joined), walk areas blue and animal warps pink, the farm's or a
room's tile grid, and the point under the mouse red. ≠N on a label: N linked maps differ
there (below).
Everything works with the mouse (the keys stay):
- Toolbar: Select (1), Move (2), Rotate (3), New exit (4), Tiles (5), Paint (6); Play, Save, Undo, Redo; GRID, LABELS, AREAS; Open a map; Help. Every button has a tooltip.
- Camera: right drag turns, middle drag moves, the wheel zooms, a double click centres on the point. A right click without moving opens a menu for what is under the mouse (delete, pick arrival, new exit here, play from here, centre here, undo/redo/save; tile commands).
- Scene: click selects (hover highlights); Move drags it (Shift: 100-unit grid), the corners of a selected box resize it; Rotate turns a prop by dragging sideways (Shift: 45 degrees); New exit draws its box by dragging on the ground.
- Panel: a number is changed by dragging it sideways, the wheel over it,
-/+, or a click and typing (Enter); x/y/z in one row. destination and music open a list (type to filter, wheel, click); flags open their bits as checkboxes. Actions are buttons.
| Keys | Action |
|---|---|
| 1 … 6 | the tools |
| W A S D, Q E, R F, T, Home | move, turn, tilt the camera; top view; the map's centre |
| Tab, M or Esc, Del | the next thing; the map's values; delete |
| [ ] (Shift: finer) | turn the selected prop by 45 degrees (a 64th turn) |
| N, P | new exit at the mouse; pick the arrival point of the selected exit on its destination |
| O, F3, Ctrl+N | open another map; rename this map's directory; copy this map as a new area |
| Ctrl+S, Ctrl+Z, Ctrl+Y | save; undo; redo |
| F5, F1, F2, G L V | play from the mouse (F5 there returns); help; close; grid, labels, areas |
Linked maps. A place exists as several maps: its four seasons, the farm also by upgrade
(00_farm_spring … 17_farm_6_winter, 24 maps), houses by size. The maps of the same area kind
and id are linked: an edit of a trigger, prop, person's step, the camera limits or a map value
(not the variant) goes to every linked map that had the same value there, value by value.
Where a linked map already differs in the original files (a prop only in winter, an exit's
destination per season), it keeps its value and the editor asks: KEEP THEIRS or CHANGE THEM
TOO. The panel lists the linked maps (= the same, ≠ different for the selected thing) and
has 1 SEASON to edit this map only. Textures and models are per map.
Tiles (5). The farm field (45 × 25 tiles) and the greenhouse (7 × 7) by the game's grids.
Click a tile, drag a rectangle, Shift adds, Ctrl toggles, Ctrl+A all, Esc none. The panel
shows the selection: ground (untilled, tilled, watered, seeded), the object and its growth
stage, walkable, and for one tile its raw Tile values. The palette on the left has the
objects the field code knows, drawn by the game's own sprites: weed, stone, branch, stump,
the 2 × 2 large rock, boulder and big stump, the crops of every season and grass. It offers
only what the game allows on the selected tiles (crops on tilled soil, nothing planted on the
farm in winter, ...); the tooltip says why not. Two things can be edited:
- NEW GAME: the farm a new game starts with,
maps/farm_tiles.json. A new game picks one of three layouts at random; the editor shows one (A, B, C) and edits all three (ALL 3). The game's init puts crops ripe on tilled soil, everything else on untilled ground. While the tile tool shows it, the field shows the layout; the game's own tiles come back afterwards. - THIS GAME ("edit save"): the tiles of the running game, kept when the game is saved: ground, objects, crops and their stage.
The farm's tiles are one grid for every season and farm variant: they are game state, not part of a map file.
Paint (6). Ctrl+click a textured ground polygon to take its texture (coordinates, CLUT,
page) as the brush; move it on its texture page in the panel or turn it; click or drag over
other polygons to give them the brush. Saved into the map's model.bin with the structure
unchanged. The farm field's quads are not painted: the game retextures them from their tiles.
Workflow:
- Edit and play-test. Changes show at once and the game uses them before they are saved:
F5 plays the map from the point under the mouse, with the edits; F5 again goes back.
The top bar says
* unsavedwhile something differs from its file. - Save. Ctrl+S (or the Save button) writes
map.jsonof every changed map,model.binof painted maps andfarm_tiles.json. Before a file is replaced it is kept as.bak(the version before) and, the first time, as.orig(as exported);index.jsonlikewise. - A new area. Ctrl+N (or the panel's copy as new area) copies the open map, with its
unsaved edits, under a new name: a new directory
<index>_<name>with the next free area index and a new area id, its model, textures, schedule and resident data copied, listed inindex.json. The copy opens in the editor. To get there, select an exit on another map (or make one), choose the new area as its destination and pick the arrival point; save; play. The copy's own exits lead where the original's do. - Rename. F3 renames the open map's directory and every name of it in
index.jsonand the other maps' exits. - Back to the original maps.
build/hmbtn-de --export-maps(ormake maps) writes every map,index.jsonandfarm_tiles.jsonanew from the game files: areas made with Ctrl+N are then no longer listed (their directories stay; delete them by hand). A single file is restored from its.orig, or the version before the last save from its.bak.
Not editable in the editor: model geometry and texture images (--map-tool, above), and the
walk areas and animal warps (shown; they belong to the place in index.json). A new area has
the next free index and is tested up to index 255 (one copy on the original maps).
Controls
Gamepads keep the PS1 layout by position (re-hmbtn's README) and vibrate where the game vibrates the pad (tools, fishing, the horse race; the Options menu's Vibration setting applies). The keyboard and mouse use the PC layout (Controls page: Layout PC), or the PS1 keyboard layout of re-hmbtn (Layout PS1, exactly the keys of before).
| PC layout | In the field | In menus, message boxes, cutscenes |
|---|---|---|
| W A S D, arrow keys | walk | move the cursor |
| Shift (hold) | run (○ held) | |
| E | talk, pick up, open (✕) | confirm (✕) |
| right click | talk, pick up, open (✕) | back (○) |
| Space, left click | use the tool, hold to charge (□) | confirm (✕) |
| Enter | status screen (Start) | confirm (✕) |
| Esc | pause menu | back (○) |
| I, Tab | rucksack (△) | Tab: Start |
| M | map (Select) | Select (settings on the status screen) |
| mouse wheel down, R | next tool (R1) | wheel: cursor down; R: R1 |
| mouse wheel up, Q | previous tool (R1 + ←) | wheel: cursor up; Q: L1 |
| F / G | next / previous rucksack item (R2 / R2 + ←) | F: △ (item info) |
| Z / X | whistle for the dog (L1) / the horse (L2) | L2 / R2 |
| C | □ | |
| Backspace | back (○); on the name screens: delete the last letter | |
| F12 | screenshot | screenshot |
| - / = | zoom out / in | zoom out / in |
Name screens (the player's, the farm's, the dog's and the animals' names): with the PC layout
type the name on the keyboard. A printable key (letters in both cases, digits, space and the
punctuation the game's font has: ! ? . - + ( ) / : ; ' " & ~) enters its letter, the grid's cursor
jumps to that letter's cell (changing the page when it is on the other one) and the usual OK sound plays; keys the
font has no letter for are ignored. Backspace deletes the last letter, Enter finishes the name (as ✕
on the grid's OK) and, in the confirmation, answers Yes; Esc is the back button (deletes
the last letter, or leaves the screen when the name is empty, or answers back at the confirmation).
Arrow keys and gamepads move the cursor and use ✕ as before; Tab is Start. While a name screen
is open, W A S D, E, Space and the other letter keys type instead of acting. Characters come from the
operating system's text input (SDL_TEXTINPUT, started only while a name screen is open), so any
keyboard layout types what its keys show. A typed letter is entered exactly as ✕ on its cell is
(NameEntry_TypeCell), so the name and the saved game are the same as a pad would have entered. The PS1
layout and gamepads see no change. rt_input_text_* (re/recomp/runtime/rt_input.h) is the runtime side;
the debug automation types with text STRING and key BackSpace / key Return.
Enter (any confirm key) also acts as Start on the title's press-start screens. In cutscenes Start is Tab only, so holding a confirm key to hurry the text never skips a cutscene. Esc on the title screen asks "Quit the game?".
Every key is rebindable on the Controls page: ✕ / Enter on an action, then press a key,
mouse button or the wheel; it is added to the action's keys (Backspace instead clears
them, Esc cancels). A key used by another action of the same context is not taken
silently: the menu names the action and offers to move the key. The same key may serve
one action in the field and another in menus (E: interact / confirm); F12 and the zoom
keys work everywhere, so they clash with any other use. "Defaults" restores the layout.
Bindings are saved as bind.<action>= lines (re-hmbtn's README); a file that binds one
key twice keeps the first binding, logs the other, and the Controls page shows it in red
marked ! with the action it clashes with.
How it works (not in the original game): src/ui/pc_controls.c turns the runtime's
actions (rt_input.h) into PS1 pad bits in Pad_Update (port 0), once per game frame,
where the game reads the pad. The context comes from game state: the field with no menu
(g_systemFlags 0x18000000), message box, cutscene or pause menu, or anything else.
Keys held while the context changes count again only after a release, so Space used on a
tool does not confirm the message it opens. Next / previous tool and item are the game's
own R1 / R2 (+ ←): Player_CycleTool / Player_CycleItem (src/ovl11/player_tools.c)
always step forward in the original (the direction argument is overwritten); in the PC
layout they step back on ←. The PS1 layout, and every headless run without
--settings-menu, sees the pad bits exactly as before.
Mouse in menus (PC layout, in the window; not in the original game). Every screen with a hand cursor follows the pointer: moving the mouse puts the cursor on the item under it (with the cursor sound, and only while the mouse actually moves, so keyboard and gamepad navigation are not fought by a resting pointer); a left click selects it as ✕ would; a right click goes back as ○; the wheel moves the cursor up / down as before. A click on nothing does nothing. Where a screen shows arrows, clicking one presses ← / → (status screen pages, the item box's pages, recipe pages, the quantity box of shops: its − / + arrows; the wheel over a quantity or over a settings value changes it by one). In the settings window a click on the left / right half of a value is its ← / → arrow, a click on a page tab opens the page, and the wheel over a value changes it. Covered: the settings window, pause menu, save list and its questions, title menu, calendar, message box choices and yes/no boxes (the common code, so every dialogue, shop answer and the name / birthday confirmations), the Start status screen (page arrows), rucksack, item box, world map, Options / Guide / Settings pages, name entry grid, birthday picker, the shop and tool-upgrade lists with their quantity box, the order form, the animal pickers, the kitchen's four screens, the bedtime prompt (diary choices) and the original memory card screens (file, slot, question, animal exchange). Not covered: the TV, mini-games and races (no menu), the memory card screen's fixed marker. The OS pointer is hidden during field play and shown in menus while it moves.
How the mouse works: a screen registers its items as rectangles of the 320x240 screen each
frame it takes input (include/mouse_menu.h, src/ui/mouse_menu.c: the item's index goes
into the screen's own cursor variable, so the original code moves, confirms and plays its
sounds as for the pad; nothing is registered without the PC layout, so game state and every
test run are unchanged). Pad_Update (pc_controls.c) turns a pointer move or click into the
cursor move and a ✕ / ← / → press of the frame after the screen registered it. The runtime
maps the window to screen pixels (rt_input_mouse, over the 4:3 picture, widescreen margins
left and right); menus sit in 0..320.
Skipping cutscenes
While a cutscene plays in the field, Hold START: Skip shows at the bottom right (Hold Tab: Skip with the PC layout); hold Start (Tab on the keyboard, rebindable as "Start" in the
controls) for 1.5 seconds to skip it. The hint fills in red while
Start is held; letting go earlier does nothing. Skipping runs the rest of the cutscene
script at once, leaving out only the waiting (messages, walks, timers, camera moves,
fades): event flags, items, time changes, area changes and the cutscenes it leads into all
happen as if it had been watched, so the game continues as normal. The skip stops for
questions, which still need an answer. Skipping the arrival takes the player through the
whole introduction (Mayor, flashback, the house) in 10–20 seconds of area loading instead
of more than four minutes, with the same event flags as watching it; the Mayor's village
tour takes about 5 seconds.
Like the settings menu, this is not part of the original game (src/ui/skip_cutscene.c) and
is offered only with a window or --settings-menu.
Saving
With Saves on Files (the default with a window), games are save files in saves/ in the
working directory (next to hmbtn_settings.ini), one .hmsave file per save:
- Loading: CONTINUE on the title, or "Read diary" at the bed, opens the list of saves, newest first, each with farm and player name, date, time of day, money and play time. ✕ loads, □ deletes (after a question), ○ goes back.
- Saving: the bed's diary has a new choice, Make diary entry: it saves the game as it was when the diary was opened and play goes on, with no sleep and no time skipped. "Make diary entry and then sleep" saves the next morning, as the game does. Both show the list: the first row is a new save; choosing an existing one overwrites it (after a question).
- Autosave: every morning the game is also saved to
saves/autosave-1..3.hmsave, the oldest of the three being replaced. - Memory cards: the diaries
FARM0..FARM2on the memory cards (hmbtn_slot1.mcd,hmbtn_slot2.mcd) are copied intosaves/once (saves/imported.txtremembers them).
A mid-day save loads exactly where it was made: same area, position, facing, time of day, weather, tool in hand and game state, and the people and animals around as they were: NPCs with their position, facing, walk and schedule step, the dog, horse, livestock and other animals with their positions and held / following state, and the camera. The game's own load starts the day in the house; the game's own area transition then takes the player to the saved place (behind the loading screen), and the saved state is put in RAM again there. On the first frame after loading, the character and animal tables equal the save moment; as on any area entry they stand still during the short fade-in. What stays approximate: weather particles and ambient effects, spinning props, the music (the same song, from its start) and sound effects. Mid-day saves from before this (file version 1) still load, with the characters placed as on entering the area.
A save file holds the game's own 32 KB memory card image unchanged (src/ovl15/saveload.c
describes it; images made in the field have no card icon/title header), a header with the
list's information and an 80×60 thumbnail, and for a mid-day save the area, the position
and the RAM of the characters, animals and camera (file version 2).
With Saves on Memory card the game's card screens run as on the PS1; headless runs without
--settings-menu always do. The save code (src/ui/save_files.c, src/ui/save_menu.c) is
not part of the original game.
Quitting
In the PS1 layout, and on the title screen in either layout, Esc pauses the game and
asks Quit the game? in a game-style window: Yes (Enter / ✕) quits, No (Esc / ○ too)
goes back to the game exactly where it was. This is runtime code (re-hmbtn), drawn over
the picture while the game waits, so game RAM is untouched.
In the PC layout, Esc in the field opens the pause menu (src/ui/pause_menu.c, not in
the original): the field waits as under the game's own menus, with Resume, Settings and
Quit (asks first: unsaved progress is lost). Esc in a menu goes back. Closing the window
still quits at once.
Working on the decompilation
See DECOMP.md. In short:
make difftest # each decompiled function vs. its recompiled original, random states
make lockstep # route vs. the pure recompilation, game RAM every game frame
make coverage # same route; lists game code still running recompiled
make lockstep ROUTE=day1 # routes: tools/routes/<name>.script, listed in DECOMP.md (Routes)
make lockstep-all DATA=files # every route, the decompiled build reading assets/
make statestep-all # every route: game state compared as objects, not addresses
make check # difftest + lockstep-all (the full suite, about 20 minutes)
make state-schema # regenerate the host-state schema after adding a region
make maps # export the areas to maps/ (the map data files)
make route-coverage # every route: remaining functions some route runs, per segment
Some routes start from a memory card (your own saves, never committed):
tools/routes/cards.json maps the card roles late and day3 to card files. The defaults in
tools/routelib.py are paths of the first machine: on another one (the Deck above) map every
role the routes use (late, day3, date_summer7, date_summer12, farm1) there.
make statestep-all compares against the recordings in build/state/ (made with make statestep-record, they hold game state and are never committed): copy that directory along when
checking on another machine.
Per-function context packs (ctx/, generated from your own disc, not in git) contain
Ghidra pseudocode and the exact recompiled C.
tools/psyq_names.pynames SDK functions using psx_psyq_signatures and generatesinclude/psyq_names.h.tools/targets_first_cutscene.jsonlists the functions on the milestone path.
Debug automation
HM_DEBUG=<file> runs a command script against the live game, one step per vblank.
src/debug/debug.c documents every command. Commands that call game code run between game
frames with the emulated clock frozen, so they don't disturb timing; skip_cutscene is the
exception: it plays out over game frames like the in-game skip.
cat > /tmp/day.txt <<'EOF2'
wait 5700 # tools/routes/new_game.script reaches the farm by then
skip_cutscene # skip the intro cutscenes as holding Start does: ends in the house
use_exit 0 # out to the farm: the Mayor's tour starts
skip_cutscene # questions are answered with x
state # area, position, facing, day/time, control
collision_map # walkability grid around the player (the game's own collision probes)
walkto -3888 2786 run # A* path around fences, rocks and buildings (searches near the player)
walkto -50 50 run # on to the field
spawn 0x109 # a cow next to the player
exits # this area's triggers; use_exit N takes one
use_exit 0 # into the house
shot /tmp/house.ppm
quit
EOF2
HM_DEBUG=/tmp/day.txt build/hmbtn-de game.iso --headless --fast --frames 100000 \
--script "$(cat tools/routes/new_game.script)"
Other commands:
- input and waiting:
press,advance(✕ until the player has control),wait_control; PC-layout input (with--settings-menu):key LShift+W 30,key E,key Esc,mouse left,mouse wheeldown 2,mouse_at X Y(the pointer at 320x240 screen pixels: hover and click in menus, thenmouse left;framerate 0makesgpushotfollow the game at once); - the world:
setpos,teleport,clear_field,tiles(raw farm tiles); - the player and calendar:
money,held_tool,day/season/weekday/weather(shown from the next area load); - raw state:
flag,peek/poke,wait_peek(wait until RAM holds a value; logs the vblank),ramdump PATH(2 MB of guest RAM); - settings as the in-game menu changes them:
setting display 1,setting scale 2,setting geometry 0,setting framerate 0|1|2(30 / 60 / display),setting controls 1(PS1 layout),setting saves 0|1(memory card / files),setting volume_music 0-100; save_file [PATH [RAM]]: a mid-day save file as the diary's "Make diary entry" makes it, from wherever the player stands (RAM: also dump the RAM it was made from);gpushot PATH: the GPU renderer's frame at internal resolution (with--renderer gpu);- the map editor (with
--editor):wait_editor(until it is open on a loaded map),editor_cam X Z DIST YAW PITCH,mouse_world X Z [Y](the mouse to where that point is drawn; without Y on the ground),mouse_at X Y,mouse_down/mouse_up [left|right|middle],key_down NAME/key_up NAME(a key held across commands: Shift-click),editor_state(tool, selection, hover, the point under the mouse, camera, linked maps, the tile selection); present_seq DIR HZ N: for the next N vblanks, alongside the following commands, the pictures a display at HZ shows with frame interpolation, asDIR/f00000.ppm, ... (the same pictures as a window at that refresh rate; withsetting framerate 0, the game's own);resume_ramdump PATH/wait_resume: write guest RAM on the first frame after a mid-day save loaded / wait until it has (compare withsave_file's RAM to check a resume).
The hooks do nothing unless HM_DEBUG is set; lockstep runs are unaffected.
How it fits together
tools/gen_thunks.pyreads the/* <segment> <ADDR> */marker above every decompiled function. It generates register-ABI thunks under the recompiler's names, so recompiled code, jump tables and interrupt handlers call the C version unchanged.- The recompiler emits the replaced functions as
orig_*, and difftest uses them as the reference. - Calls go to named C functions;
CALL(fn)is only for function pointers stored as guest addresses (no code is called by a numeric address any more). - Decompiled code reaches guest memory through typed accessors (
M32,GVAR, ...); the game's own state lives in host structs (g_player,g_npcs,g_clock, ...), not at PS1 addresses. - Game memory keeps the PS1 layout in its first 2 MB, with 32-bit guest pointers (
gptr); the area data and the packet buffers live above, in 32 MB of RAM (DECOMP.md, "Memory layout").