package codechicken.core.launch; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import cpw.mods.fml.common.versioning.ComparableVersion; import cpw.mods.fml.relauncher.FMLInjectionData; import cpw.mods.fml.relauncher.FMLLaunchHandler; import cpw.mods.fml.relauncher.IFMLCallHook; import cpw.mods.fml.relauncher.IFMLLoadingPlugin; import net.minecraft.launchwrapper.LaunchClassLoader; import sun.misc.URLClassPath; import sun.net.util.URLUtil; import javax.swing.*; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import java.awt.*; import java.awt.Dialog.ModalityType; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.*; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.ByteBuffer; import java.util.*; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * For autodownloading stuff. * This is really unoriginal, mostly ripped off FML, credits to cpw. */ public class DepLoader implements IFMLLoadingPlugin, IFMLCallHook { private static ByteBuffer downloadBuffer = ByteBuffer.allocateDirect(1 << 23); private static final String owner = "CB's DepLoader"; private static DepLoadInst inst; public interface IDownloadDisplay { void resetProgress(int sizeGuess); void setPokeThread(Thread currentThread); void updateProgress(int fullLength); boolean shouldStopIt(); void updateProgressString(String string, Object... data); Object makeDialog(); void showErrorDialog(String name, String url); } @SuppressWarnings("serial") public static class Downloader extends JOptionPane implements IDownloadDisplay { private JDialog container; private JLabel currentActivity; private JProgressBar progress; boolean stopIt; Thread pokeThread; private Box makeProgressPanel() { Box box = Box.createVerticalBox(); box.add(Box.createRigidArea(new Dimension(0, 10))); JLabel welcomeLabel = new JLabel("<html><b><font size='+1'>" + owner + " is setting up your minecraft environment</font></b></html>"); box.add(welcomeLabel); welcomeLabel.setAlignmentY(LEFT_ALIGNMENT); welcomeLabel = new JLabel("<html>Please wait, " + owner + " has some tasks to do before you can play</html>"); welcomeLabel.setAlignmentY(LEFT_ALIGNMENT); box.add(welcomeLabel); box.add(Box.createRigidArea(new Dimension(0, 10))); currentActivity = new JLabel("Currently doing ..."); box.add(currentActivity); box.add(Box.createRigidArea(new Dimension(0, 10))); progress = new JProgressBar(0, 100); progress.setStringPainted(true); box.add(progress); box.add(Box.createRigidArea(new Dimension(0, 30))); return box; } @Override public JDialog makeDialog() { if (container != null) return container; setMessageType(JOptionPane.INFORMATION_MESSAGE); setMessage(makeProgressPanel()); setOptions(new Object[]{"Stop"}); addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() == Downloader.this && evt.getPropertyName() == VALUE_PROPERTY) { requestClose("This will stop minecraft from launching\nAre you sure you want to do this?"); } } }); container = new JDialog(null, "Hello", ModalityType.MODELESS); container.setResizable(false); container.setLocationRelativeTo(null); container.add(this); this.updateUI(); container.pack(); container.setMinimumSize(container.getPreferredSize()); container.setVisible(true); container.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); container.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { requestClose("Closing this window will stop minecraft from launching\nAre you sure you wish to do this?"); } }); return container; } protected void requestClose(String message) { int shouldClose = JOptionPane.showConfirmDialog(container, message, "Are you sure you want to stop?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (shouldClose == JOptionPane.YES_OPTION) container.dispose(); stopIt = true; if (pokeThread != null) pokeThread.interrupt(); } @Override public void updateProgressString(String progressUpdate, Object... data) { //FMLLog.finest(progressUpdate, data); if (currentActivity != null) currentActivity.setText(String.format(progressUpdate, data)); } @Override public void resetProgress(int sizeGuess) { if (progress != null) progress.getModel().setRangeProperties(0, 0, 0, sizeGuess, false); } @Override public void updateProgress(int fullLength) { if (progress != null) progress.getModel().setValue(fullLength); } @Override public void setPokeThread(Thread currentThread) { this.pokeThread = currentThread; } @Override public boolean shouldStopIt() { return stopIt; } @Override public void showErrorDialog(String name, String url) { JEditorPane ep = new JEditorPane("text/html", "<html>" + owner + " was unable to download required library " + name + "<br>Check your internet connection and try restarting or download it manually from" + "<br><a href=\"" + url + "\">" + url + "</a> and put it in your mods folder" + "</html>"); ep.setEditable(false); ep.setOpaque(false); ep.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent event) { try { if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) Desktop.getDesktop().browse(event.getURL().toURI()); } catch (Exception e) { } } }); JOptionPane.showMessageDialog(null, ep, "A download error has occured", JOptionPane.ERROR_MESSAGE); } } public static class DummyDownloader implements IDownloadDisplay { @Override public void resetProgress(int sizeGuess) { } @Override public void setPokeThread(Thread currentThread) { } @Override public void updateProgress(int fullLength) { } @Override public boolean shouldStopIt() { return false; } @Override public void updateProgressString(String string, Object... data) { } @Override public Object makeDialog() { return null; } @Override public void showErrorDialog(String name, String url) { } } public static class VersionedFile { public final Pattern pattern; public final String filename; public final ComparableVersion version; public final String name; public VersionedFile(String filename, Pattern pattern) { this.pattern = pattern; this.filename = filename; Matcher m = pattern.matcher(filename); if(m.matches()) { name = m.group(1); version = new ComparableVersion(m.group(2)); } else { name = null; version = null; } } public boolean matches() { return name != null; } } public static class Dependency { public String url; public VersionedFile file; public String existing; /** * Flag set to add this dep to the classpath immediately because it is required for a coremod. */ public boolean coreLib; public Dependency(String url, VersionedFile file, boolean coreLib) { this.url = url; this.file = file; this.coreLib = coreLib; } } public static class DepLoadInst { private File modsDir; private File v_modsDir; private IDownloadDisplay downloadMonitor; private JDialog popupWindow; private Map<String, Dependency> depMap = new HashMap<String, Dependency>(); private HashSet<String> depSet = new HashSet<String>(); public DepLoadInst() { String mcVer = (String) FMLInjectionData.data()[4]; File mcDir = (File) FMLInjectionData.data()[6]; modsDir = new File(mcDir, "mods"); v_modsDir = new File(mcDir, "mods/" + mcVer); if (!v_modsDir.exists()) v_modsDir.mkdirs(); } private void addClasspath(String name) { try { ((LaunchClassLoader) DepLoader.class.getClassLoader()).addURL(new File(v_modsDir, name).toURI().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } private void deleteMod(File mod) { if (mod.delete()) return; try { ClassLoader cl = DepLoader.class.getClassLoader(); URL url = mod.toURI().toURL(); Field f_ucp = URLClassLoader.class.getDeclaredField("ucp"); Field f_loaders = URLClassPath.class.getDeclaredField("loaders"); Field f_lmap = URLClassPath.class.getDeclaredField("lmap"); f_ucp.setAccessible(true); f_loaders.setAccessible(true); f_lmap.setAccessible(true); URLClassPath ucp = (URLClassPath) f_ucp.get(cl); Closeable loader = ((Map<String, Closeable>) f_lmap.get(ucp)).remove(URLUtil.urlNoFragString(url)); if (loader != null) { loader.close(); ((List<?>) f_loaders.get(ucp)).remove(loader); } } catch (Exception e) { e.printStackTrace(); } if (!mod.delete()) { mod.deleteOnExit(); String msg = owner + " was unable to delete file " + mod.getPath() + " the game will now try to delete it on exit. If this dialog appears again, delete it manually."; System.err.println(msg); if (!GraphicsEnvironment.isHeadless()) JOptionPane.showMessageDialog(null, msg, "An update error has occured", JOptionPane.ERROR_MESSAGE); System.exit(1); } } private void download(Dependency dep) { popupWindow = (JDialog) downloadMonitor.makeDialog(); File libFile = new File(v_modsDir, dep.file.filename); try { URL libDownload = new URL(dep.url + '/' + dep.file.filename); downloadMonitor.updateProgressString("Downloading file %s", libDownload.toString()); System.out.format("Downloading file %s\n", libDownload.toString()); URLConnection connection = libDownload.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setRequestProperty("User-Agent", "" + owner + " Downloader"); int sizeGuess = connection.getContentLength(); download(connection.getInputStream(), sizeGuess, libFile); downloadMonitor.updateProgressString("Download complete"); System.out.println("Download complete"); scanDepInfo(libFile); } catch (Exception e) { libFile.delete(); if (downloadMonitor.shouldStopIt()) { System.err.println("You have stopped the downloading operation before it could complete"); System.exit(1); return; } downloadMonitor.showErrorDialog(dep.file.filename, dep.url + '/' + dep.file.filename); throw new RuntimeException("A download error occured", e); } } private void download(InputStream is, int sizeGuess, File target) throws Exception { if (sizeGuess > downloadBuffer.capacity()) throw new Exception(String.format("The file %s is too large to be downloaded by " + owner + " - the download is invalid", target.getName())); downloadBuffer.clear(); int bytesRead, fullLength = 0; downloadMonitor.resetProgress(sizeGuess); try { downloadMonitor.setPokeThread(Thread.currentThread()); byte[] smallBuffer = new byte[1024]; while ((bytesRead = is.read(smallBuffer)) >= 0) { downloadBuffer.put(smallBuffer, 0, bytesRead); fullLength += bytesRead; if (downloadMonitor.shouldStopIt()) { break; } downloadMonitor.updateProgress(fullLength); } is.close(); downloadMonitor.setPokeThread(null); downloadBuffer.limit(fullLength); downloadBuffer.position(0); } catch (InterruptedIOException e) { // We were interrupted by the stop button. We're stopping now.. clear interruption flag. Thread.interrupted(); throw new Exception("Stop"); } catch (IOException e) { throw e; } try { /*String cksum = generateChecksum(downloadBuffer); if (cksum.equals(validationHash)) {*/ if (!target.exists()) target.createNewFile(); downloadBuffer.position(0); FileOutputStream fos = new FileOutputStream(target); fos.getChannel().write(downloadBuffer); fos.close(); /*} else { throw new RuntimeException(String.format("The downloaded file %s has an invalid checksum %s (expecting %s). The download did not succeed correctly and the file has been deleted. Please try launching again.", target.getName(), cksum, validationHash)); }*/ } catch (Exception e) { throw e; } } private String checkExisting(Dependency dep) { for (File f : modsDir.listFiles()) { VersionedFile vfile = new VersionedFile(f.getName(), dep.file.pattern); if (!vfile.matches() || !vfile.name.equals(dep.file.name)) continue; if (f.renameTo(new File(v_modsDir, f.getName()))) continue; deleteMod(f); } for (File f : v_modsDir.listFiles()) { VersionedFile vfile = new VersionedFile(f.getName(), dep.file.pattern); if (!vfile.matches() || !vfile.name.equals(dep.file.name)) continue; int cmp = vfile.version.compareTo(dep.file.version); if (cmp < 0) { System.out.println("Deleted old version " + f.getName()); deleteMod(f); return null; } if (cmp > 0) { System.err.println("Warning: version of " + dep.file.name + ", " + vfile.version + " is newer than request " + dep.file.version); return f.getName(); } return f.getName();//found dependency } return null; } public void load() { scanDepInfos(); if (depMap.isEmpty()) return; loadDeps(); activateDeps(); } private void activateDeps() { for (Dependency dep : depMap.values()) if (dep.coreLib) addClasspath(dep.existing); } private void loadDeps() { downloadMonitor = FMLLaunchHandler.side().isClient() ? new Downloader() : new DummyDownloader(); try { while (!depSet.isEmpty()) { Iterator<String> it = depSet.iterator(); Dependency dep = depMap.get(it.next()); it.remove(); load(dep); } } finally { if (popupWindow != null) { popupWindow.setVisible(false); popupWindow.dispose(); } } } private void load(Dependency dep) { dep.existing = checkExisting(dep); if (dep.existing == null)//download dep { download(dep); dep.existing = dep.file.filename; } } private List<File> modFiles() { List<File> list = new LinkedList<File>(); list.addAll(Arrays.asList(modsDir.listFiles())); list.addAll(Arrays.asList(v_modsDir.listFiles())); return list; } private void scanDepInfos() { for (File file : modFiles()) { if (!file.getName().endsWith(".jar") && !file.getName().endsWith(".zip")) continue; scanDepInfo(file); } } private void scanDepInfo(File file) { try { ZipFile zip = new ZipFile(file); ZipEntry e = zip.getEntry("dependancies.info"); if (e == null) e = zip.getEntry("dependencies.info"); if (e != null) loadJSon(zip.getInputStream(e)); zip.close(); } catch (Exception e) { System.err.println("Failed to load dependencies.info from " + file.getName() + " as JSON"); e.printStackTrace(); } } private void loadJSon(InputStream input) throws IOException { InputStreamReader reader = new InputStreamReader(input); JsonElement root = new JsonParser().parse(reader); if (root.isJsonArray()) loadJSonArr(root); else loadJson(root.getAsJsonObject()); reader.close(); } private void loadJSonArr(JsonElement root) throws IOException { for (JsonElement node : root.getAsJsonArray()) loadJson(node.getAsJsonObject()); } private void loadJson(JsonObject node) throws IOException { boolean obfuscated = ((LaunchClassLoader) DepLoader.class.getClassLoader()) .getClassBytes("net.minecraft.world.World") == null; String testClass = node.get("class").getAsString(); if (DepLoader.class.getResource("/" + testClass.replace('.', '/') + ".class") != null) return; String repo = node.get("repo").getAsString(); String filename = node.get("file").getAsString(); if (!obfuscated && node.has("dev")) filename = node.get("dev").getAsString(); boolean coreLib = node.has("coreLib") && node.get("coreLib").getAsBoolean(); Pattern pattern = null; try { if(node.has("pattern")) pattern = Pattern.compile(node.get("pattern").getAsString()); } catch (PatternSyntaxException e) { System.err.println("Invalid filename pattern: "+node.get("pattern")); e.printStackTrace(); } if(pattern == null) pattern = Pattern.compile("(\\w+).*?([\\d\\.]+)[-\\w]*\\.[^\\d]+"); VersionedFile file = new VersionedFile(filename, pattern); if (!file.matches()) throw new RuntimeException("Invalid filename format for dependency: " + filename); addDep(new Dependency(repo, file, coreLib)); } private void addDep(Dependency newDep) { if (mergeNew(depMap.get(newDep.file.name), newDep)) { depMap.put(newDep.file.name, newDep); depSet.add(newDep.file.name); } } private boolean mergeNew(Dependency oldDep, Dependency newDep) { if (oldDep == null) return true; Dependency newest = newDep.file.version.compareTo(oldDep.file.version) > 0 ? newDep : oldDep; newest.coreLib = newDep.coreLib || oldDep.coreLib; return newest == newDep; } } public static void load() { if (inst == null) { inst = new DepLoadInst(); inst.load(); } } @Override public String[] getASMTransformerClass() { return null; } @Override public String getModContainerClass() { return null; } @Override public String getSetupClass() { return getClass().getName(); } @Override public void injectData(Map<String, Object> data) { } @Override public Void call() { load(); return null; } @Override public String getAccessTransformerClass() { return null; } }