Index: AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java	(revision 673)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java	(revision 698)
@@ -19,4 +19,5 @@
 import java.util.TreeSet;
 import java.util.Vector;
+import java.util.regex.Pattern;
 
 import net.oni2.aeinstaller.AEInstaller2;
@@ -29,4 +30,6 @@
 
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.RegexFileFilter;
+import org.apache.commons.io.filefilter.TrueFileFilter;
 import org.javabuilders.swing.SwingJavaBuilder;
 
@@ -260,5 +263,4 @@
 						FileUtils.deleteDirectory(f);
 					} catch (IOException e) {
-						// TODO Auto-generated catch block
 						e.printStackTrace();
 					}
@@ -274,4 +276,6 @@
 		Vector<File> foldersOni = new Vector<File>();
 		foldersOni.add(Paths.getVanillaOnisPath());
+
+		Vector<File> foldersPatches = new Vector<File>();
 
 		for (Package m : mods) {
@@ -296,7 +300,45 @@
 				}
 			}
-		}
-
-		combineBinaryFiles(foldersOni, listener, log);
+
+			File patches = new File(m.getLocalPath(), "patches");
+			if (patches.exists()) {
+				if (m.hasSeparatePlatformDirs()) {
+					File patchesCommon = new File(patches, "common");
+					File patchesMac = new File(patches, "mac_only");
+					File patchesWin = new File(patches, "win_only");
+					if (patchesCommon.exists())
+						foldersPatches.add(patchesCommon);
+					if (Settings.getPlatform() == Platform.MACOS
+							&& patchesMac.exists())
+						foldersPatches.add(patchesMac);
+					else if (patchesWin.exists())
+						foldersPatches.add(patchesWin);
+				} else {
+					foldersPatches.add(patches);
+				}
+			}
+		}
+
+		TreeMap<String, Vector<File>> levels = new TreeMap<String, Vector<File>>();
+		for (File path : foldersOni) {
+			for (File levelF : path.listFiles()) {
+				String fn = levelF.getName().toLowerCase();
+				String levelN = null;
+				if (levelF.isDirectory()) {
+					levelN = fn;
+				} else if (fn.endsWith(".dat")) {
+					levelN = fn.substring(0, fn.lastIndexOf('.')).toLowerCase();
+				}
+				if (levelN != null) {
+					if (!levels.containsKey(levelN))
+						levels.put(levelN, new Vector<File>());
+					levels.get(levelN).add(levelF);
+				}
+			}
+		}
+
+		applyPatches(levels, foldersPatches, listener, log);
+
+		combineBinaryFiles(levels, listener, log);
 		combineBSLFolders(mods, listener, log);
 
@@ -417,44 +459,128 @@
 	}
 
