Revision history for Explore (Perl distribution)

1.4  2026-10-09
     Fixes at the direction of Jim Lippard by Claude Opus 5.0, from a review
     of the runner and its interaction with the interpreter.  Requires
     MBasic 1.4, which ends a run when terminal LINPUT finds the input
     exhausted instead of returning empty lines forever -- without that, the
     game's command loop spun at its prompt at 99% CPU, writing "? " without
     bound, after something as ordinary as feeding it a command file or a
     ".." escape that consumed the rest of stdin.
     - External commands (the ".."/"m" escape and the abbrev editor) run on
       the player's terminal rather than through backticks.  The output used
       to be accumulated in memory before the player saw any of it, so a
       command with unbounded output ended the session with "Out of memory!"
       -- reachable by accident, not just deliberately.  Output now streams,
       memory is bounded, and a pager or an interactive program behaves.
     - An external command can be interrupted without taking the game with
       it.  quit_off installs an ignored SIGINT, and an ignored disposition
       survives execve, so under "-modes ^quit" a command inherited the
       ignore and could not be interrupted at all.  The child now gets the
       default disposition back while the game ignores SIGINT for as long as
       the child runs: one ^C reaches both, the command dies, the game
       carries on at its command line.  ^quit still disables break for the
       game itself.
     - The "send" command sends something.  The game calls send_message with
       the recipient only, because on Multics it then read the message from
       the terminal; the builtin expected the text as a second argument, so
       it piped `write` a bare newline and nothing could ever be sent.
     - The sorcerer prompt ends the run at end of input instead of treating
       it as an empty word, which the game retries forever and in silence.
     - Seeding the writable data directory no longer follows a symlink
       planted there.  "!-e $dst" follows the link, so a dangling one read as
       absent and the copy -- and the chmod after it -- went through it,
       creating a file outside the directory as the player.  The copy is now
       created O_CREAT|O_EXCL|O_NOFOLLOW like every other file operation
       here, which also makes "only if missing" atomic.
     - The 0002 umask for shared play is no longer process-wide, where it
       also loosened the files a player creates for themselves -- saved games,
       .explore_abbrev, start_up.explore.  On a system whose users share a
       default group (staff, on macOS) that made a player's saved games
       writable by every other local user.  It is now applied only when a
       writable shared directory is in use, and narrowly around the seeding
       and lock-file creates that need it.
     - A missing option value is refused where it happens, naming the option,
       instead of yielding undef and a run of uninitialized-value warnings.
       An empty Multics prefix in --prefix is refused too: it matched the
       front of every absolute path and silently redirected all of them.
     - Game output is unbuffered, so a redirected session that is killed or
       that hangs still shows what happened.
     - $EDITOR carrying arguments ("vim -u NONE") is split into words rather
       than used as a single program name, which always failed.
     - A NUL in a pathname is refused rather than passed to sysopen and stat,
       which only warn about it and spill the interpreter's own file and line
       numbers onto the player's screen.
     Two 1980 game defects, both verbatim in artifacts/explore.basic.5.3 and
     neither introduced nor missed by the 1.3 dispatcher fix -- that fix made
     the BARE words work, while these are the words with a TRAILING SPACE and
     nothing after them, which take the "extract a name" path and extract an
     empty name:
     - "restore " hung the game outright and in silence.  9440 extracted an
       empty name and 9450 sent it back to 9390, which failed the exact
       match, saw the space at 9400 and returned to 9440 to produce the same
       empty name again, forever; nothing in that circuit changed anything or
       printed.  An empty name now takes the default, as the bare word does.
     - "save " saved to a file called just ".explore" while reporting "Game
       saved.....".  9010 jumped over the "9040 if len(y1$)=0" guard that was
       meant to catch this; 9040 is dead code in the 1980 listing too.
     - A save or restore name that the path translator will refuse (a Multics
       "<", or "..") is now refused by the game, which returns the player to
       the prompt, rather than reaching the file statement where the refusal
       is a die that ends the session and loses the game in progress.
     The added BASIC jumps are all backward, leaving the compiler's peak
     pending forward-reference count unchanged at 100, at line 7210 -- which
     is where this program already sits against the limit of about 100, so
     any new forward reference before 7210 will fail to compile on Multics.

