package net.vhati.modmanager.cli; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import net.vhati.ftldat.FTLDat; import net.vhati.modmanager.FTLModManager; import net.vhati.modmanager.core.DelayedDeleteHook; import net.vhati.modmanager.core.FTLUtilities; import net.vhati.modmanager.core.ModPatchObserver; import net.vhati.modmanager.core.ModPatchThread; import net.vhati.modmanager.core.ModPatchThread.BackedUpDat; import net.vhati.modmanager.core.ModUtilities; import net.vhati.modmanager.core.Report; import net.vhati.modmanager.core.Report.ReportFormatter; import net.vhati.modmanager.core.Report.ReportMessage; import org.apache.commons.cli.BasicParser; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.ParseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class SlipstreamCLI { private static final Logger log = LogManager.getLogger(SlipstreamCLI.class); private static File backupDir = new File( "./backup/" ); private static File modsDir = new File( "./mods/" ); private static Thread.UncaughtExceptionHandler exceptionHandler = null; public static void main( String[] args ) { exceptionHandler = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException( Thread t, Throwable e ) { log.error( "Uncaught exception in thread: "+ t.toString(), e ); System.exit( 1 ); } }; BasicParser parser = new BasicParser(); Options options = new Options(); options.addOption( OptionBuilder.withLongOpt( "extract-dats" ) .withDescription( "extract FTL resources into a dir" ) .hasArg() .withArgName("DIR") .create() ); options.addOption( OptionBuilder.withLongOpt( "global-panic" ) .withDescription( "patch as if advanced find tags had panic='true'" ) .create() ); options.addOption( OptionBuilder.withLongOpt( "list-mods" ) .withDescription( "list available mod names" ) .create() ); options.addOption( OptionBuilder.withLongOpt( "runftl" ) .withDescription( "run the game (standalone or with 'patch')" ) .create() ); options.addOption( OptionBuilder.withLongOpt( "patch" ) .withDescription( "revert to vanilla and add named mods (if any)" ) .create() ); options.addOption( OptionBuilder.withLongOpt( "validate" ) .withDescription( "check named mods for problems" ) .create() ); options.addOption( "h", "help", false, "display this help and exit" ); options.addOption( OptionBuilder.withLongOpt( "version" ) .withDescription( "output version information and exit" ) .create() ); CommandLine cmdline = null; try { cmdline = parser.parse( options, args, true ); } catch( ParseException e ) { System.err.println( "Error parsing commandline: "+ e.getMessage() ); System.exit( 1 ); } if ( cmdline.hasOption( "h" ) ) { // Exits. HelpFormatter formatter = new HelpFormatter(); String helpHeader = "Perform actions against an FTL installation and/or a list of named mods."+ formatter.getNewLine(); String helpFooter = formatter.getNewLine(); helpFooter += "Each MODFILE is a filename in the mods/ dir."+ formatter.getNewLine(); helpFooter += "If a named mod is a directory, a temporary zip will be created."; formatter.printHelp( "modman [OPTION] [MODFILE]...", helpHeader, options, helpFooter ); System.exit( 0 ); } if ( cmdline.hasOption( "version" ) ) { // Exits. System.out.println( getVersionMessage() ); System.exit( 0 ); } DelayedDeleteHook deleteHook = new DelayedDeleteHook(); Runtime.getRuntime().addShutdownHook( deleteHook ); if ( cmdline.hasOption( "validate" ) ) { // Exits (0/1). log.info( "Validating..." ); StringBuilder resultBuf = new StringBuilder(); ReportFormatter formatter = new ReportFormatter(); boolean anyInvalid = false; for ( String modFileName : cmdline.getArgs() ) { File modFile = new File(modsDir, modFileName); if ( modFile.isDirectory() ) { log.info( String.format( "Zipping dir: %s/", modFile.getName() ) ); try { modFile = createTempMod( modFile ); deleteHook.addDoomedFile( modFile ); } catch ( IOException e ) { log.error( String.format( "Error zipping \"%s/\".", modFile.getName() ), e ); List<ReportMessage> tmpMessages = new ArrayList<ReportMessage>(); tmpMessages.add( new ReportMessage( ReportMessage.SECTION, modFileName ) ); tmpMessages.add( new ReportMessage( ReportMessage.EXCEPTION, e.getMessage() ) ); formatter.format( tmpMessages, resultBuf, 0 ); resultBuf.append( "\n" ); anyInvalid = true; continue; } } Report validateReport = ModUtilities.validateModFile( modFile ); formatter.format( validateReport.messages, resultBuf, 0 ); resultBuf.append( "\n" ); if ( validateReport.outcome == false ) anyInvalid = true; } if ( resultBuf.length() == 0 ) { resultBuf.append( "No mods were checked." ); } System.out.println(); System.out.println( resultBuf.toString() ); System.exit( anyInvalid ? 1 : 0 ); } File configFile = new File( "modman.cfg" ); Properties config = getConfig( configFile ); if ( cmdline.hasOption( "list-mods" ) ) { // Exits. log.info( "Listing mods..." ); boolean allowZip = config.getProperty( "allow_zip", "false" ).equals( "true" ); File[] modFiles = modsDir.listFiles( new ModAndDirFileFilter( allowZip, true ) ); List<String> dirList = new ArrayList<String>(); List<String> fileList = new ArrayList<String>(); for ( File f : modFiles ) { if ( f.isDirectory() ) dirList.add( f.getName() +"/" ); else fileList.add( f.getName() ); } Collections.sort( dirList ); Collections.sort( fileList ); for ( String s : dirList ) System.out.println( s ); for ( String s : fileList ) System.out.println( s ); System.exit( 0 ); } File datsDir = null; if ( cmdline.hasOption( "extract-dats" ) || cmdline.hasOption( "patch" ) || cmdline.hasOption( "runftl" ) ) { datsDir = getDatsDir( config ); } if ( cmdline.hasOption( "extract-dats" ) ) { // Exits (0/1). log.info( "Extracting dats..." ); String extractPath = cmdline.getOptionValue( "extract-dats" ); File extractDir = new File( extractPath ); File dataDatFile = new File( datsDir, "data.dat" ); File resDatFile = new File( datsDir, "resource.dat" ); File[] datFiles = new File[] {dataDatFile, resDatFile}; FTLDat.AbstractPack srcP = null; FTLDat.AbstractPack dstP = null; InputStream is = null; try { if ( !extractDir.exists() ) extractDir.mkdirs(); dstP = new FTLDat.FolderPack( extractDir ); for ( File datFile : datFiles ) { srcP = new FTLDat.FTLPack( datFile, "r" ); List<String> innerPaths = srcP.list(); for ( String innerPath : innerPaths ) { if ( dstP.contains( innerPath ) ) { log.info( "While extracting resources, this file was overwritten: "+ innerPath ); dstP.remove( innerPath ); } is = srcP.getInputStream( innerPath ); dstP.add( innerPath, is ); } srcP.close(); } } catch ( IOException e ) { log.error( "Error extracting dats.", e ); System.exit( 1 ); } finally { try {if ( is != null ) is.close();} catch ( IOException ex ) {} try {if ( srcP != null ) srcP.close();} catch ( IOException ex ) {} try {if ( dstP != null ) dstP.close();} catch ( IOException ex ) {} } System.exit( 0 ); } if ( cmdline.hasOption( "patch" ) ) { // Exits sometimes (1 on failure). log.info( "Patching..." ); List<File> modFiles = new ArrayList<File>(); for ( String modFileName : cmdline.getArgs() ) { File modFile = new File( modsDir, modFileName ); if ( modFile.isDirectory() ) { log.info( String.format( "Zipping dir: %s/", modFile.getName() ) ); try { modFile = createTempMod( modFile ); deleteHook.addDoomedFile( modFile ); } catch ( IOException e ) { log.error( String.format( "Error zipping \"%s/\".", modFile.getName() ), e ); System.exit( 1 ); } } modFiles.add( modFile ); } BackedUpDat dataDat = new BackedUpDat(); dataDat.datFile = new File( datsDir, "data.dat" ); dataDat.bakFile = new File( backupDir, "data.dat.bak" ); BackedUpDat resDat = new BackedUpDat(); resDat.datFile = new File( datsDir, "resource.dat" ); resDat.bakFile = new File( backupDir, "resource.dat.bak" ); boolean globalPanic = cmdline.hasOption( "global-panic" ); SilentPatchObserver patchObserver = new SilentPatchObserver(); ModPatchThread patchThread = new ModPatchThread( modFiles, dataDat, resDat, globalPanic, patchObserver ); patchThread.setDefaultUncaughtExceptionHandler( exceptionHandler ); deleteHook.addWatchedThread( patchThread ); patchThread.start(); while ( patchThread.isAlive() ) { try {patchThread.join();} catch ( InterruptedException e ) {} } if ( !patchObserver.hasSucceeded() ) System.exit( 1 ); } if ( cmdline.hasOption( "runftl" ) ) { // Exits (0/1). log.info( "Running FTL..." ); File exeFile = null; String[] exeArgs = null; if ( config.getProperty( "run_steam_ftl", "false" ).equals( "true" ) ) { exeFile = FTLUtilities.findSteamExe(); exeArgs = new String[] {"-applaunch", FTLUtilities.STEAM_APPID_FTL}; if ( exeFile == null ) { log.warn( "Steam executable could not be found. FTL will be launched directly." ); } } if ( exeFile == null ) { exeFile = FTLUtilities.findGameExe( datsDir ); exeArgs = new String[0]; if ( exeFile == null ) { log.warn( "FTL executable could not be found." ); } } if ( exeFile != null ) { try { FTLUtilities.launchExe( exeFile, exeArgs ); } catch ( Exception e ) { log.error( "Error launching FTL.", e ); System.exit( 1 ); } } else { log.error( "No executables were found to launch FTL." ); System.exit( 1 ); } System.exit( 0 ); } System.exit( 0 ); } /** * Loads settings from a config file. * * If an error occurs, it'll be logged, * and default settings will be returned. */ private static Properties getConfig( File configFile ) { Properties config = new Properties(); config.setProperty( "allow_zip", "false" ); config.setProperty( "ftl_dats_path", "" ); config.setProperty( "never_run_ftl", "false" ); config.setProperty( "use_default_ui", "false" ); // "update_catalog" doesn't have a default. // Read the config file. InputStream in = null; try { if ( configFile.exists() ) { log.trace( "Loading properties from config file." ); in = new FileInputStream( configFile ); config.load( new InputStreamReader( in, "UTF-8" ) ); } } catch ( IOException e ) { log.error( "Error loading config.", e ); } finally { try {if ( in != null ) in.close();} catch ( IOException e ) {} } return config; } /** * Checks the validity of the config's dats path and returns it. * Or exits if the path is invalid. */ private static File getDatsDir( Properties config ) { File datsDir = null; String datsPath = config.getProperty( "ftl_dats_path", "" ); if ( datsPath.length() > 0 ) { log.info( "Using FTL dats path from config: "+ datsPath ); datsDir = new File( datsPath ); if ( FTLUtilities.isDatsDirValid( datsDir ) == false ) { log.error( "The config's ftl_dats_path does not exist." ); datsDir = null; } } else { log.error( "No FTL dats path previously set." ); } if ( datsDir == null ) { log.error( "Run the GUI once, or edit the config file, and try again." ); System.exit( 1 ); } return datsDir; } /** * Returns a temporary zip made from a directory. * * Empty subdirs will be omitted. * The archive will be not be deleted on exit (handle that elsewhere). */ private static File createTempMod( File dir ) throws IOException { File tempFile = File.createTempFile( dir.getName() +"_temp-", ".zip" ); FileOutputStream fos = null; try { fos = new FileOutputStream( tempFile ); ZipOutputStream zos = new ZipOutputStream( new BufferedOutputStream( fos ) ); addDirToArchive( zos, dir, null ); zos.close(); } finally { try {if ( fos != null ) fos.close();} catch ( IOException e ) {} } return tempFile; } private static void addDirToArchive( ZipOutputStream zos, File dir, String pathPrefix ) throws IOException { if ( pathPrefix == null ) pathPrefix = ""; for ( File f : dir.listFiles() ) { if ( f.isDirectory() ) { addDirToArchive( zos, f, pathPrefix + f.getName() +"/" ); continue; } FileInputStream is = null; try { is = new FileInputStream( f ); zos.putNextEntry( new ZipEntry( pathPrefix + f.getName() ) ); byte[] buf = new byte[4096]; int len; while ( (len = is.read(buf)) >= 0 ) { zos.write( buf, 0, len ); } zos.closeEntry(); } finally { try {if ( is != null ) is.close();} catch ( IOException e ) {} } } } private static String getVersionMessage() { StringBuilder buf = new StringBuilder(); buf.append( String.format( "%s %s\n", FTLModManager.APP_NAME, FTLModManager.APP_VERSION ) ); buf.append( "Copyright (C) 2014 David Millis\n" ); buf.append( "\n" ); buf.append( "This program is free software; you can redistribute it and/or modify\n" ); buf.append( "it under the terms of the GNU General Public License as published by\n" ); buf.append( "the Free Software Foundation; version 2.\n" ); buf.append( "\n" ); buf.append( "This program is distributed in the hope that it will be useful,\n" ); buf.append( "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" ); buf.append( "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" ); buf.append( "GNU General Public License for more details.\n" ); buf.append( "\n" ); buf.append( "You should have received a copy of the GNU General Public License\n" ); buf.append( "along with this program. If not, see http://www.gnu.org/licenses/.\n" ); buf.append( "\n" ); return buf.toString(); } private static class SilentPatchObserver implements ModPatchObserver { private boolean done = false; private boolean succeeded = false; @Override public void patchingProgress( final int value, final int max ) { } @Override public void patchingStatus( String message ) { } @Override public void patchingMod( File modFile ) { } @Override public synchronized void patchingEnded( boolean outcome, Exception e ) { succeeded = outcome; done = true; } public synchronized boolean isDone() { return done; } public synchronized boolean hasSucceeded() { return succeeded; } } private static class ModAndDirFileFilter implements FileFilter { private boolean allowZip; private boolean allowDirs; public ModAndDirFileFilter( boolean allowZip, boolean allowDirs ) { this.allowZip = allowZip; this.allowDirs = allowDirs; } @Override public boolean accept( File f ) { if ( f.isDirectory() ) return allowDirs; if ( f.getName().endsWith(".ftl") ) return true; if ( allowZip ) { if ( f.getName().endsWith(".zip") ) return true; } return false; } } }