diff --git a/skel_common/readme_changelog.txt b/skel_common/readme_changelog.txt index feb2da8..82cd0b5 100644 --- a/skel_common/readme_changelog.txt +++ b/skel_common/readme_changelog.txt @@ -1,13 +1,17 @@ Changelog +???: +- Added LF to CR-LF conversion for *.xml.append, *.xml, and *.txt +- Added a Validate warning for text files with LF line endings + 1.0: - Changed mod list to a table with checkboxes - Instead of extracting to temp, mod data is transferred directly into dats - Added a GUI progress bar during patching - Added a Validate warning for paths with non-ASCII chars +- Added support for windows-1252 ANSI and UTF-16 text in mods Changes shared with Grognaks Mod Manager 1.8: -- Added support for windows-1252 ANSI and UTF-16 text in mods - Added periodic updates to the catalog of mod metadata - Added ini setting: update_catalog - Added a log warning during patching if a mod gets clobbered diff --git a/skel_common/readme_modders.txt b/skel_common/readme_modders.txt index 726a75e..acc9036 100644 --- a/skel_common/readme_modders.txt +++ b/skel_common/readme_modders.txt @@ -30,17 +30,22 @@ The Append Extension to your pleasure by writing an event of the same name. Whenever multiple tags share the same name, only the last one counts. + When you're not overriding something, try to use unique names, so that + it won't clobber another mod and vice versa. + General When developing a mod, save your text files as ANSI/ASCII, or UTF-8. - UTF-16 is tolerated. If all else fails, Slipstream will try decoding - text as Windows-1252 ANSI. + Slipstream will tolerate UTF-16 and Windows-1252 ANSI. - Unless you're overriding something, try to use unique names in your xml - so that it won't clobber another mod and vice versa. File and directory - names must be plain ASCII (no accents). That restriction isn't confirmed - for the game, but the mod manager enforces it just to be safe. + Dos style (CR-LF) line endings are preferred. The game only partially + accepts the unix style (LF): fine for xml, crashing for layout.txt. + Slipstream will convert both to CR-LF as it patches. + + File and directory names must be plain ASCII (no accents). That + restriction isn't confirmed for the game, but the mod manager enforces + it just to be safe. Images should be 32bit PNGs (24bit color + 8bit alpha transparency). Things that *should* be opaque rectangles like backgrounds may vary, diff --git a/src/main/java/net/vhati/modmanager/FTLModManager.java b/src/main/java/net/vhati/modmanager/FTLModManager.java index c35e260..22f871b 100644 --- a/src/main/java/net/vhati/modmanager/FTLModManager.java +++ b/src/main/java/net/vhati/modmanager/FTLModManager.java @@ -26,7 +26,7 @@ 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" ); + private static final ComparableVersion APP_VERSION = new ComparableVersion( "???" ); private static final String APP_URL = "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=17102"; private static final String APP_AUTHOR = "Vhati"; @@ -34,8 +34,8 @@ public class FTLModManager { 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") ); + log.debug( String.format( "%s %s", System.getProperty("os.name"), System.getProperty("os.version") ) ); + log.debug( String.format( "%s, %s, %s", System.getProperty("java.vm.name"), System.getProperty("java.version"), System.getProperty("os.arch") ) ); File configFile = new File( "modman.cfg" ); @@ -154,7 +154,9 @@ public class FTLModManager { 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 ); + OutputStreamWriter writer = new OutputStreamWriter( out, "UTF-8" ); + config.store( writer, configComments ); + writer.flush(); } catch ( IOException e ) { log.error( "Error saving config to "+ configFile.getPath(), e ); diff --git a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java index d46aa25..4940364 100644 --- a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java +++ b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java @@ -233,6 +233,21 @@ public class ModPatchThread extends Thread { moddedItems.add( innerPath ); } } + else if ( fileName.endsWith( ".xml" ) || fileName.endsWith( ".txt" ) ) { + String innerPath = checkCase( item.getName(), knownPaths, knownPathsLower ); + + // Normalize line endings for other text files to CR-LF. + InputStream fixedStream = ModUtilities.setLineEndings( zis, "\r\n", modFile.getName()+":"+parentPath+fileName ); + + 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, fixedStream ); + } else { String innerPath = checkCase( item.getName(), knownPaths, knownPathsLower ); diff --git a/src/main/java/net/vhati/modmanager/core/ModUtilities.java b/src/main/java/net/vhati/modmanager/core/ModUtilities.java index e5a3d1a..0d637ca 100644 --- a/src/main/java/net/vhati/modmanager/core/ModUtilities.java +++ b/src/main/java/net/vhati/modmanager/core/ModUtilities.java @@ -112,8 +112,17 @@ public class ModUtilities { } } + // Determine the original line endings. + int eol = DecodeResult.EOL_NONE; + Matcher m = Pattern.compile( "(\r(?!\n))|((?\n" ); + buf.append( dstText ); + buf.append( "\n\n\n\n" ); + buf.append( srcText ); + buf.append( "\n" ); + + String mergedString = Pattern.compile("\n").matcher( buf ).replaceAll("\r\n"); + ByteArrayOutputStream tmpData = new ByteArrayOutputStream(); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, "UTF-8" ) ); - - bw.write( "\n" ); - bw.write( dstText ); - bw.write( "\n\n\n\n"); - bw.write( srcText ); - bw.write( "\n" ); + bw.write( mergedString ); bw.flush(); InputStream result = new ByteArrayInputStream( tmpData.toByteArray() ); return result; } + + /** + * Calls decodeText() on a stream, replaces line endings, and re-encodes. + * + * The returned stream is a ByteArrayInputStream + * which doesn't need closing. + * + * The result will be UTF-8 with the desired line endings. + * + * The description argument identifies the stream for log messages. + */ + public static InputStream setLineEndings( InputStream srcStream, String eol, String srcDescription ) throws IOException { + // decodeText() returns a LF string. + String srcText = decodeText( srcStream, srcDescription ).text; + String fixedText = Pattern.compile("\n").matcher( srcText ).replaceAll( Matcher.quoteReplacement(eol) ); + + ByteArrayOutputStream tmpData = new ByteArrayOutputStream(); + BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, "UTF-8" ) ); + bw.write( fixedText ); + bw.flush(); + + InputStream result = new ByteArrayInputStream( tmpData.toByteArray() ); + return result; + } + + /** * Checks a mod file for common problems. * @@ -215,7 +256,7 @@ public class ModUtilities { ) ); modValid = false; } - else if ( innerPath.endsWith( ".png" ) ) { + else if ( innerPath.endsWith( "[.]png" ) ) { try { PngReader pngr = new PngReader( zis ); @@ -249,9 +290,7 @@ public class ModUtilities { modValid = false; } } - else if ( innerPath.matches( "^.*(?:.xml.append|.append.xml|.xml)$" ) ) { - if ( innerPath.matches( "^.*(?:.xml.append|.append.xml)$" ) ) - seenAppend = true; + else if ( innerPath.matches( "^.*(?:[.]xml[.]append|[.]append[.]xml|[.]xml|[.]txt)$" ) ) { DecodeResult decodeResult = ModUtilities.decodeText( zis, modFile.getName()+":"+innerPath ); @@ -263,6 +302,15 @@ public class ModUtilities { modValid = false; } + if ( decodeResult.eol != DecodeResult.EOL_CRLF && + decodeResult.eol != DecodeResult.EOL_NONE ) { + pendingMsgs.add( new ReportMessage( + ReportMessage.ERROR, + String.format( "%s line endings (CR-LF is safest)", decodeResult.getEOLName() ) + ) ); + modValid = false; + } + List oddCharPtns = new ArrayList(); Map oddCharSuggestions = new HashMap(); Map> oddCharLists = new HashMap>(); @@ -310,17 +358,22 @@ public class ModUtilities { // TODO: Nag if there are chars FTL can't show. - Report xmlReport = validateModXML( decodeResult.text, formatter ); + if ( innerPath.matches( "^.*(?:[.]xml[.]append|[.]append[.]xml|[.]xml)$" ) ) { + if ( innerPath.matches( "^.*(?:[.]xml[.]append|[.]append[.]xml)$" ) ) + seenAppend = true; - if ( xmlReport.text.length() > 0 ) { - pendingMsgs.add( new ReportMessage( - ReportMessage.NESTED_BLOCK, - xmlReport.text - ) ); + 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 ( xmlReport.outcome == false ) - modValid = false; } if ( !pendingMsgs.isEmpty() ) { @@ -644,17 +697,32 @@ public class ModUtilities { * * text - The decoded string. * encoding - The encoding used. + * eol - A constant describing the original line endings. * bom - The BOM bytes found, or null. */ public static class DecodeResult { + public static final int EOL_NONE = 0; + public static final int EOL_CRLF = 1; + public static final int EOL_LF = 2; + public static final int EOL_CR = 3; + public final String text; public final String encoding; + public final int eol; public final byte[] bom; - public DecodeResult( String text, String encoding, byte[] bom ) { + public DecodeResult( String text, String encoding, int eol, byte[] bom ) { this.text = text; this.encoding = encoding; + this.eol = eol; this.bom = bom; } + + public String getEOLName() { + if ( eol == EOL_CRLF ) return "CR-LF"; + if ( eol == EOL_LF ) return "LF"; + if ( eol == EOL_CR ) return "CR"; + return "None"; + } } }