#!/usr/bin/perl
# explore -- play the reconstructed Explore game on the MBasic interpreter.
# Runs with no arguments using sensible defaults; options override them.
use strict; use warnings;

# SECURITY: the game exposes a full shell escape (the ".."/"m" commands -> the
# `do` builtin) and honors environment variables (MBASIC_LIB adds to @INC), so
# it must run with the invoking user's own privileges.  Refuse to start
# setuid/setgid -- this must happen in a BEGIN, before the MBASIC_LIB `use lib`
# below (a compile-time statement) can act on an attacker's environment.
BEGIN {
    my ($rgid) = split ' ', $(;      # real primary gid
    my ($egid) = split ' ', $);      # effective primary gid
    die "explore must not be installed or run setuid/setgid.\n"
        if $< != $> || $rgid != $egid;
}

# New files the game creates (lock/registry/message files, seeded data) should
# be group-writable so other players in a shared game can use them; a 0002
# umask gives 0664/0775.  (MBasic and the lock code create with mode 0666, so
# the umask decides the group/other bits.)
umask 0002;

# Restore default SIGINT handling at exit (quit_off sets it to IGNORE and an
# abnormal path might not reach quit_on).
END { $SIG{INT} = 'DEFAULT'; }

# When installed, MBasic and Explore are on the system @INC via the p5-MBasic
# and p5-Explore packages -- no path munging is needed.  For running from an
# unpacked source tree without installing, set MBASIC_LIB to the lib directory.
use if defined $ENV{MBASIC_LIB}, lib => ($ENV{MBASIC_LIB} // '');

use MBasic::Interp;
use MBasic::Registry;
use Explore::Builtins;

# --- defaults (overridable by options / environment) -----------------------
#   The game's read-only content lives at SHAREDIR, its read-write data at
#   VARDIR.  By default the game's Multics path ">site>explore_dir" is mapped
#   to SHAREDIR, and the writable files (named in explore.rwdir) live under
#   VARDIR.  On a packaged install these default to the FHS locations.
my $sharedir = $ENV{EXPLORE_SHAREDIR} // '/usr/local/share/explore';
my $vardir   = $ENV{EXPLORE_VARDIR}   // '/var/games/explore';
my $basic    = $ENV{EXPLORE_BASIC};                  # derived from share if unset
my $helpers  = $ENV{EXPLORE_HELPERS};                # derived from share if unset
my $root     = $ENV{EXPLORE_ROOT}     // '/';       # fallback anchor for '>'
my @prefix;                                          # extra --prefix M=U pairs

while (@ARGV && $ARGV[0] =~ /^--/) {
    my $f = shift @ARGV;
    if    ($f eq '--share')   { $sharedir = shift @ARGV; }
    elsif ($f eq '--var')     { $vardir   = shift @ARGV; }
    elsif ($f eq '--basic')   { $basic    = shift @ARGV; }
    elsif ($f eq '--helpers') { $helpers  = shift @ARGV; }
    elsif ($f eq '--root')    { $root     = shift @ARGV; }
    elsif ($f eq '--prefix')  { push @prefix, shift @ARGV; }  # "MULTICS=/unix"
    elsif ($f eq '--help') {
        print <<"USAGE";
usage: explore [options] [game-args...]
  --share DIR    read-only game content (default $sharedir)
  --var DIR      read-write data directory (default $vardir)
  --basic FILE   the main BASIC program (default <share>/explore.basic)
  --helpers DIR  BASIC helper .basic files (default <share>)
  --root DIR     anchor for unmapped Multics '>' paths (default /)
  --prefix M=U   map a Multics path prefix M to a Unix path U (repeatable);
                 e.g. --prefix '>site>explore_dir=/usr/local/share/explore'
Environment: EXPLORE_SHAREDIR, EXPLORE_VARDIR, EXPLORE_BASIC, EXPLORE_HELPERS,
             EXPLORE_ROOT, MBASIC_LIB.
USAGE
        exit 0;
    }
    else { die "unknown option $f (try --help)\n"; }
}

# derive defaults that depend on --share, after parsing
$basic   //= "$sharedir/explore.basic";
$helpers //= $sharedir;

$Explore::Builtins::ROOT = $root;

# default prefix map: the game's Multics base -> the read-only share dir.
Explore::Builtins::add_prefix('>site>explore_dir', $sharedir);
# any user-supplied --prefix M=U pairs (later ones are still longest-first)
for my $spec (@prefix) {
    my ($m, $u) = split /=/, $spec, 2;
    die "bad --prefix (want MULTICS=/unix): $spec\n" unless defined $u;
    Explore::Builtins::add_prefix($m, $u);
}

my $registry = MBasic::Registry->new;
Explore::Builtins::register_all($registry);
$registry->register('set_acl',  sub { });   # commented out on Multics: no-op
$registry->register('exec_com', sub { });

# --- prepare the writable data directory (or fall back to read-only) --------
# On first run we create the writable directory and seed it from the share
# masters so that wins and hours edits persist.  If the directory cannot be
# created or written -- e.g. a normal user on a system where an administrator
# has not set up a shared, group-writable /var/games/explore -- we must run the
# game READ-ONLY, because the packaged explore.rwdir points at that directory
# and the game would otherwise try (and fail) to read its data from there.
#
# To force read-only cleanly we override the rwdir the game reads: we point the
# game's ">site>explore_dir>explore.rwdir" at a temporary file containing the
# single line "none", which the game treats as "read-only, no writable dir".
# In read-only mode the game reads hours.data/winners.data from the share
# directory (where the masters live), so it plays; it simply does not record
# wins or enable multiplayer.
my $writable = _seed_var_dir($sharedir, $vardir);
unless ($writable) {
    require File::Temp;
    my ($fh, $tmp) = File::Temp::tempfile(UNLINK => 1);
    print $fh "none\n"; close $fh;
    # map the exact rwdir path the game opens to this temporary "none" file
    Explore::Builtins::add_prefix('>site>explore_dir>explore.rwdir', $tmp);
    warn "explore: $vardir is not writable; playing read-only "
       . "(wins will not be recorded).  See the README (PERMISSIONS) to enable "
       . "writable or multiplayer play.\n";
}

my $interp = MBasic::Interp->new(
    registry    => $registry,
    search_path => [ $helpers ],
    argv        => [ @ARGV ],
);
$interp->load_main($basic);
# Validate every helper up front (load, link, index) so a load-time error in a
# helper -- a parse rejection, a dangling jump, a duplicate sub -- is reported
# now, before play begins, rather than dying deep in a session the first time
# that helper is called (which would lose the player's progress).
eval { $interp->load_all_helpers($helpers); 1 }
    or die "explore: a BASIC helper failed to load:\n$@";
$interp->run(pathxlate => \&Explore::Builtins::mult_path);

# Copy the sample writable files to the var dir if absent; return 1 if the var
# dir is usable (writable), 0 to fall back to read-only.  The read-only masters
# of hours.data/winners.data live in the share dir (they are also read directly
# in read-only mode); here we copy them to the var dir for writable play.
sub _seed_var_dir {
    my ($share, $var) = @_;
    require File::Copy;
    unless (-d $var) {
        eval { require File::Path; File::Path::make_path($var); 1 } or return 0;
    }
    return 0 unless -w $var;
    for my $name (qw(hours.data winners.data)) {
        my $dst = "$var/$name";
        my $src = "$share/$name";
        # copy the master in only if it is missing.  File::Copy::copy does not
        # preserve mode, so set the working copy group-writable (0664 & ~umask)
        # for shared play -- matching the manual PERMISSIONS instructions.
        if (!-e $dst && -e $src) {
            File::Copy::copy($src, $dst) and chmod(0664 & ~umask, $dst);
        }
    }
    return 1;
}

__END__

=head1 NAME

explore - play the reconstructed 1980 Multics game "Explore"

=head1 SYNOPSIS

    explore                          # play with default locations
    explore --share DIR --var DIR    # override the data locations
    explore --prefix '>a>b=/unix/x'  # add a Multics->Unix path mapping

=head1 DESCRIPTION

C<explore> runs the reconstructed 1980 Multics adventure game I<Explore> on the
L<MBasic> interpreter, executing the authentic BASIC source unmodified.  With no
arguments it uses sensible default locations and auto-seeds a writable data
directory so that wins and sorcerer edits persist; if that directory cannot be
written it falls back to read-only single-player.

=head1 OPTIONS

=over 4

=item B<--share> I<DIR>

The read-only game content directory (the BASIC program, the helper C<.basic>
subroutines, and the read-only data).  Default F</usr/local/share/explore>.

=item B<--var> I<DIR>

The read-write data directory (working C<hours.data>, C<winners.data>, and the
multiplayer files).  Default F</var/games/explore>.

=item B<--basic> I<FILE>

The main BASIC program.  Default F<< <share>/explore.basic >>.

=item B<--helpers> I<DIR>

Where the BASIC helper C<.basic> files live.  Default F<< <share> >>.

=item B<--root> I<DIR>

The anchor for any Multics C<< > >> path not covered by a prefix mapping.
Default F</>.

=item B<--prefix> I<MULTICS>=I<UNIX>

Map a Multics path prefix to a Unix path, repeatable.  By default
C<< >site>explore_dir >> maps to the share directory, so the game's unmodified
paths resolve without editing the BASIC.

=back

=head1 ENVIRONMENT

C<EXPLORE_SHAREDIR>, C<EXPLORE_VARDIR>, C<EXPLORE_BASIC>, C<EXPLORE_HELPERS>,
C<EXPLORE_ROOT> mirror the options.  C<MBASIC_LIB>, if set, adds a directory to
C<@INC> (for running from an unpacked source tree without installing).

=head1 FILES

The read-only content in the share directory: F<explore.basic>, the helper
F<exp_*.basic> files, F<explore.data>, F<explore.help>, and the sample
F<hours.data> / F<winners.data> masters.  The writable copies live in the var
directory.  See the distribution README for the F<hours.data> format and for
multiplayer setup.

=head1 SEE ALSO

L<Explore::Builtins>, L<MBasic>.

=head1 AUTHOR

Jim Lippard <lippard@discord.org>

=head1 LICENSE

Copyright (c) 2026 Jim Lippard.  Free software under the BSD 3-Clause License.

=cut