-	private static void combineBinaryFiles(List<File> srcFoldersFiles,
+	private static void applyPatches(
+			TreeMap<String, Vector<File>> oniLevelFolders,
+			List<File> patchFolders, InstallProgressListener listener,
+			PrintWriter log) {
+		// TODO: REMOVE
+		long startMS = new Date().getTime();
+		// TODO: Implement this
+		String tmpFolderName = "installrun_temp-"
+				+ new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss")
+						.format(new Date());
+		File tmpFolder = new File(Paths.getTempPath(), tmpFolderName);
+
+		HashMap<String, Vector<File>> patches = new HashMap<String, Vector<File>>();
+		for (File patchFolder : patchFolders) {
+			for (File levelFolder : patchFolder.listFiles(dirFileFilter)) {
+				String lvlName = levelFolder.getName().toLowerCase();
+				for (File f : FileUtils.listFiles(levelFolder,
+						new String[] { "oni-patch" }, true)) {
+					if (!patches.containsKey(lvlName))
+						patches.put(lvlName, new Vector<File>());
+					patches.get(lvlName).add(f);
+				}
+			}
+		}
+
+		for (String level : patches.keySet()) {
+			File levelFolder = new File(tmpFolder, level);
+			levelFolder.mkdir();
+
+			// Get files to be patched from vanilla.dat / packages
+			for (File patch : patches.get(level)) {
+				String patternWildcard = patch.getName();
+				patternWildcard = patternWildcard.substring(0,
+						patternWildcard.indexOf(".oni-patch"));
+				patternWildcard = patternWildcard.replace('-', '*');
+				final Pattern patternRegex = Pattern.compile(
+						patternWildcard.replaceAll("\\*", ".\\*"),
+						Pattern.CASE_INSENSITIVE);
+
+				for (File srcFolder : oniLevelFolders.get(level)) {
+					if (srcFolder.isFile()) {
+						// Extract from .dat
+						OniSplit.export(levelFolder, srcFolder, patternWildcard);
+					} else {
+						// Copy from folder with overwrite
+						for (File f : FileUtils.listFiles(srcFolder,
+								new RegexFileFilter(patternRegex),
+								TrueFileFilter.TRUE)) {
+							try {
+								FileUtils.copyFileToDirectory(f, levelFolder);
+							} catch (IOException e) {
+								e.printStackTrace();
+							}
+						}
+					}
+				}
+			}
+
+			// Extract files to XML
+			File levelFolderXML = new File(levelFolder, "xml");
+			Vector<File> files = new Vector<File>();
+			files.add(new File(levelFolder, "*.oni"));
+			OniSplit.convertOniToXML(levelFolderXML, files);
+
+			// Apply patches in levelFolderXML
+			for (File patch : patches.get(level)) {
+				String patternWildcard = patch.getName();
+				patternWildcard = patternWildcard.substring(0,
+						patternWildcard.indexOf(".oni-patch"));
+				patternWildcard = patternWildcard.replace('-', '*');
+				final Pattern patternRegex = Pattern.compile(
+						patternWildcard.replaceAll("\\*", ".\\*"),
+						Pattern.CASE_INSENSITIVE);
+
+				for (File toPatch : FileUtils.listFiles(levelFolderXML,
+						new RegexFileFilter(patternRegex), null)) {
+					// Apply patch "patch" to "toPatch"
+					XMLTools.patch(patch, toPatch);
+				}
+			}
+
+			// Create .oni files from XML
+			files.clear();
+			files.add(new File(levelFolderXML, "*.xml"));
+			OniSplit.convertXMLtoOni(levelFolder, files);
+
+			// Remove XML folder as import will only require .oni's
+			try {
+				FileUtils.deleteDirectory(levelFolderXML);
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+			
+			oniLevelFolders.get(level).add(levelFolder);
+		}
+
+		System.out.println("Took: " + (new Date().getTime() - startMS)
+				+ " msec");
+		System.exit(0);
+	}
+
+	private static void combineBinaryFiles(
+			TreeMap<String, Vector<File>> oniLevelFolders,
 			InstallProgressListener listener, PrintWriter log) {
-		TreeMap<String, Vector<File>> levels = new TreeMap<String, Vector<File>>();
-
-		for (File path : srcFoldersFiles) {
-			for (File levelF : path.listFiles()) {
-				String fn = levelF.getName().toLowerCase();
-				String levelN = null;
-				if (levelF.isDirectory()) {
-					levelN = fn;
-				} else if (fn.endsWith(".dat")) {
-					levelN = fn.substring(0, fn.lastIndexOf('.'));
-				}
-				if (levelN != null) {
-					if (!levels.containsKey(levelN))
-						levels.put(levelN, new Vector<File>());
-					levels.get(levelN).add(levelF);
-				}
-			}
-		}
-
 		int totalSteps = 0;
 		int stepsDone = 0;
 
 		for (@SuppressWarnings("unused")
-		String s : levels.keySet())
+		String s : oniLevelFolders.keySet())
 			totalSteps++;
 		totalSteps++;
 
 		log.println("Importing levels");
-		for (String l : levels.keySet()) {
+		for (String l : oniLevelFolders.keySet()) {
 			log.println("\tLevel " + l);
 			listener.installProgressUpdate(stepsDone, totalSteps,
 					"Installing level " + l);
-			for (File f : levels.get(l)) {
+			for (File f : oniLevelFolders.get(l)) {
 				log.println("\t\t\t" + f.getPath());
 			}
 
-			Vector<String> res = OniSplit.packLevel(levels.get(l), new File(
-					Paths.getEditionGDF(), sanitizeLevelName(l) + ".dat"));
+			Vector<String> res = OniSplit.packLevel(oniLevelFolders.get(l),
+					new File(Paths.getEditionGDF(), sanitizeLevelName(l)
+							+ ".dat"));
 			if (res != null && res.size() > 0) {
 				for (String s : res)
@@ -502,5 +628,4 @@
 				FileUtils.copyInputStreamToFile(is, dat);
 			} catch (IOException e) {
-				// TODO Auto-generated catch block
 				e.printStackTrace();
 			}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/oni/OniSplit.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/oni/OniSplit.java	(revision 673)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/oni/OniSplit.java	(revision 698)
@@ -33,9 +33,28 @@
 	 */
 	public static Vector<String> export(File targetFolder, File input) {
+		return export(targetFolder, input, null);
+	}
+
+	/**
+	 * Export named resources from given dat-file to target folder
+	 * 
+	 * @param targetFolder
+	 *            Target folder
+	 * @param input
+	 *            Dat file
+	 * @param pattern
+	 *            Filename pattern for files to export
+	 * @return OniSplit output
+	 */
+	public static Vector<String> export(File targetFolder, File input,
+			String pattern) {
 		if (!targetFolder.exists())
 			targetFolder.mkdir();
 
 		Vector<String> cmdLine = getProgramInvocation();
-		cmdLine.add("-export");
+		if (pattern == null)
+			cmdLine.add("-export");
+		else
+			cmdLine.add("-export:" + pattern);
 		cmdLine.add(targetFolder.getPath());
 		cmdLine.add(input.getPath());
@@ -134,4 +153,62 @@
 	}
 
+	/**
+	 * Convert given .oni-files to XML and put them in target folder
+	 * 
+	 * @param targetFolder
+	 *            Target folder
+	 * @param inputFiles
+	 *            .oni files
+	 * @return OniSplit output
+	 */
+	public static Vector<String> convertOniToXML(File targetFolder,
+			Vector<File> inputFiles) {
+		if (!targetFolder.exists())
+			targetFolder.mkdirs();
+
+		Vector<String> cmdLine = getProgramInvocation();
+		cmdLine.add("-extract:xml");
+		cmdLine.add(targetFolder.getPath());
+		for (File f : inputFiles) {
+			cmdLine.add(f.getPath());
+		}
+		Vector<String> res = null;
+		try {
+			res = AppExecution.executeAndWait(cmdLine);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return res;
+	}
+
+	/**
+	 * Convert given XML-files to .oni and put them in target folder
+	 * 
+	 * @param targetFolder
+	 *            Target folder
+	 * @param inputFiles
+	 *            XML-files
+	 * @return OniSplit output
+	 */
+	public static Vector<String> convertXMLtoOni(File targetFolder,
+			Vector<File> inputFiles) {
+		if (!targetFolder.exists())
+			targetFolder.mkdirs();
+
+		Vector<String> cmdLine = getProgramInvocation();
+		cmdLine.add("-create");
+		cmdLine.add(targetFolder.getPath());
+		for (File f : inputFiles) {
+			cmdLine.add(f.getPath());
+		}
+		Vector<String> res = null;
+		try {
+			res = AppExecution.executeAndWait(cmdLine);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return res;
+	}
+
 	private static String getImportParam() {
 		if (Settings.getPlatform() == Platform.MACOS)
Index: AE/installer2/src/net/oni2/aeinstaller/backend/oni/XMLTools.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/oni/XMLTools.java	(revision 698)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/oni/XMLTools.java	(revision 698)
@@ -0,0 +1,53 @@
+package net.oni2.aeinstaller.backend.oni;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Vector;
+
+import net.oni2.aeinstaller.backend.AppExecution;
+import net.oni2.aeinstaller.backend.DotNet;
+import net.oni2.aeinstaller.backend.Paths;
+
+/**
+ * @author Christian Illy
+ */
+public class XMLTools {
+
+	/**
+	 * Patch the given XML file with the given patch
+	 * 
+	 * @param patch
+	 *            Patchfile
+	 * @param source
+	 *            File to patch
+	 * 
+	 * @return XMLTools output
+	 */
+	public static Vector<String> patch(File patch, File source) {
+		Vector<String> cmdLine = getProgramInvocation();
+		// xmlTools.exe patchfile -filename:PATCH -forceinfiles:TOPATCH
+		cmdLine.add("patchfile");
+		cmdLine.add("-filename:" + patch.getPath());
+		cmdLine.add("-forceinfiles:" + source.getPath());
+		Vector<String> res = null;
+		try {
+			res = AppExecution.executeAndWait(cmdLine);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return res;
+	}
+
+	private static Vector<String> getProgramInvocation() {
+		Vector<String> res = new Vector<String>();
+		if (DotNet.getRuntimeExe().length() > 0)
+			res.add(DotNet.getRuntimeExe());
+		res.add(getProgramFile().getPath());
+		return res;
+	}
+
+	private static File getProgramFile() {
+		return new File(new File(Paths.getEditionBasePath(), "Tools"),
+				"xmlTools.exe");
+	}
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java	(revision 673)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java	(revision 698)
@@ -82,5 +82,5 @@
 
 	private int getTimeElapsed() {
-		int total = (int) (new Date().getTime() - startMS) / 1000;
+		int total = (int) (new Date().getTime() - startMS);
 		return total;
 	}
@@ -88,7 +88,7 @@
 	private int getDownloadSpeed() {
 		int elap = getTimeElapsed();
-		int down = downloadedComplete + downloadedCurrent;
+		long down = downloadedComplete + downloadedCurrent;
 		if (elap > 0)
-			return down / elap;
+			return (int)(down * 1000 / elap);
 		else
 			return 1;
@@ -106,10 +106,10 @@
 					downloads.get(currentDownload).getMod(), state, unpacked,
 					downloads.size(), downloadedComplete + downloadedCurrent,
-					totalSize, getTimeElapsed(), getTimeRemaining(),
+					totalSize, getTimeElapsed() / 1000, getTimeRemaining(),
 					getDownloadSpeed());
 		} else {
 			listener.updateStatus(this, null, state, unpacked,
 					downloads.size(), downloadedComplete + downloadedCurrent,
-					totalSize, getTimeElapsed(), getTimeRemaining(),
+					totalSize, getTimeElapsed() / 1000, getTimeRemaining(),
 					getDownloadSpeed());
 		}
