Index: AE/installer2/src/net/oni2/aeinstaller/backend/depot/DepotConfig.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/depot/DepotConfig.java	(revision 647)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/depot/DepotConfig.java	(revision 648)
@@ -77,8 +77,8 @@
 
 	/**
-	 * @return First package number that's not a mandatory tool/mod. Everything
-	 *         below is considered mandatory
+	 * @return First package number that's not a core tool/mod. Everything
+	 *         below is considered a core package
 	 */
-	public static int getMandatoryLimit() {
+	public static int getCoreNumberLimit() {
 		return 8000;
 	}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/depot/model/NodeMod.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/depot/model/NodeMod.java	(revision 647)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/depot/model/NodeMod.java	(revision 648)
@@ -7,5 +7,5 @@
 import net.oni2.aeinstaller.backend.depot.DepotConfig;
 import net.oni2.aeinstaller.backend.depot.DepotManager;
-import net.oni2.aeinstaller.backend.mods.ECompatiblePlatform;
+import net.oni2.aeinstaller.backend.packages.ECompatiblePlatform;
 
 import org.json.JSONArray;
Index: AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java	(revision 647)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/oni/Installer.java	(revision 648)
@@ -24,7 +24,7 @@
 import net.oni2.aeinstaller.backend.Settings;
 import net.oni2.aeinstaller.backend.Settings.Platform;
-import net.oni2.aeinstaller.backend.mods.EBSLInstallType;
-import net.oni2.aeinstaller.backend.mods.Mod;
-import net.oni2.aeinstaller.backend.mods.ModManager;
+import net.oni2.aeinstaller.backend.packages.EBSLInstallType;
+import net.oni2.aeinstaller.backend.packages.Package;
+import net.oni2.aeinstaller.backend.packages.PackageManager;
 
 import org.apache.commons.io.FileUtils;
@@ -62,5 +62,5 @@
 	public static Vector<Integer> getInstalledMods() {
 		File installCfg = new File(Paths.getEditionGDF(), "installed_mods.xml");
-		return ModManager.getInstance().loadModSelection(installCfg);
+		return PackageManager.getInstance().loadModSelection(installCfg);
 	}
 
@@ -109,7 +109,7 @@
 	 *            Tools to install
 	 */
-	public static void installTools(TreeSet<Mod> tools) {
+	public static void installTools(TreeSet<Package> tools) {
 		TreeSet<Integer> installed = getInstalledTools();
-		for (Mod m : tools) {
+		for (Package m : tools) {
 			File plain = new File(m.getLocalPath(), "plain");
 			if (plain.exists()) {
@@ -138,7 +138,7 @@
 	 *            Tools to uninstall
 	 */
-	public static void uninstallTools(TreeSet<Mod> tools) {
+	public static void uninstallTools(TreeSet<Package> tools) {
 		TreeSet<Integer> installed = getInstalledTools();
-		for (Mod m : tools) {
+		for (Package m : tools) {
 			if (installed.contains(m.getPackageNumber())) {
 				File plain = new File(m.getLocalPath(), "plain");
@@ -205,5 +205,5 @@
 	 *            Listener for install progress updates
 	 */
-	public static void install(TreeSet<Mod> mods,
+	public static void install(TreeSet<Package> mods,
 			InstallProgressListener listener) {
 		try {
@@ -214,5 +214,5 @@
 
 		File installCfg = new File(Paths.getEditionGDF(), "installed_mods.xml");
-		ModManager.getInstance().saveModSelection(installCfg, mods);
+		PackageManager.getInstance().saveModSelection(installCfg, mods);
 
 		HashSet<Integer> unlockLevels = new HashSet<Integer>();
@@ -221,5 +221,5 @@
 		foldersOni.add(Paths.getVanillaOnisPath());
 
-		for (Mod m : mods) {
+		for (Package m : mods) {
 			for (int lev : m.getUnlockLevels())
 				unlockLevels.add(lev);
@@ -253,13 +253,13 @@
 	}
 
-	private static void combineBSLFolders(TreeSet<Mod> mods,
+	private static void combineBSLFolders(TreeSet<Package> mods,
 			InstallProgressListener listener) {
 		listener.installProgressUpdate(95, 100, "Installing BSL files");
 
-		HashMap<EBSLInstallType, Vector<Mod>> modsToInclude = new HashMap<EBSLInstallType, Vector<Mod>>();
-		modsToInclude.put(EBSLInstallType.NORMAL, new Vector<Mod>());
-		modsToInclude.put(EBSLInstallType.ADDON, new Vector<Mod>());
-
-		for (Mod m : mods.descendingSet()) {
+		HashMap<EBSLInstallType, Vector<Package>> modsToInclude = new HashMap<EBSLInstallType, Vector<Package>>();
+		modsToInclude.put(EBSLInstallType.NORMAL, new Vector<Package>());
+		modsToInclude.put(EBSLInstallType.ADDON, new Vector<Package>());
+
+		for (Package m : mods.descendingSet()) {
 			File bsl = new File(m.getLocalPath(), "bsl");
 			if (bsl.exists()) {
@@ -281,8 +281,8 @@
 		}
 
-		for (Mod m : modsToInclude.get(EBSLInstallType.NORMAL)) {
+		for (Package m : modsToInclude.get(EBSLInstallType.NORMAL)) {
 			copyBSL(m, false);
 		}
-		Vector<Mod> addons = modsToInclude.get(EBSLInstallType.ADDON);
+		Vector<Package> addons = modsToInclude.get(EBSLInstallType.ADDON);
 		for (int i = addons.size() - 1; i >= 0; i--) {
 			copyBSL(addons.get(i), true);
@@ -290,5 +290,5 @@
 	}
 
-	private static void copyBSL(Mod sourceMod, boolean addon) {
+	private static void copyBSL(Package sourceMod, boolean addon) {
 		File targetBaseFolder = new File(Paths.getEditionGDF(), "IGMD");
 		if (!targetBaseFolder.exists())
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/EBSLInstallType.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/EBSLInstallType.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/EBSLInstallType.java	(revision 648)
@@ -0,0 +1,15 @@
+package net.oni2.aeinstaller.backend.packages;
+
+/**
+ * @author Christian Illy
+ */
+public enum EBSLInstallType {
+	/**
+	 * Normal BSL install mode
+	 */
+	NORMAL,
+	/**
+	 * BSL addon install mode
+	 */
+	ADDON
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/ECompatiblePlatform.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/ECompatiblePlatform.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/ECompatiblePlatform.java	(revision 648)
@@ -0,0 +1,19 @@
+package net.oni2.aeinstaller.backend.packages;
+
+/**
+ * @author Christian Illy
+ */
+public enum ECompatiblePlatform {
+	/**
+	 * Only for Win
+	 */
+	WIN,
+	/**
+	 * Only for MacOS
+	 */
+	MACOS,
+	/**
+	 * Usable with both platforms
+	 */
+	BOTH
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/Mod_Info.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/Mod_Info.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/Mod_Info.java	(revision 648)
@@ -0,0 +1,210 @@
+package net.oni2.aeinstaller.backend.packages;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+
+import net.oni2.aeinstaller.backend.Paths;
+
+/**
+ * @author Christian Illy
+ */
+public class Mod_Info {
+	private double aeVersion = 0;
+	private String name = "";
+	private String creator = "";
+	private EBSLInstallType bslInstallType = EBSLInstallType.NORMAL;
+	private String version = "";
+	private String description = "";
+
+	private HashSet<Integer> incompatibilities = new HashSet<Integer>();
+	private HashSet<Integer> dependencies = new HashSet<Integer>();
+	private HashSet<Integer> unlockLevel = new HashSet<Integer>();
+
+	private File exeFile = null;
+	private File iconFile = null;
+	private String workingDir = "Base";
+
+	/**
+	 * @param f
+	 *            Mod_Info.cfg
+	 * @param packageNumber
+	 *            Package number this Mod_Info belongs to
+	 */
+	public Mod_Info(File f, int packageNumber) {
+		InputStreamReader isr = null;
+		try {
+			FileInputStream fstream = new FileInputStream(f);
+			isr = new InputStreamReader(fstream);
+			BufferedReader br = new BufferedReader(isr);
+			String strLine;
+			while ((strLine = br.readLine()) != null) {
+				if (strLine.indexOf("->") < 1)
+					continue;
+				if (strLine.indexOf("//") >= 0)
+					strLine = strLine.substring(0, strLine.indexOf("//"));
+				String[] split = strLine.split("->", 2);
+				String sName = split[0].trim();
+				String sVal = split[1].trim();
+				if (sName.equalsIgnoreCase("AEInstallVersion")) {
+					aeVersion = Double.parseDouble(sVal);
+				} else if (sName.equalsIgnoreCase("NameOfMod")) {
+					name = sVal;
+				} else if (sName.equalsIgnoreCase("Creator")) {
+					creator = sVal;
+				} else if (sName.equalsIgnoreCase("HasBsl")) {
+					if (sVal.equalsIgnoreCase("addon"))
+						bslInstallType = EBSLInstallType.ADDON;
+				} else if (sName.equalsIgnoreCase("ModVersion")) {
+					version = sVal;
+				} else if (sName.equalsIgnoreCase("Readme")) {
+					description = "<p>" + sVal.replaceAll("\\\\n", "<br>")
+							+ "</p>";
+				} else if (sName.equalsIgnoreCase("DependsOn")) {
+					String[] depsS = sVal.split(",");
+					for (String s : depsS) {
+						try {
+							int dep = Integer.parseInt(s);
+							dependencies.add(dep);
+						} catch (NumberFormatException e) {
+							System.err
+									.format("Mod_Info of %05d does contain a non-number dependency: '%s'\n",
+											packageNumber, s);
+						}
+					}
+				} else if (sName.equalsIgnoreCase("IncompatibleWith")) {
+					String[] confS = sVal.split(",");
+					for (String s : confS) {
+						try {
+							int conf = Integer.parseInt(s);
+							incompatibilities.add(conf);
+						} catch (NumberFormatException e) {
+							System.err
+									.format("Mod_Info of %05d does contain a non-number incompatibility: '%s'\n",
+											packageNumber, s);
+						}
+					}
+				} else if (sName.equalsIgnoreCase("UnlockLevel")) {
+					String[] levelsS = sVal.split(",");
+					for (String s : levelsS) {
+						try {
+							int level = Integer.parseInt(s);
+							unlockLevel.add(level);
+						} catch (NumberFormatException e) {
+							System.err
+									.format("Mod_Info of %05d does contain a non-number UnlockLevel value: '%s'\n",
+											packageNumber, s);
+						}
+					}
+				} else if (sName.equalsIgnoreCase("ExeName")) {
+					exeFile = new File(Paths.getEditionBasePath(), sVal);
+				} else if (sName.equalsIgnoreCase("WorkingDir")) {
+					workingDir = sVal;
+				} else if (sName.equalsIgnoreCase("IconName")) {
+					iconFile = new File(Paths.getEditionBasePath(), sVal);
+				}
+			}
+		} catch (FileNotFoundException e) {
+		} catch (IOException e) {
+			e.printStackTrace();
+		} finally {
+			if (isr != null) {
+				try {
+					isr.close();
+				} catch (IOException e) {
+					// TODO Auto-generated catch block
+					e.printStackTrace();
+				}
+			}
+		}
+	}
+
+	/**
+	 * @return the aeVersion
+	 */
+	public double getAeVersion() {
+		return aeVersion;
+	}
+
+	/**
+	 * @return the name
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @return the creator
+	 */
+	public String getCreator() {
+		return creator;
+	}
+
+	/**
+	 * @return the bslInstallType
+	 */
+	public EBSLInstallType getBslInstallType() {
+		return bslInstallType;
+	}
+
+	/**
+	 * @return the version
+	 */
+	public String getVersion() {
+		return version;
+	}
+
+	/**
+	 * @return the description
+	 */
+	public String getDescription() {
+		return description;
+	}
+
+	/**
+	 * @return the incompatibilities
+	 */
+	public HashSet<Integer> getIncompatibilities() {
+		return incompatibilities;
+	}
+
+	/**
+	 * @return the dependencies
+	 */
+	public HashSet<Integer> getDependencies() {
+		return dependencies;
+	}
+
+	/**
+	 * @return the unlockLevel
+	 */
+	public HashSet<Integer> getUnlockLevel() {
+		return unlockLevel;
+	}
+
+	/**
+	 * @return the exeFile
+	 */
+	public File getExeFile() {
+		return exeFile;
+	}
+
+	/**
+	 * @return the iconFile
+	 */
+	public File getIconFile() {
+		return iconFile;
+	}
+
+	/**
+	 * @return the workingDir
+	 */
+	public String getWorkingDir() {
+		return workingDir;
+	}
+
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/Package.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/Package.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/Package.java	(revision 648)
@@ -0,0 +1,405 @@
+package net.oni2.aeinstaller.backend.packages;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashSet;
+
+import net.oni2.aeinstaller.backend.Paths;
+import net.oni2.aeinstaller.backend.Settings;
+import net.oni2.aeinstaller.backend.Settings.Platform;
+import net.oni2.aeinstaller.backend.depot.DepotConfig;
+import net.oni2.aeinstaller.backend.depot.DepotManager;
+import net.oni2.aeinstaller.backend.depot.model.NodeMod;
+import net.oni2.aeinstaller.backend.depot.model.TaxonomyTerm;
+import net.oni2.aeinstaller.backend.oni.Installer;
+
+/**
+ * @author Christian Illy
+ */
+public class Package implements Comparable<Package> {
+	private String name = "";
+	private int packageNumber = 0;
+
+	private HashSet<Type> types = new HashSet<Type>();
+	private boolean tool = false;
+	private ECompatiblePlatform platform = null;
+	private String version = "";
+	private String submitter = "";
+	private String creator = "";
+	private EBSLInstallType bslInstallType = EBSLInstallType.NORMAL;
+	private String description = "";
+	private double aeVersion = 0;
+	private int zipSize = 0;
+	private NodeMod node = null;
+	private net.oni2.aeinstaller.backend.depot.model.File file = null;
+
+	private File exeFile = null;
+	private File iconFile = null;
+	private String workingDir = "Base";
+
+	private HashSet<Integer> incompatibilities = new HashSet<Integer>();
+	private HashSet<Integer> dependencies = new HashSet<Integer>();
+	private HashSet<Integer> unlockLevel = new HashSet<Integer>();
+
+	private long localTimestamp = 0;
+
+	/**
+	 * Create a new Package entry from a given Mod-Node
+	 * 
+	 * @param nm
+	 *            Mod-Node
+	 */
+	public Package(NodeMod nm) {
+		node = nm;
+		name = nm.getTitle();
+		packageNumber = nm.getPackageNumber();
+		platform = nm.getPlatform();
+		tool = nm.isTool();
+		for (TaxonomyTerm tt : nm.getTypes()) {
+			Type t = PackageManager.getInstance().getTypeByName(tt.getName());
+			types.add(t);
+			if (!tool && !isCorePackage() && isValidOnPlatform())
+				t.addEntry(this);
+		}
+		version = nm.getVersion();
+		submitter = nm.getName();
+		creator = nm.getCreator();
+		if (nm.getBody() != null)
+			description = nm.getBody().getSafe_value();
+		file = DepotManager.getInstance().getFile(
+				nm.getUploads().firstElement().getFid());
+		zipSize = file.getFilesize();
+
+		if (isLocalAvailable())
+			updateLocalData();
+	}
+
+	/**
+	 * Update information for local package existence
+	 */
+	public void updateLocalData() {
+		File config = new File(getLocalPath(), "Mod_Info.cfg");
+		File aeicfg = new File(getLocalPath(), "aei.cfg");
+		File plain = new File(getLocalPath(), "plain");
+		if (config.exists()) {
+			Mod_Info mi = new Mod_Info(config, packageNumber);
+
+			aeVersion = mi.getAeVersion();
+			bslInstallType = mi.getBslInstallType();
+			if (node == null) {
+				name = mi.getName();
+				creator = mi.getCreator();
+				version = mi.getVersion();
+				description = mi.getDescription();
+			}
+
+			dependencies = mi.getDependencies();
+			incompatibilities = mi.getIncompatibilities();
+			unlockLevel = mi.getUnlockLevel();
+
+			exeFile = mi.getExeFile();
+			workingDir = mi.getWorkingDir();
+			iconFile = mi.getIconFile();
+		} else {
+			System.err.println("No config found for mod folder: "
+					+ getLocalPath().getPath());
+		}
+		if (aeicfg.exists()) {
+			try {
+				FileInputStream fstream = new FileInputStream(aeicfg);
+				InputStreamReader isr = new InputStreamReader(fstream);
+				BufferedReader br = new BufferedReader(isr);
+				String strLine;
+				while ((strLine = br.readLine()) != null) {
+					if (strLine.indexOf("->") < 1)
+						continue;
+					if (strLine.indexOf("//") >= 0)
+						strLine = strLine.substring(0, strLine.indexOf("//"));
+					String[] split = strLine.split("->", 2);
+					String sName = split[0].trim();
+					String sVal = split[1].trim();
+					if (sName.equalsIgnoreCase("Timestamp")) {
+						localTimestamp = Long.parseLong(sVal);
+					}
+				}
+				isr.close();
+			} catch (FileNotFoundException e) {
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		if (node == null)
+			tool = plain.exists();
+	}
+
+	/**
+	 * Create a new Mod entry from the given local mod folder
+	 * 
+	 * @param folder
+	 *            Mod folder with Mod_Info.cfg
+	 */
+	public Package(File folder) {
+		packageNumber = Integer.parseInt(folder.getName().substring(0, 5));
+		updateLocalData();
+
+		platform = ECompatiblePlatform.BOTH;
+	}
+
+	/**
+	 * @return has separate paths for win/mac/common or not
+	 */
+	public boolean hasSeparatePlatformDirs() {
+		return aeVersion >= 2;
+	}
+
+	private String getSanitizedPathName() {
+		return name.replaceAll("[^a-zA-Z0-9_.-]", "_");
+	}
+
+	/**
+	 * @return Path to local mod folder
+	 */
+	public File getLocalPath() {
+		final String folderStart = String.format("%05d", packageNumber);
+
+		if (Paths.getModsPath().exists()) {
+			for (File f : Paths.getModsPath().listFiles(new FilenameFilter() {
+				@Override
+				public boolean accept(File d, String fn) {
+					return fn.startsWith(folderStart);
+				}
+			})) {
+				return f;
+			}
+		}
+
+		return new File(Paths.getModsPath(), folderStart
+				+ getSanitizedPathName());
+	}
+
+	/**
+	 * @return Is there a newer version on the depot?
+	 */
+	public boolean isNewerAvailable() {
+		if (file != null)
+			return file.getTimestamp() > localTimestamp;
+		else
+			return false;
+	}
+
+	/**
+	 * @return Mod exists within mods folder
+	 */
+	public boolean isLocalAvailable() {
+		return getLocalPath().exists();
+	}
+
+	/**
+	 * @return Is mod installed?
+	 */
+	public boolean isInstalled() {
+		if (tool)
+			return Installer.getInstalledTools().contains(packageNumber);
+		else
+			return PackageManager.getInstance().isModInstalled(this);
+	}
+
+	/**
+	 * @return Name of mod
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @return the package number
+	 */
+	public int getPackageNumber() {
+		return packageNumber;
+	}
+
+	/**
+	 * @return the package number as 5 digit string
+	 */
+	public String getPackageNumberString() {
+		return String.format("%05d", packageNumber);
+	}
+
+	/**
+	 * @return Types of mod
+	 */
+	public HashSet<Type> getTypes() {
+		return types;
+	}
+
+	/**
+	 * @return Is this mod actually a tool?
+	 */
+	public boolean isTool() {
+		return tool;
+	}
+
+	/**
+	 * @return Compatible platforms
+	 */
+	public ECompatiblePlatform getPlatform() {
+		return platform;
+	}
+
+	/**
+	 * @return Version of mod
+	 */
+	public String getVersion() {
+		return version;
+	}
+
+	/**
+	 * @return Submitter of mod
+	 */
+	public String getSubmitter() {
+		return submitter;
+	}
+
+	/**
+	 * @return Creator of mod
+	 */
+	public String getCreator() {
+		return creator;
+	}
+
+	/**
+	 * @return Installation type of BSL files
+	 */
+	public EBSLInstallType getBSLInstallType() {
+		return bslInstallType;
+	}
+
+	/**
+	 * @return Description of mod
+	 */
+	public String getDescription() {
+		return description;
+	}
+
+	/**
+	 * @return Size of Zip file on Depot
+	 */
+	public int getZipSize() {
+		return zipSize;
+	}
+
+	/**
+	 * @return Is a package that is always installed?
+	 */
+	public boolean isCorePackage() {
+		return packageNumber < DepotConfig.getCoreNumberLimit();
+	}
+
+	/**
+	 * @return Get the depot file entry
+	 */
+	public net.oni2.aeinstaller.backend.depot.model.File getFile() {
+		return file;
+	}
+
+	/**
+	 * @return Depot page URI
+	 */
+	public URI getUrl() {
+		if (node == null)
+			return null;
+		if (node.getPath() == null)
+			return null;
+		URI res = null;
+		try {
+			res = new URI(node.getPath());
+		} catch (URISyntaxException e) {
+			e.printStackTrace();
+		}
+		return res;
+	}
+
+	@Override
+	public String toString() {
+		return name;
+	}
+
+	/**
+	 * @return the incompabitilities
+	 */
+	public HashSet<Integer> getIncompabitilities() {
+		return incompatibilities;
+	}
+
+	/**
+	 * @return the dependencies
+	 */
+	public HashSet<Integer> getDependencies() {
+		return dependencies;
+	}
+
+	/**
+	 * @return the levels this mod will unlock
+	 */
+	public HashSet<Integer> getUnlockLevels() {
+		return unlockLevel;
+	}
+
+	/**
+	 * @return Executable name of this tool
+	 */
+	public File getExeFile() {
+		return exeFile;
+	}
+
+	/**
+	 * @return Icon file of this tool
+	 */
+	public File getIconFile() {
+		return iconFile;
+	}
+
+	/**
+	 * @return Working directory of this tool
+	 */
+	public File getWorkingDir() {
+		if (workingDir.equalsIgnoreCase("Exe")) {
+			if (exeFile != null)
+				return exeFile.getParentFile();
+			else
+				return Paths.getEditionGDF();
+		} else if (workingDir.equalsIgnoreCase("GDF"))
+			return Paths.getEditionGDF();
+		else
+			return Paths.getEditionBasePath();
+	}
+
+	/**
+	 * @return Is this mod valid on the running platform?
+	 */
+	public boolean isValidOnPlatform() {
+		switch (platform) {
+			case BOTH:
+				return true;
+			case MACOS:
+				return (Settings.getPlatform() == Platform.MACOS);
+			case WIN:
+				return (Settings.getPlatform() == Platform.WIN)
+						|| (Settings.getPlatform() == Platform.LINUX);
+		}
+		return false;
+	}
+
+	@Override
+	public int compareTo(Package o) {
+		return getPackageNumber() - o.getPackageNumber();
+	}
+
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/PackageManager.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/PackageManager.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/PackageManager.java	(revision 648)
@@ -0,0 +1,345 @@
+package net.oni2.aeinstaller.backend.packages;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.Vector;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.StaxDriver;
+
+import net.oni2.aeinstaller.backend.Paths;
+import net.oni2.aeinstaller.backend.depot.DepotManager;
+import net.oni2.aeinstaller.backend.depot.model.NodeMod;
+import net.oni2.aeinstaller.backend.depot.model.TaxonomyTerm;
+import net.oni2.aeinstaller.backend.oni.Installer;
+
+/**
+ * @author Christian Illy
+ */
+public class PackageManager {
+	private static PackageManager instance = new PackageManager();
+
+	private HashMap<String, Type> types = new HashMap<String, Type>();
+	private HashMap<Integer, Package> mods = new HashMap<Integer, Package>();
+	private HashMap<Integer, Package> tools = new HashMap<Integer, Package>();
+
+	private Vector<Integer> currentlyInstalled = new Vector<Integer>();
+
+	/**
+	 * @param f
+	 *            Mod selection file
+	 * @return Mod selection
+	 */
+	@SuppressWarnings("unchecked")
+	public Vector<Integer> loadModSelection(File f) {
+		Vector<Integer> res = new Vector<Integer>();
+		try {
+			if (f.exists()) {
+				FileInputStream fis = new FileInputStream(f);
+				XStream xs = new XStream(new StaxDriver());
+				Object obj = xs.fromXML(fis);
+				if (obj instanceof Vector<?>)
+					res = (Vector<Integer>) obj;
+				fis.close();
+			}
+		} catch (FileNotFoundException e) {
+			e.printStackTrace();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return res;
+	}
+
+	/**
+	 * @param f
+	 *            Mod selection file
+	 * @param mods
+	 *            Selected mods
+	 */
+	public void saveModSelection(File f, TreeSet<Package> mods) {
+		try {
+			Vector<Integer> installed = new Vector<Integer>();
+			for (Package m : mods) {
+				installed.add(m.getPackageNumber());
+			}
+			FileOutputStream fos = new FileOutputStream(f);
+			XStream xs = new XStream(new StaxDriver());
+			xs.toXML(installed, fos);
+			fos.close();
+		} catch (FileNotFoundException e) {
+			e.printStackTrace();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * First initialization of ModManager
+	 */
+	public void init() {
+		types = new HashMap<String, Type>();
+		mods = new HashMap<Integer, Package>();
+
+		Type localType = new Type("-Local-", null);
+		types.put("-Local-", localType);
+
+		for (TaxonomyTerm tt : DepotManager.getInstance().getTypes()) {
+			types.put(tt.getName(), new Type(tt.getName(), tt));
+		}
+
+		HashMap<Integer, Package> modFolders = new HashMap<Integer, Package>();
+		if (Paths.getModsPath().exists()) {
+			for (File f : Paths.getModsPath().listFiles(new FileFilter() {
+				@Override
+				public boolean accept(File pathname) {
+					return pathname.isDirectory();
+				}
+			})) {
+				Package m = new Package(f);
+				modFolders.put(m.getPackageNumber(), m);
+			}
+		}
+
+		for (NodeMod nm : DepotManager.getInstance().getModPackageNodes()) {
+			if (nm.getUploads().size() == 1 && nm.getStatus() == 1) {
+				Package m = new Package(nm);
+				if (nm.isTool())
+					tools.put(m.getPackageNumber(), m);
+				else
+					mods.put(m.getPackageNumber(), m);
+				modFolders.remove(m.getPackageNumber());
+			}
+		}
+
+		for (Package m : modFolders.values()) {
+			if (!m.isCorePackage()) {
+				localType.addEntry(m);
+				m.getTypes().add(localType);
+			}
+			if (m.isTool())
+				tools.put(m.getPackageNumber(), m);
+			else
+				mods.put(m.getPackageNumber(), m);
+		}
+
+		updateInstalledMods();
+	}
+
+	/**
+	 * Update the list of currently installed mods
+	 */
+	public void updateInstalledMods() {
+		currentlyInstalled = Installer.getInstalledMods();
+		if (currentlyInstalled == null)
+			currentlyInstalled = new Vector<Integer>();
+	}
+
+	/**
+	 * @return Singleton instance
+	 */
+	public static PackageManager getInstance() {
+		return instance;
+	}
+
+	Type getTypeByName(String name) {
+		return types.get(name);
+	}
+
+	/**
+	 * @return Collection of types which do have mods associated
+	 */
+	public Collection<Type> getTypesWithContent() {
+		Vector<Type> res = new Vector<Type>();
+		for (Type t : types.values()) {
+			if (t.getEntries().size() > 0)
+				res.add(t);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Collection of mods valid on this platform and not core package
+	 */
+	public Collection<Package> getModsValidAndNotCore() {
+		Vector<Package> res = new Vector<Package>();
+		for (Package m : mods.values())
+			if (m.isValidOnPlatform() && !m.isCorePackage())
+				res.add(m);
+		return res;
+	}
+
+	/**
+	 * @return Mods which are always installed and valid on this platform
+	 */
+	public TreeSet<Package> getCoreMods() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : mods.values()) {
+			if (m.isValidOnPlatform() && m.isCorePackage())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Mods which are already locally available
+	 */
+	public TreeSet<Package> getLocalAvailableMods() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : mods.values()) {
+			if (m.isLocalAvailable())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Mods which can be updated
+	 */
+	public TreeSet<Package> getUpdatableMods() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : getLocalAvailableMods()) {
+			if (m.isNewerAvailable())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Collection of tools valid on this platform and not core
+	 */
+	public TreeMap<String, Package> getTools() {
+		TreeMap<String, Package> res = new TreeMap<String, Package>();
+		for (Package m : tools.values())
+			if (m.isValidOnPlatform() && !m.isCorePackage())
+				res.put(m.getName(), m);
+		return res;
+	}
+
+	/**
+	 * @return Tools which are always installed and valid on this platform
+	 */
+	public TreeSet<Package> getCoreTools() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : tools.values()) {
+			if (m.isValidOnPlatform() && m.isCorePackage())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Tools which are already locally available
+	 */
+	public TreeSet<Package> getLocalAvailableTools() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : tools.values()) {
+			if (m.isLocalAvailable())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Tools which can be updated
+	 */
+	public TreeSet<Package> getUpdatableTools() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (Package m : getLocalAvailableTools()) {
+			if (m.isNewerAvailable())
+				res.add(m);
+		}
+		return res;
+	}
+
+	/**
+	 * @return Currently installed tools
+	 */
+	public TreeSet<Package> getInstalledTools() {
+		TreeSet<Package> res = new TreeSet<Package>();
+		for (int n : Installer.getInstalledTools()) {
+			res.add(getPackageByNumber(n));
+		}
+		return res;
+	}
+
+	/**
+	 * Get a mod/tool by its package number
+	 * 
+	 * @param number
+	 *            Package number
+	 * @return Mod/tool or null
+	 */
+	private Package getPackageByNumber(int number) {
+		if (mods.containsKey(number))
+			return mods.get(number);
+		if (tools.containsKey(number))
+			return tools.get(number);
+		return null;
+	}
+
+	/**
+	 * Check for unresolved dependencies within the given mods
+	 * 
+	 * @param mods
+	 *            Mods to check
+	 * @return Unmet dependencies
+	 */
+	public HashMap<Package, HashSet<Package>> checkDependencies(TreeSet<Package> mods) {
+		HashMap<Package, HashSet<Package>> res = new HashMap<Package, HashSet<Package>>();
+
+		for (Package m : mods) {
+			for (int depNum : m.getDependencies()) {
+				Package other = getPackageByNumber(depNum);
+				if (!mods.contains(other)) {
+					if (!res.containsKey(m))
+						res.put(m, new HashSet<Package>());
+					res.get(m).add(other);
+				}
+			}
+		}
+
+		return res;
+	}
+
+	/**
+	 * Check for incompabitilites between given mods
+	 * 
+	 * @param mods
+	 *            Mods to check
+	 * @return Incompatible mods
+	 */
+	public HashMap<Package, HashSet<Package>> checkIncompabitilites(TreeSet<Package> mods) {
+		HashMap<Package, HashSet<Package>> res = new HashMap<Package, HashSet<Package>>();
+
+		for (Package m : mods) {
+			for (int confNum : m.getIncompabitilities()) {
+				Package other = getPackageByNumber(confNum);
+				if (mods.contains(other)) {
+					if (!res.containsKey(m))
+						res.put(m, new HashSet<Package>());
+					res.get(m).add(other);
+				}
+			}
+		}
+
+		return res;
+	}
+
+	/**
+	 * @param m
+	 *            Mod to check
+	 * @return Is mod installed?
+	 */
+	boolean isModInstalled(Package m) {
+		return currentlyInstalled.contains(m.getPackageNumber());
+	}
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/Type.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/Type.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/Type.java	(revision 648)
@@ -0,0 +1,52 @@
+package net.oni2.aeinstaller.backend.packages;
+
+import java.util.HashSet;
+
+import net.oni2.aeinstaller.backend.depot.model.TaxonomyTerm;
+
+/**
+ * @author Christian Illy
+ */
+public class Type {
+	private String name;
+	@SuppressWarnings("unused")
+	private TaxonomyTerm depotTerm;
+
+	private HashSet<Package> entries = new HashSet<Package>();
+
+	/**
+	 * Create a new local type declaration
+	 * 
+	 * @param name
+	 *            Name of type
+	 * @param tt
+	 *            Optional TaxTerm link
+	 */
+	public Type(String name, TaxonomyTerm tt) {
+		this.name = name;
+		this.depotTerm = tt;
+	}
+
+	void addEntry(Package m) {
+		entries.add(m);
+	}
+
+	/**
+	 * @return Name of type
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @return Entries for type
+	 */
+	public HashSet<Package> getEntries() {
+		return entries;
+	}
+
+	@Override
+	public String toString() {
+		return String.format("%s (%d)", name, entries.size());
+	}
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownload.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownload.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownload.java	(revision 648)
@@ -0,0 +1,186 @@
+package net.oni2.aeinstaller.backend.packages.download;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import net.oni2.aeinstaller.backend.Paths;
+import net.oni2.aeinstaller.backend.packages.Package;
+import net.oni2.aeinstaller.backend.network.FileDownloadListener;
+import net.oni2.aeinstaller.backend.network.FileDownloader;
+import net.oni2.aeinstaller.backend.network.FileDownloader.EState;
+import net.oni2.aeinstaller.backend.packages.unpack.UnpackListener;
+import net.oni2.aeinstaller.backend.packages.unpack.Unpacker;
+
+/**
+ * @author Christian Illy
+ */
+public class ModDownload implements FileDownloadListener, UnpackListener {
+	/**
+	 * @author Christian Illy
+	 */
+	public enum ModDownloadState {
+		/**
+		 * Downloader initialized but not started
+		 */
+		INIT,
+		/**
+		 * Download running
+		 */
+		RUNNING,
+		/**
+		 * Aborted because of an error
+		 */
+		ERROR,
+		/**
+		 * Download interrupted
+		 */
+		INTERRUPTED,
+		/**
+		 * Download finished successfully
+		 */
+		DOWNLOADED,
+		/**
+		 * Package unzipped successfully
+		 */
+		UNPACKED
+	};
+
+	private Package mod;
+	private FileDownloader downloader;
+	private Unpacker unpacker;
+	private File zipFile;
+	private File targetFolder;
+	private ModDownloadListener listener;
+	private int size;
+
+	private ModDownloadState state = ModDownloadState.INIT;
+
+	/**
+	 * Create a mod download
+	 * 
+	 * @param mod
+	 *            Mod to download
+	 * @param listener
+	 *            Listener for progress
+	 */
+	public ModDownload(Package mod, ModDownloadListener listener) {
+		this.mod = mod;
+		this.listener = listener;
+
+		zipFile = new File(Paths.getDownloadPath(),
+				mod.getPackageNumberString() + ".zip");
+		targetFolder = mod.getLocalPath();
+		try {
+			downloader = new FileDownloader(mod.getFile().getUri_full(),
+					zipFile);
+			downloader.addListener(this);
+			unpacker = new Unpacker(zipFile, targetFolder, this);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * @return Size of this download
+	 */
+	public int getSize() {
+		return mod.getZipSize();
+	}
+
+	/**
+	 * Start this download
+	 */
+	public void start() {
+		state = ModDownloadState.RUNNING;
+		downloader.start();
+	}
+
+	/**
+	 * Abort this download
+	 */
+	public void abort() {
+		switch (state) {
+			case UNPACKED:
+			case INIT:
+			case ERROR:
+			case INTERRUPTED:
+				break;
+			case RUNNING:
+				downloader.stop();
+				break;
+			case DOWNLOADED:
+				unpacker.stop();
+				break;
+		}
+		state = ModDownloadState.INTERRUPTED;
+	}
+
+	/**
+	 * @return the mod object handled by this download
+	 */
+	public Package getMod() {
+		return mod;
+	}
+
+	private void writeTimestamp() {
+		File logFile = new File(targetFolder, "aei.cfg");
+		PrintWriter log = null;
+		try {
+			log = new PrintWriter(logFile);
+			log.println("Timestamp -> " + mod.getFile().getTimestamp());
+		} catch (FileNotFoundException e) {
+			e.printStackTrace();
+		}
+		if (log != null)
+			log.close();
+	}
+
+	@Override
+	public void statusUpdate(FileDownloader source, EState state, int done,
+			int total) {
+		switch (state) {
+			case INIT:
+				break;
+			case RUNNING:
+				listener.modDownloadStatusUpdate(this, this.state, done, total);
+				break;
+			case PAUSED:
+				break;
+			case INTERRUPTED:
+				break;
+			case ERROR:
+				this.state = ModDownloadState.ERROR;
+				listener.modDownloadStatusUpdate(this, this.state, done, total);
+				break;
+			case FINISHED:
+				this.state = ModDownloadState.DOWNLOADED;
+				listener.modDownloadStatusUpdate(this, this.state, done, total);
+				this.size = done;
+				unpacker.start();
+				break;
+		}
+	}
+
+	@Override
+	public void statusUpdate(Unpacker source,
+			net.oni2.aeinstaller.backend.packages.unpack.Unpacker.EState state) {
+		switch (state) {
+			case INIT:
+				break;
+			case RUNNING:
+				break;
+			case INTERRUPTED:
+				this.state = ModDownloadState.INTERRUPTED;
+				listener.modDownloadStatusUpdate(this, this.state, size, size);
+				break;
+			case FINISHED:
+				this.state = ModDownloadState.UNPACKED;
+				writeTimestamp();
+				zipFile.delete();
+				listener.modDownloadStatusUpdate(this, this.state, size, size);
+				break;
+		}
+	}
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloadListener.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloadListener.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloadListener.java	(revision 648)
@@ -0,0 +1,23 @@
+package net.oni2.aeinstaller.backend.packages.download;
+
+import net.oni2.aeinstaller.backend.packages.download.ModDownload.ModDownloadState;
+
+/**
+ * @author Christian Illy
+ */
+public interface ModDownloadListener {
+	/**
+	 * Called for progress changes within the mod download/unpack process
+	 * 
+	 * @param source
+	 *            Source of event
+	 * @param state
+	 *            Current state
+	 * @param done
+	 *            Bytes downloaded
+	 * @param total
+	 *            Bytes total
+	 */
+	public void modDownloadStatusUpdate(ModDownload source,
+			ModDownloadState state, int done, int total);
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloader.java	(revision 648)
@@ -0,0 +1,172 @@
+package net.oni2.aeinstaller.backend.packages.download;
+
+import java.util.Date;
+import java.util.TreeSet;
+import java.util.Vector;
+
+import net.oni2.aeinstaller.backend.packages.Package;
+import net.oni2.aeinstaller.backend.packages.download.ModDownload.ModDownloadState;
+
+/**
+ * @author Christian Illy
+ */
+public class ModDownloader implements ModDownloadListener {
+	/**
+	 * @author Christian Illy
+	 */
+	public enum State {
+		/**
+		 * Downloads running
+		 */
+		RUNNING,
+		/**
+		 * Aborted because of an error
+		 */
+		ERROR,
+		/**
+		 * Downloads interrupted
+		 */
+		INTERRUPTED,
+		/**
+		 * When the last file was downloaded and only unpacking is left
+		 */
+		LAST_FILE_DOWNLOADED,
+		/**
+		 * Everything completed
+		 */
+		FINISHED
+	};
+
+	private int currentDownload = -1;
+	private int unpacked = 0;
+	private Vector<ModDownload> downloads = new Vector<ModDownload>();
+	private int totalSize = 0;
+	private int downloadedComplete = 0;
+	private int downloadedCurrent = 0;
+	private long startMS;
+	private State state = State.RUNNING;
+	private ModDownloaderListener listener;
+
+	/**
+	 * Create a mods-download-process
+	 * 
+	 * @param mods
+	 *            Mods to download
+	 * @param listener
+	 *            Listener for status updates
+	 */
+	public ModDownloader(TreeSet<Package> mods, ModDownloaderListener listener) {
+		this.listener = listener;
+		for (Package m : mods) {
+			downloads.add(new ModDownload(m, this));
+			totalSize += m.getZipSize();
+		}
+		startMS = new Date().getTime();
+		startNextDownload();
+	}
+
+	private void startNextDownload() {
+		if (currentDownload >= 0)
+			downloadedComplete += downloads.get(currentDownload).getSize();
+		currentDownload++;
+		downloadedCurrent = 0;
+		if ((state == State.RUNNING) && (currentDownload < downloads.size())) {
+			downloads.get(currentDownload).start();
+		} else if (state == State.RUNNING) {
+			state = State.LAST_FILE_DOWNLOADED;
+			notifyListener();
+		} else {
+			notifyListener();
+		}
+	}
+
+	private int getTimeElapsed() {
+		int total = (int) (new Date().getTime() - startMS) / 1000;
+		return total;
+	}
+
+	private int getDownloadSpeed() {
+		int elap = getTimeElapsed();
+		int down = downloadedComplete + downloadedCurrent;
+		if (elap > 0)
+			return down / elap;
+		else
+			return 1;
+	}
+
+	private int getTimeRemaining() {
+		int remainingSize = totalSize
+				- (downloadedComplete + downloadedCurrent);
+		return remainingSize / getDownloadSpeed();
+	}
+
+	private void notifyListener() {
+		if (currentDownload < downloads.size()) {
+			listener.updateStatus(this,
+					downloads.get(currentDownload).getMod(), state, unpacked,
+					downloads.size(), downloadedComplete + downloadedCurrent,
+					totalSize, getTimeElapsed(), getTimeRemaining(),
+					getDownloadSpeed());
+		} else {
+			listener.updateStatus(this, null, state, unpacked,
+					downloads.size(), downloadedComplete + downloadedCurrent,
+					totalSize, getTimeElapsed(), getTimeRemaining(),
+					getDownloadSpeed());
+		}
+	}
+
+	/**
+	 * @return total download size
+	 */
+	public int getTotalSize() {
+		return totalSize;
+	}
+
+	/**
+	 * @return Is this process finished
+	 */
+	public boolean isFinished() {
+		return state == State.FINISHED;
+	}
+
+	@Override
+	public void modDownloadStatusUpdate(ModDownload source,
+			ModDownloadState state, int done, int total) {
+		switch (state) {
+			case RUNNING:
+				downloadedCurrent = done;
+				notifyListener();
+				break;
+			case ERROR:
+				this.state = State.ERROR;
+				break;
+			case DOWNLOADED:
+				if (source == downloads.get(currentDownload))
+					startNextDownload();
+				break;
+			case UNPACKED:
+				source.getMod().updateLocalData();
+				unpacked++;
+				if (unpacked >= downloads.size())
+					this.state = State.FINISHED;
+				notifyListener();
+				break;
+			case INIT:
+				break;
+			case INTERRUPTED:
+				break;
+		}
+	}
+
+	/**
+	 * Abort download process
+	 */
+	public void abort() {
+		if (currentDownload < downloads.size()) {
+			state = State.INTERRUPTED;
+			ModDownload md = downloads.get(currentDownload);
+			md.abort();
+		}
+	}
+
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloaderListener.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloaderListener.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/download/ModDownloaderListener.java	(revision 648)
@@ -0,0 +1,37 @@
+package net.oni2.aeinstaller.backend.packages.download;
+
+import net.oni2.aeinstaller.backend.packages.Package;
+import net.oni2.aeinstaller.backend.packages.download.ModDownloader.State;
+
+/**
+ * @author Christian Illy
+ */
+public interface ModDownloaderListener {
+	/**
+	 * Callback for progress updates on mod downloads
+	 * 
+	 * @param source
+	 *            Event source
+	 * @param currentDownload
+	 *            Currently downloading mod
+	 * @param state
+	 *            Current state
+	 * @param filesDown
+	 *            Downloaded(+unpacked) files
+	 * @param filesTotal
+	 *            Files in total to handle
+	 * @param bytesDown
+	 *            Bytes downloaded
+	 * @param bytesTotal
+	 *            Bytes in total to handle
+	 * @param duration
+	 *            Duration of downloads in seconds
+	 * @param remaining
+	 *            Remaining time in seconds
+	 * @param speed
+	 *            Average download speed in B/s
+	 */
+	public void updateStatus(ModDownloader source, Package currentDownload,
+			State state, int filesDown, int filesTotal, int bytesDown,
+			int bytesTotal, int duration, int remaining, int speed);
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/UnpackListener.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/UnpackListener.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/UnpackListener.java	(revision 648)
@@ -0,0 +1,18 @@
+package net.oni2.aeinstaller.backend.packages.unpack;
+
+import net.oni2.aeinstaller.backend.packages.unpack.Unpacker.EState;
+
+/**
+ * @author Christian Illy
+ */
+public interface UnpackListener {
+	/**
+	 * Called for progress changes within the unpacker
+	 * 
+	 * @param source
+	 *            Source of event
+	 * @param state
+	 *            Current state
+	 */
+	public void statusUpdate(Unpacker source, EState state);
+}
Index: AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/Unpacker.java
===================================================================
--- AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/Unpacker.java	(revision 648)
+++ AE/installer2/src/net/oni2/aeinstaller/backend/packages/unpack/Unpacker.java	(revision 648)
@@ -0,0 +1,189 @@
+package net.oni2.aeinstaller.backend.packages.unpack;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+import org.apache.commons.io.FileUtils;
+
+/**
+ * @author Christian Illy
+ */
+public class Unpacker implements Runnable {
+	/**
+	 * @author Christian Illy
+	 */
+	public enum EState {
+		/**
+		 * Unpacker initialized but not started
+		 */
+		INIT,
+		/**
+		 * Unpack running
+		 */
+		RUNNING,
+		/**
+		 * Unpack interrupted
+		 */
+		INTERRUPTED,
+		/**
+		 * Unpack finished successfully
+		 */
+		FINISHED,
+	};
+
+	private UnpackListener listener;
+
+	private File zip;
+	private File target;
+
+	private Thread t = null;
+
+	private EState state = EState.INIT;
+
+	/**
+	 * Initialize a new AE package unpacker
+	 * 
+	 * @param zipFile
+	 *            AE zip package
+	 * @param targetFolder
+	 *            Target folder
+	 * @param listener
+	 *            Listener for progress updates
+	 */
+	public Unpacker(File zipFile, File targetFolder, UnpackListener listener) {
+		this.listener = listener;
+		zip = zipFile;
+		target = targetFolder;
+	}
+
+	/**
+	 * Start the unpack process
+	 */
+	public synchronized void start() {
+		if (t == null) {
+			t = new Thread(this);
+			t.start();
+			state = EState.RUNNING;
+			updateStatus();
+		}
+	}
+
+	/**
+	 * Stop (abort) the process
+	 */
+	public synchronized void stop() {
+		if (state != EState.FINISHED) {
+			state = EState.INTERRUPTED;
+			if (t != null) {
+				try {
+					t.join();
+				} catch (InterruptedException e) {
+					e.printStackTrace();
+				}
+				t = null;
+			}
+			updateStatus();
+			if (state != EState.FINISHED) {
+				if (target.exists()) {
+					try {
+						FileUtils.deleteDirectory(target);
+					} catch (IOException e) {
+						e.printStackTrace();
+					}
+				}
+			}
+		}
+	}
+
+	private synchronized void updateStatus() {
+		listener.statusUpdate(this, state);
+	}
+
+	@Override
+	public void run() {
+		try {
+			switch (state) {
+				case INTERRUPTED:
+					return;
+				case RUNNING:
+					ZipFile zf = null;
+					try {
+						int pathStart = 0;
+						String pathStartName = "";
+
+						zf = new ZipFile(zip);
+						
+						if (target.exists())
+							FileUtils.deleteDirectory(target);
+						target.mkdirs();
+
+						for (Enumeration<? extends ZipEntry> e = zf.entries(); e
+								.hasMoreElements();) {
+							ZipEntry ze = e.nextElement();
+							if (ze.getName().toLowerCase()
+									.endsWith("/mod_info.cfg")
+									|| ze.getName().toLowerCase()
+											.equals("mod_info.cfg")) {
+								pathStart = ze.getName().toLowerCase()
+										.indexOf("mod_info.cfg");
+								pathStartName = ze.getName().substring(0,
+										pathStart);
+							}
+						}
+
+						for (Enumeration<? extends ZipEntry> e = zf.entries(); e
+								.hasMoreElements();) {
+							if (state == EState.INTERRUPTED)
+								return;
+							ZipEntry ze = e.nextElement();
+							if (!ze.isDirectory()) {
+								if (ze.getName().startsWith(pathStartName)) {
+									File targetFile = new File(target, ze
+											.getName().substring(pathStart));
+									File parent = targetFile.getParentFile();
+									parent.mkdirs();
+
+									InputStream in = zf.getInputStream(ze);
+
+									int read = 0;
+									byte[] data = new byte[1024];
+									FileOutputStream fileOut = new FileOutputStream(
+											targetFile);
+									while ((read = in.read(data, 0, 1024)) != -1) {
+										fileOut.write(data, 0, read);
+									}
+									fileOut.close();
+								}
+							}
+						}
+					} catch (ZipException e) {
+						// TODO Auto-generated catch block
+						e.printStackTrace();
+					} catch (IOException e) {
+						// TODO Auto-generated catch block
+						e.printStackTrace();
+					} finally {
+						try {
+							if (zf != null)
+								zf.close();
+						} catch (IOException e) {
+							e.printStackTrace();
+						}
+					}
+					break;
+				default:
+					break;
+			}
+		} finally {
+		}
+
+		state = EState.FINISHED;
+		updateStatus();
+	}
+}
