package umodasu::umodasu; # umod - all-purpose Unreal mod tool # # Copyright (C) 2000 Mishka Gorodnitzky # and Avatar # # You may distribute this file under the terms of the Artistic License, # as distributed with Perl. A copy is available here: # http://language.perl.com/misc/Artistic.html # # $Id: umodasu.pm,v 1.2 2004/12/01 09:53:20 thomasklausner Exp $ use umodasu::Umod; use umodasu::Ini; #use Archive::Zip qw(:ERROR_CODES); use FileHandle; use File::Find; use Getopt::Long; use POSIX qw(tmpnam SEEK_END); use strict; =pod =head1 NAME umod - all-purpose Unreal mod tool =head1 SYNOPSIS B S<[ B<-i> ]> S<[ B<-x>I ]> S<[ B<-d>I ]> S<[ B<-r> ]> S<[ B<-l> > S<[ B<-f> ]> S<[ B<-b>I ]> S<[ B<-v> ]> S<[ B<-hV> ]> umod_file B S<[ B<-l> > S<[ B<-c> > S<[ B<-b>I ]> S<[ B<-v> ]> S<[ B<-hV> ]> product_name =head1 DESCRIPTION =head2 Unpack mode B will run in unpack mode if the specified non-option argument is an existing file. This file will be treated as an umod file to which unpack mode operations will be applied. =over 4 =item B<-l> B<--list> List contents of the umod file. Show the product name, optionally the localized product name in double quotes if it is different from the generic product name, and the version number. If the B<-v> option is specified, show also the packed files and changes to ini files the umod will instate when installed. =item B<-i> B<--install> Install the umod file into the base directory specified via the B<-b> option or if it is not specified on the command line the base directory setting in I<~/.umodrc> (\Windows\umod.ini for Windows users). =item B<-x>I S I> Extract the files matching the specified pattern from the umod file. Extracted files are put inside the base directory as specified via the B<-b> option. If the base directory is not specified, I<~/.umodrc> (\Windows\umod.ini for Windows users) will be consulted for the B setting, and if it is not found the current directory will be used. Multiple packed files can be specified for extraction by using the ? and * construct in the pattern, which represents any single character and any sequence of characters respectively. No other glob or regex magic character is recognized. As a special case, if no pattern is specifed such that the umod file follows immediately the B<-x> flag, and no further argument exists on the command line, all the files in the specified umod file will be dumped. =item B<-d>I S I> Take the specified file from the umod file and dump it to the standard output. The filename specification is taken literally. No glob or regex pattern is recognized. =item B<-r> B<--readme> Display the readme file if such is included in the umod file. =item B<-f> B<--force> Overwrite existing files when doing extraction or installation. =item B<-b>I S I> Set the base directory for extraction and installation. In the latter case this should be set to where an Unreal game resides. =back =head2 Maintenance mode B is in maintenance mode if no non-option argument is specified on the command line or if the non-option argument does not end in ".umod", in which case said argument is taken as the name of an installed product. =over 4 =item B<-l> B<--list> List all the installed products along with their version numbers and optionally the localized product names in double quotes if they are different from the generic names. Use the B<-v> option to show also the file lists, and changes made to ini files (only available for umods installed with this installer). If a specific product name is given on the command line only that product is listed. Lists also files not belonged to any product group under the `Others' group if two B<-v> options are given. =item B<-c> B<--check> Check all the installed product for integrity, or just one product if a specific product name is given on the command line. =item B<-b>I S I> Set the base directory of the Unreal game on which the maintenance operation is to be applied. =back =head2 Common options =over 4 =item B<-v> B<--verbose> Show more information in the output. More than one B<-v> option can be specified for increased verbosity. =item B<-h> B<--help> Display a summary of command line options. =item B<-V> B<--version> Display the version string of this umod program. =back =cut # ---------------------------------------------------------------- my( %pref ); my( $systemDir ); my( $manifestFileName, $manifestFile ); my( @installedUmod ); my( @tmpfiles ); END { foreach my $tmpfile ( @tmpfiles ) { unlink( $tmpfile ); } } sub cleanUp { exit( 1 ); } $SIG{INT} = \&cleanUp; my( $rcfile ) = "./ASU/umodasu/rc.umod"; if ($^O eq "MSWin32") { $rcfile = "$ENV{windir}/umod.ini"; } my( $versionText ) = < and Avatar This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. EOT my( $helpText ) = < ) { chomp; s/#.*//; s/^\s+//; s/\s+$//; next unless length; my ($var, $value) = split(/\s*=\s*/, $_, 2); $pref{$var} = $value unless( $value eq 'no' or $value eq 'false' ); # For backward compatibility. if( !defined( $value ) ) { $pref{basedir} = $var; } } close( RCFILE ); } my( $flagInstall, $argExtract, $argDump, $flagShowReadme, $flagUninstall, $flagCheck, $flagList, $argBaseDir, $flagVersion, $flagHelp ); my( $mode ); #Getopt::Long::Configure("no_ignore_case"); GetOptions( "install" => \$flagInstall, "uninstall" => \$flagUninstall, "check" => \$flagCheck, "extract|x=s" => \$argExtract, "dump=s" => \$argDump, "readme" => \$flagShowReadme, "list|l" => \$flagList, "force!" => \$pref{force}, "base=s" => \$argBaseDir, "verbose|v+" => \$pref{verbose}, "modversion" => \$pref{modvers}, "version|V" => \$flagVersion, "help" => \$flagHelp ); # Base dir specified on command line overrides that in rc file. $pref{basedir} = $argBaseDir if( defined( $argBaseDir ) ); # Check if help was requested. if( $flagHelp ) { print( $helpText ); exit( 0 ); } # Check if version info was requested. if( $flagVersion ) { print( $versionText ); exit( 0 ); } # Sanity check. if( $flagList + $flagCheck + $flagShowReadme + $argDump + $argExtract + $flagInstall + $flagUninstall > 1 ) { print( STDERR "$0: option combination does not make sense\n" ); print( STDERR $helpText ); exit( 1 ); } # -------------------------------- # Main logic. # No argument given. if( $#ARGV < 0 ) { my( $mode ) = "maint"; if( $flagCheck ) { scanSystem(); my( $erred ); ++$pref{verbose}; foreach my $umodName ( @installedUmod ) { $erred = 1 if( !checkInstalledUmod( $umodName ) ); } print( "all products checked ok\n" ) if( !$erred ); } elsif( $argExtract ) { # Command line is like -x foo.umod. my( $umodName ) = $argExtract; # Initialize the umod file object. my( $umodFile ) = getUmodFromFile( $umodName ); # Use the base directory if given on the command line. If # it is not given, uses the extractdir option in the rc file. # Last a last resort, extact to the current directory. # Do not use the saved option $pref{basedir} because extracting # into the real UT directory is generally not desirable for # extraction. $argBaseDir = $argBaseDir || $pref{extractdir} || '.'; if( !-d $argBaseDir and !mkdir( $argBaseDir, 0755 ) ) { die( "$0: cannot make directory $argBaseDir: $!\n" ); } if (defined $umodFile) { $umodFile->baseDir($argBaseDir); extractUmodFile($umodFile, '*'); # } elsif ($umodName =~ /\.zip$/i) { # extractPlainZipFile($umodName, '*', $argBaseDir); } else { die "$0: cannot open or parse $umodName\n"; } } elsif( $argDump ) { # Command line is like -d foo.umod, interpreted as "list verbosely". my( $umodName ) = $argDump; # Initialize the umod file object. my( $umodFile ) = getUmodFromFile( $umodName ); ++$pref{verbose}; if (defined $umodFile) { listUmodFile($umodFile); # } elsif ($umodName =~ /\.zip$/i) { # listPlainZipFile($umodName); } else { die "$0: cannot open or parse $umodName\n"; } } elsif( $flagUninstall ) { print( STDERR "$0: a product name must be supplied for uninstallation.\n" ); print( STDERR $helpText ); exit( 1 ); } else { # list scanSystem(); listAllInstalledUmod(); listUnreferencedFiles() if( $pref{verbose} > 1 ); } } # Loop throughs the arguments. foreach my $umodName ( @ARGV ) { # Set mode depending on the nature of the current argument. if( -e $umodName ) { $mode = 'unpack'; } elsif( $umodName =~ /\.umod$/i ) { print( STDERR "$0: cannot open $umodName: No such file or directory\n" ); exit( 1 ); } else { $mode = 'maint'; } if( $mode eq "maint" ) { scanSystem(); if( $flagCheck ) { $pref{verbose} += 2; checkInstalledUmod( $umodName ); $pref{verbose} -= 2; } elsif( $flagUninstall ) { uninstallUmod( $umodName ); } elsif( $flagInstall ) { print( STDERR "$0: cannot install $umodName: file not found\n" ); exit( 1 ); } else { # list listInstalledUmod( $umodName ); } } elsif( $mode eq "unpack" ) { # Initialize the umod file object. my( $umodFile ) = getUmodFromFile( $umodName ); # Set the base directory for extracted files. $umodFile->baseDir( $pref{basedir} ) unless( !defined( $umodFile ) ); if( $flagShowReadme ) { die( "$0: cannot open or parse $umodName\n" ) if( !defined( $umodFile ) ); showUmodFileReadme( $umodFile ); } elsif( $argDump ) { if (defined $umodFile) { dumpUmodFile($umodFile, $argDump); # } elsif ($umodName =~ /\.zip$/i) { # dumpPlainZipFile($umodName, $argDump); } else { die "$0: cannot open or parse $umodName\n"; } } elsif( $argExtract ) { # Use the base directory if given on the command line. If # it is not given, uses the extractdir option in the rc file. # Last a last resort, extact to the current directory. # Do not use the saved option $pref{basedir} because extracting # into the real UT directory is generally not desirable for # extraction. $argBaseDir = $argBaseDir || $pref{extractdir} || '.'; if (!-d $argBaseDir and !mkdir $argBaseDir, 0755) { die "$0: cannot make directory $argBaseDir: $!\n"; } if (defined $umodFile) { $umodFile->baseDir($argBaseDir); extractUmodFile($umodFile, $argExtract); # } elsif ($umodName =~ /\.zip$/i) { # extractPlainZipFile($umodName, $argExtract, $argBaseDir); } else { die "$0: cannot open or parse $umodName\n"; } } elsif( $flagInstall ) { scanSystem(); if(defined $umodFile) { installUmodFile($umodFile); # } elsif ($umodName =~ /\.zip$/i) { # installPlainZipFile($umodName); } else { die "$0: cannot open or parse $umodName\n"; } } elsif( $flagCheck ) { die( "$0: cannot open or parse $umodName\n" ) if( !defined( $umodFile ) ); checkUmodFile( $umodFile ); } elsif( $flagUninstall ) { # Uninstall product defined by the umod file. scanSystem(); if(defined $umodFile) { uninstallUmod($umodFile->product); # } elsif ($umodName =~ /\.zip$/i) { # uninstallPlainZip($umodName); } else { die "$0: cannot open or parse $umodName\n"; } } else { if (defined $umodFile) { listUmodFile($umodFile); # } elsif ($umodName =~ /\.zip$/i) { # listPlainZipFile($umodName); } else { die "$0: cannot open or parse $umodName\n"; } } } } # -------------------------------- # Unpack mode functions. # Return an Umod from the give file. Will obtain file from zip archive # if necessary. sub getUmodFromFile { my( $filename ) = @_; # if( $filename =~ /\.zip$/i ) { # # my( $tmpFile, $fh ); # do { $tmpFile = tmpnam(); } until $fh = new FileHandle( $tmpFile, 'w' ); # # my( $zipFile ) = new Archive::#Zip; # if( $zipFile->read( $filename ) != AZ_OK ) { # die( "$0: cannot open $filename as a zip file\n" ); # } # # my( @umodFiles ) = $zipFile->membersMatching( '.*\.umod$' ); # return( undef ) if( !defined $umodFiles[0] ); # # $umodFiles[0]->extractToFileHandle( $fh ); # # # Make sure the tmp file gets unlinked. # push( @tmpfiles, $tmpFile ); # # $filename = $tmpFile; # # } return( new umodasu::Umod( -file => $filename ) ); } # Display information about the umod file. sub listUmodFile { my( $umodFile ) = @_; my( @requirements ) = $umodFile->requirements; my( @packingList ) = $umodFile->packingList; my( @iniChanges ) = $umodFile->iniChanges; if( $pref{verbose} ) { print( " Product: ", $umodFile->product ); print( " \"", $umodFile->localproduct, "\"" ) if( $umodFile->localproduct ne $umodFile->product ); print( " (", $umodFile->producturl, ")" ) if( defined( $umodFile->producturl ) ); print( "\n" ); print( " Version: ", $umodFile->version ); print( " (", $umodFile->versionurl, ")" ) if( defined( $umodFile->versionurl ) ); print( "\n" ); print( "Developer: ", $umodFile->developer ); print( " (", $umodFile->developerurl, ")" ) if( defined( $umodFile->developerurl ) ); print( "\n" ); print( map( " Require: $_->{product} $_->{version}\n", @requirements ) ) if( $umodFile->requirements ); print( "\n", map( ' '.($_->{src} eq $umodFile->readmefile ? "R" : " ") ." $_->{src}" .( $pref{verbose} > 1 ? " (offset $_->{start})" : '') ." ($_->{size} bytes)\n", @packingList ) ); my( $prevfile, $prevsection, $prevadd ); foreach my $change ( $umodFile->iniChanges ) { if( $change->{file} ne $prevfile or $change->{add} ne $prevadd) { undef $prevsection; print( "\n" ) unless( !defined $prevfile ); print( ($change->{add} ? ' + ' : ' = ') ); print( "$change->{file}\n" ); $prevfile = $change->{file}; $prevadd = $change->{add}; } if( $change->{section} ne $prevsection ) { print( "\n [$change->{section}]\n" ); $prevsection = $change->{section}; } print( " $change->{key}=$change->{value}\n" ); } print( "\n" ); } else { print( $umodFile->product ); print( " \"", $umodFile->localproduct, "\"" ) if( $umodFile->localproduct ne $umodFile->product ); print( " ", $umodFile->version, "\n" ); } } #sub listPlainZipFile { # my ($zipname) = @_; # # my $zipfile = new Archive::Zip; # $zipfile->read($zipname) == AZ_OK # or die "$0: cannot open $zipname as a zip file\n"; # # # Determine product name. # my $product = determineProductNameFromZipFile($zipfile, $zipname); # # if ($pref{verbose}) { # print " Product: $product\n\n"; # foreach ($zipfile->memberNames()) { ## my $file = mungeFilename($_, $product); # my $file = $_; # next if (!(length $file) or ($file =~ /\/$/)); # print " $file\n"; # } # print "\n"; # } else { # print "$product\n"; # } # # return 1; #} # ## Determines product name. #sub determineProductNameFromZipFile { # my ($zipfile, $filename) = @_; # # my $product; # foreach ($zipfile->memberNames()) { # $_ =~ s-.*[\\/]--; # next if (!length $_); # # # Some maps come with .u files, in that case .unr takes precedence. # SWITCH: for ($_) { # /\.unr$/i && do { $product = $`; last; }; # /\.u$/i && do { $product = $` if (!defined $product); last; }; # /\.utx$/i && do { $product = $` if (!defined $product); last; }; # } # } # ($product) = ($filename =~ m-(.*[\\/])?(.*)\.zip-i)[1] if (!$product); # # # Munge it further. # $product =~ s/^(DM|CTF|DOM|AS|DK)/\U$1\E/i; # $product =~ s/\]I\[/-3/g; # $product =~ s/\]\[/-2/g; # $product =~ s/\]//g; # $product =~ s/\[//g; # $product =~ s-/-_-g; # $product =~ s-\\-_-g; # # return $product; #} # Determines the right place for a file from its extension. sub mungeFilename { my ($file, $product) = @_; $file =~ s-.*[\\/]--; return undef if (!length $file); my ($fileEsc) = $file; $fileEsc =~ s/\]/\\\]/g; $fileEsc =~ s/\[/\\\[/g; SWITCH: for ($fileEsc) { /\.(unr)$/i && do { $file =~ "$`".lc($&); $file =~ s/^(DM|CTF|DOM|AS|DK)/\U$1\E/i; $file = "Maps\\$file"; last; }; /\.(utx)$/i && do { $file = "Textures\\$`".lc($&); last; }; /\.(umx)$/i && do { $file = "Music\\$`".lc($&); last; }; /\.(uax)$/i && do { $file = "Sounds\\$`".lc($&); last; }; /\.(u|int|ini)$/i && do { $file = "System\\$`".lc($&); last; }; /\.(txt|html?|rtf|doc)$/i && do { $file =~ s/^readme/$product-readme/i; $file = "Help\\$file"; last; }; /\.(bmp|gif|jpe?g|png|xmp)$/i && do { $file = "Help\\$file"; last; }; { }; } return $file; } sub showUmodFileReadme { my( $umodFile ) = shift; # Display the umod's readme file. # # The umod's readme file is listed in Setup group. The umod object # grabs that and return that, if it's found. my( $readme ) = $umodFile->readme; if( defined( $readme ) ) { print( $umodFile->readme, "\n\n" ); } else { print( STDERR "$0: umod readme file not defined.\n" ); } } # Dump a file inside the umod file. sub dumpUmodFile { my ($umodfile, $pattern) = @_; my ($patternEsc) = $pattern; # Allow /. $patternEsc =~ s-/-\\-g; my ($file) = grep { $_->{src} eq $patternEsc } $umodfile->packingList; die "$0: $pattern not found for dumping: $!\n" if (!defined $file); print $umodfile->dump($file); } #sub dumpPlainZipFile { # my ($zipname, $pattern) = @_; # # # Prepare to-be-dumped file pattern. # my ($patternEsc) = $pattern; # # Allow /. # $patternEsc =~ s-/-\\-g; # # # Open the zip file. # my $zipfile = new Archive::Zip; # $zipfile->read($zipname) == AZ_OK # or die "$0: cannot open $zipname as a zip file\n"; # # die( "$0: $pattern not found for dumping\n" ) # if( !grep { $_ =~ /$patternEsc/ } $zipfile->memberNames() ); # # # Determine product name. # my $product = determineProductNameFromZipFile($zipfile, $zipname); # # # Dump matching file. # foreach ($zipfile->memberNames()) { ## my $file = mungeFilename($_, $product); # my $file = $_; # next if (!$file); # # if ($file eq $patternEsc) { # print $zipfile->contents($_); # } # } #} # Extract a file from the umod file. sub extractUmodFile { my( $umodFile ) = shift; my( $file ) = shift; my( @packingList ) = $umodFile->packingList; my( $fileEsc ) = quotemeta $file; # Allow ? and *. $fileEsc =~ s/\\\*/.*/g; $fileEsc =~ s/\\\?/./g; # Allow /. $fileEsc =~ s-/-\\-g; my( @packedFile ) = grep { $_->{src} =~ /^$fileEsc$/i } @packingList; die( "$0: $file not found for extraction: $!\n" ) if( !defined( $packedFile[0] ) ); print( map( "extracting $_->{src}\n", @packedFile ) ) if( $pref{verbose} ); $umodFile->extract( $pref{force}, @packedFile ) or die( "$0: error extracting $file: $!\n" ); } #sub extractPlainZipFile { # my ($zipname, $pattern, $basedir) = @_; # # # Prepare to-be-extracted file pattern. # my ($patternEsc) = quotemeta $pattern; # # Allow ? and *. # $patternEsc =~ s/\\\*/.*/g; # $patternEsc =~ s/\\\?/./g; # # Allow /. # $patternEsc =~ s-\\\\-/-g; # # # Open the zip file. # my $zipfile = new Archive::Zip; # $zipfile->read($zipname) == AZ_OK # or die "$0: cannot open $zipname as a zip file\n"; # # die( "$0: $pattern not found for extraction\n" ) # if( !grep { $_ =~ /$patternEsc/ } $zipfile->memberNames() ); # # # Determine product name. # my $product = determineProductNameFromZipFile($zipfile, $zipname); # # # Extract matching files. # foreach ($zipfile->memberNames()) { ## my $file = mungeFilename($_, $product); # my $file = $_; # next if (!$file); # # if ($file =~ m/$patternEsc/) { # my $rightplace = umodasu::Ini::adjustpathcase("$basedir/" # .mungeFilename($_, $product)); # # if (!$pref{force} and -e $rightplace) { # warn "$0: $rightplace already exists, skipping\n"; # next; # } # # print "extracting file: $file\n" if ($pref{verbose}); # $zipfile->extractMember($_, $rightplace); # } # } #} # Install the umod file. sub installUmodFile { my( $umodFile ) = shift; my( $product ) = $umodFile->product; my( @requirements ) = $umodFile->requirements; my( @packingList ) = $umodFile->packingList; my( @iniChanges ) = $umodFile->iniChanges; foreach my $requirement ( @requirements ) { my( $requiredProduct ) = $requirement->{product}; my( $requiredVersion ) = $requirement->{version}; if ($requirement->{product} eq "UnrealTournament" || $requirement->{product} eq "Unreal Tournament" ) { $requirement->{product} = "Setup"; $requiredProduct = "UnrealTournament"; } if( !grep { $_ eq $requiredProduct } @installedUmod ) { warn( "$0: $requiredProduct version $requiredVersion required\n"); } else { my( $installedVersion ) = $manifestFile->get( [$requirement->{product}, 'Version'], -mapping => 'single' ); if( $installedVersion < $requirement->{version} ) { warn( "$0: $requiredProduct $requiredVersion required, installed version is $installedVersion\n"); } } } my $alreadyinstalled; if (grep { $_ eq $product } @installedUmod) { if (!$pref{force}) { my $installedVersion = $manifestFile->get( [$product, 'Version'], -mapping => 'single' ); if ($umodFile->version < $installedVersion) { die "$0: installed $product is of a newer version, skipping\nUse the -f option to install this older version anyway.\n"; } elsif ($umodFile->version == $installedVersion) { if (checkInstalledUmod($product)) { die "$0: $product already installed, skipping\nUse the -f option to install anyway.\n"; } else { warn "$0: some files are missing from a previous installation of $product, proceeding to reinstallation\n"; } } else { die "$0: an older version of $product is installed, skipping\nUninstall it first, or use the -f option to install new version on top.\n"; } } $alreadyinstalled = 1; } foreach (@packingList) { print "extracting file: $_->{src}\n" if ($pref{verbose}); $umodFile->extract($pref{force}, $_) or die "$0: error extracting files: $!\n"; } my( %alreadyBackedUp ); foreach my $change ( @iniChanges ) { my( $iniFileName ) = umodasu::Ini::adjustpathcase( "$pref{basedir}/$change->{file}" ); if( !exists( $alreadyBackedUp{ $iniFileName } ) ) { backUp( $iniFileName ); $alreadyBackedUp{ $iniFileName } = 1; } print( "modifying $iniFileName\n" ) if( $pref{verbose} ); if( !$pref{force} and $iniFileName =~ m-System\\UnrealTournament\.ini-i ) { die "$0: System\\UnrealTournament.ini did not exists, skipping\nRun UnrealTournament once to create it, or use the -f option to install anyway (if you use -f the game will still not be playable afterwards).\n"; } # Actually change the .ini file. my( $iniFile ) = new umodasu::Ini( $iniFileName ); $change->{oldvalue} = $iniFile->put( [$change->{section}, $change->{key}, $change->{value}], -add => $change->{add} ); $iniFile->save; } backUp( $manifestFileName ); # Actually change the Manifest.ini file. $manifestFile->put( ['Setup', 'Group', $product], -add => 1 ); $manifestFile->put( [$product, 'Caption', $umodFile->localproduct] ); $manifestFile->put( [$product, 'Version', $umodFile->version] ); foreach (@packingList) { $manifestFile->put( [$product, 'File', $_->{ src }], -add => 1 ); if (!$alreadyinstalled) { # Increase reference count. # TODO: casing may not match existing file my $refcount = $manifestFile->get(['RefCounts', "File:$_->{src}"]); if (defined $refcount) { $manifestFile->put(['RefCounts', "File:$_->{src}", $refcount + 1]); } else { $manifestFile->put(['RefCounts', "File:$_->{ src}", 1]); } } } foreach my $requirement ( @requirements ) { $manifestFile->put( [$product, 'Requires', $requirement->{ product }], -add => 1 ); } # Recording file changes. foreach my $change ( @iniChanges ) { if( $change->{add} ) { # Undo our path canonicalization to mimic AddIni= line. $change->{file} =~ s/^System\\//; $manifestFile->put( [$product, 'AddIni', $change->{file}.','.$change->{section}.'.'.$change->{key} .'='.$change->{value}], -add => 1 ); } else { $manifestFile->put( [$product, 'Ini', $change->{file}.','.$change->{section}.'.'.$change->{key} .'='.$change->{value}.'='.$change->{oldvalue}], -add => 1 ); } } $manifestFile->save; } #sub installPlainZipFile { # my ($filename) = @_; # # my $zipfile = new Archive::Zip; # $zipfile->read($filename) == AZ_OK # or die "$0: cannot open $filename as a zip file\n"; # # # Determine product name. # my $product = determineProductNameFromZipFile($zipfile, $filename); # # # Check if product is already installed. # my $alreadyinstalled; # if (grep { $_ eq $product } @installedUmod) { # if (!$pref{force}) { # die "$0: $product is already installed, skipping\nUse the -f option to install it anyway.\n"; # } # # $alreadyinstalled = 1; # } # # # Move the files into the right place. # my @files; # foreach ($zipfile->memberNames()) { # my ($file, $rightplace); # # $file = mungeFilename($_, $product); # next if (!defined $file); # # if (!grep /\\/, $file) { # warn "$0: cannot figure out where $file belongs, a copy will be placed in the current directory for you to install it manually\n"; # $rightplace = $file; # } else { # $rightplace = umodasu::Ini::adjustpathcase("$pref{basedir}/$file"); # push @files, $file; # } # # if (!$pref{force} and -e $rightplace) { # warn "$0: $rightplace already exists, skipping\n"; # next; # } # # print "extracting file: $file\n" if ($pref{verbose}); # $zipfile->extractMember($_, $rightplace); # } # # # Update Manifest.ini. # backUp($manifestFileName); # # $manifestFile->put(['Setup', 'Group', $product], -add => 1); # $manifestFile->put([$product, 'Caption', $product]); # $manifestFile->put([$product, 'Version', 'unknown']); # # foreach (@files) { # $manifestFile->put([$product, 'File', $_], -add => 1); # # if (!$alreadyinstalled) { # # Increase reference count. # # TODO: casing may not match existing file # my $refcount = $manifestFile->get(['RefCounts', "File:$_"]); # if (defined $refcount) { # $manifestFile->put(['RefCounts', "File:$_", $refcount + 1]); # } else { # $manifestFile->put(['RefCounts', "File:$_", 1]); # } # } # } # # $manifestFile->save; # # return 1; #} # Check the consistency of the umod file. sub checkUmodFile { print( "$0: umod file checking is not yet implemented\n" ); return 1; my( $umodFile ) = shift; open( UMOD, $umodFile->file ) or die( "$0: cannot open $umodFile->file for reading: $!\n" ); binmode( UMOD ); seek( UMOD, 0, SEEK_END ) or die( "$0: cannot seek to end of file: $!\n" ); my $filesize = tell UMOD; my $calcfilesize = $umodFile->endOfManifestInt; foreach ( $umodFile->packingList ) { $calcfilesize += $_->{ size }; } close( UMOD ); } # -------------------------------- # Maint mode functions. # Display information about installed umods. sub listInstalledUmod { my( $umodName ) = shift; if( !grep { $_ eq $umodName } @installedUmod ) { print( STDERR "$0: cannot list $umodName: product not installed\nInstalled products are:\n" ); $pref{verbose} = 0; listAllInstalledUmod(); exit( 1 ); } my( $installedCaption ) = $manifestFile->get( [$umodName, 'Caption'], -mapping => 'single' ); return if( !defined( $installedCaption ) ); my( $installedVersion ) = $manifestFile->get( [$umodName, 'Version'], -mapping => 'single' ); return if( !defined( $installedVersion ) ); if( $pref{verbose} ) { print( $umodName ); print( " \"$installedCaption\"" ) if( $installedCaption and $installedCaption ne $umodName ); print( " $installedVersion\n" ); my( @packingList ) = $manifestFile->get( [$umodName, 'File'], -mapping => 'multiple' ); foreach my $file ( @packingList ) { print( " $file\n" ); } my( @iniChanges ) = $manifestFile->get( [$umodName, 'Ini'], -mapping => 'multiple' ); if( defined $iniChanges[0] ) { my( $prevfile, $prevsection ); foreach my $change ( @iniChanges ) { # These lines are written by us. We don't put multiple # changes on a single line. my( $file, $section, $key, $value ) = ( $change =~ m/([^,]*),(.*)\.([^=]*)=([^=]*)/ ); # ^^^^can ^^^^don't # contain '.' show old value if( $file ne $prevfile ) { undef $prevsection; print( "\n" ) unless( !defined $prevfile ); print( " C $file\n" ); $prevfile = $file; } if( $section ne $prevsection ) { print( "\n [$section]\n" ); $prevsection = $section; } print( " $key=$value\n" ); } print( "\n" ); } my( @iniAdditions ) = $manifestFile->get( [$umodName, 'AddIni'], -mapping => 'multiple' ); if( defined $iniAdditions[0] ) { my( $prevfile, $prevsection ); foreach my $addition ( @iniAdditions ) { # These lines are written by us. We don't put multiple # changes on a single line. my( $file, $section, $key, $value ) = ( $addition =~ m/([^,]*),(.*)\.([^=]*)=(.*)/ ); # ^^^^can contain '.' if( $file ne $prevfile ) { undef $prevsection; print( "\n" ) unless( !defined $prevfile ); print( " + System\\$file\n" ); $prevfile = $file; } if( $section ne $prevsection ) { print( "\n [$section]\n" ); $prevsection = $section; } print( " $key=$value\n" ); } print( "\n" ); } print( "\n" ) if( !defined $iniChanges[0] and !defined $iniAdditions[0] ); ## next three lines added by abf to suppress version from name } elsif( ! $pref{modvers} ) { print( $umodName ); print "\n"; ## end of lines added } else { print( $umodName ); print( " \"$installedCaption\"" ) if( $installedCaption and $installedCaption ne $umodName ); print( " $installedVersion\n" ); } } sub listAllInstalledUmod { # Loop through the umods sorted by their Caption and Version. foreach my $umodName ( map { $_->[0] } sort { $a->[1] cmp $b->[1] or $a->[2] <=> $b->[2] } map { [$_, $manifestFile->get( [$_, 'Caption'], -mapping => 'single' ), $manifestFile->get( [$_, 'Version'], -mapping => 'single' )] } @installedUmod ) { listInstalledUmod( $umodName ); } } sub listUnreferencedFiles { my (%allfiles); find({ wanted => sub { my $file = $File::Find::name; return 0 if (-d $file); $file =~ s-$pref{basedir}/--; $file =~ s-/-\\-g; $allfiles{ lc( $file ) } = $file; }, follow => 1 }, $pref{basedir}); foreach ( @installedUmod ) { foreach ( $manifestFile->get( [$_, 'File'], -mapping => 'multiple') ) { delete( $allfiles{ lc( $_ ) } ) if( exists( $allfiles{ lc( $_) } ) ); } } print "Others\n"; foreach ( sort values %allfiles ) { print " $_\n"; } print "\n"; } # Check the integrity of installed umods. sub checkInstalledUmod { my( $umodName ) = shift; if( !grep { $_ eq $umodName } @installedUmod ) { print( STDERR "$0: cannot check $umodName: product not installed\nInstalled products are:\n" ); $pref{verbose} = 0; listAllInstalledUmod(); return( 0 ); } my( $installedCaption, $installedVersion ); my( @errors ); if( $manifestFile->exists( [$umodName] ) ) { if( $manifestFile->exists( [$umodName, 'Caption'] ) ) { $installedCaption = $manifestFile->get( [$umodName, 'Caption'], -mapping => 'single' ); } else { push( @errors, "caption not defined" ); } if( $manifestFile->exists( [$umodName, 'Version'] ) ) { $installedVersion = $manifestFile->get( [$umodName, 'Version'], -mapping => 'single' ); } else { push( @errors, "version not defined" ); } # Check for existence of all files. my( @packingList ) = $manifestFile->get( [$umodName, 'File'], -mapping => 'multiple' ); foreach my $file ( @packingList ) { $file = umodasu::Ini::adjustpathcase( "$pref{basedir}/$file" ); if( !-e $file ) { push( @errors, "$file does not exist" ); } } } else { push( @errors, "dangling group declaration in Setup group" ); } if( @errors ) { if( $pref{verbose} ) { print( "checking $umodName" ); print( " \"$installedCaption\"" ) if( $installedCaption and $installedCaption ne $umodName ); print( " $installedVersion" ) if( $installedVersion ); print( "... error detected:\n" ); foreach my $error ( @errors ) { print( " $error\n" ); } } return( 0 ); } elsif( $pref{verbose} > 1 ) { print( "checking $umodName" ); print( " \"$installedCaption\"" ) if( $installedCaption ne $umodName ); print( " $installedVersion... ok\n" ); } return( 1 ); } sub uninstallUmod { my( $umodName ) = shift; if( !grep { $_ eq $umodName } @installedUmod ) { print( STDERR "$0: cannot uninstall $umodName: product not installed\nInstalled products are:\n" ); $pref{verbose} = 0; listAllInstalledUmod(); exit( 1 ); } # Delete the files. foreach ($manifestFile->get([$umodName, 'File'], -mapping => 'multiple')) { # Decrease reference count. # TODO: casing may not match existing file my $refcount = $manifestFile->get(['RefCounts', "File:$_"]); if (defined $refcount) { if (--$refcount > 0) { $manifestFile->put(['RefCounts', "File:$_", $refcount]); } else { $manifestFile->delete(['RefCounts', "File:$_"]); } } else { # No reference count. # TODO: scanning } if (!$refcount) { # Really delete it. $_ = umodasu::Ini::adjustpathcase("$pref{basedir}/$_"); unlink $_ or warn "$0: cannot remove $_: $!\n"; print "deleting $_\n" if ($pref{verbose}); } else { print "not deleting $_\n" if ($pref{verbose}); } } # Undo AddIni additions. my( %alreadyBackedUp ); foreach my $addition ( $manifestFile->get( [$umodName, 'AddIni'], -mapping => 'multiple' ) ) { last if( !$addition ); my ($file, $section, $key, $value) = ($addition =~ m/([^,]*),(.*)\.([^=]*)=(.*)/); my( $iniFileName ) = umodasu::Ini::adjustpathcase( "$systemDir/$file" ); print( "modifying $iniFileName\n" ) if( $pref{verbose} ); if( !exists( $alreadyBackedUp{ $iniFileName } ) ) { backUp( $iniFileName ); $alreadyBackedUp{ $iniFileName } = 1; } # Actually change the .ini file. my( $iniFile ) = new umodasu::Ini( $iniFileName ); $iniFile->delete( [$section, $key, $value] ); $iniFile->save; } # Undoing Ini changes is not terribly useful. foreach my $change ( $manifestFile->get( [$umodName, 'Ini'], -mapping => 'multiple' ) ) { last if( !$change ); my ($file, $section, $key, $value, $oldvalue) = ($change =~ m/([^,]*),(.*)\.([^=]*)=([^=]*)=(.*)/); my( $iniFileName ) = umodasu::Ini::adjustpathcase( "$pref{basedir}/$file" ); print( "modifying $iniFileName\n" ) if( $pref{verbose} ); if( !exists( $alreadyBackedUp{ $iniFileName } ) ) { backUp( $iniFileName ); $alreadyBackedUp{ $iniFileName } = 1; } # Actually change the .ini file. my( $iniFile ) = new umodasu::Ini( $iniFileName ); $iniFile->put( [$section, $key, $oldvalue] ); $iniFile->save; } backUp( $manifestFileName ); # Delete the groups registration and the group itself. $manifestFile->delete( ['Setup', 'Group', $umodName] ); $manifestFile->delete( [$umodName] ); $manifestFile->save(); return 1; } #sub uninstallPlainZip { # my ($filename) = @_; # # my $zipfile = new Archive::Zip; # $zipfile->read($filename) == AZ_OK # or die "$0: cannot open $filename as a zip file\n"; # # # Determine product name. # my $product = determineProductNameFromZipFile($zipfile, $filename); # # return uninstallUmod($product); #} # -------------------------------- # Initialization functions. # Scan the system for the current config. sub scanSystem { # Determine base directory. if( !defined( $pref{basedir} ) ) { # Autodetect Unreal installation. foreach my $dir ( '/usr/local/games/ut', '/usr/games/ut', '/usr/local/games/UnrealTournament', '/usr/games/UnrealTournament', 'C:/UnrealTournament', '.' ) { my $sysDir; if( $sysDir = $pref{basedir}.'/' .umodasu::Ini::adjustfilecase( 'System', $pref{basedir} ), -e $sysDir.'/' .umodasu::Ini::adjustfilecase( 'Core.u', $sysDir ) ) { $pref{basedir} = $dir; last; } } if( !defined( $pref{basedir} ) ) { die( "$0: no Unreal game found\nUse the -b option to specify a valid base directory.\n" ); } } # Determine System directory. $systemDir = $pref{basedir}."/" .umodasu::Ini::adjustfilecase( "System", $pref{basedir} ); # Unless doing extraction, bail out if base directory is invalid. if( !$argExtract and !-e $systemDir."/" .umodasu::Ini::adjustfilecase( "Core.u", $systemDir ) and !$pref{force} ) { die( "$0: no Unreal game found in $pref{basedir}\nUse the -b option to specify a valid base directory, or the -f option to use it anyway.\n" ); } # Force flag is given, try to make the base directory if it does not exist. if( !-d $pref{basedir} and !mkdir( $pref{basedir}, 0755 ) ) { die( "$0: cannot make directory $pref{basedir}: $!\n" ); } # Save base directory to the rc file if it does not already exist. # Don't save if base directory is "." because it's too transient. if( !-e $rcfile and $pref{basedir} ne "." ) { if( open( RCFILE, ">$rcfile" ) ) { print( RCFILE "basedir = $pref{basedir}\n" ); close( RCFILE ); } } # Determine Manifest.ini path. $manifestFileName = $systemDir."/" .umodasu::Ini::adjustfilecase( "Manifest.ini", $systemDir ); # Make an initial Manifest.ini if not installed. if ( !-e $manifestFileName ) { warn( "$0: $manifestFileName does not exist, creating it\n" ); makeInitialManifest($manifestFileName); } # Read list of installed mods from Manifest.ini. $manifestFile = new umodasu::Ini( $manifestFileName ); @installedUmod = $manifestFile->get( ['Setup', 'Group'], -mapping => 'multiple' ); push (@installedUmod, "UnrealTournament"); } sub makeInitialManifest { my( $manifestFileName ) = shift; # This initial Manifest.ini is for UNIX. my( $content ) = <$manifestFileName" ) or die( "$0: cannot open $manifestFileName for writing: $!\n" ); print( MANIFEST $content ); close( MANIFEST ); } # -------------------------------- # Utility functions. sub backUp { my( $filename ) = shift; if( -e $filename ) { open( INI, "<$filename" ) or die( "$0: cannot open $filename for reading: $!\n" ); undef $/; my( $content ) = ; $/ = "\n"; close( INI ); # if( exists "$filename.bak" ) { # my $i = 1; # do { $i++; } until( !exists $umod{"$filename.bak.$i"} ); # $filename .= ".bak.$i"; # } open( INI, ">$filename.bak" ) or die( "$0: cannot open $filename.bak for writing: $!\n" ); print( INI $content ); close( INI ); } } 1; __END__ =head1 EXAMPLES =over 4 =item Indentify an umod file umod -l DE.umod =item List the contents of an umod file umod -v -l DE.umod =item Install an umod file inside /usr/local/UT umod -b /usr/local/UT -i DE.umod =item Extract all the files in DE.umod into /tmp/de umod -b /tmp/de -x DE.umod =item Examine the content of de.int in DE.umod umod -d System/de.int DE.umod Forward slashes can be used in place of backslashes. =item List all the installed products umod -l =item List all the installed products, files belong to them and changes made to files umod -v -l =item List all the installed products, files belong to them, changes made to files. List also files not belonged to any product group under the group `Others' umod -v -v -l =item List the files belonging to an installed product and the changes made to files by this product umod -v -l "DE Mutators" =item Check for integrity of installed products umod -c =back =head1 AUTHOR Mishka Gorodnitzky > and Avatar >. =cut