package net.oni2.aeinstaller.backend.oni.management; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.TreeMap; import java.util.TreeSet; import java.util.Vector; import java.util.regex.Pattern; import net.oni2.SettingsManager; import net.oni2.aeinstaller.AEInstaller2; import net.oni2.aeinstaller.backend.CaseInsensitiveFile; import net.oni2.aeinstaller.backend.Paths; import net.oni2.aeinstaller.backend.RuntimeOptions; import net.oni2.aeinstaller.backend.oni.OniSplit; import net.oni2.aeinstaller.backend.oni.PersistDat; import net.oni2.aeinstaller.backend.oni.XMLTools; import net.oni2.aeinstaller.backend.oni.management.tools.ToolFileIterator; import net.oni2.aeinstaller.backend.oni.management.tools.ToolFileIteratorEntry; import net.oni2.aeinstaller.backend.oni.management.tools.ToolInstallationList; import net.oni2.aeinstaller.backend.packages.EBSLInstallType; import net.oni2.aeinstaller.backend.packages.Package; import net.oni2.aeinstaller.backend.packages.PackageManager; import net.oni2.platformtools.PlatformInformation; import net.oni2.platformtools.PlatformInformation.Platform; import net.oni2.platformtools.applicationinvoker.ApplicationInvocationResult; 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; import com.paour.NaturalOrderComparator; /** * @author Christian Illy */ public class Installer { private static FileFilter dirFileFilter = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } }; /** * Verify that the Edition is within a subfolder to vanilla Oni * (..../Oni/Edition/AEInstaller) * * @return true if GDF can be found in the parent's parent-path */ public static boolean verifyRunningDirectory() { return Paths.getVanillaGDF().exists() && Paths.getVanillaGDF().isDirectory(); } /** * @return Is Edition Core initialized */ public static boolean isEditionInitialized() { return Paths.getVanillaOnisPath().exists(); } /** * Install the given set of mods * * @param mods * Mods to install * @param listener * Listener for install progress updates */ public static void install(TreeSet mods, InstallProgressListener listener) { File logFile = new File(Paths.getInstallerPath(), "Installation.log"); Logger log = null; try { log = new Logger(logFile); } catch (FileNotFoundException e) { e.printStackTrace(); } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date start = new Date(); log.println("Installation of mods started at " + sdf.format(start)); log.println(); log.println("AEI2 version: " + SwingJavaBuilder.getConfig().getResource("appversion")); ToolInstallationList til = ToolInstallationList.getInstance(); log.println("Installed tools:"); for (Package t : PackageManager.getInstance().getInstalledTools()) { log.println(String.format(" - %s (%s)", t.getName(), t.getVersion()) + (til.isModified(t.getPackageNumber()) ? " (! LOCALLY MODIFIED !)" : "")); } log.println("Core mods:"); for (Package m : mods) { if (m.isCorePackage()) { log.println(String.format(" - %s (%s)", m.getName(), m.getVersion())); } } log.println("Selected mods:"); for (Package m : mods) { if (!m.isCorePackage()) { log.println(String.format(" - %s (%s)", m.getName(), m.getVersion())); } } log.println(); HashSet levelsAffectedBefore = null; if (ModInstallationList.getInstance().isLoadedFromFile()) { levelsAffectedBefore = ModInstallationList.getInstance() .getAffectedLevels(); } HashSet levelsAffectedNow = new HashSet(); File IGMD = new File(Paths.getEditionGDF(), "IGMD"); if (IGMD.exists()) { for (File f : IGMD.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } })) { File ignore = CaseInsensitiveFile.getCaseInsensitiveFile(f, "ignore.txt"); if (!ignore.exists()) { try { FileUtils.deleteDirectory(f); } catch (IOException e) { e.printStackTrace(); } } } } TreeSet unlockLevels = new TreeSet(); Vector foldersOni = new Vector(); foldersOni.add(Paths.getVanillaOnisPath()); Vector foldersPatches = new Vector(); for (Package m : mods) { for (int lev : m.getUnlockLevels()) unlockLevels.add(lev); File oni = CaseInsensitiveFile.getCaseInsensitiveFile( m.getLocalPath(), "oni"); if (oni.exists()) { if (m.hasSeparatePlatformDirs()) { File oniCommon = CaseInsensitiveFile .getCaseInsensitiveFile(oni, "common"); File oniMac = CaseInsensitiveFile.getCaseInsensitiveFile( oni, "mac_only"); File oniWin = CaseInsensitiveFile.getCaseInsensitiveFile( oni, "win_only"); if (oniCommon.exists()) foldersOni.add(oniCommon); if (PlatformInformation.getPlatform() == Platform.MACOS && oniMac.exists()) foldersOni.add(oniMac); else if (oniWin.exists()) foldersOni.add(oniWin); } else { foldersOni.add(oni); } } File patches = CaseInsensitiveFile.getCaseInsensitiveFile( m.getLocalPath(), "patches"); if (patches.exists()) { if (m.hasSeparatePlatformDirs()) { File patchesCommon = CaseInsensitiveFile .getCaseInsensitiveFile(patches, "common"); File patchesMac = CaseInsensitiveFile .getCaseInsensitiveFile(patches, "mac_only"); File patchesWin = CaseInsensitiveFile .getCaseInsensitiveFile(patches, "win_only"); if (patchesCommon.exists()) foldersPatches.add(patchesCommon); if (PlatformInformation.getPlatform() == Platform.MACOS && patchesMac.exists()) foldersPatches.add(patchesMac); else if (patchesWin.exists()) foldersPatches.add(patchesWin); } else { foldersPatches.add(patches); } } } TreeMap> levels = new TreeMap>( new NaturalOrderComparator()); log.println("Building sources list"); for (File path : foldersOni) { log.println("\tFolder " + path.getPath()); for (File levelF : path.listFiles()) { boolean isSecondaryFile = false; log.println("\t\tFolder/file " + levelF.getPath()); String fn = levelF.getName().toLowerCase(); String levelN = null; if (levelF.isDirectory()) { levelN = fn; levelsAffectedNow.add(fn.toLowerCase()); } else if (fn.endsWith(".dat")) { levelN = fn.substring(0, fn.lastIndexOf('.')).toLowerCase(); } else if (fn.endsWith(".raw") || fn.endsWith(".sep")) { isSecondaryFile = true; } if (levelN != null) { log.println("\t\t\tAdded for level " + levelN); if (!levels.containsKey(levelN)) levels.put(levelN, new Vector()); levels.get(levelN).add(levelF); } else if (!isSecondaryFile) { if (fn.equalsIgnoreCase(".DS_Store")) { // Ignore OSX bullshit } else { log.println("\t\t\tNot a level file!?"); } } } } Paths.getEditionGDF().mkdirs(); for (File f : Paths.getEditionGDF().listFiles(new FilenameFilter() { public boolean accept(File arg0, String arg1) { String s = arg1.toLowerCase(); return s.endsWith(".dat") || s.endsWith(".raw") || s.endsWith(".sep") || (s.equals("intro.bik") && !SettingsManager .getInstance().get("copyintro", false)) || (s.equals("outro.bik") && !SettingsManager .getInstance().get("copyoutro", false)); } })) { String l = f.getName().toLowerCase(); l = l.substring(0, l.length() - 4); if ((levelsAffectedBefore == null) || levelsAffectedBefore.contains(l) || levelsAffectedNow.contains(l)) f.delete(); } applyPatches(levels, foldersPatches, listener, log); TreeSet levelsAffectedBoth = null; if (levelsAffectedBefore != null) { levelsAffectedBoth = new TreeSet(); levelsAffectedBoth.addAll(levelsAffectedBefore); levelsAffectedBoth.addAll(levelsAffectedNow); } combineBinaryFiles(levels, levelsAffectedBoth, listener, log); try { combineBSLFolders(mods, listener, log); } catch (IOException e) { log.println("Failed installing BSL files, see aei-output.log for stacktrace"); e.printStackTrace(); } copyPlainFiles (log, mods, listener); copyVideos(log); if (unlockLevels.size() > 0) { unlockLevels(unlockLevels, log); } ModInstallationList mil = ModInstallationList.getInstance(); mil.setAffectedLevels(levelsAffectedNow); TreeSet modsInstalled = new TreeSet(); for (Package p : mods) { modsInstalled.add(p.getPackageNumber()); } mil.setInstalledMods(modsInstalled); mil.saveList(); log.println(); Date end = new Date(); log.println("Installation ended at " + sdf.format(end)); log.println("Process took " + ((end.getTime() - start.getTime()) / 1000) + " seconds"); log.close(); } private static void combineBSLFolders(TreeSet mods, InstallProgressListener listener, Logger log) throws IOException { listener.installProgressUpdate(95, 100, AEInstaller2.globalBundle.getString("modInstaller.installBsl")); log.println(); log.println("Installing BSL files"); // First install core non-addon mods log.println("\tCore, non-addon"); for (Package m : mods) { if (m.isCorePackage() && m.getBSLInstallType() == EBSLInstallType.NORMAL) { handleModBsl (m, log, null); } } // Now overwrite / add files coming from core addons log.println("\tCore, addon"); for (Package m : mods) { if (m.isCorePackage() && m.getBSLInstallType() == EBSLInstallType.ADDON) { handleModBsl (m, log, null); } } // Install non-core non-addon mods (levels affected here are first cleaned up) log.println("\tNon-core, non-addon"); HashSet clearedFolders = new HashSet(); for (Package m : mods) { clearedFolders.clear(); if (!m.isCorePackage() && m.getBSLInstallType() == EBSLInstallType.NORMAL) { handleModBsl (m, log, clearedFolders); } } // Lastly overwrite / add files coming from non-core addons log.println("\tNon-core, addon"); for (Package m : mods) { if (!m.isCorePackage() && m.getBSLInstallType() == EBSLInstallType.ADDON) { handleModBsl (m, log, null); } } } private static void handleModBsl(Package sourceMod, Logger log, HashSet clearedFolders) throws IOException { File bslFolder = CaseInsensitiveFile.getCaseInsensitiveFile( sourceMod.getLocalPath(), "bsl"); if (bslFolder.exists()) { if (sourceMod.hasSeparatePlatformDirs()) { File bslCommonFolder = CaseInsensitiveFile .getCaseInsensitiveFile(bslFolder, "common"); File bslMacFolder = CaseInsensitiveFile.getCaseInsensitiveFile( bslFolder, "mac_only"); File bslWinFolder = CaseInsensitiveFile.getCaseInsensitiveFile( bslFolder, "win_only"); if (bslCommonFolder.exists()) { copyBsl(sourceMod, log, bslCommonFolder, sourceMod.getBSLInstallType() == EBSLInstallType.ADDON, clearedFolders); } if (bslWinFolder.exists() && (PlatformInformation.getPlatform() == Platform.WIN || PlatformInformation .getPlatform() == Platform.LINUX)) { copyBsl(sourceMod, log, bslWinFolder, sourceMod.getBSLInstallType() == EBSLInstallType.ADDON, clearedFolders); } if (bslMacFolder.exists() && PlatformInformation.getPlatform() == Platform.MACOS) { copyBsl(sourceMod, log, bslMacFolder, sourceMod.getBSLInstallType() == EBSLInstallType.ADDON, clearedFolders); } } else { copyBsl(sourceMod, log, bslFolder, sourceMod.getBSLInstallType() == EBSLInstallType.ADDON, clearedFolders); } } } private static void copyBsl(Package sourceMod, Logger log, File sourceModFolder, boolean overwriteFiles, HashSet clearedFolders) throws IOException { File targetBaseFolder = new File(Paths.getEditionGDF(), "IGMD"); if (!targetBaseFolder.exists()) targetBaseFolder.mkdir(); log.println("\t\tMod \"" + sourceMod.getName() + "\" (from " + sourceModFolder.getName() + ")"); for (File sourceLevelFolder : sourceModFolder.listFiles(dirFileFilter)) { log.println("\t\t\t" + sourceLevelFolder.getName()); File targetLevelFolder = new File(targetBaseFolder, sourceLevelFolder.getName()); if ((CaseInsensitiveFile.getCaseInsensitiveFile(targetLevelFolder, "ignore.txt").exists())) { // Ignore feature: Don't apply any changes to the target level folder if it contains an "ignore.txt" file log.println("\t\t\t\tSkipping level, target contains an \"ignore.txt\" file"); continue; } if (clearedFolders != null && targetLevelFolder.exists() && !clearedFolders.contains(targetLevelFolder.getName())) { // We are in a clear-folders step and this target folder hasn't been cleared so far -> clear (aka delete) the target folder log.println("\t\t\t\tClearing target, only the highest numbered mod that applies changes to a level will be used"); FileUtils.deleteDirectory(targetLevelFolder); } if (!targetLevelFolder.exists()) { targetLevelFolder.mkdir(); if (clearedFolders != null) { // Clear-folders step and the target folder did not exist (or was removed for clearing) -> this folder counts as cleared clearedFolders.add(targetLevelFolder.getName()); } } for (File sourceBslFile : sourceLevelFolder.listFiles()) { if (sourceBslFile.getName().toLowerCase().endsWith(".bsl")) { File targetFile = new File(targetLevelFolder, sourceBslFile.getName()); if (overwriteFiles || !targetFile.exists()) { // Copy every BSL file from source to target if it either does not yet exist in target or we're currently applying addons (thus overwriting files): if (targetFile.exists()) { log.println("\t\t\t\tOverwriting file from addon: \"" + sourceBslFile.getName() + "\""); } FileUtils.copyFile(sourceBslFile, targetFile); } else { // Non-addon mod, target already exists, skip file log.println("\t\t\t\tSkipping file \"" + sourceBslFile.getName() + "\", target already exists"); } } } } } /* private static void copyBSL(Package sourceMod, boolean addon, Logger log) { File targetBaseFolder = new File(Paths.getEditionGDF(), "IGMD"); if (!targetBaseFolder.exists()) targetBaseFolder.mkdir(); Vector sources = new Vector(); File bsl = CaseInsensitiveFile.getCaseInsensitiveFile( sourceMod.getLocalPath(), "bsl"); if (sourceMod.hasSeparatePlatformDirs()) { File bslCommon = CaseInsensitiveFile.getCaseInsensitiveFile(bsl, "common"); File bslMac = CaseInsensitiveFile.getCaseInsensitiveFile(bsl, "mac_only"); File bslWin = CaseInsensitiveFile.getCaseInsensitiveFile(bsl, "win_only"); if (PlatformInformation.getPlatform() == Platform.MACOS && bslMac.exists()) { for (File f : bslMac.listFiles(dirFileFilter)) { File targetBSL = new File(targetBaseFolder, f.getName()); if (addon || !targetBSL.exists()) sources.add(f); } } if ((PlatformInformation.getPlatform() == Platform.WIN || PlatformInformation .getPlatform() == Platform.LINUX) && bslWin.exists()) { for (File f : bslWin.listFiles(dirFileFilter)) { File targetBSL = new File(targetBaseFolder, f.getName()); if (addon || !targetBSL.exists()) sources.add(f); } } if (bslCommon.exists()) { for (File f : bslCommon.listFiles(dirFileFilter)) { File targetBSL = new File(targetBaseFolder, f.getName()); if (addon || !targetBSL.exists()) sources.add(f); } } } else { for (File f : bsl.listFiles(dirFileFilter)) { File targetBSL = new File(targetBaseFolder, f.getName()); if (addon || !targetBSL.exists()) sources.add(f); } } log.println("\tMod \"" + sourceMod.getName() + "\""); for (File f : sources) { log.println("\t\t" + f.getName()); File targetPath = new File(targetBaseFolder, f.getName()); if (!targetPath.exists()) targetPath.mkdir(); if (!(CaseInsensitiveFile.getCaseInsensitiveFile(targetPath, "ignore.txt").exists())) { for (File fbsl : f.listFiles()) { if (fbsl.getName().toLowerCase().endsWith(".bsl")) { File targetFile = new File(targetPath, fbsl.getName()); if (addon || !targetFile.exists()) { try { FileUtils.copyFile(fbsl, targetFile); } catch (IOException e) { e.printStackTrace(); } } } } } } } */ private static void applyPatches( TreeMap> oniLevelFolders, List patchFolders, InstallProgressListener listener, Logger log) { log.println(); log.println("Applying XML patches"); listener.installProgressUpdate(0, 1, AEInstaller2.globalBundle.getString("modInstaller.applyXmlPatches")); long startMS = new Date().getTime(); String tmpFolderName = "installrun_temp-" + new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss") .format(new Date()); File tmpFolder = new File(Paths.getTempPath(), tmpFolderName); tmpFolder.mkdir(); TreeMap> patches = new TreeMap>( new NaturalOrderComparator()); 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()); patches.get(lvlName).add(f); } } } for (String level : patches.keySet()) { File levelFolder = new File(tmpFolder, level); levelFolder.mkdir(); log.println("\t\tPatches for " + level); log.println("\t\t\tSource files/folders:"); for (File srcFolder : oniLevelFolders.get(level)) { log.println("\t\t\t\t" + srcFolder.getPath()); } // Get files to be patched from vanilla.dat Vector exportPatterns = new Vector(); for (File patch : patches.get(level)) { String patternWildcard = patch.getName(); patternWildcard = patternWildcard.substring(0, patternWildcard.indexOf(".oni-patch")); patternWildcard = patternWildcard.replace('-', '*'); exportPatterns.add(patternWildcard); } for (File srcFolder : oniLevelFolders.get(level)) { if (srcFolder.isFile()) { if (srcFolder.getPath().toLowerCase().contains("vanilla")) { // Extract from .dat ApplicationInvocationResult res = OniSplit.export( levelFolder, srcFolder, exportPatterns); log.logAppOutput(res, true); } } } // Get files to be patched from packages for (File patch : patches.get(level)) { String patternWildcard = patch.getName(); patternWildcard = patternWildcard.substring(0, patternWildcard.indexOf(".oni-patch")); patternWildcard = patternWildcard.replace('-', '*'); Vector patterns = new Vector(); patterns.add(patternWildcard); patternWildcard = patternWildcard + ".oni"; final Pattern patternRegex = Pattern.compile( patternWildcard.replaceAll("\\*", ".\\*"), Pattern.CASE_INSENSITIVE); for (File srcFolder : oniLevelFolders.get(level)) { if (srcFolder.isFile()) { if (!srcFolder.getPath().toLowerCase() .contains("vanilla")) { // Extract from .dat ApplicationInvocationResult res = OniSplit.export( levelFolder, srcFolder, patterns); log.logAppOutput(res, true); } } 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 files = new Vector(); files.add(new File(levelFolder, "*.oni")); ApplicationInvocationResult res = OniSplit.convertOniToXML( levelFolderXML, files); log.logAppOutput(res, true); // Create masterpatch file (containing calls to all individual // patches) File masterpatch = new File(levelFolderXML, "masterpatch.txt"); PrintWriter masterpatchWriter = null; try { masterpatchWriter = new PrintWriter(new OutputStreamWriter( new FileOutputStream(masterpatch), "UTF-8")); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } for (File patch : patches.get(level)) { String patternWildcard = patch.getName(); patternWildcard = patternWildcard.substring(0, patternWildcard.indexOf(".oni-patch")); patternWildcard = patternWildcard + ".xml"; patternWildcard = patternWildcard.replace('-', '*'); File xmlFilePath = new File(levelFolderXML, patternWildcard); masterpatchWriter.println(String.format("\"%s\" \"%s\"", patch.getPath(), xmlFilePath.getPath())); } masterpatchWriter.close(); // Apply patches through masterpatch in levelFolderXML res = XMLTools.patch(masterpatch); log.logAppOutput(res, true); // Create .oni files from XML files.clear(); files.add(new File(levelFolderXML, "*.xml")); res = OniSplit.convertXMLtoOni(levelFolder, files); log.logAppOutput(res, true); if (!RuntimeOptions.isDebug()) { // Remove XML folder as import will only require .oni's try { FileUtils.deleteDirectory(levelFolderXML); } catch (IOException e) { e.printStackTrace(); } } oniLevelFolders.get(level).add(levelFolder); } log.println("Applying XML patches took " + (new Date().getTime() - startMS) + " ms"); } private static void combineBinaryFiles( TreeMap> oniLevelFolders, TreeSet levelsUpdated, InstallProgressListener listener, Logger log) { long startMS = new Date().getTime(); int totalSteps = oniLevelFolders.size() + 1; int stepsDone = 0; log.println(); log.println("Importing levels"); for (String l : oniLevelFolders.keySet()) { log.println("\tLevel " + l); listener.installProgressUpdate(stepsDone, totalSteps, AEInstaller2.globalBundle.getString("modInstaller.buildingLevelN").replaceAll("%1", l.toString())); if ((levelsUpdated == null) || levelsUpdated.contains(l.toLowerCase())) { ApplicationInvocationResult res = OniSplit.packLevel( oniLevelFolders.get(l), new File(Paths.getEditionGDF(), sanitizeLevelName(l) + ".dat")); log.logAppOutput(res, true); } else { log.println("\t\tLevel not affected by new mod selection"); log.println(); } stepsDone++; } log.println("Importing levels took " + (new Date().getTime() - startMS) + " ms"); log.println(); } private static void copyVideos(Logger log) { log.println(); if (SettingsManager.getInstance().get("copyintro", false)) { File src = new File(Paths.getVanillaGDF(), "intro.bik"); File target = new File(Paths.getEditionGDF(), "intro.bik"); log.println("Copying intro"); if (src.exists() && !target.exists()) { try { FileUtils.copyFileToDirectory(src, Paths.getEditionGDF()); } catch (IOException e) { e.printStackTrace(); } } } else { log.println("NOT copying intro"); } if (SettingsManager.getInstance().get("copyoutro", true)) { File src = new File(Paths.getVanillaGDF(), "outro.bik"); File target = new File(Paths.getEditionGDF(), "outro.bik"); log.println("Copying outro"); if (src.exists() && !target.exists()) { try { FileUtils.copyFileToDirectory(src, Paths.getEditionGDF()); } catch (IOException e) { e.printStackTrace(); } } } else { log.println("NOT copying outro"); } } private static void copyPlainFiles(final Logger log, TreeSet mods, InstallProgressListener listener) { listener.installProgressUpdate(97, 100, AEInstaller2.globalBundle.getString("modInstaller.copyPlainFiles")); log.println(); log.println("Copying plain files from mods"); for (Package p : mods) { ToolFileIterator.iteratePlatformToolFiles(p, new ToolFileIteratorEntry() { @Override public void toolFile(File source, File target, boolean isDir) { copyPlainFile(source, target, log); } }); } } private static void copyPlainFile(File src, File target, Logger log) { try { if (src.getAbsolutePath().toLowerCase().contains("gamedatafolder")) { File targetFile = CaseInsensitiveFile.getCaseInsensitiveFile( target.getParentFile(), target.getName()); // Case mismatch? if (!targetFile.getName().equals(src.getName())) targetFile.delete(); FileUtils.copyFile(src, target); } else { log.printlnFmt("Not copying \"%s\": Not within GameDataFolder", src.getPath()); } } catch (IOException e) { e.printStackTrace(); } } private static void unlockLevels(TreeSet unlockLevels, Logger log) { File dat = new File(Paths.getEditionBasePath(), "persist.dat"); log.println(); log.println("Unlocking levels: " + unlockLevels.toString()); if (!dat.exists()) { InputStream is = AEInstaller2.class .getResourceAsStream("/net/oni2/aeinstaller/resources/persist.dat"); try { FileUtils.copyInputStreamToFile(is, dat); } catch (IOException e) { e.printStackTrace(); } } PersistDat save = new PersistDat(dat); HashSet currentlyUnlocked = save.getUnlockedLevels(); currentlyUnlocked.addAll(unlockLevels); save.setUnlockedLevels(currentlyUnlocked); save.close(); } private static String sanitizeLevelName(String ln) { int ind = ln.indexOf("_"); String res = ln.substring(0, ind + 1); res += ln.substring(ind + 1, ind + 2).toUpperCase(); res += ln.substring(ind + 2); return res; } }