#!/perl # just to get emacs into perl mode when editing this package packets; use strict; use English '-no_match_vars'; use GameConfig; use Getopt::Long; local $WARNING = 1; # same as usual -w option local $OUTPUT_AUTOFLUSH = 1; ############################################################################# # Command-line options and configuration file. # We do a first pass through the command-line options since that # might give a non-default config file, then we parse the config file, # then we parse the command line leftovers using that data. ### First pass through command line: Getopt::Long::config("default"); Getopt::Long::config(qw( auto_abbrev bundling permute )); my @argv = (); # @ARGV "leftovers" to re-process my %opt; # defaults: $opt{config} = "bin/packets.config"; Getopt::Long::GetOptions (\%opt, qw( extract|e latex|l xdvi|x dvips|d gv|g print|p ), # actions qw( all-characters|a ingame|i:s config=s ), # flags "only" => \&later, "except" => \&later, "help|h" => sub { die "\n" }, "<>" => \&later) or &usage; sub later { my $arg = shift; defined shift and $arg = "--$arg"; push @argv, $arg; } sub usage { die "Syntax: gmX packets [options] [charfiles] [--only|--except fieldlist] [charfiles] is one or more Charsheets/ files (.tex suffix optional), unless --all-characters was used, for whom to do packets. By default all extractable fields are used. --only uses only the listed ones. --except uses all except the listed ones. Non-options after --only or --except are fieldnames; before that they are charfile names. Otherwise, options may occur in any order, and the single-character versions may be bundled (-abc). --help, -h print this usage message and exit --config=configfile use 'configfile' instead of bin/packets.config --all-characters, -a do not specify [charfiles]; do (almost) all present --extract, -e extract fields from charsheets --latex, -l latex tex-type extractables (must --extract first) --xdvi, -x preview results of --latex --dvips, -d generate ps for all material (must --latex first) --gv, -g preview results of --dvips --print, -p lpr results of --dvips --ingame, -i[=mode] do initial read and/or write ingame (see html doc) See html/printing-packets.html for details.\n"; } ### Now that we know what the config file is, read it: my %config; # misc config file information, like extra dvips headers my %extract; # config file defns of extractable fields (incl charsheets) GameConfig::packets_config($opt{config}, \%config, \%extract); ### Second (and last) pass through command line: my $characters_in = GAME::path "Charsheets"; # .structure translation GAME::chdir "Charsheets"; # will want to look at what charfiles exist my %optstate = (); my @char = (); @ARGV = @argv; # parsing leftovers saved from first pass Getopt::Long::GetOptions ("only" => \&inclusion, "except" => \&inclusion, "<>" => \&process) or &usage; defined $optstate{"inclusion"} or inclusion("except"); # do all if nothing said sub inclusion { my ($flag, $val) = @_; &usage if defined $optstate{"inclusion"}; # can't do it twice $optstate{"inclusion"} = $flag eq "except"; # default "do this extract?" for (keys %extract) { $extract{$_}{"do"} = $optstate{inclusion}; # set to default } } sub process { my $arg = shift; if (defined $optstate{"inclusion"}) { # treat as extractable field name my $field = GameConfig::match_extractable(\%extract, $arg); $extract{$field}{"do"} = !$optstate{"inclusion"}; # switch to non-default } else { # treat as character file $arg .= ".tex" unless $arg =~ /\.tex$/; -f $arg or die "no such character file $arg in $characters_in\n"; $arg =~ s/\.tex$//; push @char, $arg; } } ######################################################################## # Remaining state setup based on results of parsing command line. if (exists $opt{"all-characters"}) { @char and die "may not give specific chars when using --all-characters\n"; @char = sort grep { !/^\./ && -f && s/\.tex$// && !/^template$/ } GM::contents; } @char or die "no chars!\n"; my @dofield = sort grep { $extract{$_}{'do'} } keys %extract or die "no extractable fields specified to extract\n"; my @action = grep { exists $opt{$_} } qw( extract latex xdvi dvips gv print ) or die "no actions specified to perform\n"; my $anylists = grep { $extract{$_}{"type"} eq "list" } @dofield; # determine whether reading, writing, both, or neither should be # in game dir ("") instead of the usual tmp dir ("/") my ($readfrom, $writeto) = qw( / / ); if (exists $opt{ingame}) { $opt{ingame} =~ /\A[rw]?\Z/ or die "valid -i,--ingame args are --ingame --ingame=r --ingame=w -i -ir -iw\n"; $opt{ingame} =~ /\Ar?\Z/ and $readfrom = ""; $opt{ingame} =~ /\Aw?\Z/ and $writeto = ""; } # # make sure the dirs we might need to write to exist for my $sub (qw( / Extracts DVI PS )) { my @relative = split "/", GAME::path "Charsheets/$sub"; my $need = $writeto; while (defined (my $step = shift @relative)) { $need .= "/$step"; my $dir = GM::path $need; -d $dir or GM::mkdir ($dir, 0700) or die "Unable to create $dir: $!\n"; } } # after the first write-to, we reset $readfrom to $writeto. # that is, with -ld you read the latex from somewhere, write the # dvi to somewhere else, then read the dvi from the latter and write ps. ############################################################################ # Perform requested actions, in order. # only really nasty conditions will make us die immediately; usually we # will attempt to continue to the end of the current action as much as # possible. at that point, we'll abort if anything sufficiently bad happened. # my $abort = 0; if (exists $opt{"extract"}) { print "\tExtracting fields\n"; # clean up previous extracts first GAME::chdir "${writeto}Charsheets/Extracts"; my @old = GM::contents; for my $char (@char) { GM::unlink grep /^\Q$char\E\^/, @old; } # regardless of $readfrom, we get charsheets themselves from actual game dir my $truechar = GM::path "Charsheets"; GAME::chdir "${writeto}Charsheets/DVI"; @old = GM::contents; for my $char (@char) { print "\t\t$char\n"; # a little cleanup here too GM::unlink grep /^\Q$char\E\....\Z/, @old; # and then latex with extraction activated my $sendto = GM::path "${writeto}Charsheets/Extracts"; GM::latex "\\def\\WriteGameExtractsAlongPath{$sendto} \\input $truechar/$char", "$char.tex to extract fields", $writeto; } $readfrom = $writeto; $abort and die "Fatal errors while extracting\n" } if (exists $opt{"latex"}) { print "\tLatex'ing tex-type extractables\n"; GAME::chdir "${writeto}Charsheets/DVI"; my @old = GM::contents; for my $char (@char) { print "\t\t$char\n"; for my $field (@dofield) { if ($extract{$field}{"type"} eq "tex") { print "\t\t\t$field "; my $base = "$char^$extract{$field}{'compress'}"; GM::unlink grep /^\Q$base\E\....\Z/, @old; # cleanup of old stuff my $file = GM::path "${readfrom}Charsheets/Extracts/$base"; if (-e "$file.tex") { print "\n"; GM::latex $file, "${char}'s $field"; } else { print "not present - presumed not in charsheet\n"; } # if necessary, make zero-length dvi file as a placeholder -e "$base.dvi" or close GM::open ">$base.dvi"; } } if ($readfrom ne $writeto) { # things created by -e and used by -d must be copied over local $INPUT_RECORD_SEPARATOR = undef; local $OUTPUT_RECORD_SEPARATOR = undef; for my $special ("Extracts/$char^.Lists", "Extracts/$char^.Name.tex", "DVI/$char.dvi") { my $rd = GAME::open "${readfrom}Charsheets/$special"; my $wr = GAME::open ">${writeto}Charsheets/$special"; print $wr (<$rd>); close $rd; close $wr; } } } $readfrom = $writeto; $abort and die "Fatal errors while latex'ing tex-type extractables\n"; } if (exists $opt{"xdvi"}) { print "\tXdvi'ing material latex'd by packets\n"; GAME::chdir "${readfrom}Charsheets/DVI"; for my $char (@char) { print "\t\t$char\n"; for my $field (@dofield) { print "\t\t\t$field"; if ($extract{$field}{"type"} eq "list") { print " are list-type\n"; next; } my $file = ($extract{$field}{"type"} eq "tex") ? "$char^$extract{$field}{'compress'}.dvi" : "$char.dvi"; # must be type "char" if (not -e $file) { print " file not found!\n"; } elsif (-z $file) { print " (none; presumed none in charsheet)\n"; } else { print "\n"; GM::xdvi $file; } } } if (exists $opt{"dvips"}) { print "Abort to fix things? [n] "; =~ /^\s*y/i and exit 0; } } ### Read in the ^.Lists file (if $anylists), parse the ones we're doing, ### return and cache anon hash of that info my %listinfo; sub read_lists { my $char = shift; exists $listinfo{$char} and return $listinfo{$char}; $listinfo{$char} = {}; $anylists or return; # info should never get looked at in this case! my $fh = GAME::open "${readfrom}Charsheets/Extracts/$char^.Lists"; my ($field, $base); while (<$fh>) { if (/^\s*EXTRACTABLE\s+(.*?)\s*$/) { $field = $1; exists $extract{$field} or die " nonexistent extractable '$field' listed!\n"; $extract{$field}{"type"} eq "list" or die "non-list-type extractable '$field' listed!\n"; if ($extract{$field}{"do"}) { my $compress = $extract{$field}{"compress"}; my $path = GM::path "$compress/DVI"; $listinfo{$char}{$field}{"path"} = $path; if (-d $path) { $listinfo{$char}{$field}{"base"} = "$char^$compress"; $listinfo{$char}{$field}{"files"} = {}; } else { print "no $field source dir $path; will abort\n"; delete $listinfo{$char}{$field}; $abort = 1; } } } elsif (/^\s*(.*?):\s*(NORMAL|SECRET)\s+(\S+)\s*$/) { my ($f, $expose, $file) = ($1, $2, $3); if ($f ne $field) { print "$f listed in the $field section! will abort\n"; $abort = 1; } elsif (exists $listinfo{$char}{$field}) { $listinfo{$char}{$field}{"files"}{$file} = $expose; } } else { print "$char^.Lists has bad line; will abort:\n\t$_\n"; $abort = 1; } } close $fh; return $listinfo{$char}; } # When you dvips foo.dvi -o bar.ps, the bar.ps gets comments in it with # that command line. So if either "foo" or "bar" contains information, # e.g. "traitorcop^Greensheets:eattheirbrains.dvi", it's bad if we want # to give the postscript to players, e.g. with email parts. So we # make symlinks pointing blah1.dvi to foo.dvi and blah2.ps to bar.ps; # then "dvips blah1.dvi -o blah2.ps" creates bar.ps out of foo.dvi but # knows only that it's dealing with these meaningless blahs, so only # those appear in the ps file comments. Whee! ### mask_dvips (source.dvi, dest.ps, options) sub mask_dvips { my ($dvi, $ps, @opt) = @_; my $mask = GM::path "/.gm.$PID/game-file-mask"; GM::unlink "$mask.dvi", "$mask.ps"; GM::symlink $dvi, "$mask.dvi"; GM::symlink $ps, "$mask.ps"; GM::dvips "$mask.dvi", "-q", @opt, "$mask.ps"; } if (exists $opt{"dvips"}) { print "\tDvips'ing to postscript files\n"; GAME::chdir "/.gm.$PID"; # process-specific temporary storage # include @dvipsopt for all but special things my @dvipsopt = map { "-h $_" } split " ", $config{headers}; for my $char (@char) { print "\t\t$char\n"; my $list = read_lists($char); GM::unlink "charnameheader.eps"; # cleanup of old stuff for list-types GAME::latex ("${readfrom}Charsheets/Extracts/$char^.Name", "${char}'s name header"); # no @dvipsopt for this one, it's a middleman mask_dvips("$char^.Name.dvi", "charnameheader.eps", "-E"); # .Lists, .Name prep done; now do actual extracts for my $field (@dofield) { print "\t\t\t$field "; if ($extract{$field}{"type"} eq "char") { my $ps = GM::path "${writeto}Charsheets/PS/$char.ps"; GM::unlink $ps; # cleanup of old stuff my $file = GM::path "${readfrom}Charsheets/DVI/$char"; if (-e (my $dvi = "$file.dvi")) { # should never be zero-length print "\n"; mask_dvips($dvi, $ps, @dvipsopt); } else { print ".dvi not found! did you do -l? will abort.\n"; $abort = 1; } } elsif ($extract{$field}{"type"} eq "tex") { my $base = "$char^$extract{$field}{'compress'}"; GM::unlink "$base.ps"; # cleanup of old stuff my $file = GM::path "${readfrom}Charsheets/DVI/$base"; my $to = GM::path "${writeto}Charsheets/PS/$base.ps"; if (-e (my $dvi = "$file.dvi")) { if (-s $dvi) { print "\n"; mask_dvips($dvi, $to, @dvipsopt); } else { print "(none; presumed none in charsheet)\n"; # make zero-length ps file as a placeholder close GM::open ">$to"; } } else { print ".dvi not found! did you do -l? will abort.\n"; $abort = 1; } } elsif ($extract{$field}{"type"} eq "list") { my $stuff = $list->{$field} or print "absent\n" and next; print "\n"; my @files = keys %{$stuff->{"files"}} or print "\t\t\t\t(none)\n" and next; my $base = $stuff->{"base"}; my $path = $stuff->{"path"}; for my $file (@files) { print "\t\t\t\t$file "; my $expose = $stuff->{"files"}{$file}; my $dvi = "$path/$file.dvi"; unless (-e $dvi) { print ".dvi not found! will abort.\n"; $abort = 1; next; } print "\n"; my @header = ($expose eq "SECRET") ? qw( -h charname_secret ) : (); my $dest = GM::path "${writeto}Charsheets/PS/$base:$file.ps"; mask_dvips($dvi, $dest, @header, @dvipsopt); } } } GM::unlink "charnameheader.eps"; # so as not to screw up later dvipsing } $readfrom = $writeto; $abort and die "Fatal errors while dvips'ing\n"; } if (exists $opt{gv}) { print "\tGV'ing dvips output\n"; GAME::chdir "${readfrom}Charsheets/PS"; for my $char (@char) { print "\t\t$char\n"; my $list = read_lists($char); for my $field (@dofield) { print "\t\t\t$field "; if ($extract{$field}{"type"} eq "list") { my $stuff = $list->{$field} or print "absent\n" and next; print "\n"; my @files = keys %{$stuff->{"files"}} or print "\t\t\t\t(none)\n" and next; my $base = $stuff->{"base"}; for my $file (@files) { print "\t\t\t\t$file\n"; GM::gv "$base:$file"; } } else { my $file = ($extract{$field}{"type"} eq "tex") ? "$char^$extract{$field}{'compress'}.ps" : "$char.ps"; # must be type "char" if (not -e $file) { print " file not found!\n"; } elsif (-z $file) { print " (none; presumed none in charsheet)\n"; } else { print "\n"; GM::gv $file; } } } } if (exists $opt{"print"}) { print "Abort to fix things? [n] "; =~ /^s*y/i and exit 0; } } my (%bychar, %byfile, %byprinter, %guild, %host, %ip, $jobid, %queuemap); if (exists $opt{"print"}) { print "\tPrint: planning out what to print where\n"; GAME::chdir "${readfrom}Charsheets/PS"; for my $char (@char) { print "\t\t$char\n"; my $list = read_lists($char); for my $field (@dofield) { print "\t\t\t$field "; my $base = "$char^$extract{$field}{'compress'}"; if ($extract{$field}{"type"} eq "char") { $abort += intend($char, $field, "$char.ps"); } elsif ($extract{$field}{"type"} eq "tex") { $abort += intend($char, $field, "$base.ps"); } elsif ($extract{$field}{"type"} eq "list") { my $stuff = $list->{$field} or print "absent\n" and next; print "\n"; my @files = keys %{$stuff->{"files"}} or print "\t\t\t\t(none)\n" and next; my $base = $stuff->{"base"}; for my $file (@files) { print "\t\t\t\t$file "; $abort += intend($char, $field, "$base:$file.ps"); } } } } $abort and die "Fatal errors while planning out printing\n"; print "\tPrint: examining printers"; # look at info for printers, set which are thought to be the Guild's; # see snmptranslate [-n or -d] on the dotted strings for info my $snmploc = ".1.3.6.1.2.1.1.6.0"; # location, w20-4xx* assumed Guild my $snmpcon = ".1.3.6.1.2.1.1.4.0"; # contact info for my $pr (keys %byprinter) { if (grep /^assassin\b/i, $pr, $host{$pr}) { # ok, these are obvious ones $guild{$pr} = 1; } else { my $ip = $ip{$pr}; my $info = GM::snmpget "-v 1 $ip public $snmploc $snmpcon"; $guild{$pr} = ($info =~ /sysLocation.*\"w20-4\d\d/i or $info =~ /sysContact.*\"(high-council\@|assassin\b)/i); } print "."; } print "\n"; # check for potential printer problems for (my $recheck = 1; $recheck; $recheck = !~ /\A\s*y\s*\Z/i) { for my $printer (sort keys %byprinter) { my $filterstatus; my $q = queue($printer, \$filterstatus); my $busy = ($$q{jobs} > 10 ? "($$q{jobs} jobs already)" : ""); print "\t\t$printer $$q{status} $busy\n"; print "\t\t $filterstatus\n"; } print "\tAre those ok? Hit 'y' to go on, 'n' to recheck, ctrl-c to quit: "; } print "\tPrint: sending jobs to printers\n"; my $qmapfile = GM::path "Charsheets/Q-$ENV{USER}-$PID"; my $qmap = GM::open ">$qmapfile"; select $qmap and $OUTPUT_AUTOFLUSH = 1 and select STDOUT; my @todo = @char; # chars not finished yet my %sent = (); # everything that's gone to the printers while (@todo) { my %current = (); # what's currently thought to be in printers my %q = (); # printer queue information for my $printer (keys %byprinter) { $q{$printer} = queue($printer); $q{$printer}{status} and warn "\t\tPRINTER TROUBLE: $printer status $q{$printer}{status}\n"; if (my @gmfiles = @{$q{$printer}{gmfiles}}) { undef @current{@gmfiles}; } } my @ps = (); # things to lpr this round # char on top should be all in printers if (my @left = grep { not exists $sent{$_} } @{$bychar{$todo[0]}}) { push @ps, @left; } elsif (not grep { exists $current{$_} } @{$bychar{$todo[0]}}) { print "\t\tCharacter $todo[0] done\n"; shift @todo; } else { # if a printer has no current game stuff, has a short queue, # or is thought to be the Guild's, send more stuff for my $printer (keys %byprinter) { my %qp = %{$q{$printer}}; my ($job) = grep { not exists $sent{$_} } @{$byprinter{$printer}}; push @ps, $job if $job and ($guild{$printer} or not @{$qp{gmfiles}} or $qp{jobs} < 10); unless ($job or @{$qp{gmfiles}}) { print "\t\tPrinter $printer done\n"; delete $byprinter{$printer}; } } } for my $ps (@ps) { my ($printer, $lpropt) = @{$byfile{$ps}}{"printer", "lpropt"}; print "\t\tSending $ps to $printer $lpropt\n"; my $job = "$PID-" . ++$jobid . ".ps"; $queuemap{$job} = $ps; print $qmap "$job\t$printer\t\t$ps\n"; GM::lpr("-P$printer $lpropt -J$job $ps"); undef $sent{$ps}; } sleep 1; } print "\t\tEverything seems to be out of the printers now\n"; GM::unlink $qmapfile; $readfrom = $writeto; $abort and die "Fatal errors while printing\n"; } sub intend { my ($char, $field, $ps) = @_; unless (-e $ps) { print "file not found; will abort\n"; return 1; } if (-z $ps) { print "file is empty; will skip\n"; return 0; } my $printer = $extract{$field}{"printer"}; my $lpropt = $extract{$field}{"lpropt"}; unless ($ip{$printer}) { ($host{$printer}) = (GM::lpc("printcap $printer") =~ /:lp=(.*)%/, "$printer-p"); if (my $addr = gethostbyname $host{$printer}) { $ip{$printer} = join ".", unpack('C4', $addr); } else { print "printer $printer not found; will abort\n"; return 1; } } push @{$bychar{$char}}, $ps; # list of jobs for each character push @{$byprinter{$printer}}, $ps; # list of jobs for each printer $byfile{$ps} = { "printer" => $printer, "lpropt" => $lpropt }; print "will go to $printer $lpropt\n"; return 0; } sub queue { my $printer = shift; my @q = split "\n", GM::lpq("-P$printer"); if (my $filter = shift) { ($$filter) = map { /(Filter_status: .*)/ } @q } my $x = ""; # shift off everything before jobs header line $x = shift @q while @q and not ($x =~ /rank/i and $x =~ /owner/i and $x =~ /job/i and $x =~ /files/i); # topmost job is usually rank "active" but may be "1" momentarily first my ($status) = split / /, (@q ? $q[0] : "active"); $status = "" if $status =~ /^active/i or $status eq "1"; my @gmfiles = @queuemap{ map { /\b($PID-\d+.ps)\b/o } grep { /\b\Q$ENV{USER}\E\b/io } @q }; return { "status" => $status, "jobs" => scalar(@q), "gmfiles" => \@gmfiles }; } print "Packets done.\n";