#!/usr/bin/perl # Filename: asq # Author: David Ljung Madison # See License: http://MarginalHacks.com/License my $VERSION= 2.03; # Description: Mame Cabinet front end # # Runs in a unix terminal window and is controlled by mame keys # (Hence the name - "asq" is buttons 1,2,3 for player 2) # # Also reads ~/.asqrc or $0.rc for a list of non-mame games # to add to the top of the menu, format is: # # Bugs: Can't remap shifted keys # Requires: Curses::UI # SMALL PATCH TO CURSES::UI REQUIRED! # # Change Curses/UI/Listbox.pm (line 430 in v0.71) from: # # $this->run_event('-onchange') if $changed; # to: # $this->run_event('-onchange') if $changed || $this->{-select_again}; # # SMALL PATCH TO CURSES::UI REQUIRED! use strict; use Curses; # Gives us KEY_UP, etc... use Curses::UI; # Also optionally uses Tk, see setup_Tk() below ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my $XMODMAP = "xmodmap"; # For key remapping my %GAMES; ################################################## # Usage ################################################## sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR "\n"; print STDERR "Usage:\t$PROGNAME [-d] \n"; print STDERR "\tDoes something to the given file\n"; print STDERR "\t-d\tSet debug mode\n"; print STDERR "\n"; exit -1; } sub parse_args { my $file; while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } usage("Too many files specified [$arg and $file]") if (defined($file)); $file=$arg; } #usage("No file defined") if (!defined($file)); ($file); } sub debug { return unless $MAIN::DEBUG; foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; } } ################################################## # Image stuff (TK) ################################################## my $MW; my $PHOTO; my %LOADED; sub show_img { my ($img) = @_; return 0 unless $MW; $PHOTO->blank; if ($img && -f $img) { eval { $PHOTO->read($img, -shrink) }; # Avoid fatal errors my $w = $@; if ($w) { $w .= "\nMaybe you need a Tk::Loader for image type: [$img]?\n" if $w =~ /recognize data in image/; mywarn($w); } } $MW->update; return 1; } sub image_window { return if defined $MW; $MW = new Tk::MainWindow(-width => '290', -height => '290'); my $canvas = $MW->Canvas(-height => 290, -width => 290, -background => 'white'); $PHOTO = $canvas->Photo(); my $image = $canvas->createImage(5,5, -image => $PHOTO, -anchor => 'nw'); $canvas->pack(-side => 'top'); $MW->MapWindow; show_img(); # Blank it.. } # "require" instead of "use" so we don't require Tk if they don't use images. # And even cooler, we only load JPEG/PNG/.. loaders if they need them. :) use Tk; # fix a bug.. sub setup_Tk { my ($types) = @_; require Tk unless $LOADED{Tk}++; # Also check "type" - and get that loader if we haven't yet. foreach my $type ( split(',',$types) ) { require Tk::JPEG if ($type =~ /jpe?g$/i) && !$LOADED{jpeg}++; require Tk::PNG if ($type =~ /png$/i) && !$LOADED{png}++; require Tk::TIFF if ($type =~ /tiff$/i) && !$LOADED{tiff}++; } # If you know of any other generic Tk::type loaders, let me know. # And bring up the window image_window(); } # This is poking into Curses::UI::Listbox, so it might break.. sub get_listbox_active { my ($win) = @_; $win->{-values}->[$win->{-ypos}]; } sub try_show_img { my ($dir,$name,$types) = @_; foreach my $type ( split(',',$types) ) { my $file = "$dir/$name.$type"; return show_img($file) if -f $file; } return 0; } # Update the image window with the current active list item sub update_img { my ($win,$name) = @_; return show_img() unless $win; return show_img() unless $GAMES{$name}{images}; my $item = get_listbox_active($win); return show_img() unless $item; # Try to find the corresponding file my $dir = $GAMES{$name}{images}; my $type = $GAMES{$name}{image_type}; # Obvious attempt return if try_show_img($dir,$item,$type); # Remove path from item $item =~ s|.*/||; return if try_show_img($dir,$item,$type); # Remove .postfix from item $item =~ s|\..{0,4}$||; return if try_show_img($dir,$item,$type); # "Pacman (Japan)" -> "Pacman" my $try = $item; $try =~ s/\s*\([^\)]*\)$//; $try =~ s/\s*\[[^\]]*\]$//; return if try_show_img($dir,$try,$type); $try =~ s/\s*\([^\)]*\)//g; $try =~ s/\s*\[[^\]]*\]//g; return if try_show_img($dir,$try,$type); # Kludge: "'88 Games.jpg" -> "`88 Games.jpg" $try =~ s/'/`/g; return if try_show_img($dir,$try,$type); # If it's mame, try the short name if ($GAMES{$name}{type} eq 'MAME' && ($try = $GAMES{_mame}{short}{$item})) { return if try_show_img($dir,$try,$type); } # Nope - just blank the window return show_img(); } ################################################## # Curses utilities ################################################## my $ROOT; my $ROOTWIN; my $MAIN_LIST; sub curses_init { $ROOT = new Curses::UI ( -clear_on_exit => 0 ); $ROOT->set_binding( sub{ exit }, "\cQ" ); $ROOT->set_binding( sub{ exit }, "q" ); $ROOTWIN = $ROOT->add('root', 'Window', -padtop=>0, -padbottom=>0, ipad=>0); } my $IN_CURSES = 0; my %WARNINGS; sub mywarn { my ($str,@args) = @_; return if $WARNINGS{$str}++; $IN_CURSES ? $ROOT->error(sprintf "[$PROGNAME] ".$str."\n",@args) : printf STDERR "[$PROGNAME] ".$str."\n",@args; } sub fatal { mywarn(@_); exit(-1); } ######################### # Bindings for all selectors ######################### # hack! escape looks like "" to curses. Unfortunately it has a delay as well.. sub KEY_ESCAPE() { "" }; sub mame_bindings { my ($win,$name) = @_; $win->set_binding( sub { $win->option_first; update_img($win,$name) }, "5" ); $win->set_binding( sub { $win->option_last; update_img($win,$name) }, "7" ); $win->set_binding( sub { $win->option_prevpage; update_img($win,$name) }, KEY_LEFT() ); $win->set_binding( sub { $win->option_nextpage; update_img($win,$name) }, KEY_RIGHT() ); $win->set_binding( sub { $win->option_prev; update_img($win,$name) }, KEY_UP ); $win->set_binding( sub { $win->option_next; update_img($win,$name) }, KEY_DOWN ); $win->set_binding( sub { $win->option_prev; update_img($win,$name) }, "k" ); $win->set_binding( sub { $win->option_next; update_img($win,$name) }, "j" ); $win->set_binding( sub { $win->option_prev; update_img($win,$name) }, "r" ); $win->set_binding( sub { $win->option_next; update_img($win,$name) }, "f" ); $win->set_binding( sub { foreach (1..5) { $win->option_prevpage; } update_img($win,$name) }, "d"); $win->set_binding( sub { foreach (1..5) { $win->option_nextpage; } update_img($win,$name) }, "g"); $win->set_binding( sub { $win->option_select; update_img($win,$name) }, "1" ); $win->set_binding( sub { $win->search_forward; update_img($win,$name) }, "/" ); $win->set_binding( sub { $win->search_backward; update_img($win,$name) }, "?" ); update_img($win,$name); } # For curses_system sub add_text { my ($out,$height,@text) = @_; $height-=2; # Space for border @text=splice(@text,$#text+1-$height) if $#text+1>$height; $out->text(join('',@text)); $out->draw(); @text; } ######################### # Keyboard remapping ######################### my %KEYBOARD; # Initial keyboard mapping sub get_initial_map { return if %KEYBOARD; open(MAP,"$XMODMAP -pke|") || fatal("Remapping keys and can't run xmodmap [$XMODMAP]"); while () { chomp; fatal("Couldn't understand xmodmap output [$XMODMAP -pke]:\n $_") unless /^\s*keycode\s+(\d+)\s*=\s*((\S+)(\s+\S+)?)?$/; my ($code,$key,$keys) = ($1,$3,$2); $KEYBOARD{$key}{code} = $code; $KEYBOARD{$key}{init} = $_; } close MAP; } sub do_map { my ($map) = @_; return unless $map; get_initial_map(); my @change; my @fix; open(MAP,"<$map") || return mywarn("Can't read map file [$map]"); while () { # Format should be "" chomp; s/#.*//; # Ignore comments next if /^\s*$/; # Ignore whitespace fatal("Couldn't understand entry in key remap file [$map]:\n $_") unless /^\s*(\S+)\s+(\S+)\s*$/; my ($from,$to) = ($1,$2); # BUG: We can't remap shifted keys... fatal("[$map, $.] Unknown key [$from]\n". "See 'xmodmap -pk' for spelling.\n") unless $KEYBOARD{$from}; fatal("[$map, $.] Unknown key [$to]\n". "See 'xmodmap -pk' for spelling.\n") unless $KEYBOARD{$to}; my $code = $KEYBOARD{$from}{code}; push(@fix,"-e",$KEYBOARD{$from}{init}); push(@change,"-e","keycode $code = $to"); } close MAP; system($XMODMAP,@change) if (@change); @fix; } ######################### # Run a command, output in a window ######################### sub curses_system { my ($sys,$title,$map,$height) = @_; $height = $height || 20; $title = $title || "Run: $sys"; my @text=(""); my $out = $ROOTWIN->add('sys_output', 'Label', -title=>$title, -x => 2, -y => 2, -height=>$height, -width=>75, -border=>1); @text = add_text($out,$height,@text); # Remap keys my @fix = do_map($map); my $start = time; open(SYS,"$sys 2>&1 |") || fatal("Couldn't run $sys"); select((select(SYS), $| = 1) [0]); # Unbuffer output while() { @text = add_text($out,$height,@text,$_); } close SYS; # Unremap keys system($XMODMAP,@fix) if (@fix); # "Hit any key" if we ran for <10 seconds if (time-$start<10) { @text = add_text($out,$height,@text,"\n","Done. Hit any key"); my $ans; read(STDIN,$ans,1); } $ROOTWIN->delete('sys_output'); } ######################### # Create a listbox of selections to run ######################### my $SELECT_MENU; sub handle_select() { my $select_run = shift; my $name = $select_run->get; return unless $name; my $g = $GAMES{$SELECT_MENU}; fatal("Internal error: Can't find submenu info for $SELECT_MENU??\n") unless $g; my $cmd = $g->{command}; if ($cmd =~ /%s/) { $cmd =~ s/\%s/$name/g; } else { $cmd .= " \Q$name\E"; } curses_system($cmd,$g->{title},$g->{map}); $select_run->focus(); update_img($select_run,$SELECT_MENU); } sub select_run { my ($name,$cmd,$games,$labels) = @_; $SELECT_MENU = $name; my $height = $#$games+3; my $select_run = $ROOTWIN->add( 'select_run', 'Listbox', -values => \@$games, -labels => \%$labels, -width => 60, -height => $height, -y => 7, -x => 3, -padbottom => 0, -border => 1, -title => $name, -vscrollbar => 1, -wraparound => 1, -select_again => 1, -onchange => \&handle_select, ); mame_bindings($select_run,$name); $select_run->focus(); $select_run->draw(); my $quit = 0; $ROOT->set_binding( sub{ $quit = 1; }, "q" ); while(!$quit) { Curses::UI::do_one_event($ROOT); } $ROOT->set_binding( sub{ exit }, "q" ); $MAIN_LIST->focus(); $MAIN_LIST->draw(); $ROOTWIN->delete('select_run'); } ################################################## # Handle a selection in the main menu ################################################## sub main_select() { my $listbox = shift; my $name = $listbox->get; return unless $name; return if $name =~ /^--/; # Separator my $g = $GAMES{$name}; fatal("Internal error: Unknown game?? [$name]") unless $g; if ($g->{type} eq "MAME") { #get_mamelist($g->{mame}); select_run($name,$g->{command},$GAMES{_mame}{games},$GAMES{_mame}{labels}); } elsif ($g->{type} eq "DIR") { select_run($name,$g->{command},$g->{games},$g->{labels}); } else { curses_system($g->{command},$g->{title},$g->{map}); } $listbox->focus(); update_img($listbox,"_main"); } ################################################## # MENUS ################################################## ######################### # Mame games (from 'mame -list') ######################### sub get_mamelist { my ($mame) = @_; return if $GAMES{_mame}; my @games; my %labels; # Get full gamelist for manufacturer/year info open(G,"$mame -listgames 2>/dev/null|") || fatal("Couldn't run mame [$mame] to get gamelist"); my @lg = ; close G; # Check header my $hdr = shift(@lg); shift(@lg); fatal("Output format of gamelist changed?? [$mame -listgames]") unless ($hdr =~ /^year (manufacturer\s+)name$/); my $manu_re = "."x(length($1)-1).'+?\s+'; my $p = $ROOT->progress( -max => $#lg , -message => "Reading mame list..", ); #$p->draw(); # Read gamelist my $c = 0; foreach ( @lg ) { # Update progressbar every 50 games $ROOT->setprogress($c) unless (($c++)%50); next if /^\s*$/; next if /^Total Supported:/; if (/^([\d\?]{4})[\s\?]+(\S.+)$/) { my ($y,$mn,$m,$n) = ($1,$2); print STDERR "[$PROGNAME] WARNING: Unknown listgames line:\n $_" unless ($mn =~ /^($manu_re)(.+)$/); ($m,$n) = ($1,$2); # Stupid kludge for the fact that some manufacturer lines go way into # the name column, and we have no way to figure this out... ($m,$n) = ($m.$1,$2) if $n =~ /^(license\))\s+(\S.+)/; $m =~ s/\s+$//; push(@games,$n); $labels{$n} = sprintf("%4.4d| $n",$y); # # We don't currently use this info... # # $GAMES{_mame}{info}{$n}{manufacturer} = $m; # $GAMES{_mame}{info}{$n}{year} = $y; # $GAMES{_mame}{info}{$n}{line} = $.; # $GAMES{_mame}{info}{$n}{title} = "$mame $n [$y] ($m)"; } else { print STDERR "[$PROGNAME] WARNING: Unknown gamelist line:\n $_"; } } # Get shortnames for each game open(G,"$mame -listfull 2>/dev/null|") || fatal("Couldn't run mame -listfull [$mame] to get gamelist"); # Ignore first two lines $_ = ; $_ = ; $c = 0; $ROOT->noprogress; $p = $ROOT->progress( -max => $#lg , -message => "Getting mame aliases..", ); while() { # Update progressbar every 50 games $ROOT->setprogress($c) unless (($c++)%50); last if /^\s*$/; $GAMES{_mame}{short}{$2} = $1 if (/^(\S+)\s*"([^"]+)"\s*$/); } close G; @games = sort @games; $GAMES{_mame}{games} = \@games; $GAMES{_mame}{labels} = \%labels; $ROOT->noprogress; scalar @games; } ######################### # A directory of selections ######################### my %DIRLIST_CACHE; sub get_dirlist { my ($name,$dir,$re) = @_; if ($DIRLIST_CACHE{$dir}{$re}) { my $cache = $DIRLIST_CACHE{$dir}{$re}; $GAMES{$name}{games} = $cache->{games}; $GAMES{$name}{labels} = $cache->{labels}; return 0; } my @games; my %labels; opendir(DIR,$dir) || fatal("ERROR: Couldn't read directory [$dir]\n"); my @dir = grep(!/^\.{1,2}$/, readdir(DIR)); closedir(DIR); @dir = grep(/$re/i, @dir) if $re; foreach my $game ( sort @dir ) { my $name = $game; $name =~ s/\.z$//; $name =~ s/\.[^\.]+$//; #push(@games,$name); #$labels{$name} = "$dir/$game"; push(@games,"$dir/$game"); $labels{"$dir/$game"} = $name; } $GAMES{$name}{games} = \@games; $GAMES{$name}{labels} = \%labels; $DIRLIST_CACHE{$dir}{$re} = $GAMES{$name}; scalar @games; } ######################### # Main menu (Games listed in the rc file) ######################### sub get_gamelist { my ($rc) = @_; my (@games,%labels,$total); my $counted_mame; $rc = $ENV{HOME}."/.${PROGNAME}rc" unless -f $rc; $rc = "${0}.rc" unless -f $rc; $rc = "${PROGNAME}.rc" unless -f $rc; open(RC,"<$rc") || fatal("Can't read rc file: [$rc]\n"); while() { chomp; s/#.*//; # Ignore comments next if /^\s*$/; # Ignore whitespace ######################### # Separator ######################### if (/^\s*--(.*)/) { $_.="-"x(40-length($_)); push(@games,$_); next; } ######################### # IMAGES= for main menu ######################### if (/^\s*IMAGES(\[(.*)\])?=(.*)/) { $GAMES{_main}{images} = $3; $GAMES{_main}{image_type} = $2; next; } ######################### # Launch item ######################### my @l = split(/\t+/); fatal("Couldn't parse rc line [$rc, line $.]:\n $_\n") unless $#l>0; my ($name,$what,$opt,@args) = @l; fatal("Can't name a game '$name' - that's reserved!\n") if $name eq '_mame' || $name eq '_main'; my $num; if ($what =~ /^MAME:(.*)/) { my ($cmd,$roms) = ($opt,$1); my ($mame,$args) = ($cmd); ($mame,$args) = ($1,$2) if $mame =~ /^\s*(\S+)\s+(\S.*)$/; fatal("ERROR [$rc, line $.]: Couldn't find mame rompath 'MAME:':\n $_\n") unless -d $roms; fatal("ERROR [$rc, line $.]: Couldn't find mame executable $mame:\n $_\n") unless -x $mame; $GAMES{$name}{mame} = $mame; $GAMES{$name}{command} = "$mame -rp $roms $args"; $GAMES{$name}{type} = "MAME"; $num = 0; } elsif ($what =~ /^DIR(\[(.*)\])?:(.*)/) { my ($cmd,$dir,$re) = ($opt,$3,$2); my $exec = $cmd; $exec =~ s/^esddsp\s*//; # Ignore esddsp (real kludgy) $exec =~ s/\s.*//; # Take first word (kinda kludgy) fatal("ERROR [$rc, line $.]: Couldn't find directory 'DIR:':\n $_\n") unless -d $dir; fatal("ERROR [$rc, line $.]: Couldn't find executable [$exec]:\n $_\n") unless -x $exec; $GAMES{$name}{command} = $cmd; $GAMES{$name}{type} = "DIR"; $num = get_dirlist($name,$dir,$re); } else { $GAMES{$name}{command} = $what; $GAMES{$name}{title} = $opt || $name; $num = 1; } # Handle the optional arguments foreach my $arg ( @args ) { if ($arg =~ /^MAP=(.*)/) { $GAMES{$name}{map} = $1; } elsif ($arg =~ /^IMAGES(\[(.*)\])?=(.*)/) { my ($type,$dir) = ($2,$3); setup_Tk($type); $GAMES{$name}{images} = $dir; $GAMES{$name}{image_type} = $type; } else { fatal("ERROR [$rc, line $.]: Unknown option [$arg]:\n $_\n"); } } push(@games,$name); $num = $num>2 ? sprintf("%4d|",$num) : " |"; $labels{$name} = "$num $name"; $total+=$num; } close RC; (\@games,\%labels,$total); } ################################################## # Main code ################################################## sub main { my ($games,$labels,$tot) = get_gamelist(parse_args); curses_init(); $IN_CURSES=1; # Do we need to get mame games? foreach my $game ( @$games ) { if ($GAMES{$game}{mame}) { $tot += get_mamelist($GAMES{$game}{mame}); $labels->{$game} = sprintf("%4d|", scalar @{$GAMES{_mame}{games}})." $game"; last; } } $ROOTWIN->add('directions', 'Label', -text => <add( 'main_list', 'Listbox', -values => $games, -labels => $labels, -width => 60, -height => $height, -y => 7, -x => 3, -padbottom => 0, -border => 1, -title => ' # - Games', -vscrollbar => 1, -wraparound => 1, -select_again => 1, -onchange => \&main_select, ); mame_bindings($MAIN_LIST,"_main"); $ROOTWIN->add('total', 'Label', -y => 7+$height, -text => sprintf(" %5d| Total games\n",$tot)); $MAIN_LIST->focus(); MainLoop(); } main();