first commit
This commit is contained in:
parent
352e1653f8
commit
16a197e856
44 changed files with 5942 additions and 3 deletions
190
src/main/java/net/vhati/modmanager/FTLModManager.java
Normal file
190
src/main/java/net/vhati/modmanager/FTLModManager.java
Normal file
|
@ -0,0 +1,190 @@
|
|||
package net.vhati.modmanager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.Properties;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import net.vhati.modmanager.core.ComparableVersion;
|
||||
import net.vhati.modmanager.core.FTLUtilities;
|
||||
import net.vhati.modmanager.ui.ManagerFrame;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
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( "1.0" );
|
||||
|
||||
|
||||
public static void main( String[] args ) {
|
||||
|
||||
log.debug( String.format( "%s v%s", APP_NAME, APP_VERSION ) );
|
||||
log.debug( System.getProperty("os.name") +" "+ System.getProperty("os.version") +" "+ System.getProperty("os.arch") );
|
||||
log.debug( System.getProperty("java.vm.name") +", "+ System.getProperty("java.version") );
|
||||
|
||||
|
||||
File configFile = new File( "modman.cfg" );
|
||||
|
||||
boolean writeConfig = false;
|
||||
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" ) );
|
||||
} else {
|
||||
writeConfig = true; // Create a new cfg, but only if necessary.
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error( "Error loading config.", e );
|
||||
showErrorDialog( "Error loading config from "+ configFile.getPath() );
|
||||
}
|
||||
finally {
|
||||
try {if ( in != null ) in.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
|
||||
// Look-and-Feel.
|
||||
String useDefaultUI = config.getProperty( "use_default_ui", "false" );
|
||||
|
||||
if ( !useDefaultUI.equals("true") ) {
|
||||
try {
|
||||
log.trace( "Using system Look and Feel" );
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error( "Error setting system Look and Feel.", e );
|
||||
log.info( "Setting 'useDefaultUI=true' in the config file will prevent this error." );
|
||||
}
|
||||
} else {
|
||||
log.debug( "Using default Look and Feel." );
|
||||
}
|
||||
|
||||
// FTL Resources Path.
|
||||
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, or it lacks data.dat." );
|
||||
datsDir = null;
|
||||
}
|
||||
} else {
|
||||
log.trace( "No FTL dats path previously set." );
|
||||
}
|
||||
|
||||
// Find/prompt for the path to set in the config.
|
||||
if ( datsDir == null ) {
|
||||
datsDir = FTLUtilities.findDatsDir();
|
||||
if ( datsDir != null ) {
|
||||
int response = JOptionPane.showConfirmDialog(null, "FTL resources were found in:\n"+ datsDir.getPath() +"\nIs this correct?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
if ( response == JOptionPane.NO_OPTION ) datsDir = null;
|
||||
}
|
||||
|
||||
if ( datsDir == null ) {
|
||||
log.debug( "FTL dats path was not located automatically. Prompting user for location." );
|
||||
datsDir = FTLUtilities.promptForDatsDir( null );
|
||||
}
|
||||
|
||||
if ( datsDir != null ) {
|
||||
config.setProperty( "ftl_dats_path", datsDir.getAbsolutePath() );
|
||||
writeConfig = true;
|
||||
log.info( "FTL dats located at: "+ datsDir.getAbsolutePath() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( datsDir == null ) {
|
||||
showErrorDialog( "FTL resources were not found.\nThe Mod Manager will now exit." );
|
||||
log.debug( "No FTL dats path found, exiting." );
|
||||
System.exit( 1 );
|
||||
}
|
||||
|
||||
// Prompt if update_catalog is invalid or hasn't been set.
|
||||
|
||||
String updateCatalog = config.getProperty( "update_catalog" );
|
||||
if ( updateCatalog == null || !updateCatalog.matches("^true|false$") ) {
|
||||
String message = "";
|
||||
message += "Would you like Slipstream to periodically\n";
|
||||
message += "download descriptions for the latest mods?\n\n";
|
||||
message += "You can change this later in modman.cfg.";
|
||||
|
||||
int response = JOptionPane.showConfirmDialog(null, message, "Catalog Updates", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
if ( response == JOptionPane.YES_OPTION )
|
||||
config.setProperty( "update_catalog", "true" );
|
||||
else
|
||||
config.setProperty( "update_catalog", "false" );
|
||||
}
|
||||
|
||||
|
||||
if ( writeConfig ) {
|
||||
OutputStream out = null;
|
||||
try {
|
||||
out = new FileOutputStream( configFile );
|
||||
String configComments = "";
|
||||
configComments += "\n";
|
||||
configComments += " allow_zip - Sets whether to treat .zip files as .ftl files. Default: false.\n";
|
||||
configComments += " ftl_dats_path - The path to FTL's resources folder. If invalid, you'll be prompted.\n";
|
||||
configComments += " never_run_ftl - If true, there will be no offer to run FTL after patching. Default: false.\n";
|
||||
configComments += " update_catalog - If true, periodically download descriptions for the latest mods. If invalid, you'll be prompted.\n";
|
||||
configComments += " use_default_ui - If true, no attempt will be made to resemble a native GUI. Default: false.\n";
|
||||
|
||||
config.store( new OutputStreamWriter( out, "UTF-8" ), configComments );
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
log.error( "Error saving config to "+ configFile.getPath(), e );
|
||||
showErrorDialog( "Error saving config to "+ configFile.getPath() );
|
||||
}
|
||||
finally {
|
||||
try {if ( out != null ) out.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the GUI.
|
||||
try {
|
||||
final ManagerFrame frame = new ManagerFrame( config, APP_NAME, APP_VERSION );
|
||||
frame.setDefaultCloseOperation( frame.EXIT_ON_CLOSE );
|
||||
frame.setVisible(true);
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
frame.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( "Exception while creating ManagerFrame.", e );
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void showErrorDialog( String message ) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
340
src/main/java/net/vhati/modmanager/core/ComparableVersion.java
Normal file
340
src/main/java/net/vhati/modmanager/core/ComparableVersion.java
Normal file
|
@ -0,0 +1,340 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* A version string (eg, 10.4.2_17 or 2.7.5rc1 ).
|
||||
*
|
||||
* It is composed of three parts:
|
||||
* - A series of period-separated positive ints.
|
||||
*
|
||||
* - The numbers may be immediately followed by a short
|
||||
* suffix string.
|
||||
*
|
||||
* - Finally, a string comment, separated from the rest
|
||||
* by a space.
|
||||
*
|
||||
* The (numbers + suffix) or comment may appear alone.
|
||||
*
|
||||
* For details, see the string constructor and compareTo().
|
||||
*/
|
||||
public class ComparableVersion implements Comparable<ComparableVersion> {
|
||||
|
||||
private Pattern numbersPtn = Pattern.compile( "^((?:\\d+[.])*\\d+)" );
|
||||
private Pattern suffixPtn = Pattern.compile( "([-_]|(?:[-_]?(?:[ab]|r|rc)))(\\d+)|([A-Za-z](?= |$))" );
|
||||
private Pattern commentPtn = Pattern.compile( "(.+)$" );
|
||||
|
||||
private int[] numbers;
|
||||
private String suffix;
|
||||
private String comment;
|
||||
|
||||
private String suffixDivider; // Suffix prior to a number, if there was a number.
|
||||
private int suffixNum;
|
||||
|
||||
|
||||
public ComparableVersion( int[] numbers, String suffix, String comment ) {
|
||||
this.numbers = numbers;
|
||||
setSuffix( suffix );
|
||||
setComment( comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an AppVersion by parsing a string.
|
||||
*
|
||||
* The suffix can be:
|
||||
* - A divider string followed by a number.
|
||||
* - Optional Hyphen/underscore, then a|b|r|rc, then 0-9+.
|
||||
* - Hyphen/underscore, then 0-9+.
|
||||
* Or the suffix can be a single letter without a number.
|
||||
*
|
||||
* Examples:
|
||||
* 1
|
||||
* 1 Blah
|
||||
* 1.2 Blah
|
||||
* 1.2.3 Blah
|
||||
* 1.2.3-8 Blah
|
||||
* 1.2.3_b9 Blah
|
||||
* 1.2.3a1 Blah
|
||||
* 1.2.3b1 Blah
|
||||
* 1.2.3rc2 Blah
|
||||
* 1.2.3z Blah
|
||||
* 1.2.3D
|
||||
* Alpha
|
||||
*
|
||||
* @throws IllegalArgumentException if the string is unsuitable
|
||||
*/
|
||||
public ComparableVersion( String s ) {
|
||||
boolean noNumbers = true;
|
||||
boolean noComment = true;
|
||||
|
||||
Matcher numbersMatcher = numbersPtn.matcher( s );
|
||||
Matcher suffixMatcher = suffixPtn.matcher( s );
|
||||
Matcher commentMatcher = commentPtn.matcher( s );
|
||||
|
||||
if ( numbersMatcher.lookingAt() ) {
|
||||
noNumbers = false;
|
||||
setNumbers( numbersMatcher.group( 0 ) );
|
||||
|
||||
commentMatcher.region( numbersMatcher.end(), s.length() );
|
||||
|
||||
// We have numbers; do we have a suffix?
|
||||
suffixMatcher.region( numbersMatcher.end(), s.length() );
|
||||
if ( suffixMatcher.lookingAt() ) {
|
||||
setSuffix( suffixMatcher.group( 0 ) );
|
||||
|
||||
commentMatcher.region( suffixMatcher.end(), s.length() );
|
||||
}
|
||||
else {
|
||||
setSuffix( null );
|
||||
}
|
||||
|
||||
// If a space occurs after (numbers +suffix?), skip it.
|
||||
// Thus the comment matcher will start on the first comment char.
|
||||
//
|
||||
if ( commentMatcher.regionStart()+1 < s.length() ) {
|
||||
if ( s.charAt( commentMatcher.regionStart() ) == ' ' ) {
|
||||
commentMatcher.region( commentMatcher.regionStart()+1, s.length() );
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
numbers = new int[0];
|
||||
setSuffix( null );
|
||||
}
|
||||
|
||||
// Check for a comment (at the start, elsewhere if region was set).
|
||||
if ( commentMatcher.lookingAt() ) {
|
||||
noComment = false;
|
||||
setComment( commentMatcher.group( 1 ) );
|
||||
}
|
||||
|
||||
if ( noNumbers && noComment ) {
|
||||
throw new IllegalArgumentException( "Could not parse version string: "+ s );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void setNumbers( String s ) {
|
||||
if ( s == null || s.length() == 0 ) {
|
||||
numbers = new int[0];
|
||||
return;
|
||||
}
|
||||
|
||||
Matcher m = numbersPtn.matcher( s );
|
||||
if ( m.matches() ) {
|
||||
String numString = m.group( 1 );
|
||||
String[] numChunks = numString.split("[.]");
|
||||
|
||||
numbers = new int[ numChunks.length ];
|
||||
for ( int i=0; i < numChunks.length; i++ ) {
|
||||
numbers[i] = Integer.parseInt( numChunks[i] );
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException( "Could not parse version numbers string: "+ s );
|
||||
}
|
||||
}
|
||||
|
||||
private void setSuffix( String s ) {
|
||||
if ( s == null || s.length() == 0 ) {
|
||||
suffix = null;
|
||||
suffixNum = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
Matcher m = suffixPtn.matcher( s );
|
||||
if ( m.matches() ) {
|
||||
suffix = s;
|
||||
|
||||
// Matched groups 1 and 2... or 3.
|
||||
|
||||
if ( m.group(1) != null ) {
|
||||
suffixDivider = m.group(1);
|
||||
}
|
||||
|
||||
if ( m.group(2) != null ) {
|
||||
suffixNum = Integer.parseInt( m.group(2) );
|
||||
} else {
|
||||
suffixNum = -1;
|
||||
}
|
||||
|
||||
suffix = m.group(0);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException( "Could not parse version suffix string: "+ s );
|
||||
}
|
||||
}
|
||||
|
||||
private void setComment( String s ) {
|
||||
if ( s == null || s.length() == 0 ) {
|
||||
comment = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Matcher m = commentPtn.matcher( s );
|
||||
if ( m.matches() ) {
|
||||
comment = m.group(1);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException( "Could not parse version comment string: "+ s );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the array of major/minor/etc version numbers.
|
||||
*/
|
||||
public int[] getNumbers() {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pre-number portion of the suffix, or null if there was no number.
|
||||
*/
|
||||
public String getSuffixDivider() {
|
||||
return suffixDivider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number in the suffix, or -1 if there was no number.
|
||||
*/
|
||||
public int getSuffixNumber() {
|
||||
return suffixNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entire suffix, or null.
|
||||
*/
|
||||
public String getSuffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable comment, or null.
|
||||
*/
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for ( int number : numbers ) {
|
||||
if ( buf.length() > 0 ) buf.append( "." );
|
||||
buf.append( number );
|
||||
}
|
||||
if ( suffix != null ) {
|
||||
buf.append( suffix );
|
||||
}
|
||||
if ( comment != null ) {
|
||||
if ( buf.length() > 0 ) buf.append( " " );
|
||||
buf.append( comment );
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares this object with the specified object for order.
|
||||
*
|
||||
* - The ints are compared arithmetically. In case of ties,
|
||||
* the version with the most numbers wins.
|
||||
* - If both versions' suffixes have a number, and the same
|
||||
* characters appear before that number, then the suffix number
|
||||
* is compared arithmetically.
|
||||
* - Then the entire suffix is compared alphabetically.
|
||||
* - Then the comment is compared alphabetically.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo( ComparableVersion other ) {
|
||||
if ( other == null ) return -1;
|
||||
if ( other == this ) return 0;
|
||||
|
||||
int[] oNumbers = other.getNumbers();
|
||||
for ( int i=0; i < numbers.length && i < oNumbers.length; i++ ) {
|
||||
if ( numbers[i] < oNumbers[i] ) return -1;
|
||||
if ( numbers[i] > oNumbers[i] ) return 1;
|
||||
}
|
||||
if ( numbers.length < oNumbers.length ) return -1;
|
||||
if ( numbers.length > oNumbers.length ) return 1;
|
||||
|
||||
if ( suffixDivider != null && other.getSuffixDivider() != null ) {
|
||||
if ( suffixDivider.equals( other.getSuffixDivider() ) ) {
|
||||
if ( suffixNum < other.getSuffixNumber() ) return -1;
|
||||
if ( suffixNum > other.getSuffixNumber() ) return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ( suffix == null && other.getSuffix() != null ) return -1;
|
||||
if ( suffix != null && other.getSuffix() == null ) return 1;
|
||||
if ( suffix != null && other.getSuffix() != null ) {
|
||||
int cmp = suffix.compareTo( other.getSuffix() );
|
||||
if ( cmp != 0 ) return cmp;
|
||||
}
|
||||
|
||||
if ( comment == null && other.getComment() != null ) return -1;
|
||||
if ( comment != null && other.getComment() == null ) return 1;
|
||||
if ( comment != null && other.getComment() != null ) {
|
||||
int cmp = comment.compareTo( other.getComment() );
|
||||
if ( cmp != 0 ) return cmp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
if ( o == null ) return false;
|
||||
if ( o == this ) return true;
|
||||
if ( o instanceof ComparableVersion == false ) return false;
|
||||
ComparableVersion other = (ComparableVersion)o;
|
||||
|
||||
int[] oNumbers = other.getNumbers();
|
||||
for ( int i=0; i < numbers.length && i < oNumbers.length; i++ ) {
|
||||
if ( numbers[i] != oNumbers[i] ) return false;
|
||||
}
|
||||
if ( numbers.length != oNumbers.length ) return false;
|
||||
|
||||
if ( suffix == null && other.getSuffix() != null ) return false;
|
||||
if ( suffix != null && other.getSuffix() == null ) return false;
|
||||
if ( !suffix.equals( other.getSuffix() ) ) return false;
|
||||
|
||||
if ( comment == null && other.getComment() != null ) return false;
|
||||
if ( comment != null && other.getComment() == null ) return false;
|
||||
if ( !comment.equals( other.getComment() ) ) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 79;
|
||||
int salt = 35;
|
||||
int nullCode = 13;
|
||||
|
||||
List<Integer> tmpNumbers = new ArrayList<Integer>( getNumbers().length );
|
||||
for ( int n : getNumbers() )
|
||||
tmpNumbers.add( new Integer(n) );
|
||||
result = salt * result + tmpNumbers.hashCode();
|
||||
|
||||
String tmpSuffix = getSuffix();
|
||||
if ( tmpSuffix == null )
|
||||
result = salt * result + nullCode;
|
||||
else
|
||||
result = salt * result + tmpSuffix.hashCode();
|
||||
|
||||
String tmpComment = getComment();
|
||||
if ( tmpComment == null )
|
||||
result = salt * result + nullCode;
|
||||
else
|
||||
result = salt * result + tmpComment.hashCode();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
163
src/main/java/net/vhati/modmanager/core/FTLUtilities.java
Normal file
163
src/main/java/net/vhati/modmanager/core/FTLUtilities.java
Normal file
|
@ -0,0 +1,163 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.filechooser.FileFilter;
|
||||
|
||||
|
||||
public class FTLUtilities {
|
||||
|
||||
/**
|
||||
* Confirms the FTL resources dir exists and contains the dat files.
|
||||
*/
|
||||
public static boolean isDatsDirValid( File d ) {
|
||||
if ( !d.exists() || !d.isDirectory() ) return false;
|
||||
if ( !new File(d, "data.dat").exists() ) return false;
|
||||
if ( !new File(d, "resource.dat").exists() ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FTL resources dir, or null.
|
||||
*/
|
||||
public static File findDatsDir() {
|
||||
String steamPath = "Steam/steamapps/common/FTL Faster Than Light/resources";
|
||||
String gogPath = "GOG.com/Faster Than Light/resources";
|
||||
|
||||
String xdgDataHome = System.getenv("XDG_DATA_HOME");
|
||||
if (xdgDataHome == null)
|
||||
xdgDataHome = System.getProperty("user.home") +"/.local/share";
|
||||
|
||||
File[] candidates = new File[] {
|
||||
// Windows - Steam
|
||||
new File( new File(""+System.getenv("ProgramFiles(x86)")), steamPath ),
|
||||
new File( new File(""+System.getenv("ProgramFiles")), steamPath ),
|
||||
// Windows - GOG
|
||||
new File( new File(""+System.getenv("ProgramFiles(x86)")), gogPath ),
|
||||
new File( new File(""+System.getenv("ProgramFiles")), gogPath ),
|
||||
// Linux - Steam
|
||||
new File( xdgDataHome +"/Steam/SteamApps/common/FTL Faster Than Light/data/resources" ),
|
||||
// OSX - Steam
|
||||
new File( System.getProperty("user.home") +"/Library/Application Support/Steam/SteamApps/common/FTL Faster Than Light/FTL.app/Contents/Resources" ),
|
||||
// OSX
|
||||
new File( "/Applications/FTL.app/Contents/Resources" )
|
||||
};
|
||||
|
||||
File result = null;
|
||||
|
||||
for ( File candidate : candidates ) {
|
||||
if ( isDatsDirValid( candidate ) ) {
|
||||
result = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modally prompts the user for the FTL resources dir.
|
||||
*
|
||||
* @param parentComponent a parent for Swing popups, or null
|
||||
*/
|
||||
public static File promptForDatsDir( Component parentComponent ) {
|
||||
File result = null;
|
||||
|
||||
String message = "";
|
||||
message += "You will now be prompted to locate FTL manually.\n";
|
||||
message += "Select '(FTL dir)/resources/data.dat'.\n";
|
||||
message += "Or 'FTL.app', if you're on OSX.";
|
||||
JOptionPane.showMessageDialog(null, message, "Find FTL", JOptionPane.INFORMATION_MESSAGE);
|
||||
|
||||
final JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle( "Find data.dat or FTL.app" );
|
||||
fc.addChoosableFileFilter(new FileFilter() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "FTL Data File - (FTL dir)/resources/data.dat";
|
||||
}
|
||||
@Override
|
||||
public boolean accept(File f) {
|
||||
return f.isDirectory() || f.getName().equals("data.dat") || f.getName().equals("FTL.app");
|
||||
}
|
||||
});
|
||||
fc.setMultiSelectionEnabled(false);
|
||||
|
||||
if ( fc.showOpenDialog(null) == JFileChooser.APPROVE_OPTION ) {
|
||||
File f = fc.getSelectedFile();
|
||||
if ( f.getName().equals("data.dat") ) {
|
||||
result = f.getParentFile();
|
||||
}
|
||||
else if ( f.getName().endsWith(".app") && f.isDirectory() ) {
|
||||
File contentsPath = new File(f, "Contents");
|
||||
if( contentsPath.exists() && contentsPath.isDirectory() && new File(contentsPath, "Resources").exists() )
|
||||
result = new File(contentsPath, "Resources");
|
||||
}
|
||||
}
|
||||
|
||||
if ( result != null && isDatsDirValid( result ) ) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the executable that will launch FTL, or null.
|
||||
*
|
||||
* On Windows, FTLGame.exe is one dir above "resources/".
|
||||
* On OSX, FTL.app is the grandparent dir itself (a bundle).
|
||||
*/
|
||||
public static File findGameExe( File datsDir ) {
|
||||
File result = null;
|
||||
|
||||
if ( System.getProperty("os.name").startsWith("Windows") ) {
|
||||
File ftlDir = datsDir.getParentFile();
|
||||
if ( ftlDir != null ) {
|
||||
File exeFile = new File( ftlDir, "FTLGame.exe" );
|
||||
if ( exeFile.exists() ) result = exeFile;
|
||||
}
|
||||
}
|
||||
else if ( System.getProperty("os.name").contains("OS X") ) {
|
||||
// FTL.app/Contents/Resources/
|
||||
File contentsDir = datsDir.getParentFile();
|
||||
if ( contentsDir != null ) {
|
||||
File bundleDir = contentsDir.getParentFile();
|
||||
if ( bundleDir != null ) {
|
||||
if ( new File( bundleDir, "Contents/Info.plist" ).exists() ) {
|
||||
result = bundleDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Spawns the game (FTLGame.exe or FTL.app).
|
||||
*
|
||||
* @param exeFile see findGameExe()
|
||||
* @return a Process object, or null
|
||||
*/
|
||||
public static Process launchGame( File exeFile ) throws IOException {
|
||||
if ( exeFile == null ) return null;
|
||||
|
||||
Process result = null;
|
||||
ProcessBuilder pb = null;
|
||||
if ( System.getProperty("os.name").contains("OS X") ) {
|
||||
pb = new ProcessBuilder( "open", "-a", exeFile.getAbsolutePath() );
|
||||
} else {
|
||||
pb = new ProcessBuilder( exeFile.getAbsolutePath() );
|
||||
}
|
||||
if ( pb != null ) {
|
||||
pb.directory( exeFile.getParentFile() );
|
||||
result = pb.start();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public interface HashObserver {
|
||||
public void hashCalculated( File f, String hash );
|
||||
}
|
58
src/main/java/net/vhati/modmanager/core/HashThread.java
Normal file
58
src/main/java/net/vhati/modmanager/core/HashThread.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import net.vhati.ftldat.FTLDat;
|
||||
import net.vhati.modmanager.core.HashObserver;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* A thread to calculate MD5 hashes of files in the background.
|
||||
*
|
||||
* As each file is hashed, a class implementing HashObserver is notified.
|
||||
* Note: The callback on that class needs to be thread-safe.
|
||||
*/
|
||||
public class HashThread extends Thread {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(HashThread.class);
|
||||
|
||||
private List<File> fileList = new ArrayList<File>();
|
||||
private HashObserver hashObserver = null;
|
||||
|
||||
|
||||
public HashThread( File[] files, HashObserver hashObserver ) {
|
||||
this.fileList.addAll( Arrays.asList(files) );
|
||||
this.hashObserver = hashObserver;
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
for ( File f : fileList ) {
|
||||
String hash = calcFileMD5( f );
|
||||
if ( hash != null ) {
|
||||
hashObserver.hashCalculated( f, hash );
|
||||
}
|
||||
}
|
||||
|
||||
log.info( "Background hashing finished." );
|
||||
}
|
||||
|
||||
|
||||
private String calcFileMD5( File f ) {
|
||||
String result = null;
|
||||
try {
|
||||
result = FTLDat.calcFileMD5( f );
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( "Error while calculating hash for file: "+ f.getPath(), e );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
61
src/main/java/net/vhati/modmanager/core/ModDB.java
Normal file
61
src/main/java/net/vhati/modmanager/core/ModDB.java
Normal file
|
@ -0,0 +1,61 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import net.vhati.modmanager.core.ModInfo;
|
||||
|
||||
|
||||
public class ModDB {
|
||||
|
||||
// Accociates Forum thread urls with hashes of their forst post's content.
|
||||
private HashMap<String,String> threadHashMap = new HashMap<String,String>();
|
||||
|
||||
private ArrayList<ModInfo> catalog = new ArrayList<ModInfo>();
|
||||
|
||||
|
||||
/**
|
||||
* Returns mod info for a given file hash.
|
||||
*/
|
||||
public ModInfo getModInfo( String hash ) {
|
||||
if ( hash == null ) return null;
|
||||
|
||||
for ( ModInfo modInfo : catalog ) {
|
||||
if ( modInfo.getFileHash().equals(hash) ) {
|
||||
return modInfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addMod( ModInfo modInfo ) {
|
||||
catalog.add( modInfo );
|
||||
}
|
||||
|
||||
public void removeMod( ModInfo modInfo ) {
|
||||
catalog.remove( modInfo );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the first-post content hash of a forum thread.
|
||||
*/
|
||||
public void putThreadHash( String url, String threadHash ) {
|
||||
threadHashMap.put( url, threadHash );
|
||||
}
|
||||
|
||||
public String getThreadHash( String url ) {
|
||||
return threadHashMap.get( url );
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
threadHashMap.clear();
|
||||
catalog.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal ArrayList of mod info.
|
||||
*/
|
||||
public ArrayList<ModInfo> getCatalog() {
|
||||
return catalog;
|
||||
}
|
||||
}
|
54
src/main/java/net/vhati/modmanager/core/ModFileInfo.java
Normal file
54
src/main/java/net/vhati/modmanager/core/ModFileInfo.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public class ModFileInfo implements Comparable<ModFileInfo> {
|
||||
private File file;
|
||||
private String name;
|
||||
|
||||
|
||||
public ModFileInfo( File f ) {
|
||||
this.file = f;
|
||||
this.name = f.getName().replaceAll( "[.][^.]+$", "" );
|
||||
}
|
||||
|
||||
public File getFile() { return this.file; }
|
||||
public String getName() { return this.name; }
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo( ModFileInfo other ) {
|
||||
if ( other == null ) return -1;
|
||||
if ( other == this ) return 0;
|
||||
|
||||
return getName().compareTo( other.getName() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
if ( o == null ) return false;
|
||||
if ( o == this ) return true;
|
||||
if ( o instanceof ModFileInfo == false ) return false;
|
||||
|
||||
ModFileInfo other = (ModFileInfo)o;
|
||||
if ( !getFile().equals( other.getFile() ) ) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 89;
|
||||
int salt = 36;
|
||||
|
||||
result = salt * result + getFile().hashCode();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
62
src/main/java/net/vhati/modmanager/core/ModInfo.java
Normal file
62
src/main/java/net/vhati/modmanager/core/ModInfo.java
Normal file
|
@ -0,0 +1,62 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
|
||||
public class ModInfo {
|
||||
private String title = "???";
|
||||
private String author = "???";
|
||||
private String url = "???";
|
||||
private String description = "";
|
||||
private String fileHash = "???";
|
||||
private String version = "???";
|
||||
|
||||
|
||||
public void setTitle( String s ) { this.title = s; }
|
||||
public void setAuthor( String s ) { this.author = s; }
|
||||
public void setURL( String s ) { this.url = s; }
|
||||
public void setDescription( String s ) { this.description = s; }
|
||||
public void setFileHash( String s ) { this.fileHash = s; }
|
||||
public void setVersion( String s ) { this.version = s; }
|
||||
|
||||
public String getTitle() { return this.title; }
|
||||
public String getAuthor() { return this.author; }
|
||||
public String getURL() { return this.url; }
|
||||
public String getDescription() { return this.description; }
|
||||
public String getFileHash() { return this.fileHash; }
|
||||
public String getVersion() { return this.version; }
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
if ( o == null ) return false;
|
||||
if ( o == this ) return true;
|
||||
if ( o instanceof ModInfo == false ) return false;
|
||||
|
||||
ModInfo other = (ModInfo)o;
|
||||
if ( !getTitle().equals( other.getTitle() ) ) return false;
|
||||
if ( !getAuthor().equals( other.getAuthor() ) ) return false;
|
||||
if ( !getURL().equals( other.getURL() ) ) return false;
|
||||
if ( !getDescription().equals( other.getDescription() ) ) return false;
|
||||
if ( !getFileHash().equals( other.getFileHash() ) ) return false;
|
||||
if ( !getVersion().equals( other.getVersion() ) ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 79;
|
||||
int salt = 35;
|
||||
|
||||
result = salt * result + getTitle().hashCode();
|
||||
result = salt * result + getAuthor().hashCode();
|
||||
result = salt * result + getURL().hashCode();
|
||||
result = salt * result + getDescription().hashCode();
|
||||
result = salt * result + getFileHash().hashCode();
|
||||
result = salt * result + getVersion().hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public interface ModPatchObserver {
|
||||
|
||||
/**
|
||||
* Updates a progress bar.
|
||||
*
|
||||
* If either arg is -1, the bar will become indeterminate.
|
||||
*
|
||||
* @param value the new value
|
||||
* @param max the new maximum
|
||||
*/
|
||||
public void patchingProgress( final int value, final int max );
|
||||
|
||||
/**
|
||||
* Non-specific activity.
|
||||
*
|
||||
* @param message a string, or null
|
||||
*/
|
||||
public void patchingStatus( String message );
|
||||
|
||||
/**
|
||||
* A mod is about to be processed.
|
||||
*/
|
||||
public void patchingMod( File modFile );
|
||||
|
||||
/**
|
||||
* Patching ended.
|
||||
* If anything went wrong, e may be non-null.
|
||||
*/
|
||||
public void patchingEnded( boolean success, Exception e );
|
||||
}
|
325
src/main/java/net/vhati/modmanager/core/ModPatchThread.java
Normal file
325
src/main/java/net/vhati/modmanager/core/ModPatchThread.java
Normal file
|
@ -0,0 +1,325 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import net.vhati.ftldat.FTLDat;
|
||||
import net.vhati.ftldat.FTLDat.AbstractPack;
|
||||
import net.vhati.ftldat.FTLDat.FTLPack;
|
||||
import net.vhati.modmanager.core.ModPatchObserver;
|
||||
import net.vhati.modmanager.core.ModUtilities;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class ModPatchThread extends Thread {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ModPatchThread.class);
|
||||
|
||||
// Other threads can check or set this.
|
||||
public volatile boolean keepRunning = true;
|
||||
|
||||
private Thread shutdownHook = null;
|
||||
|
||||
private List<File> modFiles = new ArrayList<File>();
|
||||
private BackedUpDat dataDat = null;
|
||||
private BackedUpDat resDat = null;
|
||||
private ModPatchObserver observer = null;
|
||||
|
||||
private final int progMax = 100;
|
||||
private final int progBackupMax = 25;
|
||||
private final int progClobberMax = 25;
|
||||
private final int progModsMax = 40;
|
||||
private final int progRepackMax = 5;
|
||||
private int progMilestone = 0;
|
||||
|
||||
public ModPatchThread( List<File> modFiles, BackedUpDat dataDat, BackedUpDat resDat, ModPatchObserver observer ) {
|
||||
this.modFiles.addAll( modFiles );
|
||||
this.dataDat = dataDat;
|
||||
this.resDat = resDat;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
boolean result;
|
||||
Exception exception = null;
|
||||
|
||||
// When JVM tries to exit, stall until this thread ends on its own.
|
||||
shutdownHook = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
keepRunning = false;
|
||||
boolean interrupted = false;
|
||||
try {
|
||||
while ( true ) {
|
||||
try {
|
||||
ModPatchThread.this.join();
|
||||
break;
|
||||
}
|
||||
catch ( InterruptedException e ) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ( interrupted ) Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
};
|
||||
Runtime.getRuntime().addShutdownHook( shutdownHook );
|
||||
|
||||
try {
|
||||
result = patch();
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( "Patching failed. See log for details.", e );
|
||||
exception = e;
|
||||
result = false;
|
||||
}
|
||||
|
||||
observer.patchingEnded( result, exception );
|
||||
|
||||
Runtime.getRuntime().removeShutdownHook( shutdownHook );
|
||||
}
|
||||
|
||||
|
||||
private boolean patch() throws IOException {
|
||||
|
||||
observer.patchingProgress( 0, progMax );
|
||||
|
||||
BackedUpDat[] allDats = new BackedUpDat[] {dataDat, resDat};
|
||||
|
||||
FTLPack dataP = null;
|
||||
FTLPack resP = null;
|
||||
|
||||
try {
|
||||
int backupsCreated = 0;
|
||||
int datsClobbered = 0;
|
||||
int modsInstalled = 0;
|
||||
int datsRepacked = 0;
|
||||
|
||||
// Create backup dats, if necessary.
|
||||
for ( BackedUpDat dat : allDats ) {
|
||||
if ( !dat.bakFile.exists() ) {
|
||||
log.info( String.format( "Backing up \"%s\".", dat.datFile.getName() ) );
|
||||
observer.patchingStatus( String.format( "Backing up \"%s\".", dat.datFile.getName() ) );
|
||||
|
||||
FTLDat.copyFile( dat.datFile, dat.bakFile );
|
||||
backupsCreated++;
|
||||
observer.patchingProgress( progMilestone + progBackupMax/allDats.length*backupsCreated, progMax );
|
||||
|
||||
if ( !keepRunning ) return false;
|
||||
}
|
||||
}
|
||||
progMilestone += progBackupMax;
|
||||
observer.patchingProgress( progMilestone, progMax );
|
||||
observer.patchingStatus( null );
|
||||
|
||||
if ( backupsCreated != allDats.length ) {
|
||||
// Clobber current dat files with their respective backups.
|
||||
// But don't bother if we made those backups just now.
|
||||
|
||||
for ( BackedUpDat dat : allDats ) {
|
||||
log.info( String.format( "Restoring vanilla \"%s\"...", dat.datFile.getName() ) );
|
||||
observer.patchingStatus( String.format( "Restoring vanilla \"%s\"...", dat.datFile.getName() ) );
|
||||
|
||||
FTLDat.copyFile( dat.bakFile, dat.datFile );
|
||||
datsClobbered++;
|
||||
observer.patchingProgress( progMilestone + progClobberMax/allDats.length*datsClobbered, progMax );
|
||||
|
||||
if ( !keepRunning ) return false;
|
||||
}
|
||||
observer.patchingStatus( null );
|
||||
}
|
||||
progMilestone += progClobberMax;
|
||||
observer.patchingProgress( progMilestone, progMax );
|
||||
|
||||
if ( modFiles.isEmpty() ) {
|
||||
// No mods. Nothing else to do.
|
||||
observer.patchingProgress( progMax, progMax );
|
||||
return true;
|
||||
}
|
||||
|
||||
dataP = new FTLPack( dataDat.datFile, false );
|
||||
resP = new FTLPack( resDat.datFile, false );
|
||||
|
||||
Map<String,AbstractPack> topFolderMap = new HashMap<String,AbstractPack>();
|
||||
topFolderMap.put( "data", dataP );
|
||||
topFolderMap.put( "audio", resP );
|
||||
topFolderMap.put( "fonts", resP );
|
||||
topFolderMap.put( "img", resP );
|
||||
|
||||
// Track modified innerPaths in case they're clobbered.
|
||||
List<String> moddedItems = new ArrayList<String>();
|
||||
|
||||
List<String> knownPaths = new ArrayList<String>();
|
||||
knownPaths.addAll( dataP.list() );
|
||||
knownPaths.addAll( resP.list() );
|
||||
|
||||
List<String> knownPathsLower = new ArrayList<String>( knownPaths.size() );
|
||||
for ( String innerPath : knownPaths ) {
|
||||
knownPathsLower.add( innerPath.toLowerCase() );
|
||||
}
|
||||
|
||||
// Group1: parentPath, Group2: topFolder, Group3: fileName
|
||||
Pattern pathPtn = Pattern.compile( "^(([^/]+)/(?:.*/)?)([^/]+)$" );
|
||||
|
||||
for ( File modFile : modFiles ) {
|
||||
if ( !keepRunning ) return false;
|
||||
|
||||
ZipInputStream zis = null;
|
||||
try {
|
||||
log.info( "" );
|
||||
log.info( String.format( "Installing mod: %s", modFile.getName() ) );
|
||||
observer.patchingMod( modFile );
|
||||
|
||||
zis = new ZipInputStream( new FileInputStream( modFile ) );
|
||||
ZipEntry item;
|
||||
while ( (item = zis.getNextEntry()) != null ) {
|
||||
if ( item.isDirectory() ) continue;
|
||||
|
||||
Matcher m = pathPtn.matcher( item.getName() );
|
||||
if ( !m.matches() ) {
|
||||
log.warn( String.format( "Unexpected innerPath: %s", item.getName() ) );
|
||||
zis.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
String parentPath = m.group(1);
|
||||
String topFolder = m.group(2);
|
||||
String fileName = m.group(3);
|
||||
|
||||
AbstractPack ftlP = topFolderMap.get( topFolder );
|
||||
if ( ftlP == null ) {
|
||||
log.warn( String.format( "Unexpected innerPath: %s", item.getName() ) );
|
||||
zis.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( fileName.endsWith( ".xml.append" ) || fileName.endsWith( ".append.xml" ) ) {
|
||||
String innerPath = parentPath + fileName.replaceAll( "[.](?:xml[.]append|append[.]xml)$", ".xml" );
|
||||
innerPath = checkCase( innerPath, knownPaths, knownPathsLower );
|
||||
|
||||
if ( !ftlP.contains( innerPath ) ) {
|
||||
log.warn( String.format( "Non-existent innerPath wasn't appended: %s", innerPath ) );
|
||||
}
|
||||
else {
|
||||
InputStream dstStream = null;
|
||||
try {
|
||||
dstStream = ftlP.getInputStream(innerPath);
|
||||
InputStream mergedStream = ModUtilities.appendXMLFile( zis, dstStream, ftlP.getName()+":"+innerPath, modFile.getName()+":"+parentPath+fileName );
|
||||
dstStream.close();
|
||||
ftlP.remove( innerPath );
|
||||
ftlP.add( innerPath, mergedStream );
|
||||
}
|
||||
finally {
|
||||
try {if ( dstStream != null ) dstStream.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
|
||||
if ( !moddedItems.contains(innerPath) )
|
||||
moddedItems.add( innerPath );
|
||||
}
|
||||
}
|
||||
else {
|
||||
String innerPath = checkCase( item.getName(), knownPaths, knownPathsLower );
|
||||
|
||||
if ( !moddedItems.contains(innerPath) )
|
||||
moddedItems.add( innerPath );
|
||||
else
|
||||
log.warn( String.format( "Clobbering earlier mods: %s", innerPath ) );
|
||||
|
||||
if ( ftlP.contains( innerPath ) )
|
||||
ftlP.remove( innerPath );
|
||||
ftlP.add( innerPath, zis );
|
||||
}
|
||||
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try {if (zis != null) zis.close();}
|
||||
catch ( Exception e ) {}
|
||||
|
||||
System.gc();
|
||||
}
|
||||
|
||||
modsInstalled++;
|
||||
observer.patchingProgress( progMilestone + progModsMax/modFiles.size()*modsInstalled, progMax );
|
||||
}
|
||||
progMilestone += progModsMax;
|
||||
observer.patchingProgress( progMilestone, progMax );
|
||||
|
||||
// Prune 'removed' files from dats.
|
||||
for ( AbstractPack ftlP : new AbstractPack[]{dataP,resP} ) {
|
||||
if ( ftlP instanceof FTLPack ) {
|
||||
observer.patchingStatus( String.format( "Repacking \"%s\"...", ftlP.getName() ) );
|
||||
|
||||
long bytesChanged = ((FTLPack)ftlP).repack().bytesChanged;
|
||||
log.info( String.format( "Repacked \"%s\" (%d bytes affected)", ftlP.getName(), bytesChanged ) );
|
||||
|
||||
datsRepacked++;
|
||||
observer.patchingProgress( progMilestone + progRepackMax/allDats.length*datsRepacked, progMax );
|
||||
}
|
||||
}
|
||||
progMilestone += progRepackMax;
|
||||
observer.patchingProgress( progMilestone, progMax );
|
||||
|
||||
observer.patchingProgress( 100, progMax );
|
||||
return true;
|
||||
}
|
||||
finally {
|
||||
try {if (dataP != null) dataP.close();}
|
||||
catch( Exception e ) {}
|
||||
|
||||
try {if (resP != null) resP.close();}
|
||||
catch( Exception e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if an innerPath exists, ignoring letter case.
|
||||
*
|
||||
* If there is no collision, the innerPath is added to the known lists.
|
||||
* A warning will be logged if a path with differing case exists.
|
||||
*
|
||||
* @param knownPaths a list of innerPaths seen so far
|
||||
* @param knownPathsLower a copy of knownPaths, lower-cased
|
||||
* @return the existing path (if different), or innerPath
|
||||
*/
|
||||
private String checkCase( String innerPath, List<String> knownPaths, List<String> knownPathsLower ) {
|
||||
if ( knownPaths.contains( innerPath ) ) return innerPath;
|
||||
|
||||
String lowerPath = innerPath.toLowerCase();
|
||||
int lowerIndex = knownPathsLower.indexOf( lowerPath );
|
||||
if ( lowerIndex != -1 ) {
|
||||
String knownPath = knownPaths.get( lowerIndex );
|
||||
log.warn( String.format( "Modded file's case doesn't match existing path: \"%s\" vs \"%s\"", innerPath, knownPath ) );
|
||||
return knownPath;
|
||||
}
|
||||
|
||||
knownPaths.add( innerPath );
|
||||
knownPathsLower.add( lowerPath );
|
||||
return innerPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class BackedUpDat {
|
||||
public File datFile = null;
|
||||
public File bakFile = null;
|
||||
}
|
||||
}
|
660
src/main/java/net/vhati/modmanager/core/ModUtilities.java
Normal file
660
src/main/java/net/vhati/modmanager/core/ModUtilities.java
Normal file
|
@ -0,0 +1,660 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StringReader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import net.vhati.modmanager.core.Report;
|
||||
import net.vhati.modmanager.core.Report.ReportFormatter;
|
||||
import net.vhati.modmanager.core.Report.ReportMessage;
|
||||
|
||||
import ar.com.hjg.pngj.PngReader;
|
||||
|
||||
import org.jdom2.input.JDOMParseException;
|
||||
import org.jdom2.input.SAXBuilder;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class ModUtilities {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ModUtilities.class);
|
||||
|
||||
/**
|
||||
* Determines text encoding for an InputStream and decodes its bytes as a string.
|
||||
*
|
||||
* CR and CR-LF line endings will be normalized to LF.
|
||||
*
|
||||
* @param is a stream to read
|
||||
* @param description how error messages should refer to the stream, or null
|
||||
*/
|
||||
public static DecodeResult decodeText( InputStream is, String description ) throws IOException {
|
||||
String result = null;
|
||||
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
ByteArrayOutputStream tmpData = new ByteArrayOutputStream();
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
tmpData.write( buf, 0, len );
|
||||
}
|
||||
byte[] allBytes = tmpData.toByteArray();
|
||||
tmpData.reset();
|
||||
|
||||
Map<byte[],String> boms = new LinkedHashMap<byte[],String>();
|
||||
boms.put( new byte[] {(byte)0xEF,(byte)0xBB,(byte)0xBF}, "UTF-8" );
|
||||
boms.put( new byte[] {(byte)0xFF,(byte)0xFE}, "UTF-16LE" );
|
||||
boms.put( new byte[] {(byte)0xFE,(byte)0xFF}, "UTF-16BE" );
|
||||
|
||||
String encoding = null;
|
||||
byte[] bom = null;
|
||||
|
||||
for ( Map.Entry<byte[],String> entry : boms.entrySet() ) {
|
||||
byte[] tmpBom = entry.getKey();
|
||||
byte[] firstBytes = Arrays.copyOfRange( allBytes, 0, tmpBom.length );
|
||||
if ( Arrays.equals( tmpBom, firstBytes ) ) {
|
||||
encoding = entry.getValue();
|
||||
bom = tmpBom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( encoding != null ) {
|
||||
// This may throw CharacterCodingException.
|
||||
CharsetDecoder decoder = Charset.forName( encoding ).newDecoder();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap( allBytes, bom.length, allBytes.length-bom.length );
|
||||
result = decoder.decode( byteBuffer ).toString();
|
||||
}
|
||||
else {
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap( allBytes );
|
||||
|
||||
Map<String,Exception> errorMap = new LinkedHashMap<String,Exception>();
|
||||
for ( String guess : new String[] {"UTF-8", "windows-1252"} ) {
|
||||
try {
|
||||
CharsetDecoder decoder = Charset.forName( guess ).newDecoder();
|
||||
result = decoder.decode( byteBuffer ).toString();
|
||||
encoding = guess;
|
||||
break;
|
||||
}
|
||||
catch ( CharacterCodingException e ) {
|
||||
errorMap.put( guess, e );
|
||||
}
|
||||
}
|
||||
if ( encoding == null ) {
|
||||
// All guesses failed!?
|
||||
String msg = String.format( "Could not guess encoding for %s.", (description!=null ? "\""+description+"\"" : "a file") );
|
||||
for ( Map.Entry<String,Exception> entry : errorMap.entrySet() ) {
|
||||
msg += String.format( "\nFailed to decode as %s: %s", entry.getKey(), entry.getValue() );
|
||||
}
|
||||
throw new IOException( msg );
|
||||
}
|
||||
}
|
||||
|
||||
result = result.replaceAll( "\r(?!\n)|\r\n", "\n" );
|
||||
return new DecodeResult( result, encoding, bom );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Semi-intelligently appends XML from one file (src) onto another (dst).
|
||||
*
|
||||
* The two InputStreams are read, and the combined result
|
||||
* is returned as a new third InputStream.
|
||||
*
|
||||
* The returned stream is a ByteArrayInputStream
|
||||
* which doesn't need closing.
|
||||
*
|
||||
* The description arguments identify the streams for log messages.
|
||||
*/
|
||||
public static InputStream appendXMLFile( InputStream srcStream, InputStream dstStream, String srcDescription, String dstDescription ) throws IOException {
|
||||
Pattern xmlDeclPtn = Pattern.compile( "<[?]xml version=\"1.0\" encoding=\"[^\"]+?\"[?]>\n*" );
|
||||
|
||||
String srcText = decodeText( srcStream, srcDescription ).text;
|
||||
srcText = xmlDeclPtn.matcher(srcText).replaceFirst( "" );
|
||||
|
||||
String dstText = decodeText( dstStream, dstDescription ).text;
|
||||
dstText = xmlDeclPtn.matcher(dstText).replaceFirst( "" );
|
||||
|
||||
ByteArrayOutputStream tmpData = new ByteArrayOutputStream();
|
||||
BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, "UTF-8" ) );
|
||||
|
||||
bw.write( "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" );
|
||||
bw.write( dstText );
|
||||
bw.write( "\n\n<!-- Appended by GMM -->\n\n");
|
||||
bw.write( srcText );
|
||||
bw.write( "\n" );
|
||||
bw.flush();
|
||||
|
||||
InputStream result = new ByteArrayInputStream( tmpData.toByteArray() );
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a mod file for common problems.
|
||||
*
|
||||
* @param modFile an *.ftl file to check
|
||||
* @param formatter custom message decoration/indention, or null
|
||||
*/
|
||||
public static Report validateModFile( File modFile, ReportFormatter formatter ) {
|
||||
if ( formatter == null ) formatter = new ReportFormatter();
|
||||
|
||||
List<ReportMessage> messages = new ArrayList<ReportMessage>();
|
||||
List<ReportMessage> pendingMsgs = new ArrayList<ReportMessage>();
|
||||
boolean modValid = true;
|
||||
boolean seenAppend = false;
|
||||
|
||||
Pattern junkFilePtn = Pattern.compile( "[.]DS_Store$|^thumbs[.]db$" );
|
||||
|
||||
Pattern validRootDirPtn = Pattern.compile( "^(?:audio|data|fonts|img)/" );
|
||||
List<String> seenJunkDirs = new ArrayList<String>();
|
||||
|
||||
CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder();
|
||||
|
||||
ZipInputStream zis = null;
|
||||
try {
|
||||
zis = new ZipInputStream( new FileInputStream( modFile ) );
|
||||
ZipEntry item;
|
||||
while ( (item = zis.getNextEntry()) != null ) {
|
||||
String innerPath = item.getName();
|
||||
pendingMsgs.clear();
|
||||
|
||||
if ( !asciiEncoder.canEncode( innerPath ) ) {
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
String.format( "Non-ASCII characters in path." )
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
|
||||
if ( innerPath.indexOf("/") == -1 ) {
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.WARNING,
|
||||
String.format( "Extraneous top-level file." )
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
else if ( !validRootDirPtn.matcher(innerPath).find() ) {
|
||||
String junkDir = innerPath.replaceFirst( "/.*", "/" );
|
||||
if ( !seenJunkDirs.contains( junkDir ) ) {
|
||||
seenJunkDirs.add( junkDir );
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
String.format( "Unsupported top-level folder: %s", junkDir )
|
||||
) );
|
||||
}
|
||||
modValid = false;
|
||||
}
|
||||
else if ( item.isDirectory() ) {
|
||||
}
|
||||
else if ( junkFilePtn.matcher(innerPath).find() ) {
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
String.format( "Junk file" )
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
else if ( innerPath.endsWith( ".png" ) ) {
|
||||
try {
|
||||
PngReader pngr = new PngReader( zis );
|
||||
|
||||
// Check for Truecolor+Alpha (32bit RGBA).
|
||||
if ( pngr.imgInfo.channels != 4 || pngr.imgInfo.bitDepth != 8 ) {
|
||||
|
||||
String colorTypeString = "???";
|
||||
if ( pngr.imgInfo.channels == 4 )
|
||||
colorTypeString = "RGB+Alpha";
|
||||
else if ( pngr.imgInfo.channels == 3 )
|
||||
colorTypeString = "RGB";
|
||||
else if ( pngr.imgInfo.channels == 2 )
|
||||
colorTypeString = "Gray+Alpha";
|
||||
else if ( pngr.imgInfo.channels == 1 && !pngr.imgInfo.greyscale )
|
||||
colorTypeString = "Indexed Color";
|
||||
else if ( pngr.imgInfo.channels == 1 && pngr.imgInfo.greyscale )
|
||||
colorTypeString = "Gray";
|
||||
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.WARNING,
|
||||
String.format( "ColorType: %s (Usually 32bit Truecolor+Alpha)", colorTypeString )
|
||||
) );
|
||||
}
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( String.format( "Error while validating \"%s:%s\".", modFile.getName(), innerPath ), e );
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"An error occurred. See log for details."
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
}
|
||||
else if ( innerPath.matches( "^.*(?:.xml.append|.append.xml|.xml)$" ) ) {
|
||||
if ( innerPath.matches( "^.*(?:.xml.append|.append.xml)$" ) )
|
||||
seenAppend = true;
|
||||
|
||||
DecodeResult decodeResult = ModUtilities.decodeText( zis, modFile.getName()+":"+innerPath );
|
||||
|
||||
if ( decodeResult.bom != null ) {
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.WARNING,
|
||||
String.format( "%s BOM detected. (ascii is safest)", decodeResult.encoding )
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
|
||||
List<Pattern> oddCharPtns = new ArrayList<Pattern>();
|
||||
Map<Pattern,String> oddCharSuggestions = new HashMap<Pattern,String>();
|
||||
Map<Pattern,List<Character>> oddCharLists = new HashMap<Pattern,List<Character>>();
|
||||
|
||||
oddCharPtns.add( Pattern.compile( "\\u0060|\\u201A|\\u2018|\\u2019" ) );
|
||||
oddCharSuggestions.put( oddCharPtns.get(oddCharPtns.size()-1), "'" );
|
||||
|
||||
oddCharPtns.add( Pattern.compile( "\\u201E|\\u201C|\\u201D" ) );
|
||||
oddCharSuggestions.put( oddCharPtns.get(oddCharPtns.size()-1), "\"" );
|
||||
|
||||
oddCharPtns.add( Pattern.compile( "\\u2013|\\u2014" ) );
|
||||
oddCharSuggestions.put( oddCharPtns.get(oddCharPtns.size()-1), "-" );
|
||||
|
||||
oddCharPtns.add( Pattern.compile( "\\u2026" ) );
|
||||
oddCharSuggestions.put( oddCharPtns.get(oddCharPtns.size()-1), "..." );
|
||||
|
||||
for ( Pattern ptn : oddCharPtns ) {
|
||||
Matcher m = ptn.matcher( decodeResult.text );
|
||||
List<Character> chars = null;
|
||||
while ( m.find() ) {
|
||||
if ( chars == null )
|
||||
chars = new ArrayList<Character>();
|
||||
|
||||
Character cObj = new Character( m.group(0).charAt(0) );
|
||||
if ( !chars.contains( cObj ) )
|
||||
chars.add( cObj );
|
||||
}
|
||||
if ( chars != null )
|
||||
oddCharLists.put( ptn, chars );
|
||||
}
|
||||
for ( Pattern ptn : oddCharPtns ) {
|
||||
List<Character> chars = oddCharLists.get( ptn );
|
||||
if ( chars != null ) {
|
||||
String suggestion = oddCharSuggestions.get( ptn );
|
||||
StringBuilder charBuf = new StringBuilder( chars.size() );
|
||||
for ( Character cObj : chars )
|
||||
charBuf.append( cObj.charValue() );
|
||||
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.WARNING,
|
||||
String.format( "Odd characters resembling %s : %s", suggestion, charBuf.toString() )
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Nag if there are chars FTL can't show.
|
||||
|
||||
Report xmlReport = validateModXML( decodeResult.text, formatter );
|
||||
|
||||
if ( xmlReport.text.length() > 0 ) {
|
||||
pendingMsgs.add( new ReportMessage(
|
||||
ReportMessage.NESTED_BLOCK,
|
||||
xmlReport.text
|
||||
) );
|
||||
}
|
||||
|
||||
if ( xmlReport.outcome == false )
|
||||
modValid = false;
|
||||
}
|
||||
|
||||
if ( !pendingMsgs.isEmpty() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.SUBSECTION,
|
||||
innerPath
|
||||
) );
|
||||
messages.addAll( pendingMsgs );
|
||||
}
|
||||
|
||||
zis.closeEntry();
|
||||
}
|
||||
|
||||
if ( !seenAppend ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.WARNING_SUBSECTION,
|
||||
"This mod doesn't append. It clobbers."
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( String.format( "Error while validating mod: %s", modFile.getName() ), e );
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"An error occurred. See log for details."
|
||||
) );
|
||||
modValid = false;
|
||||
}
|
||||
finally {
|
||||
try {if ( zis != null ) zis.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
|
||||
if ( modValid ) {
|
||||
//messages.clear(); // Nothing bad enough to mention.
|
||||
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.INFO,
|
||||
String.format( "No Problems", modFile.getName() )
|
||||
) );
|
||||
}
|
||||
|
||||
// Insert the mod's filename at the top.
|
||||
messages.add( 0, new ReportMessage(
|
||||
ReportMessage.SECTION,
|
||||
String.format( "%s:", modFile.getName() )
|
||||
) );
|
||||
|
||||
StringBuilder resultBuf = new StringBuilder();
|
||||
formatter.format( messages, resultBuf );
|
||||
return new Report( resultBuf, modValid );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks a mod's xml for problems.
|
||||
*
|
||||
* It first tries to preemptively fix and report
|
||||
* common typos all at once.
|
||||
* Then a real XML parser runs, which stops at the
|
||||
* first typo it sees. :/
|
||||
*
|
||||
* @param text unparsed xml
|
||||
* @param formatter custom message decoration/indention, or null
|
||||
*/
|
||||
public static Report validateModXML( String text, ReportFormatter formatter ) {
|
||||
if ( formatter == null ) formatter = new ReportFormatter();
|
||||
|
||||
List<ReportMessage> messages = new ArrayList<ReportMessage>();
|
||||
boolean xmlValid = true;
|
||||
|
||||
StringBuffer srcBuf = new StringBuffer( text );
|
||||
StringBuffer dstBuf = new StringBuffer( text.length() );
|
||||
StringBuffer tmpBuf; // For swapping;
|
||||
String ptn;
|
||||
Matcher m;
|
||||
|
||||
// Wrap everything in a root tag, while mindful of the xml declaration.
|
||||
Pattern xmlDeclPtn = Pattern.compile( "<[?]xml version=\"1.0\" encoding=\"[^\"]+?\"[?]>" );
|
||||
m = xmlDeclPtn.matcher( srcBuf );
|
||||
if ( m.find() ) {
|
||||
if ( m.start() == 0 ) {
|
||||
dstBuf.append( srcBuf.subSequence( 0, m.end() ) );
|
||||
dstBuf.append( "\n<wrapper>\n" );
|
||||
dstBuf.append( srcBuf.subSequence( m.end(), srcBuf.length() ) );
|
||||
}
|
||||
else {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<?xml... ?> should only occur on the first line."
|
||||
) );
|
||||
dstBuf.append( "<wrapper>\n" );
|
||||
dstBuf.append( srcBuf.subSequence( 0, m.start() ) );
|
||||
dstBuf.append( srcBuf.subSequence( m.end(), srcBuf.length() ) );
|
||||
}
|
||||
dstBuf.append( "\n</wrapper>" );
|
||||
}
|
||||
else {
|
||||
dstBuf.insert( 0, "<wrapper>\n" );
|
||||
dstBuf.append( srcBuf );
|
||||
dstBuf.append( "\n</wrapper>" );
|
||||
}
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// Comments with long tails or double-dashes.
|
||||
m = Pattern.compile( "(?s)<!--(-*)(.*?)(-*)-->" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
if ( m.group(1).length() > 0 || m.group(3).length() > 0 || m.group(2).indexOf("--") != -1 ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<!-- No other dashes should touch. -->"
|
||||
) );
|
||||
}
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(2).replaceAll("[^\n]", "")) ); // Strip comments, but preserve line count.
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// Mismatched single-line tags.
|
||||
// Example: blueprints.xml: <title>...</type>
|
||||
m = Pattern.compile( "<([^/!][^> ]+?)((?: [^>]+?)?)(?<!/)>([^<]+?)</([^>]+?)>" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
if ( m.group(1).equals( m.group(4) ) == false ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<"+ m.group(1) +"...>...</"+ m.group(4) +">"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement("<"+ m.group(1) + m.group(2) +">"+ m.group(3) +"</"+ m.group(1) +">") );
|
||||
}
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// <pilot power="1"max="3" room="0"/>
|
||||
// Groan, \t separates attribs sometimes.
|
||||
m = Pattern.compile( "<([^> ]+?)( [^>]+?\")([^\"= \t>]+?=\"[^\"]+?\")((?:[^>]+?)?)>" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<"+ m.group(1) +"...\""+ m.group(3) +"...>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement("<"+ m.group(1) + m.group(2) +" "+ m.group(3) + m.group(4) +">") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// sector_data.xml closing tag.
|
||||
m = Pattern.compile( "((?s)<sectorDescription[^>]*>.*?)</sectorDescrption>" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<sectorDescription>...</sectorDescrption>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(1) +"</sectorDescription>") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// {anyship}.xml: <gib1>...</gib2>
|
||||
m = Pattern.compile( "(?s)<(gib[0-9]+)>(.*?)</(gib[0-9]+)>" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
if ( m.group(1).equals( m.group(3) ) == false ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<"+ m.group(1) +">...</"+ m.group(3) +">"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement("<"+ m.group(1) +">"+ m.group(2) +"</"+ m.group(1) +">") );
|
||||
}
|
||||
else {
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(0)) );
|
||||
}
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// event*.xml: <choice... hidden="true" hidden="true">
|
||||
m = Pattern.compile( "<([a-zA-Z0-9_-]+?)((?: [^>]+?)?) ([^>]+?)(=\"[^\">]+?\") \\3(?:=\"[^\">]+?\")([^>]*)>" ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<"+ m.group(1) +"... "+ m.group(3) +"=... "+ m.group(3) +"=...>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement("<"+ m.group(1) + m.group(2) +" "+ m.group(3) + m.group(4) +" "+ m.group(5) +">") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// <shields>...</slot>
|
||||
ptn = "";
|
||||
ptn += "(<shields *(?: [^>]*)?>\\s*";
|
||||
ptn += "<slot *(?: [^>]*)?>\\s*";
|
||||
ptn += "(?:<direction>[^<]*</direction>\\s*)?";
|
||||
ptn += "(?:<number>[^<]*</number>\\s*)?";
|
||||
ptn += "</slot>\\s*)";
|
||||
ptn += "</slot>"; // Wrong closing tag.
|
||||
m = Pattern.compile( ptn ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<shields>...</slot>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(1) +"</shields>") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// <shipBlueprint>...</ship>
|
||||
ptn = "";
|
||||
ptn += "(<shipBlueprint *(?: [^>]*)?>\\s*";
|
||||
ptn += "<class>[^<]*</class>\\s*";
|
||||
ptn += "<systemList *(?: [^>]*)?>\\s*";
|
||||
ptn += "(?:<[a-zA-Z]+ *(?: [^>]*)?/>\\s*)*";
|
||||
ptn += "</systemList>\\s*";
|
||||
ptn += "(?:<droneList *(?: [^>]*)?>\\s*";
|
||||
ptn += "(?:<[a-zA-Z]+ *(?: [^>]*)?/>\\s*)*";
|
||||
ptn += "</droneList>\\s*)?";
|
||||
ptn += "(?:<weaponList *(?: [^>]*)?>\\s*";
|
||||
ptn += "(?:<[a-zA-Z]+ *(?: [^>]*)?/>\\s*)*";
|
||||
ptn += "</weaponList>\\s*)?";
|
||||
ptn += "(?:<[a-zA-Z]+ *(?: [^>]*)?/>\\s*)*)";
|
||||
ptn += "</ship>"; // Wrong closing tag.
|
||||
m = Pattern.compile( ptn ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<shipBlueprint>...</ship>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(1) +"</shipBlueprint>") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
// <textList>...</text>
|
||||
ptn = "";
|
||||
ptn += "(?u)(<textList *(?: [^>]*)?>\\s*";
|
||||
ptn += "(?:<text *(?: [^>]*)?>[^<]*</text>\\s*)*)";
|
||||
ptn += "</text>"; // Wrong closing tag.
|
||||
m = Pattern.compile( ptn ).matcher( srcBuf );
|
||||
while ( m.find() ) {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.ERROR,
|
||||
"<textList>...</text>"
|
||||
) );
|
||||
m.appendReplacement( dstBuf, m.quoteReplacement(m.group(1) +"</textList>") );
|
||||
}
|
||||
m.appendTail( dstBuf );
|
||||
tmpBuf = srcBuf; srcBuf = dstBuf; dstBuf = tmpBuf; dstBuf.setLength(0);
|
||||
|
||||
try {
|
||||
SAXBuilder saxBuilder = new SAXBuilder();
|
||||
saxBuilder.build( new StringReader(srcBuf.toString()) );
|
||||
|
||||
xmlValid = true;
|
||||
for ( ReportMessage message : messages ) {
|
||||
if ( message.type == ReportMessage.ERROR ) {
|
||||
xmlValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( JDOMParseException e ) {
|
||||
int lineNum = e.getLineNumber();
|
||||
if ( lineNum != -1 ) {
|
||||
int badStart = -1;
|
||||
int badEnd = -1;
|
||||
String badLine = "???";
|
||||
m = Pattern.compile( "\n" ).matcher( srcBuf );
|
||||
for ( int i=1; i <= lineNum && m.find(); i++) {
|
||||
if ( i == lineNum-1 ) {
|
||||
badStart = m.end();
|
||||
} else if ( i == lineNum ) {
|
||||
badEnd = m.start();
|
||||
badLine = srcBuf.substring( badStart, badEnd );
|
||||
}
|
||||
}
|
||||
String msg = String.format( "Fix this and try again:\n%s", e );
|
||||
msg += "\n";
|
||||
msg += "~ ~ ~ ~ ~\n";
|
||||
msg += badLine +"\n";
|
||||
msg += "~ ~ ~ ~ ~";
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.EXCEPTION,
|
||||
msg
|
||||
) );
|
||||
}
|
||||
else {
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.EXCEPTION,
|
||||
"An error occurred. See log for details."
|
||||
) );
|
||||
}
|
||||
xmlValid = false;
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
log.error( "Error while validating mod xml.", e );
|
||||
messages.add( new ReportMessage(
|
||||
ReportMessage.EXCEPTION,
|
||||
"An error occurred. See log for details."
|
||||
) );
|
||||
xmlValid = false;
|
||||
}
|
||||
|
||||
StringBuilder resultBuf = new StringBuilder();
|
||||
|
||||
ReportMessage prevMessage = null;
|
||||
for ( ReportMessage message : messages ) {
|
||||
if ( message.equals(prevMessage) ) continue;
|
||||
|
||||
formatter.format( message, resultBuf );
|
||||
prevMessage = message;
|
||||
}
|
||||
|
||||
return new Report( resultBuf, xmlValid );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A holder for results from decodeText().
|
||||
*
|
||||
* text - The decoded string.
|
||||
* encoding - The encoding used.
|
||||
* bom - The BOM bytes found, or null.
|
||||
*/
|
||||
public static class DecodeResult {
|
||||
public final String text;
|
||||
public final String encoding;
|
||||
public final byte[] bom;
|
||||
|
||||
public DecodeResult( String text, String encoding, byte[] bom ) {
|
||||
this.text = text;
|
||||
this.encoding = encoding;
|
||||
this.bom = bom;
|
||||
}
|
||||
}
|
||||
}
|
198
src/main/java/net/vhati/modmanager/core/Report.java
Normal file
198
src/main/java/net/vhati/modmanager/core/Report.java
Normal file
|
@ -0,0 +1,198 @@
|
|||
package net.vhati.modmanager.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* A human-readable block of text, with a boolean outcome.
|
||||
*/
|
||||
public class Report {
|
||||
public final CharSequence text;
|
||||
public final boolean outcome;
|
||||
|
||||
public Report( CharSequence text, boolean outcome ) {
|
||||
this.text = text;
|
||||
this.outcome = outcome;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Formats ReportMessages to include in buffered reports.
|
||||
*
|
||||
* Symbols are prepended to indicate type.
|
||||
*
|
||||
* Methods can accept a formatter as an argument,
|
||||
* internally accumulate messages, format them,
|
||||
* and return an Appendable CharSequence.
|
||||
*
|
||||
* To nest reports, that buffer can be intented
|
||||
* and appended to another buffer; or it can be
|
||||
* wrapped in a NESTED_BLOCK message of its own.
|
||||
*
|
||||
* The Appendable interface claims to throw
|
||||
* IOException, but StringBuffer and StringBuilder
|
||||
* never do. So extra methods specifically accept
|
||||
* those classes and swallow the exception.
|
||||
*
|
||||
* If exceptions are desired, cast args to the
|
||||
* more general type.
|
||||
*/
|
||||
public static class ReportFormatter {
|
||||
protected Pattern breakPtn = Pattern.compile( "(^|\n)(?=[^\n])" );
|
||||
|
||||
public String getIndent() { return " "; }
|
||||
|
||||
public String getPrefix( int messageType ) {
|
||||
switch ( messageType ) {
|
||||
case ReportMessage.WARNING: return "~ ";
|
||||
case ReportMessage.ERROR: return "! ";
|
||||
case ReportMessage.EXCEPTION: return "! ";
|
||||
case ReportMessage.SECTION : return "@ ";
|
||||
case ReportMessage.SUBSECTION: return "> ";
|
||||
case ReportMessage.WARNING_SUBSECTION: return "~ ";
|
||||
case ReportMessage.ERROR_SUBSECTION: return "! ";
|
||||
case ReportMessage.NESTED_BLOCK: return "";
|
||||
default: return getIndent();
|
||||
}
|
||||
}
|
||||
|
||||
public void format( List<ReportMessage> messages, Appendable buf ) throws IOException {
|
||||
for ( ReportMessage message : messages )
|
||||
format( message, buf );
|
||||
}
|
||||
|
||||
public void format( ReportMessage message, Appendable buf ) throws IOException {
|
||||
if ( message.type == ReportMessage.NESTED_BLOCK ) {
|
||||
// Already formatted this once, indent it instead.
|
||||
indent( message.text, buf );
|
||||
return;
|
||||
}
|
||||
|
||||
// Subsections get an extra linebreak above them.
|
||||
switch ( message.type ) {
|
||||
case ReportMessage.SUBSECTION:
|
||||
case ReportMessage.WARNING_SUBSECTION:
|
||||
case ReportMessage.ERROR_SUBSECTION:
|
||||
buf.append( "\n" );
|
||||
default:
|
||||
// Not a subsection.
|
||||
}
|
||||
|
||||
buf.append( getPrefix( message.type ) );
|
||||
buf.append( message.text );
|
||||
buf.append( "\n" );
|
||||
|
||||
// Sections get underlined.
|
||||
if ( message.type == ReportMessage.SECTION ) {
|
||||
buf.append( getIndent() );
|
||||
for ( int i=0; i < message.text.length(); i++ )
|
||||
buf.append( "-" );
|
||||
buf.append( "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
public void indent( CharSequence src, Appendable dst ) throws IOException {
|
||||
Matcher m = breakPtn.matcher( src );
|
||||
int lastEnd = 0;
|
||||
while ( m.find() ) {
|
||||
if ( m.start() - lastEnd > 0 )
|
||||
dst.append( src.subSequence( lastEnd, m.start() ) );
|
||||
|
||||
if ( m.group(1).length() > 0 ) // Didn't match beginning (^).
|
||||
dst.append( "\n" );
|
||||
dst.append( getIndent() );
|
||||
lastEnd = m.end();
|
||||
}
|
||||
int srcLen = src.length();
|
||||
if ( lastEnd < srcLen )
|
||||
dst.append( src.subSequence( lastEnd, srcLen ) );
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void format( List<ReportMessage> messages, StringBuffer buf ) {
|
||||
try { format( messages, (Appendable)buf ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void format( List<ReportMessage> messages, StringBuilder buf ) {
|
||||
try { format( messages, (Appendable)buf ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void format( ReportMessage message, StringBuffer buf ) {
|
||||
try { format( message, (Appendable)buf ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void format( ReportMessage message, StringBuilder buf ) {
|
||||
try { format( message, (Appendable)buf ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void indent( CharSequence src, StringBuffer dst ) {
|
||||
try { indent( src, (Appendable)dst ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
|
||||
/** Exception-swallowing wrapper. */
|
||||
public void indent( CharSequence src, StringBuilder dst ) {
|
||||
try { indent( src, (Appendable)dst ); }
|
||||
catch( IOException e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Notice text, with a formatting hint.
|
||||
*
|
||||
* Messages can be compared for equality
|
||||
* to ignore repeats.
|
||||
*/
|
||||
public static class ReportMessage {
|
||||
public static final int INFO = 0;
|
||||
public static final int WARNING = 1;
|
||||
public static final int ERROR = 2;
|
||||
public static final int EXCEPTION = 3;
|
||||
public static final int SECTION = 4;
|
||||
public static final int SUBSECTION = 5;
|
||||
public static final int WARNING_SUBSECTION = 6;
|
||||
public static final int ERROR_SUBSECTION = 7;
|
||||
public static final int NESTED_BLOCK = 8;
|
||||
|
||||
public final int type;
|
||||
public final CharSequence text;
|
||||
|
||||
public ReportMessage( int type, CharSequence text ) {
|
||||
this.type = type;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
if ( o == null ) return false;
|
||||
if ( o == this ) return true;
|
||||
if ( o instanceof ReportMessage == false ) return false;
|
||||
ReportMessage other = (ReportMessage)o;
|
||||
return ( this.type == other.type && this.text.equals(other.text) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 236;
|
||||
int salt = 778;
|
||||
|
||||
result = salt * result + this.type;
|
||||
result = salt * result + text.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package net.vhati.modmanager.json;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class GrognakCatalogFetcher {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(GrognakCatalogFetcher.class);
|
||||
|
||||
public static final String CATALOG_URL = "https://raw.github.com/Grognak/Grognaks-Mod-Manager/master/backup/current_catalog.json";
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the latest mod catalog.
|
||||
*
|
||||
* @return true if the catalog successfully downloaded, false otherwise
|
||||
*/
|
||||
public static boolean fetchCatalog( String catalogURL, File catalogFile, File eTagFile ) {
|
||||
String localETag = null;
|
||||
|
||||
log.debug( "Attempting to download a newer catalog..." );
|
||||
if ( eTagFile.exists() ) {
|
||||
// Load the old eTag.
|
||||
InputStream etagIn = null;
|
||||
try {
|
||||
etagIn = new FileInputStream( eTagFile );
|
||||
BufferedReader br = new BufferedReader( new InputStreamReader( etagIn, "UTF-8" ) );
|
||||
String line = br.readLine();
|
||||
if ( line.length() > 0 )
|
||||
localETag = line;
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
// Not serious enough to be a real error.
|
||||
log.debug( String.format( "Error reading catalog eTag from \"%s\".", eTagFile.getName() ), e );
|
||||
}
|
||||
finally {
|
||||
try {if ( etagIn != null ) etagIn.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
String remoteETag = null;
|
||||
InputStream urlIn = null;
|
||||
OutputStream catalogOut = null;
|
||||
try {
|
||||
URL url = new URL( catalogURL );
|
||||
URLConnection conn = url.openConnection();
|
||||
|
||||
if ( conn instanceof HttpURLConnection == false ) {
|
||||
log.error( String.format( "Non-Http(s) URL given for catalog fetching: %s", catalogURL ) );
|
||||
return false;
|
||||
}
|
||||
HttpURLConnection httpConn = (HttpURLConnection)conn;
|
||||
|
||||
httpConn.setReadTimeout( 10000 );
|
||||
if ( localETag != null )
|
||||
httpConn.setRequestProperty( "If-None-Match", localETag );
|
||||
httpConn.connect();
|
||||
|
||||
int responseCode = httpConn.getResponseCode();
|
||||
|
||||
if ( responseCode == HttpURLConnection.HTTP_NOT_MODIFIED ) {
|
||||
log.debug( "The server's catalog has not been modified since the previous check." );
|
||||
|
||||
// Update the catalog file's timestamp as if it had downloaded.
|
||||
catalogFile.setLastModified( new Date().getTime() );
|
||||
|
||||
return false;
|
||||
}
|
||||
else if ( responseCode == HttpURLConnection.HTTP_OK ) {
|
||||
Map<String, List<String>> headerMap = httpConn.getHeaderFields();
|
||||
List<String> eTagValues = headerMap.get( "ETag" );
|
||||
if ( eTagValues != null && eTagValues.size() > 0 )
|
||||
remoteETag = eTagValues.get( 0 );
|
||||
|
||||
urlIn = httpConn.getInputStream();
|
||||
catalogOut = new FileOutputStream( catalogFile );
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = urlIn.read(buf)) >= 0 ) {
|
||||
catalogOut.write( buf, 0, len );
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.error( String.format( "Catalog download request failed: HTTP Code %d (%s).", responseCode, httpConn.getResponseMessage() ) );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch ( MalformedURLException e ) {
|
||||
log.error( "Error fetching latest catalog.", e );
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
log.error( "Error fetching latest catalog.", e );
|
||||
}
|
||||
finally {
|
||||
try {if ( urlIn != null ) urlIn.close();}
|
||||
catch ( IOException e ) {}
|
||||
|
||||
try {if ( catalogOut != null ) catalogOut.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
|
||||
if ( remoteETag != null ) {
|
||||
// Save the new eTag.
|
||||
OutputStream etagOut = null;
|
||||
try {
|
||||
etagOut = new FileOutputStream( eTagFile );
|
||||
BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( etagOut, "UTF-8" ) );
|
||||
bw.append( remoteETag );
|
||||
bw.flush();
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
log.error( String.format( "Error writing catalog eTag to \"%s\".", eTagFile.getName() ), e );
|
||||
}
|
||||
finally {
|
||||
try {if ( etagOut != null ) etagOut.close();}
|
||||
catch ( IOException e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package net.vhati.modmanager.json;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import net.vhati.modmanager.core.ModDB;
|
||||
import net.vhati.modmanager.core.ModInfo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class JacksonGrognakCatalogReader {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(JacksonGrognakCatalogReader.class);
|
||||
|
||||
|
||||
public static ModDB parse( File jsonFile ) {
|
||||
ModDB modDB = new ModDB();
|
||||
|
||||
Exception exception = null;
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
|
||||
|
||||
JsonNode rootNode = mapper.readTree(jsonFile);
|
||||
JsonNode catalogsNode = rootNode.get("catalog_versions");
|
||||
JsonNode catalogNode = catalogsNode.get("1");
|
||||
|
||||
for ( JsonNode infoNode : catalogNode ) {
|
||||
String threadURL = infoNode.get("url").textValue();
|
||||
String threadHash = infoNode.get("thread_hash").textValue();
|
||||
if ( !threadURL.equals("???") && !threadHash.equals("???") )
|
||||
modDB.putThreadHash( threadURL, threadHash );
|
||||
|
||||
JsonNode versionsNode = infoNode.get("versions");
|
||||
for ( JsonNode versionNode : versionsNode ) {
|
||||
ModInfo modInfo = new ModInfo();
|
||||
modInfo.setTitle( infoNode.get("title").textValue() );
|
||||
modInfo.setAuthor( infoNode.get("author").textValue() );
|
||||
modInfo.setURL( infoNode.get("url").textValue() );
|
||||
modInfo.setDescription( infoNode.get("desc").textValue() );
|
||||
modInfo.setFileHash( versionNode.get("hash").textValue() );
|
||||
modInfo.setVersion( versionNode.get("version").textValue() );
|
||||
modDB.addMod( modInfo );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( JsonProcessingException e ) {
|
||||
exception = e;
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
exception = e;
|
||||
}
|
||||
if ( exception != null ) {
|
||||
log.error( exception );
|
||||
return null;
|
||||
}
|
||||
|
||||
return modDB;
|
||||
}
|
||||
}
|
111
src/main/java/net/vhati/modmanager/ui/ChecklistTableModel.java
Normal file
111
src/main/java/net/vhati/modmanager/ui/ChecklistTableModel.java
Normal file
|
@ -0,0 +1,111 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
import net.vhati.modmanager.core.ModInfo;
|
||||
|
||||
|
||||
public class ChecklistTableModel<T> extends AbstractTableModel implements Reorderable {
|
||||
|
||||
private static final int COLUMN_CHECK = 0;
|
||||
private static final int COLUMN_PAYLOAD = 1;
|
||||
|
||||
private static final int DATA_CHECK = 0;
|
||||
private static final int DATA_PAYLOAD = 1;
|
||||
|
||||
private String[] columnNames = new String[] {"?", "Name"};
|
||||
private Class[] columnTypes = new Class[] {Boolean.class, String.class};
|
||||
|
||||
private List<List<Object>> rowsList = new ArrayList<List<Object>>();
|
||||
|
||||
|
||||
public void addItem( T o ) {
|
||||
insertItem( rowsList.size(), false, o );
|
||||
}
|
||||
|
||||
public void insertItem( int row, boolean selected, T o ) {
|
||||
int newRowIndex = rowsList.size();
|
||||
|
||||
List<Object> rowData = new ArrayList<Object>();
|
||||
rowData.add( new Boolean(selected) );
|
||||
rowData.add( o );
|
||||
rowsList.add( row, rowData );
|
||||
|
||||
fireTableRowsInserted( row, row );
|
||||
}
|
||||
|
||||
public void removeItem( int row ) {
|
||||
rowsList.remove( row );
|
||||
fireTableRowsDeleted( row, row );
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T getItem( int row ) {
|
||||
return (T)rowsList.get(row).get(DATA_PAYLOAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reorder( int fromRow, int toRow ) {
|
||||
if ( toRow > fromRow ) toRow--;
|
||||
List<Object> rowData = rowsList.get( fromRow );
|
||||
rowsList.remove( fromRow );
|
||||
fireTableRowsDeleted( fromRow, fromRow );
|
||||
rowsList.add( toRow, rowData );
|
||||
fireTableRowsInserted( toRow, toRow );
|
||||
}
|
||||
|
||||
public void setSelected( int row, boolean b ) {
|
||||
rowsList.get(row).set( DATA_CHECK, new Boolean(b) );
|
||||
fireTableRowsUpdated( row, row );
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean isSelected( int row ) {
|
||||
return ((Boolean)rowsList.get(row).get(DATA_CHECK)).booleanValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return columnNames.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return rowsList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt( int row, int column ) {
|
||||
if ( column == COLUMN_CHECK ) {
|
||||
return rowsList.get(row).get(DATA_CHECK);
|
||||
}
|
||||
else if ( column == COLUMN_PAYLOAD ) {
|
||||
Object o = rowsList.get(row).get(DATA_PAYLOAD);
|
||||
return o.toString();
|
||||
}
|
||||
throw new ArrayIndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setValueAt( Object o, int row, int column ) {
|
||||
if ( column == COLUMN_CHECK ) {
|
||||
Boolean bool = (Boolean)o;
|
||||
rowsList.get(row).set( DATA_CHECK, bool );
|
||||
fireTableRowsUpdated( row, row );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable( int row, int column ) {
|
||||
if ( column == COLUMN_CHECK ) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class getColumnClass( int column ) {
|
||||
return columnTypes[column];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
||||
|
||||
/**
|
||||
* A Cut/Copy/Paste/SelectAll context menu for JTextComponents.
|
||||
*/
|
||||
public class ClipboardMenuMouseListener extends MouseAdapter {
|
||||
|
||||
private JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
private Action cutAction;
|
||||
private Action copyAction;
|
||||
private Action pasteAction;
|
||||
private Action selectAllAction;
|
||||
|
||||
private JTextComponent textComponent = null;
|
||||
|
||||
|
||||
public ClipboardMenuMouseListener() {
|
||||
cutAction = new AbstractAction( "Cut" ) {
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent ae ) {
|
||||
textComponent.cut();
|
||||
}
|
||||
};
|
||||
copyAction = new AbstractAction( "Copy" ) {
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent ae ) {
|
||||
textComponent.copy();
|
||||
}
|
||||
};
|
||||
pasteAction = new AbstractAction( "Paste" ) {
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent ae ) {
|
||||
textComponent.paste();
|
||||
}
|
||||
};
|
||||
selectAllAction = new AbstractAction( "Select All" ) {
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent ae ) {
|
||||
textComponent.selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
popup.add( cutAction );
|
||||
popup.add( copyAction );
|
||||
popup.add( pasteAction );
|
||||
popup.addSeparator();
|
||||
popup.add( selectAllAction );
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void mousePressed( MouseEvent e ) {
|
||||
if ( e.isPopupTrigger() ) showMenu( e );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased( MouseEvent e ) {
|
||||
if ( e.isPopupTrigger() ) showMenu( e );
|
||||
}
|
||||
|
||||
public void showMenu( MouseEvent e ) {
|
||||
if ( e.getSource() instanceof JTextComponent == false ) return;
|
||||
|
||||
textComponent = (JTextComponent)e.getSource();
|
||||
textComponent.requestFocus();
|
||||
|
||||
boolean enabled = textComponent.isEnabled();
|
||||
boolean editable = textComponent.isEditable();
|
||||
boolean nonempty = !(textComponent.getText() == null || textComponent.getText().equals(""));
|
||||
boolean marked = textComponent.getSelectedText() != null;
|
||||
|
||||
boolean pasteAvailable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null).isDataFlavorSupported(DataFlavor.stringFlavor);
|
||||
|
||||
cutAction.setEnabled( enabled && editable && marked );
|
||||
copyAction.setEnabled( enabled && marked );
|
||||
pasteAction.setEnabled( enabled && editable && pasteAvailable );
|
||||
selectAllAction.setEnabled( enabled && nonempty );
|
||||
|
||||
int nx = e.getX();
|
||||
if ( nx > 500 ) nx = nx - popup.getSize().width;
|
||||
|
||||
popup.show( e.getComponent(), nx, e.getY() - popup.getSize().height );
|
||||
}
|
||||
}
|
532
src/main/java/net/vhati/modmanager/ui/ManagerFrame.java
Normal file
532
src/main/java/net/vhati/modmanager/ui/ManagerFrame.java
Normal file
|
@ -0,0 +1,532 @@
|
|||
// http://docs.oracle.com/javase/tutorial/uiswing/dnd/emptytable.html
|
||||
// http://stackoverflow.com/questions/638807/how-do-i-drag-and-drop-a-row-in-a-jtable
|
||||
// http://www.java2s.com/Tutorial/Java/0240__Swing/UsingdefaultBooleanvaluecelleditorandrenderer.htm
|
||||
|
||||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.DropMode;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
|
||||
import net.vhati.modmanager.core.ComparableVersion;
|
||||
import net.vhati.modmanager.core.FTLUtilities;
|
||||
import net.vhati.modmanager.core.HashObserver;
|
||||
import net.vhati.modmanager.core.HashThread;
|
||||
import net.vhati.modmanager.core.ModDB;
|
||||
import net.vhati.modmanager.core.ModFileInfo;
|
||||
import net.vhati.modmanager.core.ModInfo;
|
||||
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.json.GrognakCatalogFetcher;
|
||||
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
||||
import net.vhati.modmanager.ui.ChecklistTableModel;
|
||||
import net.vhati.modmanager.ui.ClipboardMenuMouseListener;
|
||||
import net.vhati.modmanager.ui.ModInfoArea;
|
||||
import net.vhati.modmanager.ui.ModPatchDialog;
|
||||
import net.vhati.modmanager.ui.Statusbar;
|
||||
import net.vhati.modmanager.ui.StatusbarMouseListener;
|
||||
import net.vhati.modmanager.ui.TableRowTransferHandler;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class ManagerFrame extends JFrame implements ActionListener, HashObserver, Statusbar {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ManagerFrame.class);
|
||||
|
||||
private File backupDir = new File( "./backup/" );
|
||||
private File modsDir = new File( "./mods/" );
|
||||
|
||||
private int catalogFetchInterval = 7; // Days.
|
||||
private File catalogFile = new File( backupDir, "current_catalog.json" );
|
||||
private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" );
|
||||
|
||||
private Properties config;
|
||||
private String appName;
|
||||
private ComparableVersion appVersion;
|
||||
|
||||
private HashMap<File,String> modFileHashes = new HashMap<File,String>();
|
||||
private ModDB modDB = new ModDB();
|
||||
|
||||
private ChecklistTableModel<ModFileInfo> localModsTableModel;
|
||||
private JTable localModsTable;
|
||||
|
||||
private JButton patchBtn;
|
||||
private JButton toggleAllBtn;
|
||||
private JButton validateBtn;
|
||||
private JButton aboutBtn;
|
||||
|
||||
private ModInfoArea infoArea;
|
||||
|
||||
private JLabel statusLbl;
|
||||
|
||||
|
||||
public ManagerFrame( Properties config, String appName, ComparableVersion appVersion ) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.appName = appName;
|
||||
this.appVersion = appVersion;
|
||||
|
||||
this.setTitle( String.format( "%s v%s", appName, appVersion ) );
|
||||
|
||||
JPanel contentPane = new JPanel( new BorderLayout() );
|
||||
|
||||
JPanel mainPane = new JPanel( new BorderLayout() );
|
||||
contentPane.add( mainPane, BorderLayout.CENTER );
|
||||
|
||||
JPanel topPanel = new JPanel( new BorderLayout() );
|
||||
|
||||
localModsTableModel = new ChecklistTableModel<ModFileInfo>();
|
||||
|
||||
localModsTable = new JTable( localModsTableModel );
|
||||
localModsTable.setFillsViewportHeight( true );
|
||||
localModsTable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
|
||||
localModsTable.setTableHeader( null );
|
||||
localModsTable.getColumnModel().getColumn(0).setMinWidth(30);
|
||||
localModsTable.getColumnModel().getColumn(0).setMaxWidth(30);
|
||||
localModsTable.getColumnModel().getColumn(0).setPreferredWidth(30);
|
||||
|
||||
JScrollPane localModsScroll = new JScrollPane( null, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
|
||||
localModsScroll.setViewportView( localModsTable );
|
||||
//localModsScroll.setColumnHeaderView( null ); // Counterpart to setTableHeader().
|
||||
localModsScroll.setPreferredSize( new Dimension(Integer.MIN_VALUE, Integer.MIN_VALUE) );
|
||||
topPanel.add( localModsScroll, BorderLayout.CENTER );
|
||||
|
||||
JPanel modActionsPanel = new JPanel();
|
||||
modActionsPanel.setLayout( new BoxLayout(modActionsPanel, BoxLayout.Y_AXIS) );
|
||||
modActionsPanel.setBorder( BorderFactory.createEmptyBorder(0,5,5,0) );
|
||||
Insets actionInsets = new Insets(5,10,5,10);
|
||||
|
||||
patchBtn = new JButton("Patch");
|
||||
patchBtn.setMargin( actionInsets );
|
||||
patchBtn.addMouseListener( new StatusbarMouseListener( this, "Incorporate all selected mods into the game." ) );
|
||||
patchBtn.addActionListener(this);
|
||||
modActionsPanel.add( patchBtn );
|
||||
|
||||
toggleAllBtn = new JButton("Toggle All");
|
||||
toggleAllBtn.setMargin( actionInsets );
|
||||
toggleAllBtn.addMouseListener( new StatusbarMouseListener( this, "Select all mods, or none." ) );
|
||||
toggleAllBtn.addActionListener(this);
|
||||
modActionsPanel.add( toggleAllBtn );
|
||||
|
||||
validateBtn = new JButton("Validate");
|
||||
validateBtn.setMargin( actionInsets );
|
||||
validateBtn.addMouseListener( new StatusbarMouseListener( this, "Check selected mods for problems." ) );
|
||||
validateBtn.addActionListener(this);
|
||||
modActionsPanel.add( validateBtn );
|
||||
|
||||
aboutBtn = new JButton("About");
|
||||
aboutBtn.setMargin( actionInsets );
|
||||
aboutBtn.addMouseListener( new StatusbarMouseListener( this, "Show info about this program." ) );
|
||||
aboutBtn.addActionListener(this);
|
||||
modActionsPanel.add( aboutBtn );
|
||||
|
||||
topPanel.add( modActionsPanel, BorderLayout.EAST );
|
||||
|
||||
JButton[] actionBtns = new JButton[] {patchBtn, toggleAllBtn, validateBtn, aboutBtn};
|
||||
int actionBtnWidth = Integer.MIN_VALUE;
|
||||
int actionBtnHeight = Integer.MIN_VALUE;
|
||||
for ( JButton btn : actionBtns ) {
|
||||
actionBtnWidth = Math.max( actionBtnWidth, btn.getPreferredSize().width );
|
||||
actionBtnHeight = Math.max( actionBtnHeight, btn.getPreferredSize().height );
|
||||
}
|
||||
for ( JButton btn : actionBtns ) {
|
||||
Dimension size = new Dimension( actionBtnWidth, actionBtnHeight );
|
||||
btn.setPreferredSize( size );
|
||||
btn.setMinimumSize( size );
|
||||
btn.setMaximumSize( size );
|
||||
}
|
||||
|
||||
mainPane.add( topPanel, BorderLayout.NORTH );
|
||||
|
||||
infoArea = new ModInfoArea();
|
||||
infoArea.setPreferredSize( new Dimension(504, 220) );
|
||||
mainPane.add( infoArea, BorderLayout.CENTER );
|
||||
|
||||
JPanel statusPanel = new JPanel();
|
||||
statusPanel.setLayout( new BoxLayout(statusPanel, BoxLayout.Y_AXIS) );
|
||||
statusPanel.setBorder( BorderFactory.createLoweredBevelBorder() );
|
||||
statusLbl = new JLabel(" ");
|
||||
statusLbl.setBorder( BorderFactory.createEmptyBorder(2, 4, 2, 4) );
|
||||
statusLbl.setAlignmentX( Component.LEFT_ALIGNMENT );
|
||||
statusPanel.add( statusLbl );
|
||||
contentPane.add( statusPanel, BorderLayout.SOUTH );
|
||||
|
||||
|
||||
localModsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
|
||||
@Override
|
||||
public void valueChanged( ListSelectionEvent e ) {
|
||||
if ( e.getValueIsAdjusting() ) return;
|
||||
|
||||
int row = localModsTable.getSelectedRow();
|
||||
if ( row == -1 ) return;
|
||||
|
||||
ModFileInfo modFileInfo = localModsTableModel.getItem( row );
|
||||
showLocalModInfo( modFileInfo );
|
||||
}
|
||||
});
|
||||
|
||||
localModsTable.setTransferHandler( new TableRowTransferHandler( localModsTable ) );
|
||||
localModsTable.setDropMode( DropMode.INSERT ); // Drop between rows, not on them.
|
||||
localModsTable.setDragEnabled( true );
|
||||
|
||||
this.setContentPane( contentPane );
|
||||
this.pack();
|
||||
this.setLocationRelativeTo(null);
|
||||
|
||||
showAboutInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra initialization that must be called after the constructor.
|
||||
* This must be called on the Swing event thread (use invokeLater()).
|
||||
*/
|
||||
public void init() {
|
||||
File[] modFiles = modsDir.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept( File f ) {
|
||||
if ( f.isFile() ) {
|
||||
if ( f.getName().endsWith(".ftl") ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
List<ModFileInfo> unsortedMods = new ArrayList<ModFileInfo>();
|
||||
for ( File f : modFiles ) {
|
||||
ModFileInfo modFileInfo = new ModFileInfo( f );
|
||||
unsortedMods.add( modFileInfo );
|
||||
}
|
||||
|
||||
List<ModFileInfo> sortedMods = loadModOrder( unsortedMods );
|
||||
for ( ModFileInfo modFileInfo : sortedMods ) {
|
||||
localModsTableModel.addItem( modFileInfo );
|
||||
}
|
||||
|
||||
HashThread hashThread = new HashThread( modFiles, this );
|
||||
hashThread.setDaemon( true );
|
||||
hashThread.start();
|
||||
|
||||
boolean needNewCatalog = false;
|
||||
|
||||
if ( catalogFile.exists() ) {
|
||||
// Load the catalog first, before updating.
|
||||
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
|
||||
if ( currentDB != null ) modDB = currentDB;
|
||||
|
||||
// Check if the downloaded catalog is stale.
|
||||
Date catalogDate = new Date( catalogFile.lastModified() );
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add( Calendar.DATE, catalogFetchInterval * -1 );
|
||||
if ( catalogDate.before( cal.getTime() ) ) {
|
||||
log.debug( String.format( "Catalog is older than %d days.", catalogFetchInterval ) );
|
||||
needNewCatalog = true;
|
||||
} else {
|
||||
log.debug( "Catalog isn't stale yet." );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Catalog file doesn't exist.
|
||||
needNewCatalog = true;
|
||||
}
|
||||
|
||||
// Don't update if the user doesn't want to.
|
||||
String updatesAllowed = config.getProperty( "update_catalog", "false" );
|
||||
if ( !updatesAllowed.equals("true") ) needNewCatalog = false;
|
||||
|
||||
if ( needNewCatalog ) {
|
||||
Runnable fetchTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String catalogURL = GrognakCatalogFetcher.CATALOG_URL;
|
||||
boolean fetched = GrognakCatalogFetcher.fetchCatalog( catalogURL, catalogFile, catalogETagFile );
|
||||
|
||||
if ( fetched ) reloadCatalog();
|
||||
}
|
||||
};
|
||||
Thread fetchThread = new Thread( fetchTask );
|
||||
fetchThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reparses and replace the downloaded ModDB catalog. (thread-safe)
|
||||
*/
|
||||
public void reloadCatalog() {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if ( catalogFile.exists() ) {
|
||||
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
|
||||
if ( currentDB != null ) modDB = currentDB;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads modorder.txt and returns a mod list in that order.
|
||||
*
|
||||
* Mods not mentioned in the text appear at the end, alphabetically.
|
||||
* If an error occurs, an alphabetized list is returned.
|
||||
*/
|
||||
private List<ModFileInfo> loadModOrder( List<ModFileInfo> unsortedMods ) {
|
||||
List<ModFileInfo> sortedMods = new ArrayList<ModFileInfo>();
|
||||
List<ModFileInfo> availableMods = new ArrayList<ModFileInfo>( unsortedMods );
|
||||
Collections.sort( availableMods );
|
||||
|
||||
FileInputStream is = null;
|
||||
try {
|
||||
is = new FileInputStream( new File( modsDir, "modorder.txt" ) );
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader( is, Charset.forName("UTF-8") ));
|
||||
String line;
|
||||
while ( (line = br.readLine()) != null ) {
|
||||
Iterator<ModFileInfo> it = availableMods.iterator();
|
||||
while ( it.hasNext() ) {
|
||||
ModFileInfo modFileInfo = it.next();
|
||||
if ( modFileInfo.getName().equals(line) ) {
|
||||
it.remove();
|
||||
sortedMods.add( modFileInfo );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( FileNotFoundException e ) {
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
log.error( "Error reading modorder.txt.", e );
|
||||
}
|
||||
finally {
|
||||
try {if (is != null) is.close();}
|
||||
catch (Exception e) {}
|
||||
}
|
||||
sortedMods.addAll( availableMods );
|
||||
|
||||
return sortedMods;
|
||||
}
|
||||
|
||||
private void saveModOrder( List<ModFileInfo> sortedMods ) {
|
||||
FileOutputStream os = null;
|
||||
try {
|
||||
os = new FileOutputStream( new File( modsDir, "modorder.txt" ) );
|
||||
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( os, Charset.forName("UTF-8") ));
|
||||
|
||||
for ( ModFileInfo modFileInfo : sortedMods ) {
|
||||
bw.write( modFileInfo.getName() );
|
||||
bw.write( "\r\n" );
|
||||
}
|
||||
bw.flush();
|
||||
}
|
||||
catch ( IOException e ) {
|
||||
log.error( "Error writing modorder.txt.", e );
|
||||
}
|
||||
finally {
|
||||
try {if (os != null) os.close();}
|
||||
catch (Exception e) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void showAboutInfo() {
|
||||
String body = "";
|
||||
body += "- Drag to reorder mods.\n";
|
||||
body += "- Click the checkboxes to select.\n";
|
||||
body += "- Click 'Patch' to apply mods ( select none for vanilla ).\n";
|
||||
body += "\n";
|
||||
body += "Thanks for using this mod manager.\n";
|
||||
body += "Make sure to visit the forum for updates!";
|
||||
|
||||
infoArea.setDescription( appName, "Vhati", appVersion.toString(), "http://abc.net", body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows info about a local mod in the text area.
|
||||
*/
|
||||
public void showLocalModInfo( ModFileInfo modFileInfo ) {
|
||||
String modHash = modFileHashes.get( modFileInfo.getFile() );
|
||||
|
||||
ModInfo modInfo = modDB.getModInfo( modHash );
|
||||
if ( modInfo != null ) {
|
||||
infoArea.setDescription( modInfo.getTitle(), modInfo.getAuthor(), modInfo.getVersion(), modInfo.getURL(), modInfo.getDescription() );
|
||||
}
|
||||
else {
|
||||
String body = "";
|
||||
body += "No info is available for the selected mod.\n\n";
|
||||
body += "If it's stable, please let the Slipstream devs know ";
|
||||
body += "where you found it and include this md5 hash:\n";
|
||||
body += modHash +"\n";
|
||||
infoArea.setDescription( modFileInfo.getName(), body );
|
||||
log.info( String.format("No info for selected mod: %s (%s).", modFileInfo.getName(), modHash) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setStatusText( String text ) {
|
||||
if (text.length() > 0)
|
||||
statusLbl.setText(text);
|
||||
else
|
||||
statusLbl.setText(" ");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e ) {
|
||||
Object source = e.getSource();
|
||||
|
||||
if ( source == patchBtn ) {
|
||||
List<ModFileInfo> sortedMods = new ArrayList<ModFileInfo>();
|
||||
List<File> modFiles = new ArrayList<File>();
|
||||
|
||||
for ( int i=0; i < localModsTableModel.getRowCount(); i++ ) {
|
||||
if ( localModsTableModel.isSelected(i) ) {
|
||||
sortedMods.add( localModsTableModel.getItem(i) );
|
||||
modFiles.add( localModsTableModel.getItem(i).getFile() );
|
||||
}
|
||||
}
|
||||
saveModOrder( sortedMods );
|
||||
|
||||
File datsDir = new File( config.getProperty( "ftl_dats_path" ) );
|
||||
|
||||
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" );
|
||||
|
||||
ModPatchDialog patchDlg = new ModPatchDialog( this );
|
||||
patchDlg.setSuccessTask( new SpawnGameTask() );
|
||||
|
||||
log.info( "" );
|
||||
log.info( "Patching..." );
|
||||
log.info( "" );
|
||||
ModPatchThread patchThread = new ModPatchThread( modFiles, dataDat, resDat, patchDlg );
|
||||
patchThread.start();
|
||||
|
||||
patchDlg.setVisible( true );
|
||||
}
|
||||
else if ( source == toggleAllBtn ) {
|
||||
int selectedCount = 0;
|
||||
for ( int i = localModsTableModel.getRowCount()-1; i >= 0; i-- ) {
|
||||
if ( localModsTableModel.isSelected(i) ) selectedCount++;
|
||||
}
|
||||
boolean b = ( selectedCount != localModsTableModel.getRowCount() );
|
||||
|
||||
for ( int i = localModsTableModel.getRowCount()-1; i >= 0; i-- ) {
|
||||
localModsTableModel.setSelected( i, b );
|
||||
}
|
||||
}
|
||||
else if ( source == validateBtn ) {
|
||||
StringBuilder resultBuf = new StringBuilder();
|
||||
boolean anyInvalid = false;
|
||||
|
||||
for ( int i = localModsTableModel.getRowCount()-1; i >= 0; i-- ) {
|
||||
if ( !localModsTableModel.isSelected(i) ) continue;
|
||||
|
||||
ModFileInfo modFileInfo = localModsTableModel.getItem( i );
|
||||
Report validateReport = ModUtilities.validateModFile( modFileInfo.getFile(), null );
|
||||
resultBuf.append( validateReport.text );
|
||||
resultBuf.append( "\n" );
|
||||
|
||||
if ( validateReport.outcome == false ) anyInvalid = true;
|
||||
}
|
||||
|
||||
if ( resultBuf.length() == 0 ) {
|
||||
resultBuf.append( "No mods were checked." );
|
||||
}
|
||||
else if ( anyInvalid ) {
|
||||
resultBuf.append( "FTL itself can tolerate lots of errors and still run. " );
|
||||
resultBuf.append( "But invalid XML may break tools that do proper parsing, " );
|
||||
resultBuf.append( "and it hinders the development of new tools.\n" );
|
||||
}
|
||||
infoArea.setDescription( "Results", resultBuf.toString() );
|
||||
}
|
||||
else if ( source == aboutBtn ) {
|
||||
showAboutInfo();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void hashCalculated( final File f, final String hash ) {
|
||||
SwingUtilities.invokeLater( new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
modFileHashes.put( f, hash );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class SpawnGameTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
String neverRunFtl = config.getProperty( "never_run_ftl", "false" );
|
||||
if ( !neverRunFtl.equals("true") ) {
|
||||
File datsDir = new File( config.getProperty( "ftl_dats_path" ) );
|
||||
|
||||
File exeFile = FTLUtilities.findGameExe( datsDir );
|
||||
if ( exeFile != null ) {
|
||||
int response = JOptionPane.showConfirmDialog( ManagerFrame.this, "Do you want to run the game now?", "Ready to Play", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE );
|
||||
if ( response == JOptionPane.YES_OPTION ) {
|
||||
log.info( "Launching FTL..." );
|
||||
try {
|
||||
FTLUtilities.launchGame( exeFile );
|
||||
} catch ( Exception e ) {
|
||||
log.error( "Error launching FTL.", e );
|
||||
}
|
||||
System.exit( 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
152
src/main/java/net/vhati/modmanager/ui/ModInfoArea.java
Normal file
152
src/main/java/net/vhati/modmanager/ui/ModInfoArea.java
Normal file
|
@ -0,0 +1,152 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextPane;
|
||||
import javax.swing.event.MouseInputAdapter;
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.SimpleAttributeSet;
|
||||
import javax.swing.text.StyleConstants;
|
||||
import javax.swing.text.StyledDocument;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
public class ModInfoArea extends JScrollPane {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ModInfoArea.class);
|
||||
|
||||
private static final String HYPERLINK_TARGET = "hyperlink-target";
|
||||
|
||||
private JTextPane textPane;
|
||||
private StyledDocument doc;
|
||||
private HashMap<String,SimpleAttributeSet> attrMap = new HashMap<String,SimpleAttributeSet>();
|
||||
|
||||
|
||||
public ModInfoArea() {
|
||||
super();
|
||||
|
||||
textPane = new JTextPane();
|
||||
textPane.setEditable( false );
|
||||
|
||||
doc = textPane.getStyledDocument();
|
||||
|
||||
SimpleAttributeSet tmpAttr = new SimpleAttributeSet();
|
||||
StyleConstants.setFontFamily( tmpAttr, "Monospaced" );
|
||||
StyleConstants.setFontSize( tmpAttr, 12 );
|
||||
attrMap.put( "regular", tmpAttr );
|
||||
|
||||
tmpAttr = new SimpleAttributeSet();
|
||||
StyleConstants.setFontFamily( tmpAttr, "SansSerif" );
|
||||
StyleConstants.setFontSize( tmpAttr, 24 );
|
||||
StyleConstants.setBold( tmpAttr, true );
|
||||
attrMap.put( "title", tmpAttr );
|
||||
|
||||
tmpAttr = new SimpleAttributeSet();
|
||||
StyleConstants.setFontFamily( tmpAttr, "Monospaced" );
|
||||
StyleConstants.setFontSize( tmpAttr, 12 );
|
||||
StyleConstants.setForeground( tmpAttr, Color.BLUE );
|
||||
StyleConstants.setUnderline( tmpAttr, true );
|
||||
attrMap.put( "hyperlink", tmpAttr );
|
||||
|
||||
MouseInputAdapter hyperlinkListener = new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked( MouseEvent e ) {
|
||||
AttributeSet tmpAttr = doc.getCharacterElement( textPane.viewToModel(e.getPoint()) ).getAttributes();
|
||||
Object targetObj = tmpAttr.getAttribute( HYPERLINK_TARGET );
|
||||
if ( targetObj != null ) {
|
||||
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
|
||||
if ( desktop != null && desktop.isSupported(Desktop.Action.BROWSE) ) {
|
||||
try {
|
||||
desktop.browse( new URI(targetObj.toString()) );
|
||||
}
|
||||
catch ( Exception f ) {
|
||||
log.error( "Error browsing clicked url: "+ targetObj.toString(), f );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved( MouseEvent e ) {
|
||||
AttributeSet tmpAttr = doc.getCharacterElement( textPane.viewToModel(e.getPoint()) ).getAttributes();
|
||||
if ( tmpAttr.getAttribute( HYPERLINK_TARGET ) != null ) {
|
||||
textPane.setCursor( new Cursor(Cursor.HAND_CURSOR) );
|
||||
} else {
|
||||
textPane.setCursor( new Cursor(Cursor.DEFAULT_CURSOR) );
|
||||
}
|
||||
}
|
||||
};
|
||||
textPane.addMouseListener( hyperlinkListener );
|
||||
textPane.addMouseMotionListener( hyperlinkListener );
|
||||
|
||||
textPane.addMouseListener( new ClipboardMenuMouseListener() );
|
||||
|
||||
this.setViewportView( textPane );
|
||||
}
|
||||
|
||||
|
||||
public void setDescription( String title, String body ) {
|
||||
setDescription( title, null, null, null, body );
|
||||
}
|
||||
|
||||
public void setDescription( String title, String author, String version, String url, String body ) {
|
||||
try {
|
||||
doc.remove( 0, doc.getLength() );
|
||||
doc.insertString( doc.getLength(), title +"\n", attrMap.get("title") );
|
||||
|
||||
boolean first = true;
|
||||
if ( author != null ) {
|
||||
doc.insertString( doc.getLength(), String.format("%sby %s", (first ? "" : " "), author), attrMap.get("regular") );
|
||||
first = false;
|
||||
}
|
||||
if ( version != null ) {
|
||||
doc.insertString( doc.getLength(), String.format("%s(version %s)", (first ? "" : " "), version), attrMap.get("regular") );
|
||||
first = false;
|
||||
}
|
||||
if ( !first ) {
|
||||
doc.insertString( doc.getLength(), "\n", attrMap.get("regular") );
|
||||
}
|
||||
|
||||
if ( url != null ) {
|
||||
SimpleAttributeSet tmpAttr;
|
||||
doc.insertString( doc.getLength(), "Website: ", attrMap.get("regular") );
|
||||
|
||||
boolean browseWorks = false;
|
||||
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
|
||||
if ( desktop != null && desktop.isSupported(Desktop.Action.BROWSE) ) {
|
||||
browseWorks = true;
|
||||
}
|
||||
|
||||
if ( browseWorks && url.matches("^(?:https?|ftp)://.*") ) {
|
||||
tmpAttr = new SimpleAttributeSet( attrMap.get("hyperlink") );
|
||||
tmpAttr.addAttribute( HYPERLINK_TARGET, url );
|
||||
doc.insertString( doc.getLength(), "Link", tmpAttr );
|
||||
} else {
|
||||
tmpAttr = new SimpleAttributeSet( attrMap.get("regular") );
|
||||
doc.insertString( doc.getLength(), url, tmpAttr );
|
||||
}
|
||||
|
||||
doc.insertString( doc.getLength(), "\n", attrMap.get("regular") );
|
||||
}
|
||||
|
||||
doc.insertString( doc.getLength(), "\n", attrMap.get("regular") );
|
||||
|
||||
if ( body != null ) {
|
||||
doc.insertString( doc.getLength(), body, attrMap.get("regular") );
|
||||
}
|
||||
}
|
||||
catch ( BadLocationException e) {
|
||||
log.error( e );
|
||||
}
|
||||
|
||||
textPane.setCaretPosition(0);
|
||||
}
|
||||
}
|
182
src/main/java/net/vhati/modmanager/ui/ModPatchDialog.java
Normal file
182
src/main/java/net/vhati/modmanager/ui/ModPatchDialog.java
Normal file
|
@ -0,0 +1,182 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.File;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import net.vhati.modmanager.core.ModPatchObserver;
|
||||
|
||||
|
||||
public class ModPatchDialog extends JDialog implements ActionListener, ModPatchObserver {
|
||||
|
||||
private JProgressBar progressBar;
|
||||
private JTextArea statusArea;
|
||||
private JButton continueBtn;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean patchingSucceeded = false;
|
||||
private Runnable successTask = null;
|
||||
|
||||
|
||||
public ModPatchDialog( Frame owner ) {
|
||||
super( owner, "Patching...", true );
|
||||
this.setDefaultCloseOperation( JDialog.DO_NOTHING_ON_CLOSE );
|
||||
|
||||
progressBar = new JProgressBar();
|
||||
progressBar.setBorderPainted( true );
|
||||
|
||||
JPanel progressHolder = new JPanel( new BorderLayout() );
|
||||
progressHolder.setBorder( BorderFactory.createEmptyBorder( 10, 15, 0, 15 ) );
|
||||
progressHolder.add( progressBar );
|
||||
getContentPane().add( progressHolder, BorderLayout.NORTH );
|
||||
|
||||
statusArea = new JTextArea();
|
||||
statusArea.setBorder( BorderFactory.createEtchedBorder() );
|
||||
statusArea.setLineWrap( true );
|
||||
statusArea.setWrapStyleWord( true );
|
||||
statusArea.setEditable( false );
|
||||
|
||||
JPanel statusHolder = new JPanel( new BorderLayout() );
|
||||
statusHolder.setBorder( BorderFactory.createEmptyBorder( 15, 15, 15, 15 ) );
|
||||
statusHolder.add( statusArea );
|
||||
getContentPane().add( statusHolder, BorderLayout.CENTER );
|
||||
|
||||
continueBtn = new JButton( "Continue" );
|
||||
continueBtn.setEnabled( false );
|
||||
continueBtn.addActionListener( this );
|
||||
|
||||
JPanel continueHolder = new JPanel();
|
||||
continueHolder.setLayout( new BoxLayout( continueHolder, BoxLayout.X_AXIS ) );
|
||||
continueHolder.setBorder( BorderFactory.createEmptyBorder( 0, 0, 10, 0 ) );
|
||||
continueHolder.add( Box.createHorizontalGlue() );
|
||||
continueHolder.add( continueBtn );
|
||||
continueHolder.add( Box.createHorizontalGlue() );
|
||||
getContentPane().add( continueHolder, BorderLayout.SOUTH );
|
||||
|
||||
this.setSize( 400, 160 );
|
||||
this.setLocationRelativeTo( owner );
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e ) {
|
||||
Object source = e.getSource();
|
||||
|
||||
if ( source == continueBtn ) {
|
||||
this.setVisible( false );
|
||||
this.dispose();
|
||||
|
||||
if ( done && patchingSucceeded && successTask != null ) {
|
||||
successTask.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void setStatusText( String message ) {
|
||||
statusArea.setText( message != null ? message : "..." );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the progress bar.
|
||||
*
|
||||
* If either arg is -1, the bar will become indeterminate.
|
||||
*
|
||||
* @param value the new value
|
||||
* @param max the new maximum
|
||||
*/
|
||||
@Override
|
||||
public void patchingProgress( final int value, final int max ) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if ( value >= 0 && max >= 0 ) {
|
||||
if ( progressBar.isIndeterminate() )
|
||||
progressBar.setIndeterminate( false );
|
||||
|
||||
if ( progressBar.getMaximum() != max ) {
|
||||
progressBar.setValue( 0 );
|
||||
progressBar.setMaximum( max );
|
||||
}
|
||||
progressBar.setValue( value );
|
||||
}
|
||||
else {
|
||||
if ( !progressBar.isIndeterminate() )
|
||||
progressBar.setIndeterminate( true );
|
||||
progressBar.setValue( 0 );
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-specific activity.
|
||||
*
|
||||
* @param message a string, or null
|
||||
*/
|
||||
@Override
|
||||
public void patchingStatus( final String message ) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusText( message != null ? message : "..." );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A mod is about to be processed.
|
||||
*/
|
||||
@Override
|
||||
public void patchingMod( final File modFile ) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusText( String.format( "Installing mod \"%s\"...", modFile.getName() ) );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Patching ended.
|
||||
*
|
||||
* If anything went wrong, e may be non-null.
|
||||
*/
|
||||
@Override
|
||||
public void patchingEnded( final boolean success, final Exception e ) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if ( success )
|
||||
setStatusText( "Patching completed." );
|
||||
else
|
||||
setStatusText( String.format( "Patching failed: %s", e ) );
|
||||
|
||||
done = true;
|
||||
patchingSucceeded = success;
|
||||
|
||||
continueBtn.setEnabled( true );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets a runnable to trigger after patching successfully.
|
||||
*/
|
||||
public void setSuccessTask( Runnable r ) {
|
||||
successTask = r;
|
||||
}
|
||||
}
|
9
src/main/java/net/vhati/modmanager/ui/Reorderable.java
Normal file
9
src/main/java/net/vhati/modmanager/ui/Reorderable.java
Normal file
|
@ -0,0 +1,9 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
|
||||
public interface Reorderable {
|
||||
/**
|
||||
* Moves an element at fromIndex to toIndex.
|
||||
*/
|
||||
public void reorder( int fromIndex, int toIndex );
|
||||
}
|
6
src/main/java/net/vhati/modmanager/ui/Statusbar.java
Normal file
6
src/main/java/net/vhati/modmanager/ui/Statusbar.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
|
||||
public interface Statusbar {
|
||||
public void setStatusText( String text );
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
|
||||
import net.vhati.modmanager.ui.Statusbar;
|
||||
|
||||
|
||||
/**
|
||||
* A MouseListener to show rollover help text in a status bar.
|
||||
*
|
||||
* Construct this with the help text, and a class
|
||||
* implementing the Statusbar interface.
|
||||
*
|
||||
* Then add this mouseListener to a component.
|
||||
*/
|
||||
public class StatusbarMouseListener extends MouseAdapter {
|
||||
private Statusbar bar = null;
|
||||
private String text = null;
|
||||
|
||||
public StatusbarMouseListener( Statusbar bar, String text ) {
|
||||
this.bar = bar;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered( MouseEvent e ) {
|
||||
bar.setStatusText( text );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited( MouseEvent e ) {
|
||||
bar.setStatusText( "" );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package net.vhati.modmanager.ui;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.dnd.DragSource;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.TransferHandler;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Allows drag and drop reordering of JTable rows.
|
||||
*
|
||||
* Its TableModel must implement the Reorderable interface.
|
||||
*/
|
||||
public class TableRowTransferHandler extends TransferHandler {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(TableRowTransferHandler.class);
|
||||
|
||||
private DataFlavor localIntegerFlavor = null;
|
||||
|
||||
private JTable table = null;
|
||||
|
||||
|
||||
public TableRowTransferHandler( JTable table ) {
|
||||
if ( table.getModel() instanceof Reorderable == false ) {
|
||||
throw new IllegalArgumentException( "The tableModel doesn't implement Reorderable." );
|
||||
}
|
||||
this.table = table;
|
||||
|
||||
try {
|
||||
localIntegerFlavor = new DataFlavor( DataFlavor.javaJVMLocalObjectMimeType + ";class=\""+ Integer.class.getName() +"\"" );
|
||||
}
|
||||
catch ( ClassNotFoundException e ) {
|
||||
log.error( e );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Transferable createTransferable( JComponent c ) {
|
||||
assert ( c == table );
|
||||
int row = table.getSelectedRow();
|
||||
return new IntegerTransferrable( new Integer(row) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canImport( TransferHandler.TransferSupport ts ) {
|
||||
boolean b = ( ts.getComponent() == table && ts.isDrop() && ts.isDataFlavorSupported(localIntegerFlavor) );
|
||||
table.setCursor( b ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop );
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSourceActions( JComponent comp ) {
|
||||
return TransferHandler.MOVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("Unchecked")
|
||||
public boolean importData( TransferHandler.TransferSupport ts ) {
|
||||
if ( !canImport(ts) ) return false;
|
||||
|
||||
JTable target = (JTable)ts.getComponent();
|
||||
JTable.DropLocation dl = (JTable.DropLocation)ts.getDropLocation();
|
||||
int dropRow = dl.getRow();
|
||||
int rowCount = table.getModel().getRowCount();
|
||||
if ( dropRow < 0 || dropRow > rowCount ) dropRow = rowCount;
|
||||
|
||||
target.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
|
||||
try {
|
||||
Integer draggedRow = (Integer)ts.getTransferable().getTransferData(localIntegerFlavor);
|
||||
if ( draggedRow != -1 && draggedRow != dropRow ) {
|
||||
((Reorderable)table.getModel()).reorder( draggedRow, dropRow );
|
||||
if ( dropRow > draggedRow ) dropRow--;
|
||||
target.getSelectionModel().addSelectionInterval( dropRow, dropRow );
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error( e );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exportDone( JComponent source, Transferable data, int action ) {
|
||||
if ( action == TransferHandler.MOVE || action == TransferHandler.NONE ) {
|
||||
table.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Drag and drop Integer data, constructed with a raw object
|
||||
* from a drag source, to be transformed into a flavor
|
||||
* suitable for the drop target.
|
||||
*/
|
||||
private class IntegerTransferrable implements Transferable{
|
||||
private Integer data;
|
||||
|
||||
public IntegerTransferrable( Integer data ) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTransferData( DataFlavor flavor ) {
|
||||
if ( flavor.equals( localIntegerFlavor ) ) {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataFlavor[] getTransferDataFlavors() {
|
||||
return new DataFlavor[] {localIntegerFlavor};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataFlavorSupported( DataFlavor flavor ) {
|
||||
return flavor.equals( localIntegerFlavor );
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue