Complete snapshot capture of per-env state + datadir reset speedup + arena memory-map - #5
Merged
Merged
Conversation
…jection Adds three ctypes entry points for the curriculum-learning traversal: - nle_goto_abs(dnum, dlevel): a branch-unpinned variant of nle_goto_depth. Unlike goto_depth (which pins dest.dnum = u.uz.dnum and clamps to the Dungeons-of-Doom branch, ~27 levels), this schedules a deferred goto_level to an arbitrary (dnum, dlevel), so a curriculum can jump DoD 1-3 -> Gehennom 47-50 or into the Elemental Planes. Reuses goto_level's cross-branch move + on-demand mklev. Grants the Amulet when targeting the endgame so the goto_level gate routes the hero to the Plane of Earth. - nle_num_dungeons / nle_dungeon_info: expose the dungeon table (name, depth_start, num_dunlevs) so the caller maps absolute Dlvl N -> (dnum, dlevel) and locates Gehennom / the Elemental Planes by name. - nle_set_state now accepts str/dex/con/int/wis/cha (NetHack-encoded value), setting acurr + amax, for the stats-only deep-jump upgrade. Validated: seed 19 reaches Gehennom depths 47-50 (real generated floors, hero alive, steppable); Earth/Air/Water planes reached with no bubble panic; attributes round-trip. Existing engine suite 28/28 green.
The reveal_map render-time overlay filled obs->glyphs/chars/colors but not obs->tty_chars. Harness consumers (render_map_view, feature/hostile extraction) render the agent's map from tty_chars, so full vision never reached the agent. Overlay the revealed terrain + monsters into the tty buffer's map region (level (x,y) -> tty row y+1, col x-1; tty_chars is filled by the TMT callback before fill_obs). Guarded to reveal_map>0, so default obs is unchanged (golden parity holds).
…et dat copy) nle_settings gains an optional `datadir`. When set, nle_start points the read-only file prefixes (DATA/HACK/SYSCONF/CONFIG) at it and keeps only the writable prefixes (LEVEL/SAVE/BONES/SCORE/LOCK/TROUBLE) under `hackdir`. The engine opens the bulk game data (the DLB nhdat, data/oracles/rumors, config, help) strictly O_RDONLY, so many envs can share one read-only data directory while each keeps only a tiny per-env writable hackdir. Previously the binding copied the whole ~3.7MB dat tree into a fresh temp dir on every reset (~29ms, the dominant reset cost); with this split no copy is needed. datadir empty => every prefix resolves under hackdir, exactly reproducing the prior single-directory behavior (verified byte-identical game traces across 30 seeds x 200 steps, old build vs new).
Adds nle_dbg_memmap(ctx, path): writes the per-env arena layout to a file — arena base/used/cap, every named per-env buffer by arena offset, the live monster (fmon) and object (fobj) chains, and the monster grid (level.monsters[x][y]) annotated with fmon-membership and data-validity per cell. Chain/grid walks are bounded and arena-range-guarded so it is safe to call even on a corrupted state. Lets an arbitrary arena pointer be classified (which buffer/monster/object it falls in) and surfaces dangling pointers directly: grid entries not in fmon (stale grid pointers) and monsters whose ->data is outside &mons[]. Built while investigating the goto_depth+snapshot+reseed monster-corruption crash.
nle_dbg_memmap now leads with a "whole-game regions" section listing every region that holds per-env game state and that a snapshot must cover: the nle_ctx_t struct, the per-env arena (all dynamic NetHack state), the fcontext coroutine stack, and the rl display mirror (libc-allocated, outside the arena). The arena named-buffer + fmon/fobj/grid detail follows as before. Also adds ASAN-build support to nle_fast_reset.c (guarded by __SANITIZE_ADDRESS__, no-op otherwise): unpoison the fcontext stack region around the snapshot/restore memcpys (ASAN otherwise reports false stack-buffer-underflows on the fiber stack), and #undef the NetHack free macro so the malloc'd snapshot scratch buffers are freed through ASAN's interceptor instead of __libc_free. Lets the engine run under -fsanitize=address for memory-safety debugging.
…ergence) Two snapshot-completeness bugs, both from per-env state that lived OUTSIDE the arena and so was missed by nle_fr_snapshot (which captures arena + coroutine stack + rl mirror): 1) CRASH (SIGSEGV). nle_pline_state (pline.c) was libc-calloc'd but holds `_you_buf`, an ARENA pointer. After a restore rewinds the arena, that stale pointer aliases a reused arena offset, so the next You_hear/pline writes its message string over whatever now lives there — observed corrupting a live monster's `data` field, then SIGSEGV'ing in passivemm during pet combat. Reproduced deterministically via snapshot/restore/reseed branching after modify(goto_depth). Root-caused with a per-env arena memory map + non-allocating invariant probes (mon->data == &mons[mnum]); ASAN ruled out buffer overflows (the write is arena-internal). Fix: arena-allocate nle_pline_state so the snapshot captures it. 2) OBSERVATION DIVERGENCE. The same class affected ~20 other lazy-init per-env state structs (apply/botl/cmd/display/dokick/.../uhitm/windows) and the rl display mirror's cached inventory_ + WIN_MESSAGE last_msg. Uncaptured, they leaked the prior branch's state through a restore, so replaying a fixed action line after a divergent branch produced different glyphs/blstats/ message/inv_* than the first replay. Fix: route every per-env *_state struct through nle_arena_calloc, and serialize inventory_ + WIN_MESSAGE last_msg in NetHackRL::save_mirror/load_mirror (the only obs-relevant rl-instance state not already mirrored). Forward-game behavior is unchanged (these moves only relocate where per-env state lives / what the snapshot captures): byte-identical observation traces across 30 seeds x 200 steps, pre vs post. With both fixes the differential snapshot test (snapshot -> divergent reseeded branch -> restore -> replay fixed line == first replay) passes all 40 seeds x 30 rounds: 0 crashes, 0 divergence.
…t overflow) The vision_radius tune knob set u.nv_range = (int) vision_radius with no bound. nv_range indexes circle_data[] via circle_ptr() (valid only up to MAX_RADIUS=15), so a large knob value (e.g. 1e9, or inf which is UB to cast) walked off the table -> SIGSEGV in vision_recalc at the first docrt()/newgame(). The hard-sight-limit path also computed vr*vr in int, overflowing for large vr. Clamp both uses to [1, MAX_RADIUS], testing `>= MAX_RADIUS` on the double before the (int) cast so inf/huge are caught safely. Values in the normal 1..14 range are unchanged (golden parity preserved). Found by fuzzing every tune knob with extreme values (0/neg/huge/inf/nan); vision_radius was the only one that crashed.
liujonathan24
added a commit
that referenced
this pull request
Jun 26, 2026
fix(nle): sync top-level include/nleobs.h with src/include (ABI fix, missed by PR #5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Engine-side changes (the harness PR liujonathan24/NetHack-engine#21 bumps the submodule to this branch).
Snapshot completeness (fixes a SIGSEGV + observation divergence).
nle_fr_snapshotcaptures the arena + coroutine stack + rl mirror, but ~21 lazy per-env*_statestructs werecalloc'd on the libc heap and escaped it. The worst,nle_pline_state, holds_you_buf(an ARENA pointer): after a restore rewinds the arena that pointer dangles and the nextYou_hear/plinewrites its message over a reused slot (a live monster'sdata) -> SIGSEGV inpassivemm. Fix: arena-allocate all*_statestructs, and serialize the rl mirror's cachedinventory_+WIN_MESSAGElast_msginNetHackRL::save_mirror/load_mirror.Reset speedup.
nle_settingsgains a read-onlydatadir; read-only file prefixes resolve there so the harness no longer copytree's the 3.7MB dat tree per reset (35ms -> ~1.3ms).Debug tool.
nle_dbg_memmapdumps the whole-game arena memory map (named buffers, fmon/fobj chains, monster grid) — used to root-cause the corruption.Forward-game behavior byte-identical (these only relocate where per-env state lives). Differential snapshot test passes 40 seeds x 30 rounds (0 crash / 0 divergence).