1.3  2026-09-28
     Fixes at the direction of Jim Lippard by Claude Opus 5.0.  The two game
     bugs below, and the MBasic DIM bug behind "-abbrev NAME", were found and
     reported by Gunther Schmidl, who ported the game to Windows.  Requires
     MBasic 1.3.
     - "get all" no longer picks up the invisible barrier markers (the abyss,
       fissure and rock-1 objects, which carry an empty description and exist
       only to block movement).  The 2026 guard had been added to the loop
       that counts what is visible for a plain "get" (7005), but the "all"
       path at 7240-7350 is a separate pair of loops and had no such test, so
       "get all" silently added the markers to the player's inventory.  A
       room holding nothing but a marker now correctly reports "There is
       nothing here to get!".
     - A bare "save" or "restore" works and uses the default name.  The
       command dispatcher matched only "save " and "restore " WITH a trailing
       space (6230, 6240), so the bare word fell through to "I don't know the
       word" -- while the handlers at 8970 and 9390 already implemented the
       default SAVED_GAME_ name that explore.help documents, as unreachable
       dead code.  The 1980 source (artifacts/explore.basic.5.3) has the same
       four lines, so this is an original defect rather than a transcription
       or porting error; the internal evidence is that BASIC strings were not
       blank-padded (only h9$, which comes back from a PL/I helper, is
       trimmed at line 45), so the bare word really could not match.  6230
       and 6240 now match the word without the trailing blank; "saveXYZ" is
       still rejected, because 8970/8980 and 9390/9400 test exactly.

     Both changes are deliberately frugal with FORWARD references.  The
     Multics BASIC compiler allows only about 100 goto/then targets that are
     defined later in the program, and the game is near that limit -- an
     earlier version of these fixes added four and the compiler rejected it
     with "ERROR - 36, Severity 3 ... Too many missing lines".  Rewriting
     6230/6240 in place adds none (their targets are unchanged), and the
     "get all" guard adds exactly one.  For the same reason the presence loop
     at 7240 is left alone: guarding it too would report "There is nothing
     here to get!" in a room holding only markers, but would cost a second
     forward reference.  As it stands such a room simply takes nothing.
     - "-abbrev NAME" works again, given MBasic 1.3: the abbreviations are
       read during argument processing and were erased when control reached
       the dim block (and a file of more than ten entries faulted first).
     - New t/12_game_fixes.t covers all three.

1.2  2026-09-26
     Fix at the direction of Jim Lippard by Claude Opus 5.0:
     - The runner now separates its own options from the game's control
       arguments properly.  It had stopped scanning at the first argument that
       was not double-dash, so any runner option written AFTER a game argument
       was silently not parsed and was handed to the game instead ("explore
       -brief --share DIR" left the share directory at its default and passed
       "--share" and "DIR" to the BASIC, inflating cnt to 3).  Runner options
       are now recognized anywhere on the line and removed, so cnt counts only
       real game arguments and arg$(n) indexes only them; the relative order
       and adjacency of what passes through is preserved, which -abbrev,
       -modes and -pathname depend on.  "--" is honored as end-of-options for
       the one ambiguous case, a game argument or pathname beginning with
       "--".
     Docs:
     - The game's control arguments are now documented.  They had always been
       passed through and honored, but nothing said so beyond "[game-args...]"
       in the usage line: "explore --help", the POD, and the README now list
       all eight (-abbrev, -brief, -modes, -no_startup, -no_version,
       -pathname, -table_space, -version), note which three take the argument
       that follows, and explain the "--" separator.  New t/11_args.t covers
       the split.

