phonehack patches

Table of Contents

The patches phonehack carries against the pinned NetHack core (upstream/, submodule tag NetHack-5.0.0_Released). They are deliberately minimal: build glue, three tiny non-cheating event hooks, and one backport of upstream's own post-release bug fix. We add no game logic of our own. All integration goes through the shim windowport (see ARCHITECTURE), so these patches only add cross-compile targets, three one-line upcalls, and a verbatim upstream fix.

They are applied idempotently by build/build-android-libs.sh (Android) and build/test/build-host.sh (host test), not by a manual step.

The stack

0001-cross-to-android.patch (build glue; required)

Adds the CROSS_TO_ANDROID cross-compile target to NetHack's Makefile hint system and fixes one link error. Touches only build hints + config.h + libnhmain.c; no game code.

  • sys/unix/hints/include/cross-pre1.500 - the CROSS_TO_ANDROID variable block (per-ABI targets/android/<abi> output dir, CROSS~/~HACKDIR setup).
  • sys/unix/hints/include/cross-pre2.500 - the NDK clang recipe: toolchain paths from ANDROID_NDK~/~ANDROID_ABI~/~ANDROID_API (default NDK r27d), the Android CFLAGS (-DSECURE -DDLB -DLIBNH -DSHIM_GRAPHICS -DNOTTYGRAPHICS …, -fPIC, function/data sections), and the SYSOBJ set built from sys/libnh/libnhmain.c + the shim windowport + minimal unix glue.
  • sys/unix/hints/include/cross-post.500 - the libnh.a archive rule and the androidpkg target.
  • include/config.h - excludes CROSS_TO_ANDROID from the external-COMPRESS block (see the #undef COMPRESS note in BUILDING): a failed fork on Android leaves a stray <save>.Z whose later uncompress deletes the real save.
  • sys/libnh/libnhmain.c - drops a duplicate after_opt_showpaths that the 5.0.0 release also defines in src/earlyarg.c; the duplicate breaks the link. This is the one edit that is not pure build config.

Because the WASM port (CROSS_TO_WASM) already established this pattern upstream, our block is modeled on it and slots into the same hint files.

0002-phonehack-game-end-hook.patch (non-cheating hook; non-fatal)

One line in src/end.c really_done(), right after fixup_death(how): ph_note_game_end(how) latches the game-end type ('how') so nethack_jni.c's nativeQueryEnd() can format the exact death/ascension line for the in-app end-of-game screen. ph_note_game_end lives in nethack_jni.c; the patch adds only the call. Applied non-fatally: if it can't apply, nativeQueryEnd degrades to a killer-format-only prefix and the death screen still works.

0003-phonehack-combat-hook.patch (non-cheating hook; non-fatal)

Three bare one-liner ph_combat_event(...) calls at upstream combat funnel points, feeding the perceivable-combat UI:

  • src/uhitm.c known_hitum() - the hero attacks a monster (read before hmon() can free the target).
  • src/mhitu.c hitmsg() - a monster lands a blow on the hero.
  • src/mon.c xkilled() - the hero kills a monster.

All logic lives in nethack_jni.c's ph_combat_event(), which fires only for a perceivable combatant (canspotmon) and forwards only an event kind, a Hallucination-safe name, and the target tile - never damage, to-hit, or HP (see the non-cheating invariant in ARCHITECTURE). Applied non-fatally: if it can't apply, the function is simply never called and the combat feed is off; the build does not abort.

0005-phonehack-speech-hook.patch (non-cheating hook; non-fatal)

One macro in include/sndprocs.h: the no-soundlib SetVoice(mon, tone, vol, moreinfo) (which upstream expands to nothing when no soundlib is integrated) instead forwards (mon, vol, moreinfo) to ph_note_voice() in nethack_jni.c. That shim function records the speaker's m_id + the voice_* moreinfo flags for the current turn; shim_putstr then combines it with PLINE_VERBALIZE (still set during putmesg, so read directly - no patch needed for that half) to attribute spoken message lines. A speaker is named only via canspotmon + distant_monnam at message time. set_voice()'s own #ifdef SND_SPEECH body is untouched; zero game-behavior change. Applied non-fatally: if it can't apply, speech attribution degrades to spoken-vs-not-spoken (no speaker names, no bubbles) and the build goes on. Not applied by the host test build (which patches only 0001 + 0004), so no host-side stub is needed.

0006-vault-guard-newsym.patch (upstream backport; non-fatal)

A vault guard that has finished escorting you is parked at <0,0> ("never-never land", vault.c parkguard()) and stays alive there until its temporary corridor can be torn down. Two places then tried to redraw that off-map spot, and newsym() answers an off-map coordinate with impossible(), so the player got "Program in disorder!" right after "Suddenly, the guard disappears."

Backported verbatim from two post-5.0.0 upstream commits:

  • c42d35ea "avoid newsym(0,0) for vault guard" - src/monmove.c postmov() takes the gd_move() return of 1 as "the guard moved" and redraws it at its new position, which is <0,0>. Guard the call with if (mtmp->mx), matching the identical guard 145 lines earlier.
  • d13eceb2 "eliminate one more source of vault guard newsym msgs" - src/display.c see_monsters() walks fmon and redraws every monster, parked guard included. Skip it via the new PARKEDMONSTER() macro in include/monst.h. This one fires on redraw and on restore, which for phonehack means every background-save/resume cycle, not just ^R.

We take only the two guards and the macro, not that commit's MON_PARKED mstate bit and its restore.c fixup: the macro upstream added doesn't use the bit yet, and skipping it keeps the backport off the save format entirely.

Drop this patch at the next release bump if the fixes are in the new tag. Applied non-fatally: without it the game still plays correctly (the message is cosmetic; newsym() returns immediately and no game state is touched).

0009-phonehack-multi-stop-hook.patch (non-cheating hook; non-fatal)

One weak upcall in nomul() (src/hack.c): when a counted command (20s) is cut short, it reports how many of the player's own count were left, so the progress chip counts actions rather than turns and can show the line that stopped it. Without it the chip falls back to counting turns.

0004-cross-to-host-test.patch (test-only build glue; not applied on Android)

Adds a CROSS_TO_HOST target mirroring CROSS_TO_ANDROID but with host == target (system cc~/~ar, no NDK, no -fPIC), so the off-device C harness in build/test/ can build libnh.a natively. Same three hint files, plus it relies on the after_opt_showpaths de-dup from 0001 to link. Applied only by build/test/build-host.sh; the Android build never applies it.

Note the dependency: 0004 and 0001 edit adjacent regions of the same hint files, so build-host.sh applies 0001 then 0004 in order and gates each on a text sentinel rather than a reverse-check.

Rebasing the stack onto a new NetHack release

The whole point of the shim-windowport design is that a version bump is mechanical. Discipline:

  1. Keep the shim boundary. Do not add game-logic edits. If a new feature needs data, prefer a read-only nativeQuery* gated on the parked safe point (mapReady()) over a patch. Patches are a last resort and must stay one-liners whose logic lives in nethack_jni.c.
  2. Bump the submodule, then re-apply. Point upstream/ at the new tag and git -C upstream apply patches/*.patch. Fix any offset/conflict, then refresh the patch files so they apply cleanly against the new base. The two event hooks (0002, 0003) apply non-fatally, so drift there degrades a feature rather than breaking the build - but fix them anyway.
  3. Re-verify the callback contract. Diff win/shim/winshim.c for new/changed callback names or signatures and mirror them in the shim_* dispatch in nethack_jni.c and the cb* methods in NetHackBridge.kt (dispatch is by name; the fmt string is advisory).
  4. Re-check status enums. Compare include/botl.h (the BL_ enum and BL_MASK_*) against GameSession's status handling and the condtab, since the status bar decodes those indices.
  5. Re-check glyph flags. If new MG_* glyph flags appear, decide whether the Compose renderer must honor a corresponding rc option (as it does for hilite_pet~/~hilite_pile; see ARCHITECTURE).
  6. Re-check the COMPRESS / build guards. Confirm the config.h COMPRESS block and the build-android-libs.sh #undef COMPRESS / REPRODUCIBLE_BUILD sentinels still apply against the new tree.
  7. Rebuild and smoke. cd android && ./gradlew assembleDebug, then run a new game: move, inventory, save, background, kill, restore. Run the host and on-device save-integrity harnesses (see BUILDING).

Reminder: remember to git -C upstream reset --hard between an Android build and the host test build - each bakes its own target into the generated Makefiles (the host-vs-Android pollution gotcha in BUILDING).