diff --git a/pom.xml b/pom.xml index 808ba86..3d2fabc 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,11 @@ jdom2 2.0.5 + + commons-cli + commons-cli + 1.2 + diff --git a/readme_developers.txt b/readme_developers.txt index 8397f65..a400263 100644 --- a/readme_developers.txt +++ b/readme_developers.txt @@ -39,6 +39,10 @@ This project depends on the following libraries. http://logging.apache.org/log4j/2.x/ (JavaDocs are not available.) +- Apache Commons CLI 1.2 + http://commons.apache.org/proper/commons-cli/ + (For JavaDocs, scroll down.) + Here's a batch file that builds when double-clicked (edit the vars). diff --git a/skel_common/readme_changelog.txt b/skel_common/readme_changelog.txt index 1007e94..69b91f6 100644 --- a/skel_common/readme_changelog.txt +++ b/skel_common/readme_changelog.txt @@ -1,5 +1,8 @@ Changelog +???: +- Added a commandline interface + 1.1: - Added a button to open the mods/ folder - Added FTL resource extraction diff --git a/skel_unix/modman-cli.sh b/skel_unix/modman-cli.sh new file mode 100644 index 0000000..a5ad913 --- /dev/null +++ b/skel_unix/modman-cli.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Get the absolute path to this script's folder. +if echo "$0" | awk '{exit(!/^\//);}'; then + maindir=$(dirname "$0"); +else + maindir=$PWD/$(dirname "$0"); +fi + +cd "${maindir}" +java -jar modman.jar "$@" diff --git a/src/main/java/net/vhati/ftldat/FTLDat.java b/src/main/java/net/vhati/ftldat/FTLDat.java index 14326a8..875d252 100644 --- a/src/main/java/net/vhati/ftldat/FTLDat.java +++ b/src/main/java/net/vhati/ftldat/FTLDat.java @@ -81,11 +81,11 @@ public class FTLDat { } } finally { - try {if (is != null) is.close();} - catch (IOException e) {} + try {if ( is != null ) is.close();} + catch ( IOException e ) {} - try {if (os != null) os.close();} - catch (IOException e) {} + try {if ( os != null ) os.close();} + catch ( IOException e ) {} } } @@ -368,8 +368,8 @@ public class FTLDat { } } finally { - try {if (os != null) os.close();} - catch (IOException e) {} + try {if ( os != null ) os.close();} + catch ( IOException e ) {} } } @@ -388,8 +388,8 @@ public class FTLDat { } } finally { - try {if (is != null) is.close();} - catch (IOException e) {} + try {if ( is != null ) is.close();} + catch ( IOException e ) {} } } diff --git a/src/main/java/net/vhati/modmanager/FTLModManager.java b/src/main/java/net/vhati/modmanager/FTLModManager.java index 665e887..e8a747c 100644 --- a/src/main/java/net/vhati/modmanager/FTLModManager.java +++ b/src/main/java/net/vhati/modmanager/FTLModManager.java @@ -13,6 +13,7 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; +import net.vhati.modmanager.cli.SlipstreamCLI; import net.vhati.modmanager.core.ComparableVersion; import net.vhati.modmanager.core.FTLUtilities; import net.vhati.modmanager.ui.ManagerFrame; @@ -25,13 +26,14 @@ public class FTLModManager { private static final Logger log = LogManager.getLogger(FTLModManager.class); - private static final String APP_NAME = "Slipstream Mod Manager"; - private static final ComparableVersion APP_VERSION = new ComparableVersion( "???" ); - private static final String APP_URL = "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=17102"; - private static final String APP_AUTHOR = "Vhati"; + public static final String APP_NAME = "Slipstream Mod Manager"; + public static final ComparableVersion APP_VERSION = new ComparableVersion( "???" ); + public static final String APP_URL = "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=17102"; + public static final String APP_AUTHOR = "Vhati"; public static void main( String[] args ) { + if ( args.length > 0 ) SlipstreamCLI.main( args ); log.debug( String.format( "%s v%s", APP_NAME, APP_VERSION ) ); log.debug( String.format( "%s %s", System.getProperty("os.name"), System.getProperty("os.version") ) ); diff --git a/src/main/java/net/vhati/modmanager/cli/SlipstreamCLI.java b/src/main/java/net/vhati/modmanager/cli/SlipstreamCLI.java new file mode 100644 index 0000000..6876941 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/cli/SlipstreamCLI.java @@ -0,0 +1,486 @@ +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/" ); + + + public static void main( String[] args ) { + + 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( "list-mods" ) + .withDescription( "list available mod names" ) + .create() ); + options.addOption( OptionBuilder.withLongOpt( "runftl" ) + .withDescription( "run the game" ) + .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 tmpMessages = new ArrayList(); + 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 dirList = new ArrayList(); + List fileList = new ArrayList(); + 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, false ); + List 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 modFiles = new ArrayList(); + 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" ); + + SilentPatchObserver patchObserver = new SilentPatchObserver(); + ModPatchThread patchThread = new ModPatchThread( modFiles, dataDat, resDat, patchObserver ); + 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 = FTLUtilities.findGameExe( datsDir ); + if ( exeFile != null ) { + try { + FTLUtilities.launchGame( exeFile ); + } catch ( Exception e ) { + log.error( "Error launching FTL.", e ); + System.exit( 1 ); + } + } + else { + log.error( "Could not find FTL's executable." ); + 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) 2013 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; + } + } +} diff --git a/src/main/java/net/vhati/modmanager/core/DelayedDeleteHook.java b/src/main/java/net/vhati/modmanager/core/DelayedDeleteHook.java new file mode 100644 index 0000000..c14e016 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/core/DelayedDeleteHook.java @@ -0,0 +1,74 @@ +package net.vhati.modmanager.core; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Iterator; +import java.util.List; + + +/** + * A shutdown hook that waits on threads before deleting files. + * + * This hook's waiting will keep the VM alive until the threads complete. + * + * Usage: + * DelayedDeleteHook deleteHook = new DelayedDeleteHook(); + * Runtime.getRuntime().addShutdownHook( deleteHook ); + */ +public class DelayedDeleteHook extends Thread { + private LinkedHashSet watchedThreads = new LinkedHashSet(); + private LinkedHashSet doomedFiles = new LinkedHashSet(); + + public synchronized void addWatchedThread( Thread t ) { + if ( watchedThreads == null ) + throw new IllegalStateException( "Shutdown in progress" ); + watchedThreads.add( t ); + } + + public synchronized void addDoomedFile( File f ) { + if ( doomedFiles == null ) + throw new IllegalStateException( "Shutdown in progress" ); + doomedFiles.add( f ); + } + + @Override + public void run() { + ArrayList pendingThreads; + ArrayList pendingFiles; + boolean interrupted = false; + + synchronized ( this ) { + pendingThreads = new ArrayList( watchedThreads ); + pendingFiles = new ArrayList( doomedFiles ); + watchedThreads = null; + doomedFiles = null; + } + + try { + // Wait on each thread. + Iterator it = pendingThreads.iterator(); + while ( it.hasNext() ) { + Thread t = it.next(); + while ( t.isAlive() ) { + try { + t.join(); + } + catch ( InterruptedException e ) { + interrupted = true; + } + } + it.remove(); + } + + Collections.reverse( pendingFiles ); + for ( File f : pendingFiles ) { + f.delete(); + } + } + finally { + if ( interrupted ) Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/net/vhati/modmanager/core/ModPatchObserver.java b/src/main/java/net/vhati/modmanager/core/ModPatchObserver.java index ec29dcd..591f1e8 100644 --- a/src/main/java/net/vhati/modmanager/core/ModPatchObserver.java +++ b/src/main/java/net/vhati/modmanager/core/ModPatchObserver.java @@ -31,5 +31,5 @@ public interface ModPatchObserver { * Patching ended. * If anything went wrong, e may be non-null. */ - public void patchingEnded( boolean success, Exception e ); + public void patchingEnded( boolean outcome, Exception e ); } diff --git a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java index b136cc9..93fc99a 100644 --- a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java +++ b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java @@ -63,10 +63,9 @@ public class ModPatchThread extends Thread { keepRunning = false; boolean interrupted = false; try { - while ( true ) { + while ( ModPatchThread.this.isAlive() ) { try { ModPatchThread.this.join(); - break; } catch ( InterruptedException e ) { interrupted = true; @@ -84,7 +83,7 @@ public class ModPatchThread extends Thread { result = patch(); } catch ( Exception e ) { - log.error( "Patching failed. See log for details.", e ); + log.error( "Patching failed.", e ); exception = e; result = false; } diff --git a/src/main/java/net/vhati/modmanager/scraper/ForumScraper.java b/src/main/java/net/vhati/modmanager/scraper/ForumScraper.java index e5c8a6b..bc50b01 100644 --- a/src/main/java/net/vhati/modmanager/scraper/ForumScraper.java +++ b/src/main/java/net/vhati/modmanager/scraper/ForumScraper.java @@ -14,6 +14,7 @@ import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -50,6 +51,13 @@ import org.jdom2.input.SAXBuilder; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; +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; @@ -69,83 +77,137 @@ public class ForumScraper { ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=11083" ); ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=4&t=2938" ); ignoredURLs.add( "http://www.moddb.com/mods/better-planets-and-backgrounds/downloads/better-asteroids" ); + ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=4&t=2947" ); + ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=11604" ); // SpaceDock is an app. ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=11&t=16842" ); // Beginning Scrap Advantage is bundled in GMM. ignoredURLs.add( "http://www.ftlgame.com/forum/viewtopic.php?f=4&t=2464" ); - List