1.1  2026-09-19
     Fixes and hardening from an external review (Claude Opus 5.0).  Requires
     MBasic 1.1.

     Multi-user / security:
     - mult_path (the interpreter's pathxlate containment boundary) now refuses
       parent-directory traversal ("..", and the Multics "<" parent operator),
       so a path built from player-controlled text cannot escape the mapped
       subtree.
     - exp_lock_, create, and sort_seg open their files O_NOFOLLOW, so a planted
       symlink in a world-writable (chmod 1777) game directory is refused rather
       than followed; sort_seg now rewrites atomically (temp + rename) so a
       crash mid-sort cannot lose the file.
     - Lock and run-time files are created mode 0666 (honoring the umask) and the
       runner sets umask 0002, so shared files are group-writable and the next
       player is not locked out (previously lock files came out 0644 and the
       write pre-check failed with "error" instead of "busy").
     - exp_lock_ is now re-entrant within a process: re-locking a path it already
       holds succeeds without reopening (which had leaked the first handle and
       dropped the lock on the next unlock).
     - The "explore" program refuses to start setuid/setgid (it has a shell
       escape via the ".."/"m" commands and honors environment variables), and
       the README says so next to the group-permission setup.

     Correctness:
     - exp_home_ now returns the player's real home directory, so per-user files
       (saved games, abbreviations, start_up.explore) are created there as
       documented.  Previously it returned an empty string, which -- with the
       runner's ROOT of "/" -- pointed every per-user path at the filesystem
       root, where a normal user cannot write, so the whole abbreviation feature
       silently did nothing.
     - _read_noecho always restores terminal echo, even if the prompt read dies
       (MBasic 1.1 can die on more run-time errors), so a failed sorcerer-word
       prompt no longer strands the terminal with echo off; if echo cannot be
       disabled it warns instead of silently echoing the word.
     - Auto-seeded hours.data / winners.data are made group-writable (0664),
       matching the manual PERMISSIONS instructions (File::Copy does not preserve
       mode).
     - The runner restores default SIGINT handling at exit.
     - The runner now eagerly loads and validates all helper .basic files at
       startup (via MBasic::Interp::load_all_helpers), so a load-time error in a
       helper is reported before play begins instead of dying deep in a session
       (where it would lose the player's progress) the first time that helper is
       called.  t/09_game.t asserts all ten shipped helpers load cleanly.

     Tests / docs:
     - With exp_home_ now returning a writable home, the abbreviation feature
       actually runs; its expander (which jumps out of an inner FOR to expand a
       match) exposed a strict-NEXT incompatibility that MBasic 1.1 fixes
       (NEXT now closes abandoned inner loops).  New t/10_abbrev.t exercises the
       whole path (define an abbrev, invoke it) as a regression guard.
     - t/09_game.t pins the clock to a fixed weekday (so it is date-independent
       and never hits the cave-closed path) and its input callback dies when
       exhausted, so a desync fails in seconds instead of hanging the build.
     - README: corrected the append-only-flag advice (this implementation
       rewrites files through MBasic, so uappnd/+a would stop wins being
       recorded), noted that the rot13 sorcerer word is not a secret, and added
       the setuid/setgid warning.

     NOTE: a separate weekend/holiday bug in explore.basic (the cave being
     reported closed on Saturday/Sunday/holidays even with always-open hours)
     is fixed in the BASIC source, not here.

1.0  2026-09-15
     - Initial release of the Perl distribution.
     - Runs the reconstructed 1980 Multics game "Explore" (game version 5.3)
       on the MBasic interpreter, executing the authentic BASIC source
       unmodified.
     - Provides Explore::Builtins (the native helpers and Multics command
       stubs, and Multics-to-Unix path translation) and the "explore" program.
     - Includes the game data (share/) and historical artifacts (the original
       transcribed BASIC and the 6.0 database).
