Added support for embedded descriptions
This commit is contained in:
parent
2cdba9062e
commit
5cd19480ad
22 changed files with 889 additions and 333 deletions
File diff suppressed because one or more lines are too long
BIN
skel_common/mods/Beginning Scrap Advantage 1.1.ftl
Normal file
BIN
skel_common/mods/Beginning Scrap Advantage 1.1.ftl
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
skel_common/mods/Engi Scrap Advantage 1.1.ftl
Normal file
BIN
skel_common/mods/Engi Scrap Advantage 1.1.ftl
Normal file
Binary file not shown.
|
@ -1,13 +1,3 @@
|
||||||
Drop any .ftl files here and restart Slipstream in order to be able to patch them in.
|
Drop any .ftl files here and restart Slipstream in order to be able to patch them in.
|
||||||
|
|
||||||
The last-used order is remembered with modorder.txt, if it exists here.
|
The last-used order is remembered with modorder.txt, if it exists here.
|
||||||
|
|
||||||
|
|
||||||
"Beginning Scrap Advantage" is a simple example mod. All it does is give you some extra scrap when you first start a game. Rename .ftl to .zip and extract it in order to study it further.
|
|
||||||
|
|
||||||
|
|
||||||
"Engi Scrap Advantage" is another example mod demonstrating advanced XML tags to chain-load existing events. All it does is give you some extra scrap when you enter an Engi sector.
|
|
||||||
|
|
||||||
See also: "readme_modders.txt".
|
|
||||||
|
|
||||||
Those special tags require Slipstream Mod Manager 1.2+.
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ Changelog
|
||||||
- Fixed sloppy parser Validate error about things not allowed at root
|
- Fixed sloppy parser Validate error about things not allowed at root
|
||||||
- Added a Preferences dialog as an alternative to editing modman.cfg
|
- Added a Preferences dialog as an alternative to editing modman.cfg
|
||||||
- Added a troubleshooting note about Java 1.7.0_25 to readmes
|
- Added a troubleshooting note about Java 1.7.0_25 to readmes
|
||||||
|
- Added support for embedded descriptions in *.ftl files
|
||||||
|
|
||||||
1.2:
|
1.2:
|
||||||
- Added a commandline interface
|
- Added a commandline interface
|
||||||
|
|
|
@ -3,23 +3,29 @@ Mod Developer Notes
|
||||||
Creating an .ftl File
|
Creating an .ftl File
|
||||||
|
|
||||||
An .ftl file is simply a renamed .zip with a specific file structure.
|
An .ftl file is simply a renamed .zip with a specific file structure.
|
||||||
For an example, try renaming and unpacking the example .ftl file that
|
For an example, try renaming and unpacking the example mods.
|
||||||
comes with the program.
|
|
||||||
|
|
||||||
The root of the ZIP file should contain one or more of these folders:
|
The root of the ZIP file should contain one or more of these folders:
|
||||||
data/
|
data/
|
||||||
audio/
|
audio/
|
||||||
fonts/
|
fonts/
|
||||||
img/
|
img/
|
||||||
|
mod-appendix/
|
||||||
|
|
||||||
You should ONLY put in the files that you want to modify. This keeps
|
You should ONLY put in the files that you want to modify. This keeps
|
||||||
mod sizes low and prevents major conflict between mods.
|
mod sizes low and prevents major conflict between mods.
|
||||||
|
|
||||||
|
The "mod-appendix/" folder is for extra files that will not be inserted
|
||||||
|
into the game's resources. Slipstream will look for the following inside.
|
||||||
|
|
||||||
|
metadata.xml
|
||||||
|
Optional embedded description. (See the example mods.)
|
||||||
|
|
||||||
|
|
||||||
The Append Extension
|
The Append Extension
|
||||||
|
|
||||||
Any file in your .ftl with the extension .xml.append will be appended to
|
Any file in your .ftl with the extension .xml.append will be appended to
|
||||||
its respective vanilla file. (See the example mod.)
|
its respective vanilla file. (See the example mods.)
|
||||||
|
|
||||||
It is highly recommended that you take advantage of this as much as
|
It is highly recommended that you take advantage of this as much as
|
||||||
possible. As a rule of thumb, if you're editing an event xml file,
|
possible. As a rule of thumb, if you're editing an event xml file,
|
||||||
|
|
|
@ -3,8 +3,10 @@ package net.vhati.modmanager.core;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import net.vhati.modmanager.core.ModInfo;
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
|
import net.vhati.modmanager.core.ModsInfo;
|
||||||
|
|
||||||
|
|
||||||
public class ModDB {
|
public class ModDB {
|
||||||
|
@ -13,12 +15,25 @@ public class ModDB {
|
||||||
public static final String FUZZY = "fuzzy";
|
public static final String FUZZY = "fuzzy";
|
||||||
|
|
||||||
|
|
||||||
// Accociates Forum thread urls with hashes of their forst post's content.
|
|
||||||
private HashMap<String,String> threadHashMap = new HashMap<String,String>();
|
private HashMap<String,String> threadHashMap = new HashMap<String,String>();
|
||||||
|
|
||||||
private List<ModInfo> catalog = new ArrayList<ModInfo>();
|
private List<ModInfo> catalog = new ArrayList<ModInfo>();
|
||||||
|
|
||||||
|
|
||||||
|
public ModDB() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a shallow copy of an existing ModDB.
|
||||||
|
*
|
||||||
|
* Different catalog list, same ModInfos.
|
||||||
|
*/
|
||||||
|
public ModDB( ModDB srcDB ) {
|
||||||
|
threadHashMap.putAll( srcDB.getThreadHashMap() );
|
||||||
|
catalog.addAll( srcDB.getCatalog() );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns mod info for a given file hash.
|
* Returns mod info for a given file hash.
|
||||||
*/
|
*/
|
||||||
|
@ -58,8 +73,16 @@ public class ModDB {
|
||||||
catalog.clear();
|
catalog.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the internal ArrayList of mod info.
|
* Returns the internal Map of forum thread urls and hashes of their first posts' content.
|
||||||
|
*/
|
||||||
|
public Map<String,String> getThreadHashMap() {
|
||||||
|
return threadHashMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal List of mod info.
|
||||||
*/
|
*/
|
||||||
public List<ModInfo> getCatalog() {
|
public List<ModInfo> getCatalog() {
|
||||||
return catalog;
|
return catalog;
|
||||||
|
@ -97,4 +120,41 @@ public class ModDB {
|
||||||
|
|
||||||
return resultsMap;
|
return resultsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects ModInfo objects that differ only in version, and creates ModsInfo objects.
|
||||||
|
*/
|
||||||
|
public List<ModsInfo> getCollatedModInfo() {
|
||||||
|
List<ModsInfo> results = new ArrayList<ModsInfo>();
|
||||||
|
List<ModInfo> seenList = new ArrayList<ModInfo>();
|
||||||
|
|
||||||
|
for ( ModInfo modInfo : catalog ) {
|
||||||
|
if ( seenList.contains( modInfo ) ) continue;
|
||||||
|
seenList.add( modInfo );
|
||||||
|
|
||||||
|
ModsInfo modsInfo = new ModsInfo();
|
||||||
|
modsInfo.setTitle( modInfo.getTitle() );
|
||||||
|
modsInfo.setAuthor( modInfo.getAuthor() );
|
||||||
|
modsInfo.setThreadURL( modInfo.getURL() );
|
||||||
|
modsInfo.setDescription( modInfo.getDescription() );
|
||||||
|
|
||||||
|
String threadHash = getThreadHash( modInfo.getURL() );
|
||||||
|
modsInfo.setThreadHash( ( threadHash != null ? threadHash : "???" ) );
|
||||||
|
|
||||||
|
modsInfo.putVersion( modInfo.getFileHash(), modInfo.getVersion() );
|
||||||
|
|
||||||
|
Map<String,List<ModInfo>> similarMods = getSimilarMods( modInfo );
|
||||||
|
for ( ModInfo altInfo : similarMods.get( ModDB.EXACT ) ) {
|
||||||
|
if ( seenList.contains( altInfo ) ) continue;
|
||||||
|
seenList.add( altInfo );
|
||||||
|
|
||||||
|
modsInfo.putVersion( altInfo.getFileHash(), altInfo.getVersion() );
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add( modsInfo );
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ public class ModInfo {
|
||||||
private String title = "???";
|
private String title = "???";
|
||||||
private String author = "???";
|
private String author = "???";
|
||||||
private String url = "???";
|
private String url = "???";
|
||||||
private String description = "";
|
private String description = "???";
|
||||||
private String fileHash = "???";
|
private String fileHash = "???";
|
||||||
private String version = "???";
|
private String version = "???";
|
||||||
|
|
||||||
|
@ -25,6 +25,19 @@ public class ModInfo {
|
||||||
public String getVersion() { return this.version; }
|
public String getVersion() { return this.version; }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all fields, aside from fileHash, are "???".
|
||||||
|
*/
|
||||||
|
public boolean isBlank() {
|
||||||
|
if ( !getTitle().equals( "???" ) ) return false;
|
||||||
|
if ( !getAuthor().equals( "???" ) ) return false;
|
||||||
|
if ( !getURL().equals( "???" ) ) return false;
|
||||||
|
if ( !getDescription().equals( "???" ) ) return false;
|
||||||
|
if ( !getVersion().equals( "???" ) ) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getTitle();
|
return getTitle();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.vhati.modmanager.core;
|
package net.vhati.modmanager.core;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -163,6 +164,7 @@ public class ModPatchThread extends Thread {
|
||||||
topFolderMap.put( "audio", resP );
|
topFolderMap.put( "audio", resP );
|
||||||
topFolderMap.put( "fonts", resP );
|
topFolderMap.put( "fonts", resP );
|
||||||
topFolderMap.put( "img", resP );
|
topFolderMap.put( "img", resP );
|
||||||
|
topFolderMap.put( "mod-appendix", null );
|
||||||
|
|
||||||
// Track modified innerPaths in case they're clobbered.
|
// Track modified innerPaths in case they're clobbered.
|
||||||
List<String> moddedItems = new ArrayList<String>();
|
List<String> moddedItems = new ArrayList<String>();
|
||||||
|
@ -182,16 +184,21 @@ public class ModPatchThread extends Thread {
|
||||||
for ( File modFile : modFiles ) {
|
for ( File modFile : modFiles ) {
|
||||||
if ( !keepRunning ) return false;
|
if ( !keepRunning ) return false;
|
||||||
|
|
||||||
|
FileInputStream fis = null;
|
||||||
ZipInputStream zis = null;
|
ZipInputStream zis = null;
|
||||||
try {
|
try {
|
||||||
log.info( "" );
|
log.info( "" );
|
||||||
log.info( String.format( "Installing mod: %s", modFile.getName() ) );
|
log.info( String.format( "Installing mod: %s", modFile.getName() ) );
|
||||||
observer.patchingMod( modFile );
|
observer.patchingMod( modFile );
|
||||||
|
|
||||||
zis = new ZipInputStream( new FileInputStream( modFile ) );
|
fis = new FileInputStream( modFile );
|
||||||
|
zis = new ZipInputStream( new BufferedInputStream( fis ) );
|
||||||
ZipEntry item;
|
ZipEntry item;
|
||||||
while ( (item = zis.getNextEntry()) != null ) {
|
while ( (item = zis.getNextEntry()) != null ) {
|
||||||
if ( item.isDirectory() ) continue;
|
if ( item.isDirectory() ) {
|
||||||
|
zis.closeEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
String innerPath = item.getName();
|
String innerPath = item.getName();
|
||||||
innerPath = innerPath.replace( '\\', '/' ); // Non-standard zips.
|
innerPath = innerPath.replace( '\\', '/' ); // Non-standard zips.
|
||||||
|
@ -209,6 +216,7 @@ public class ModPatchThread extends Thread {
|
||||||
|
|
||||||
AbstractPack ftlP = topFolderMap.get( topFolder );
|
AbstractPack ftlP = topFolderMap.get( topFolder );
|
||||||
if ( ftlP == null ) {
|
if ( ftlP == null ) {
|
||||||
|
if ( !topFolderMap.containsKey( topFolder ) )
|
||||||
log.warn( String.format( "Unexpected innerPath: %s", innerPath ) );
|
log.warn( String.format( "Unexpected innerPath: %s", innerPath ) );
|
||||||
zis.closeEntry();
|
zis.closeEntry();
|
||||||
continue;
|
continue;
|
||||||
|
@ -292,6 +300,9 @@ public class ModPatchThread extends Thread {
|
||||||
try {if ( zis != null ) zis.close();}
|
try {if ( zis != null ) zis.close();}
|
||||||
catch ( Exception e ) {}
|
catch ( Exception e ) {}
|
||||||
|
|
||||||
|
try {if ( fis != null ) fis.close();}
|
||||||
|
catch ( Exception e ) {}
|
||||||
|
|
||||||
System.gc();
|
System.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -316,7 +316,7 @@ public class ModUtilities {
|
||||||
|
|
||||||
Pattern junkFilePtn = Pattern.compile( "[.]DS_Store$|^thumbs[.]db$" );
|
Pattern junkFilePtn = Pattern.compile( "[.]DS_Store$|^thumbs[.]db$" );
|
||||||
|
|
||||||
Pattern validRootDirPtn = Pattern.compile( "^(?:audio|data|fonts|img)/" );
|
Pattern validRootDirPtn = Pattern.compile( "^(?:audio|data|fonts|img|mod-appendix)/" );
|
||||||
List<String> seenJunkDirs = new ArrayList<String>();
|
List<String> seenJunkDirs = new ArrayList<String>();
|
||||||
|
|
||||||
CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder();
|
CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder();
|
||||||
|
|
46
src/main/java/net/vhati/modmanager/core/ModsInfo.java
Normal file
46
src/main/java/net/vhati/modmanager/core/ModsInfo.java
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package net.vhati.modmanager.core;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined information from several similar ModInfo objects of varying versions.
|
||||||
|
*/
|
||||||
|
public class ModsInfo {
|
||||||
|
public String title = null;
|
||||||
|
public String author = null;
|
||||||
|
public String threadURL = null;
|
||||||
|
public String threadHash = null;
|
||||||
|
public String description = null;
|
||||||
|
private Map<String,String> versionsMap = new LinkedHashMap<String,String>();
|
||||||
|
|
||||||
|
|
||||||
|
public ModsInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setTitle( String s ) { this.title = s; }
|
||||||
|
public void setAuthor( String s ) { this.author = s; }
|
||||||
|
public void setThreadURL( String s ) { this.threadURL = s; }
|
||||||
|
public void setThreadHash( String s ) { this.threadHash = s; }
|
||||||
|
public void setDescription( String s ) { this.description = s; }
|
||||||
|
|
||||||
|
public String getTitle() { return this.title; }
|
||||||
|
public String getAuthor() { return this.author; }
|
||||||
|
public String getThreadURL() { return this.threadURL; }
|
||||||
|
public String getThreadHash() { return this.threadHash; }
|
||||||
|
public String getDescription() { return this.description; }
|
||||||
|
|
||||||
|
|
||||||
|
public void putVersion( String fileHash, String fileVersion ) {
|
||||||
|
versionsMap.put( fileHash, fileVersion );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal Map of mod file hashes and version strings.
|
||||||
|
*/
|
||||||
|
public Map<String,String> getVersionsMap() {
|
||||||
|
return versionsMap;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package net.vhati.modmanager.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import net.vhati.modmanager.core.ModDB;
|
||||||
|
|
||||||
|
|
||||||
|
public interface ModsScanObserver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file's hash has been calculated.
|
||||||
|
*/
|
||||||
|
public void hashCalculated( File f, String hash );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A new ModDB of cached metadata is ready to use.
|
||||||
|
*/
|
||||||
|
public void localModDBUpdated( ModDB newDB );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mods scanning ended.
|
||||||
|
*/
|
||||||
|
public void modsScanEnded();
|
||||||
|
}
|
91
src/main/java/net/vhati/modmanager/core/ModsScanThread.java
Normal file
91
src/main/java/net/vhati/modmanager/core/ModsScanThread.java
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package net.vhati.modmanager.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import net.vhati.ftldat.FTLDat;
|
||||||
|
import net.vhati.modmanager.core.ModDB;
|
||||||
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
|
import net.vhati.modmanager.core.ModsScanObserver;
|
||||||
|
import net.vhati.modmanager.xml.JDOMModMetadataReader;
|
||||||
|
|
||||||
|
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 ModsScanThread extends Thread {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(ModsScanThread.class);
|
||||||
|
|
||||||
|
private List<File> fileList = new ArrayList<File>();
|
||||||
|
private ModDB newDB;
|
||||||
|
private ModsScanObserver scanObserver;
|
||||||
|
|
||||||
|
|
||||||
|
public ModsScanThread( File[] files, ModDB knownDB, ModsScanObserver scanObserver ) {
|
||||||
|
this.fileList.addAll( Arrays.asList(files) );
|
||||||
|
this.newDB = new ModDB( knownDB );
|
||||||
|
this.scanObserver = scanObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
Map<File,String> hashMap = new HashMap<File,String>();
|
||||||
|
|
||||||
|
for ( File f : fileList ) {
|
||||||
|
String hash = calcFileMD5( f );
|
||||||
|
if ( hash != null ) {
|
||||||
|
hashMap.put( f, hash );
|
||||||
|
scanObserver.hashCalculated( f, hash );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info( "Background hashing finished." );
|
||||||
|
|
||||||
|
// Cache info about new files.
|
||||||
|
for ( File f : fileList ) {
|
||||||
|
String fileHash = hashMap.get( f );
|
||||||
|
|
||||||
|
if ( fileHash != null && newDB.getModInfo( fileHash ) == null ) {
|
||||||
|
ModInfo modInfo = JDOMModMetadataReader.parseModFile( f );
|
||||||
|
if ( modInfo != null ) {
|
||||||
|
modInfo.setFileHash( fileHash );
|
||||||
|
newDB.addMod( modInfo );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prune info about absent files.
|
||||||
|
for ( Iterator<ModInfo> it = newDB.getCatalog().iterator(); it.hasNext(); ) {
|
||||||
|
ModInfo modInfo = it.next();
|
||||||
|
if ( !hashMap.containsValue( modInfo.getFileHash() ) )
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
scanObserver.localModDBUpdated( new ModDB( newDB ) );
|
||||||
|
log.info( "Background metadata caching finished." );
|
||||||
|
|
||||||
|
scanObserver.modsScanEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,15 @@ public class SlipstreamConfig {
|
||||||
this.configFile = configFile;
|
this.configFile = configFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of an existing SlipstreamConfig object.
|
||||||
|
*/
|
||||||
|
public SlipstreamConfig( SlipstreamConfig srcConfig ) {
|
||||||
|
this.configFile = srcConfig.getConfigFile();
|
||||||
|
this.config = new Properties();
|
||||||
|
this.config.putAll( srcConfig.getConfig() );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public Properties getConfig() { return config; }
|
public Properties getConfig() { return config; }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package net.vhati.modmanager.json;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import net.vhati.modmanager.core.ModsInfo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
|
||||||
|
public class JacksonCatalogWriter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes collated catalog entries to a file, as condensed json.
|
||||||
|
*/
|
||||||
|
public static void write( List<ModsInfo> modsInfoList, File dstFile ) throws IOException {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ObjectNode rootNode = mapper.createObjectNode();
|
||||||
|
|
||||||
|
ObjectNode catalogsNode = rootNode.objectNode();
|
||||||
|
rootNode.put( "catalog_versions", catalogsNode );
|
||||||
|
|
||||||
|
ArrayNode catalogNode = rootNode.arrayNode();
|
||||||
|
catalogsNode.put( "1", catalogNode );
|
||||||
|
|
||||||
|
for ( ModsInfo modsInfo : modsInfoList ) {
|
||||||
|
ObjectNode infoNode = rootNode.objectNode();
|
||||||
|
catalogNode.add( infoNode );
|
||||||
|
|
||||||
|
infoNode.put( "title", modsInfo.getTitle() );
|
||||||
|
infoNode.put( "author", modsInfo.getAuthor() );
|
||||||
|
infoNode.put( "desc", modsInfo.getDescription() );
|
||||||
|
infoNode.put( "url", modsInfo.getThreadURL() );
|
||||||
|
|
||||||
|
infoNode.put( "thread_hash", modsInfo.threadHash );
|
||||||
|
|
||||||
|
ArrayNode versionsNode = rootNode.arrayNode();
|
||||||
|
infoNode.put( "versions", versionsNode );
|
||||||
|
|
||||||
|
for ( Map.Entry<String,String> entry : modsInfo.getVersionsMap().entrySet() ) {
|
||||||
|
String versionFileHash = entry.getKey();
|
||||||
|
String versionString = entry.getValue();
|
||||||
|
|
||||||
|
ObjectNode versionNode = rootNode.objectNode();
|
||||||
|
versionNode.put( "hash", versionFileHash );
|
||||||
|
versionNode.put( "version", versionString );
|
||||||
|
versionsNode.add( versionNode );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputStream os = null;
|
||||||
|
try {
|
||||||
|
os = new FileOutputStream( dstFile );
|
||||||
|
OutputStreamWriter writer = new OutputStreamWriter( os, Charset.forName("US-ASCII") );
|
||||||
|
mapper.writeValue( writer, rootNode );
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {if ( os != null ) os.close();}
|
||||||
|
catch ( IOException e ) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,6 @@ package net.vhati.modmanager.json;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import net.vhati.modmanager.core.ModDB;
|
import net.vhati.modmanager.core.ModDB;
|
||||||
import net.vhati.modmanager.core.ModInfo;
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
|
@ -63,7 +61,7 @@ public class JacksonGrognakCatalogReader {
|
||||||
exception = e;
|
exception = e;
|
||||||
}
|
}
|
||||||
if ( exception != null ) {
|
if ( exception != null ) {
|
||||||
log.error( exception );
|
log.error( String.format( "While processing \"%s\", json parsing failed: %s", jsonFile.getName(), exception.getMessage() ), exception );
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,9 @@ import java.util.regex.Pattern;
|
||||||
import net.vhati.ftldat.FTLDat;
|
import net.vhati.ftldat.FTLDat;
|
||||||
import net.vhati.modmanager.core.ModDB;
|
import net.vhati.modmanager.core.ModDB;
|
||||||
import net.vhati.modmanager.core.ModInfo;
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
|
import net.vhati.modmanager.core.ModsInfo;
|
||||||
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
||||||
|
import net.vhati.modmanager.json.JacksonCatalogWriter;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
@ -186,15 +188,15 @@ public class ForumScraper {
|
||||||
log.info( "Dumping json..." );
|
log.info( "Dumping json..." );
|
||||||
|
|
||||||
File dstFile = new File( cmdline.getOptionValue( "dump-json" ) );
|
File dstFile = new File( cmdline.getOptionValue( "dump-json" ) );
|
||||||
List<ModsInfo> data = getCollatedModInfo( modDB );
|
List<ModsInfo> data = modDB.getCollatedModInfo();
|
||||||
if ( data.size() > 0 ) writeJSON( data, dstFile );
|
if ( data.size() > 0 ) JacksonCatalogWriter.write( data, dstFile );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cmdline.hasOption( "dump-xml" ) ) {
|
if ( cmdline.hasOption( "dump-xml" ) ) {
|
||||||
log.info( "Dumping xml..." );
|
log.info( "Dumping xml..." );
|
||||||
|
|
||||||
File dstFile = new File( cmdline.getOptionValue( "dump-xml" ) );
|
File dstFile = new File( cmdline.getOptionValue( "dump-xml" ) );
|
||||||
List<ModsInfo> data = getCollatedModInfo( modDB );
|
List<ModsInfo> data = modDB.getCollatedModInfo();
|
||||||
if ( data.size() > 0 ) writeXML( data, dstFile );
|
if ( data.size() > 0 ) writeXML( data, dstFile );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,43 +220,6 @@ public class ForumScraper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects ModInfo objects that differ only in version, and creates ModsInfo objects.
|
|
||||||
*/
|
|
||||||
private static List<ModsInfo> getCollatedModInfo( ModDB modDB ) {
|
|
||||||
List<ModsInfo> results = new ArrayList<ModsInfo>();
|
|
||||||
List<ModInfo> seenList = new ArrayList<ModInfo>();
|
|
||||||
|
|
||||||
for ( ModInfo modInfo : modDB.getCatalog() ) {
|
|
||||||
if ( seenList.contains( modInfo ) ) continue;
|
|
||||||
seenList.add( modInfo );
|
|
||||||
|
|
||||||
ModsInfo modsInfo = new ModsInfo();
|
|
||||||
modsInfo.title = modInfo.getTitle();
|
|
||||||
modsInfo.author = modInfo.getAuthor();
|
|
||||||
modsInfo.threadURL = modInfo.getURL();
|
|
||||||
modsInfo.description = modInfo.getDescription();
|
|
||||||
|
|
||||||
String threadHash = modDB.getThreadHash( modInfo.getURL() );
|
|
||||||
modsInfo.threadHash = ( threadHash != null ? threadHash : "???" );
|
|
||||||
|
|
||||||
modsInfo.putVersion( modInfo.getFileHash(), modInfo.getVersion() );
|
|
||||||
|
|
||||||
HashMap<String,List<ModInfo>> similarMods = modDB.getSimilarMods( modInfo );
|
|
||||||
for ( ModInfo altInfo : similarMods.get( ModDB.EXACT ) ) {
|
|
||||||
if ( seenList.contains( altInfo ) ) continue;
|
|
||||||
seenList.add( altInfo );
|
|
||||||
|
|
||||||
modsInfo.putVersion( altInfo.getFileHash(), altInfo.getVersion() );
|
|
||||||
}
|
|
||||||
|
|
||||||
results.add( modsInfo );
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scrapes the forum for changed posts and returns info from updated mods.
|
* Scrapes the forum for changed posts and returns info from updated mods.
|
||||||
*/
|
*/
|
||||||
|
@ -265,11 +230,11 @@ public class ForumScraper {
|
||||||
|
|
||||||
for ( ScrapeResult scrapedInfo : scrapeList ) {
|
for ( ScrapeResult scrapedInfo : scrapeList ) {
|
||||||
ModsInfo modsInfo = new ModsInfo();
|
ModsInfo modsInfo = new ModsInfo();
|
||||||
modsInfo.title = scrapedInfo.title;
|
modsInfo.setTitle( scrapedInfo.title );
|
||||||
modsInfo.author = scrapedInfo.author;
|
modsInfo.setAuthor( scrapedInfo.author );
|
||||||
modsInfo.threadURL = scrapedInfo.threadURL;
|
modsInfo.setThreadURL( scrapedInfo.threadURL );
|
||||||
modsInfo.threadHash = scrapedInfo.threadHash;
|
modsInfo.setThreadHash( scrapedInfo.threadHash );
|
||||||
modsInfo.description = scrapedInfo.rawDesc;
|
modsInfo.setDescription( scrapedInfo.rawDesc );
|
||||||
modsInfo.putVersion( "???", "???"+ (scrapedInfo.wip ? " WIP" : "") );
|
modsInfo.putVersion( "???", "???"+ (scrapedInfo.wip ? " WIP" : "") );
|
||||||
results.add( modsInfo );
|
results.add( modsInfo );
|
||||||
}
|
}
|
||||||
|
@ -562,23 +527,26 @@ public class ForumScraper {
|
||||||
XMLOutputter xmlOut = new XMLOutputter( xmlFormat );
|
XMLOutputter xmlOut = new XMLOutputter( xmlFormat );
|
||||||
|
|
||||||
writeIndent( dst, indent, depth++ ).append( "<modsinfo>\n" );
|
writeIndent( dst, indent, depth++ ).append( "<modsinfo>\n" );
|
||||||
writeIndent( dst, indent, depth ); dst.append("<title>").append( xmlOut.escapeElementEntities( modsInfo.title ) ).append( "</title>\n" );
|
writeIndent( dst, indent, depth ); dst.append("<title>").append( xmlOut.escapeElementEntities( modsInfo.getTitle() ) ).append( "</title>\n" );
|
||||||
writeIndent( dst, indent, depth ); dst.append("<author>").append( xmlOut.escapeElementEntities( modsInfo.author ) ).append( "</author>\n" );
|
writeIndent( dst, indent, depth ); dst.append("<author>").append( xmlOut.escapeElementEntities( modsInfo.getAuthor() ) ).append( "</author>\n" );
|
||||||
writeIndent( dst, indent, depth ); dst.append("<threadUrl><![CDATA[ ").append( modsInfo.threadURL ).append( " ]]></threadUrl>\n" );
|
writeIndent( dst, indent, depth ); dst.append("<threadUrl><![CDATA[ ").append( modsInfo.getThreadURL() ).append( " ]]></threadUrl>\n" );
|
||||||
|
|
||||||
writeIndent( dst, indent, depth++ ).append( "<versions>\n" );
|
writeIndent( dst, indent, depth++ ).append( "<versions>\n" );
|
||||||
for ( String[] entry : modsInfo.versions ) {
|
for ( Map.Entry<String,String> entry : modsInfo.getVersionsMap().entrySet() ) {
|
||||||
|
String versionFileHash = entry.getKey();
|
||||||
|
String versionString = entry.getValue();
|
||||||
|
|
||||||
writeIndent( dst, indent, depth );
|
writeIndent( dst, indent, depth );
|
||||||
dst.append( "<version hash=\"" ).append( xmlOut.escapeAttributeEntities( entry[0] ) ).append( "\">" );
|
dst.append( "<version hash=\"" ).append( xmlOut.escapeAttributeEntities( versionFileHash ) ).append( "\">" );
|
||||||
dst.append( xmlOut.escapeElementEntities( entry[1] ) );
|
dst.append( xmlOut.escapeElementEntities( versionString ) );
|
||||||
dst.append( "</version>" ).append( "\n" );
|
dst.append( "</version>" ).append( "\n" );
|
||||||
}
|
}
|
||||||
writeIndent( dst, indent, --depth ).append( "</versions>\n" );
|
writeIndent( dst, indent, --depth ).append( "</versions>\n" );
|
||||||
writeIndent( dst, indent, depth ); dst.append("<threadHash>").append( modsInfo.threadHash ).append( "</threadHash>\n" );
|
writeIndent( dst, indent, depth ); dst.append("<threadHash>").append( modsInfo.getThreadHash() ).append( "</threadHash>\n" );
|
||||||
dst.append( "\n" );
|
dst.append( "\n" );
|
||||||
|
|
||||||
writeIndent( dst, indent, depth ); dst.append( "<description>" ).append( "<![CDATA[" );
|
writeIndent( dst, indent, depth ); dst.append( "<description>" ).append( "<![CDATA[" );
|
||||||
dst.append( modsInfo.description );
|
dst.append( modsInfo.getDescription() );
|
||||||
dst.append( "]]>\n" );
|
dst.append( "]]>\n" );
|
||||||
writeIndent( dst, indent, depth ); dst.append( "</description>\n" );
|
writeIndent( dst, indent, depth ); dst.append( "</description>\n" );
|
||||||
|
|
||||||
|
@ -641,55 +609,6 @@ public class ForumScraper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes collated catalog entries to a file, as condensed json.
|
|
||||||
*/
|
|
||||||
private static void writeJSON( List<ModsInfo> data, File dstFile ) throws IOException {
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
ObjectNode rootNode = mapper.createObjectNode();
|
|
||||||
|
|
||||||
ObjectNode catalogsNode = rootNode.objectNode();
|
|
||||||
rootNode.put( "catalog_versions", catalogsNode );
|
|
||||||
|
|
||||||
ArrayNode catalogNode = rootNode.arrayNode();
|
|
||||||
catalogsNode.put( "1", catalogNode );
|
|
||||||
|
|
||||||
for ( ModsInfo modsInfo : data ) {
|
|
||||||
ObjectNode infoNode = rootNode.objectNode();
|
|
||||||
catalogNode.add( infoNode );
|
|
||||||
|
|
||||||
infoNode.put( "title", modsInfo.title );
|
|
||||||
infoNode.put( "author", modsInfo.author );
|
|
||||||
infoNode.put( "desc", modsInfo.description );
|
|
||||||
infoNode.put( "url", modsInfo.threadURL );
|
|
||||||
|
|
||||||
infoNode.put( "thread_hash", modsInfo.threadHash );
|
|
||||||
|
|
||||||
ArrayNode versionsNode = rootNode.arrayNode();
|
|
||||||
infoNode.put( "versions", versionsNode );
|
|
||||||
|
|
||||||
for ( String[] entry : modsInfo.versions ) {
|
|
||||||
ObjectNode versionNode = rootNode.objectNode();
|
|
||||||
versionNode.put( "hash", entry[0] );
|
|
||||||
versionNode.put( "version", entry[1] );
|
|
||||||
versionsNode.add( versionNode );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream os = null;
|
|
||||||
try {
|
|
||||||
os = new FileOutputStream( dstFile );
|
|
||||||
OutputStreamWriter writer = new OutputStreamWriter( os, Charset.forName("US-ASCII") );
|
|
||||||
mapper.writeValue( writer, rootNode );
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
try {if ( os != null ) os.close();}
|
|
||||||
catch ( IOException e ) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Information gleaned from scraping the forum. */
|
/** Information gleaned from scraping the forum. */
|
||||||
private static class ScrapeResult {
|
private static class ScrapeResult {
|
||||||
public String threadURL = null;
|
public String threadURL = null;
|
||||||
|
@ -699,18 +618,4 @@ public class ForumScraper {
|
||||||
public String rawDesc = null;
|
public String rawDesc = null;
|
||||||
public String threadHash = null;
|
public String threadHash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Combined information from several similar ModInfo objects of varying versions. */
|
|
||||||
private static class ModsInfo {
|
|
||||||
public String threadURL = null;
|
|
||||||
public String title = null;
|
|
||||||
public String author = null;
|
|
||||||
public String description = null;
|
|
||||||
public String threadHash = null;
|
|
||||||
public ArrayList<String[]> versions = new ArrayList<String[]>();
|
|
||||||
|
|
||||||
public void putVersion( String fileHash, String fileVersion ) {
|
|
||||||
versions.add( new String[] {fileHash, fileVersion} );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,9 @@ import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
|
@ -65,22 +68,24 @@ import net.vhati.ftldat.FTLDat;
|
||||||
import net.vhati.modmanager.core.AutoUpdateInfo;
|
import net.vhati.modmanager.core.AutoUpdateInfo;
|
||||||
import net.vhati.modmanager.core.ComparableVersion;
|
import net.vhati.modmanager.core.ComparableVersion;
|
||||||
import net.vhati.modmanager.core.FTLUtilities;
|
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.ModDB;
|
||||||
import net.vhati.modmanager.core.ModFileInfo;
|
import net.vhati.modmanager.core.ModFileInfo;
|
||||||
import net.vhati.modmanager.core.ModInfo;
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
import net.vhati.modmanager.core.ModPatchThread;
|
import net.vhati.modmanager.core.ModPatchThread;
|
||||||
import net.vhati.modmanager.core.ModPatchThread.BackedUpDat;
|
import net.vhati.modmanager.core.ModPatchThread.BackedUpDat;
|
||||||
|
import net.vhati.modmanager.core.ModsScanObserver;
|
||||||
|
import net.vhati.modmanager.core.ModsScanThread;
|
||||||
import net.vhati.modmanager.core.ModUtilities;
|
import net.vhati.modmanager.core.ModUtilities;
|
||||||
import net.vhati.modmanager.core.Report;
|
import net.vhati.modmanager.core.Report;
|
||||||
import net.vhati.modmanager.core.Report.ReportFormatter;
|
import net.vhati.modmanager.core.Report.ReportFormatter;
|
||||||
import net.vhati.modmanager.core.SlipstreamConfig;
|
import net.vhati.modmanager.core.SlipstreamConfig;
|
||||||
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
|
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
|
||||||
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
||||||
|
import net.vhati.modmanager.json.JacksonCatalogWriter;
|
||||||
import net.vhati.modmanager.json.URLFetcher;
|
import net.vhati.modmanager.json.URLFetcher;
|
||||||
import net.vhati.modmanager.ui.ChecklistTableModel;
|
import net.vhati.modmanager.ui.ChecklistTableModel;
|
||||||
import net.vhati.modmanager.ui.InertPanel;
|
import net.vhati.modmanager.ui.InertPanel;
|
||||||
|
import net.vhati.modmanager.ui.ManagerInitThread;
|
||||||
import net.vhati.modmanager.ui.ModInfoArea;
|
import net.vhati.modmanager.ui.ModInfoArea;
|
||||||
import net.vhati.modmanager.ui.ModPatchDialog;
|
import net.vhati.modmanager.ui.ModPatchDialog;
|
||||||
import net.vhati.modmanager.ui.ModXMLSandbox;
|
import net.vhati.modmanager.ui.ModXMLSandbox;
|
||||||
|
@ -93,7 +98,7 @@ import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
public class ManagerFrame extends JFrame implements ActionListener, HashObserver, Nerfable, Statusbar {
|
public class ManagerFrame extends JFrame implements ActionListener, ModsScanObserver, Nerfable, Statusbar {
|
||||||
|
|
||||||
private static final Logger log = LogManager.getLogger(ManagerFrame.class);
|
private static final Logger log = LogManager.getLogger(ManagerFrame.class);
|
||||||
|
|
||||||
|
@ -103,12 +108,20 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
private File backupDir = new File( "./backup/" );
|
private File backupDir = new File( "./backup/" );
|
||||||
private File modsDir = new File( "./mods/" );
|
private File modsDir = new File( "./mods/" );
|
||||||
|
|
||||||
|
private File modorderFile = new File( modsDir, "modorder.txt" );
|
||||||
|
|
||||||
|
private File metadataFile = new File( backupDir, "cached_metadata.json" );
|
||||||
|
|
||||||
private File catalogFile = new File( backupDir, "current_catalog.json" );
|
private File catalogFile = new File( backupDir, "current_catalog.json" );
|
||||||
private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" );
|
private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" );
|
||||||
|
|
||||||
private File appUpdateFile = new File( backupDir, "auto_update.json" );
|
private File appUpdateFile = new File( backupDir, "auto_update.json" );
|
||||||
private File appUpdateETagFile = new File( backupDir, "auto_update_etag.txt" );
|
private File appUpdateETagFile = new File( backupDir, "auto_update_etag.txt" );
|
||||||
|
|
||||||
|
private final Lock managerLock = new ReentrantLock();
|
||||||
|
private final Condition scanEndedCond = managerLock.newCondition();
|
||||||
|
private boolean scanning = false;
|
||||||
|
|
||||||
private SlipstreamConfig appConfig;
|
private SlipstreamConfig appConfig;
|
||||||
private String appName;
|
private String appName;
|
||||||
private ComparableVersion appVersion;
|
private ComparableVersion appVersion;
|
||||||
|
@ -116,7 +129,8 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
private String appAuthor;
|
private String appAuthor;
|
||||||
|
|
||||||
private HashMap<File,String> modFileHashes = new HashMap<File,String>();
|
private HashMap<File,String> modFileHashes = new HashMap<File,String>();
|
||||||
private ModDB modDB = new ModDB();
|
private ModDB catalogModDB = new ModDB();
|
||||||
|
private ModDB localModDB = new ModDB();
|
||||||
|
|
||||||
private AutoUpdateInfo appUpdateInfo = null;
|
private AutoUpdateInfo appUpdateInfo = null;
|
||||||
private Color updateBtnDisabledColor = UIManager.getColor( "Button.foreground" );
|
private Color updateBtnDisabledColor = UIManager.getColor( "Button.foreground" );
|
||||||
|
@ -295,7 +309,14 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
appConfig.writeConfig();
|
appConfig.writeConfig();
|
||||||
}
|
}
|
||||||
catch ( IOException f ) {
|
catch ( IOException f ) {
|
||||||
log.error( String.format( "Error writing config to \"%s\".", appConfig.getConfigFile() ), f );
|
log.error( String.format( "Error writing config to \"%s\".", appConfig.getConfigFile().getName() ), f );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JacksonCatalogWriter.write( localModDB.getCollatedModInfo(), metadataFile );
|
||||||
|
}
|
||||||
|
catch ( IOException f ) {
|
||||||
|
log.error( String.format( "Error writing metadata from local mods to \"%s\".", metadataFile.getName() ), f );
|
||||||
}
|
}
|
||||||
|
|
||||||
System.gc(); // Ward off an intermittent InterruptedException from exit()?
|
System.gc(); // Ward off an intermittent InterruptedException from exit()?
|
||||||
|
@ -441,133 +462,17 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
*/
|
*/
|
||||||
public void init() {
|
public void init() {
|
||||||
|
|
||||||
List<String> preferredOrder = loadModOrder();
|
ManagerInitThread initThread = new ManagerInitThread( this,
|
||||||
rescanMods( preferredOrder );
|
new SlipstreamConfig( appConfig ),
|
||||||
|
modorderFile,
|
||||||
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
|
metadataFile,
|
||||||
boolean needNewCatalog = false;
|
catalogFile,
|
||||||
|
catalogETagFile,
|
||||||
if ( catalogFile.exists() ) {
|
appUpdateFile,
|
||||||
// Load the catalog first, before updating.
|
appUpdateETagFile
|
||||||
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
|
);
|
||||||
if ( currentDB != null ) modDB = currentDB;
|
initThread.setDaemon( true );
|
||||||
|
initThread.start();
|
||||||
if ( catalogUpdateInterval > 0 ) {
|
|
||||||
// Check if the downloaded catalog is stale.
|
|
||||||
Date catalogDate = new Date( catalogFile.lastModified() );
|
|
||||||
Calendar cal = Calendar.getInstance();
|
|
||||||
cal.add( Calendar.DATE, catalogUpdateInterval * -1 );
|
|
||||||
if ( catalogDate.before( cal.getTime() ) ) {
|
|
||||||
log.debug( String.format( "Catalog is older than %d days.", catalogUpdateInterval ) );
|
|
||||||
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.
|
|
||||||
if ( catalogUpdateInterval <= 0 ) needNewCatalog = false;
|
|
||||||
|
|
||||||
if ( needNewCatalog ) {
|
|
||||||
Runnable fetchTask = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean fetched = URLFetcher.refetchURL( CATALOG_URL, catalogFile, catalogETagFile );
|
|
||||||
|
|
||||||
if ( fetched ) reloadCatalog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Thread fetchThread = new Thread( fetchTask );
|
|
||||||
fetchThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
int appUpdateInterval = appConfig.getPropertyAsInt( "update_app", 0 );
|
|
||||||
boolean needAppUpdate = false;
|
|
||||||
|
|
||||||
if ( appUpdateFile.exists() ) {
|
|
||||||
// Load the info first, before downloading.
|
|
||||||
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
|
|
||||||
if ( aui != null ) {
|
|
||||||
appUpdateInfo = aui;
|
|
||||||
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
|
|
||||||
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
|
|
||||||
updateBtn.setEnabled( isUpdateAvailable );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( appUpdateInterval > 0 ) {
|
|
||||||
// Check if the app update info is stale.
|
|
||||||
Date catalogDate = new Date( appUpdateFile.lastModified() );
|
|
||||||
Calendar cal = Calendar.getInstance();
|
|
||||||
cal.add( Calendar.DATE, catalogUpdateInterval * -1 );
|
|
||||||
if ( catalogDate.before( cal.getTime() ) ) {
|
|
||||||
log.debug( String.format( "App update info is older than %d days.", appUpdateInterval ) );
|
|
||||||
needAppUpdate = true;
|
|
||||||
} else {
|
|
||||||
log.debug( "App update info isn't stale yet." );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// App update file doesn't exist.
|
|
||||||
needAppUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't update if the user doesn't want to.
|
|
||||||
if ( appUpdateInterval <= 0 ) needAppUpdate = false;
|
|
||||||
|
|
||||||
if ( needAppUpdate ) {
|
|
||||||
Runnable fetchTask = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean fetched = URLFetcher.refetchURL( APP_UPDATE_URL, appUpdateFile, appUpdateETagFile );
|
|
||||||
|
|
||||||
if ( fetched ) reloadAppUpdateInfo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Thread fetchThread = new Thread( fetchTask );
|
|
||||||
fetchThread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reparses and replaces 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reparses info about available app updates. (thread-safe)
|
|
||||||
*/
|
|
||||||
public void reloadAppUpdateInfo() {
|
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if ( appUpdateFile.exists() ) {
|
|
||||||
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
|
|
||||||
if ( aui != null ) {
|
|
||||||
appUpdateInfo = aui;
|
|
||||||
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
|
|
||||||
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
|
|
||||||
updateBtn.setEnabled( isUpdateAvailable );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -599,38 +504,11 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
return sortedMods;
|
return sortedMods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads modorder.txt and returns a list of mod names in preferred order.
|
|
||||||
*/
|
|
||||||
private List<String> loadModOrder() {
|
|
||||||
List<String> result = new ArrayList<String>();
|
|
||||||
|
|
||||||
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 ) {
|
|
||||||
result.add( line );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( FileNotFoundException e ) {
|
|
||||||
}
|
|
||||||
catch ( IOException e ) {
|
|
||||||
log.error( "Error reading modorder.txt.", e );
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
try {if (is != null) is.close();}
|
|
||||||
catch (Exception e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveModOrder( List<ModFileInfo> sortedMods ) {
|
private void saveModOrder( List<ModFileInfo> sortedMods ) {
|
||||||
FileOutputStream os = null;
|
FileOutputStream os = null;
|
||||||
try {
|
try {
|
||||||
os = new FileOutputStream( new File( modsDir, "modorder.txt" ) );
|
os = new FileOutputStream( modorderFile );
|
||||||
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( os, Charset.forName("UTF-8") ));
|
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( os, Charset.forName("UTF-8") ));
|
||||||
|
|
||||||
for ( ModFileInfo modFileInfo : sortedMods ) {
|
for ( ModFileInfo modFileInfo : sortedMods ) {
|
||||||
|
@ -640,7 +518,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
bw.flush();
|
bw.flush();
|
||||||
}
|
}
|
||||||
catch ( IOException e ) {
|
catch ( IOException e ) {
|
||||||
log.error( "Error writing modorder.txt.", e );
|
log.error( String.format( "Error writing \"%s\".", modorderFile.getName() ), e );
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
try {if (os != null) os.close();}
|
try {if (os != null) os.close();}
|
||||||
|
@ -648,12 +526,20 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears and syncs the mods list with mods/ dir, then starts a new hash thread.
|
* Clears and syncs the mods list with mods/ dir, then starts a new hash thread.
|
||||||
*/
|
*/
|
||||||
private void rescanMods( List<String> preferredOrder ) {
|
public void rescanMods( List<String> preferredOrder ) {
|
||||||
|
managerLock.lock();
|
||||||
|
try {
|
||||||
|
scanning = true;
|
||||||
if ( rescanMenuItem.isEnabled() == false ) return;
|
if ( rescanMenuItem.isEnabled() == false ) return;
|
||||||
rescanMenuItem.setEnabled( false );
|
rescanMenuItem.setEnabled( false );
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
managerLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
modFileHashes.clear();
|
modFileHashes.clear();
|
||||||
localModsTableModel.removeAllItems();
|
localModsTableModel.removeAllItems();
|
||||||
|
@ -672,9 +558,9 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
localModsTableModel.addItem( modFileInfo );
|
localModsTableModel.addItem( modFileInfo );
|
||||||
}
|
}
|
||||||
|
|
||||||
HashThread hashThread = new HashThread( modFiles, this );
|
ModsScanThread scanThread = new ModsScanThread( modFiles, localModDB, this );
|
||||||
hashThread.setDaemon( true );
|
scanThread.setDaemon( true );
|
||||||
hashThread.start();
|
scanThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -739,11 +625,19 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows info about a local mod in the text area.
|
* Shows info about a local mod in the text area.
|
||||||
|
*
|
||||||
|
* Priority is given to embedded metadata.xml, but when that's absent,
|
||||||
|
* the gatalog's info is used. If the catalog doesn't have the info,
|
||||||
|
* an 'info missing' notice is shown instead.
|
||||||
*/
|
*/
|
||||||
public void showLocalModInfo( ModFileInfo modFileInfo ) {
|
public void showLocalModInfo( ModFileInfo modFileInfo ) {
|
||||||
String modHash = modFileHashes.get( modFileInfo.getFile() );
|
String modHash = modFileHashes.get( modFileInfo.getFile() );
|
||||||
|
|
||||||
ModInfo modInfo = modDB.getModInfo( modHash );
|
ModInfo modInfo = localModDB.getModInfo( modHash );
|
||||||
|
if ( modInfo == null || modInfo.isBlank() ) {
|
||||||
|
modInfo = catalogModDB.getModInfo( modHash );
|
||||||
|
}
|
||||||
|
|
||||||
if ( modInfo != null ) {
|
if ( modInfo != null ) {
|
||||||
infoArea.setDescription( modInfo.getTitle(), modInfo.getAuthor(), modInfo.getVersion(), modInfo.getURL(), modInfo.getDescription() );
|
infoArea.setDescription( modInfo.getTitle(), modInfo.getAuthor(), modInfo.getVersion(), modInfo.getURL(), modInfo.getDescription() );
|
||||||
}
|
}
|
||||||
|
@ -768,7 +662,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
|
|
||||||
body += "If it is stable and has been out for over a month,\n";
|
body += "If it is stable and has been out for over a month,\n";
|
||||||
body += "please let the Slipstream devs know where you ";
|
body += "please let the Slipstream devs know where you ";
|
||||||
body += "found it.\n";
|
body += "found it.\n\n";
|
||||||
body += "Include the mod's version, and this hash.\n";
|
body += "Include the mod's version, and this hash.\n";
|
||||||
body += "MD5: "+ modHash +"\n";
|
body += "MD5: "+ modHash +"\n";
|
||||||
infoArea.setDescription( modFileInfo.getName(), body );
|
infoArea.setDescription( modFileInfo.getName(), body );
|
||||||
|
@ -954,22 +848,123 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hashCalculated( final File f, final String hash ) {
|
public void hashCalculated( final File f, final String hash ) {
|
||||||
SwingUtilities.invokeLater( new Runnable() {
|
Runnable r = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() { modFileHashes.put( f, hash ); }
|
||||||
modFileHashes.put( f, hash );
|
};
|
||||||
}
|
if ( SwingUtilities.isEventDispatchThread() ) r.run();
|
||||||
});
|
else SwingUtilities.invokeLater( r );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hashingEnded() {
|
public void localModDBUpdated( ModDB newDB ) {
|
||||||
SwingUtilities.invokeLater( new Runnable() {
|
setLocalModDB( newDB );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modsScanEnded() {
|
||||||
|
Runnable r = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
managerLock.lock();
|
||||||
|
try {
|
||||||
rescanMenuItem.setEnabled( true );
|
rescanMenuItem.setEnabled( true );
|
||||||
|
scanning = false;
|
||||||
|
scanEndedCond.signalAll();
|
||||||
}
|
}
|
||||||
});
|
finally {
|
||||||
|
managerLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ( SwingUtilities.isEventDispatchThread() ) r.run();
|
||||||
|
else SwingUtilities.invokeLater( r );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a lock for synchronizing thread operations.
|
||||||
|
*/
|
||||||
|
public Lock getLock() {
|
||||||
|
return managerLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a condition that will signal when the "mods/" dir has been scanned.
|
||||||
|
*
|
||||||
|
* Call getLock().lock() first.
|
||||||
|
* Loop while isScanning() is true, calling this condition's await().
|
||||||
|
* Finally, call getLock().unlock().
|
||||||
|
*/
|
||||||
|
public Condition getScanEndedCondition() {
|
||||||
|
return scanEndedCond;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the "mods/" folder is currently being scanned. (thread-safe)
|
||||||
|
*/
|
||||||
|
public boolean isScanning() {
|
||||||
|
managerLock.lock();
|
||||||
|
try {
|
||||||
|
return scanning;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
managerLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNerfed( boolean b ) {
|
||||||
|
Component glassPane = this.getGlassPane();
|
||||||
|
if (b) {
|
||||||
|
glassPane.setVisible(true);
|
||||||
|
glassPane.requestFocusInWindow();
|
||||||
|
} else {
|
||||||
|
glassPane.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ModDB for local metadata. (thread-safe)
|
||||||
|
*/
|
||||||
|
public void setLocalModDB( final ModDB newDB ) {
|
||||||
|
Runnable r = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() { localModDB = newDB; }
|
||||||
|
};
|
||||||
|
if ( SwingUtilities.isEventDispatchThread() ) r.run();
|
||||||
|
else SwingUtilities.invokeLater( r );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ModDB for the catalog. (thread-safe)
|
||||||
|
*/
|
||||||
|
public void setCatalogModDB( final ModDB newDB ) {
|
||||||
|
Runnable r = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() { catalogModDB = newDB; }
|
||||||
|
};
|
||||||
|
if ( SwingUtilities.isEventDispatchThread() ) r.run();
|
||||||
|
else SwingUtilities.invokeLater( r );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets info about available app updates. (thread-safe)
|
||||||
|
*/
|
||||||
|
public void setAppUpdateInfo( final AutoUpdateInfo aui ) {
|
||||||
|
Runnable r = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
appUpdateInfo = aui;
|
||||||
|
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
|
||||||
|
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
|
||||||
|
updateBtn.setEnabled( isUpdateAvailable );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ( SwingUtilities.isEventDispatchThread() ) r.run();
|
||||||
|
else SwingUtilities.invokeLater( r );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -999,18 +994,6 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setNerfed( boolean b ) {
|
|
||||||
Component glassPane = this.getGlassPane();
|
|
||||||
if (b) {
|
|
||||||
glassPane.setVisible(true);
|
|
||||||
glassPane.requestFocusInWindow();
|
|
||||||
} else {
|
|
||||||
glassPane.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles a main window's nerfed state as popups are opened/disposed.
|
* Toggles a main window's nerfed state as popups are opened/disposed.
|
||||||
|
|
216
src/main/java/net/vhati/modmanager/ui/ManagerInitThread.java
Normal file
216
src/main/java/net/vhati/modmanager/ui/ManagerInitThread.java
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
package net.vhati.modmanager.ui;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
|
||||||
|
import net.vhati.modmanager.core.AutoUpdateInfo;
|
||||||
|
import net.vhati.modmanager.core.ModDB;
|
||||||
|
import net.vhati.modmanager.core.SlipstreamConfig;
|
||||||
|
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
|
||||||
|
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
|
||||||
|
import net.vhati.modmanager.json.URLFetcher;
|
||||||
|
import net.vhati.modmanager.ui.ManagerFrame;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs I/O-related setup for ManagerFrame in the background.
|
||||||
|
*
|
||||||
|
* Reads cached local metadata.
|
||||||
|
* Rescans the "mods/" folder.
|
||||||
|
* Reads saved catalog, and redownloads if stale.
|
||||||
|
* Reads saved info about app updates, and redownloads if stale.
|
||||||
|
*/
|
||||||
|
public class ManagerInitThread extends Thread {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(ManagerInitThread.class);
|
||||||
|
|
||||||
|
private final ManagerFrame frame;
|
||||||
|
private final SlipstreamConfig appConfig;
|
||||||
|
private final File modorderFile;
|
||||||
|
private final File metadataFile;
|
||||||
|
private final File catalogFile;
|
||||||
|
private final File catalogETagFile;
|
||||||
|
private final File appUpdateFile;
|
||||||
|
private final File appUpdateETagFile;
|
||||||
|
|
||||||
|
|
||||||
|
public ManagerInitThread( ManagerFrame frame, SlipstreamConfig appConfig, File modorderFile, File metadataFile, File catalogFile, File catalogETagFile, File appUpdateFile, File appUpdateETagFile ) {
|
||||||
|
this.frame = frame;
|
||||||
|
this.appConfig = appConfig;
|
||||||
|
this.modorderFile = modorderFile;
|
||||||
|
this.metadataFile = metadataFile;
|
||||||
|
this.catalogFile = catalogFile;
|
||||||
|
this.catalogETagFile = catalogETagFile;
|
||||||
|
this.appUpdateFile = appUpdateFile;
|
||||||
|
this.appUpdateETagFile = appUpdateETagFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
catch ( Exception e ) {
|
||||||
|
log.error( "Error during ManagerFrame init.", e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void init() throws InterruptedException {
|
||||||
|
if ( metadataFile.exists() ) {
|
||||||
|
// Load cached metadata first, before scanning for new info.
|
||||||
|
ModDB cachedDB = JacksonGrognakCatalogReader.parse( metadataFile );
|
||||||
|
if ( cachedDB != null ) frame.setLocalModDB( cachedDB );
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> preferredOrder = loadModOrder();
|
||||||
|
|
||||||
|
Lock managerLock = frame.getLock();
|
||||||
|
managerLock.lock();
|
||||||
|
try {
|
||||||
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() { frame.rescanMods( preferredOrder ); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until notified that "mods/" has been scanned.
|
||||||
|
while ( frame.isScanning() ) {
|
||||||
|
frame.getScanEndedCondition().await();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
managerLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
|
||||||
|
boolean needNewCatalog = false;
|
||||||
|
|
||||||
|
if ( catalogFile.exists() ) {
|
||||||
|
// Load the catalog first, before updating.
|
||||||
|
reloadCatalog();
|
||||||
|
|
||||||
|
if ( catalogUpdateInterval > 0 ) {
|
||||||
|
// Check if the downloaded catalog is stale.
|
||||||
|
if ( isFileStale( catalogFile, catalogUpdateInterval ) ) {
|
||||||
|
log.debug( String.format( "Catalog is older than %d days.", catalogUpdateInterval ) );
|
||||||
|
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.
|
||||||
|
if ( catalogUpdateInterval <= 0 ) needNewCatalog = false;
|
||||||
|
|
||||||
|
if ( needNewCatalog ) {
|
||||||
|
boolean fetched = URLFetcher.refetchURL( ManagerFrame.CATALOG_URL, catalogFile, catalogETagFile );
|
||||||
|
if ( fetched && catalogFile.exists() ) {
|
||||||
|
reloadCatalog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int appUpdateInterval = appConfig.getPropertyAsInt( "update_app", 0 );
|
||||||
|
boolean needAppUpdate = false;
|
||||||
|
|
||||||
|
if ( appUpdateFile.exists() ) {
|
||||||
|
// Load the info first, before downloading.
|
||||||
|
reloadAppUpdateInfo();
|
||||||
|
|
||||||
|
if ( appUpdateInterval > 0 ) {
|
||||||
|
// Check if the app update info is stale.
|
||||||
|
if ( isFileStale( appUpdateFile, catalogUpdateInterval ) ) {
|
||||||
|
log.debug( String.format( "App update info is older than %d days.", appUpdateInterval ) );
|
||||||
|
needAppUpdate = true;
|
||||||
|
} else {
|
||||||
|
log.debug( "App update info isn't stale yet." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// App update file doesn't exist.
|
||||||
|
needAppUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't update if the user doesn't want to.
|
||||||
|
if ( appUpdateInterval <= 0 ) needAppUpdate = false;
|
||||||
|
|
||||||
|
if ( needAppUpdate ) {
|
||||||
|
boolean fetched = URLFetcher.refetchURL( ManagerFrame.APP_UPDATE_URL, appUpdateFile, appUpdateETagFile );
|
||||||
|
if ( fetched && appUpdateFile.exists() ) {
|
||||||
|
reloadAppUpdateInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads modorder.txt and returns a list of mod names in preferred order.
|
||||||
|
*/
|
||||||
|
private List<String> loadModOrder() {
|
||||||
|
List<String> result = new ArrayList<String>();
|
||||||
|
|
||||||
|
FileInputStream is = null;
|
||||||
|
try {
|
||||||
|
is = new FileInputStream( modorderFile );
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader( is, Charset.forName("UTF-8") ));
|
||||||
|
String line;
|
||||||
|
while ( (line = br.readLine()) != null ) {
|
||||||
|
result.add( line );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch ( FileNotFoundException e ) {
|
||||||
|
}
|
||||||
|
catch ( IOException e ) {
|
||||||
|
log.error( String.format( "Error reading \"%s\".", modorderFile.getName() ), e );
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {if ( is != null ) is.close();}
|
||||||
|
catch ( Exception e ) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void reloadCatalog() {
|
||||||
|
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
|
||||||
|
if ( currentDB != null ) frame.setCatalogModDB( currentDB );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadAppUpdateInfo() {
|
||||||
|
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
|
||||||
|
if ( aui != null ) frame.setAppUpdateInfo( aui );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a file is older than N days.
|
||||||
|
*/
|
||||||
|
private boolean isFileStale( File f, int maxDays ) {
|
||||||
|
Date modifiedDate = new Date( f.lastModified() );
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.add( Calendar.DATE, maxDays * -1 );
|
||||||
|
return modifiedDate.before( cal.getTime() );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package net.vhati.modmanager.xml;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import net.vhati.modmanager.core.ModDB;
|
||||||
|
import net.vhati.modmanager.core.ModInfo;
|
||||||
|
import net.vhati.modmanager.core.ModUtilities;
|
||||||
|
import net.vhati.modmanager.core.ModUtilities.DecodeResult;
|
||||||
|
|
||||||
|
import org.jdom2.Document;
|
||||||
|
import org.jdom2.Element;
|
||||||
|
import org.jdom2.JDOMException;
|
||||||
|
import org.jdom2.input.SAXBuilder;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class JDOMModMetadataReader {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(JDOMModMetadataReader.class);
|
||||||
|
|
||||||
|
public static final String METADATA_INNERPATH = "mod-appendix/metadata.xml";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads metadata.xml from a mod file and returns a ModInfo object.
|
||||||
|
*
|
||||||
|
* @return the read metadata, a blank ModInfo, or null if an error occurred
|
||||||
|
*/
|
||||||
|
public static ModInfo parseModFile( File modFile ) {
|
||||||
|
ModInfo modInfo = null;
|
||||||
|
|
||||||
|
InputStream fis = null;
|
||||||
|
ZipInputStream zis = null;
|
||||||
|
Exception exception = null;
|
||||||
|
try {
|
||||||
|
fis = new FileInputStream( modFile );
|
||||||
|
zis = new ZipInputStream( new BufferedInputStream( fis ) );
|
||||||
|
ZipEntry item;
|
||||||
|
while ( (item = zis.getNextEntry()) != null ) {
|
||||||
|
if ( item.isDirectory() ) {
|
||||||
|
zis.closeEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String innerPath = item.getName();
|
||||||
|
innerPath = innerPath.replace( '\\', '/' ); // Non-standard zips.
|
||||||
|
|
||||||
|
if ( innerPath.equals( METADATA_INNERPATH ) ) {
|
||||||
|
String metadataText = ModUtilities.decodeText( zis, modFile.getName()+":"+METADATA_INNERPATH ).text;
|
||||||
|
modInfo = parse( metadataText );
|
||||||
|
zis.closeEntry();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch ( JDOMException e ) {
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
catch ( IOException e ) {
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {if ( zis != null ) zis.close();}
|
||||||
|
catch ( IOException e ) {}
|
||||||
|
|
||||||
|
try {if ( fis != null ) fis.close();}
|
||||||
|
catch ( IOException e ) {}
|
||||||
|
}
|
||||||
|
if ( exception != null ) {
|
||||||
|
log.error( String.format( "While processing \"%s:%s\", strict parsing failed: %s", modFile.getName(), METADATA_INNERPATH, exception.getMessage() ), exception );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( modInfo == null ) modInfo = new ModInfo();
|
||||||
|
return modInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a mod's metadata.xml and returns a ModInfo object.
|
||||||
|
*/
|
||||||
|
public static ModInfo parse( String metadataText ) throws IOException, JDOMException {
|
||||||
|
ModInfo modInfo = new ModInfo();
|
||||||
|
|
||||||
|
SAXBuilder strictParser = new SAXBuilder();
|
||||||
|
Document doc = strictParser.build( new StringReader( metadataText ) );
|
||||||
|
Element root = doc.getRootElement();
|
||||||
|
|
||||||
|
String modTitle = root.getChildTextTrim( "title" );
|
||||||
|
if ( modTitle != null && modTitle.length() > 0 )
|
||||||
|
modInfo.setTitle( modTitle );
|
||||||
|
else
|
||||||
|
throw new JDOMException( "Missing title." );
|
||||||
|
|
||||||
|
String modURL = root.getChildTextTrim( "threadUrl" );
|
||||||
|
if ( modURL != null && modURL.length() > 0 )
|
||||||
|
modInfo.setURL( modURL );
|
||||||
|
else
|
||||||
|
throw new JDOMException( "Missing threadUrl." );
|
||||||
|
|
||||||
|
String modAuthor = root.getChildTextTrim( "author" );
|
||||||
|
if ( modAuthor != null && modAuthor.length() > 0 )
|
||||||
|
modInfo.setAuthor( modAuthor );
|
||||||
|
else
|
||||||
|
throw new JDOMException( "Missing author." );
|
||||||
|
|
||||||
|
String modVersion = root.getChildTextTrim( "version" );
|
||||||
|
if ( modVersion != null && modVersion.length() > 0 )
|
||||||
|
modInfo.setVersion( modVersion );
|
||||||
|
else
|
||||||
|
throw new JDOMException( "Missing version." );
|
||||||
|
|
||||||
|
String modDesc = root.getChildTextTrim( "description" );
|
||||||
|
if ( modDesc != null && modDesc.length() > 0 )
|
||||||
|
modInfo.setDescription( modDesc );
|
||||||
|
else
|
||||||
|
throw new JDOMException( "Missing description." );
|
||||||
|
|
||||||
|
return modInfo;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue