package codechicken.core.launch; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.common.asm.transformers.ModAccessTransformer; import net.minecraftforge.fml.common.versioning.ComparableVersion; import net.minecraftforge.fml.relauncher.*; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; 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.lang.reflect.Method; 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.jar.Attributes; import java.util.jar.JarFile; 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 = "DepLoader"; private static DepLoadInst inst; private static final Logger logger = LogManager.getLogger(owner); 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 { /** * Zip file to extract packed dependencies from */ public File source; public String repo; public String packed; public VersionedFile file; public String testClass; public boolean coreLib; public String existing; /** * Flag set to add this dep to the classpath immediately because it is required for a coremod. */ public Dependency(File source, String repo, String packed, VersionedFile file, String testClass, boolean coreLib) { this.source = source; this.repo = repo; this.packed = packed; this.file = file; this.coreLib = coreLib; this.testClass = testClass; } public void set(Dependency dep) { this.source = dep.source; this.repo = dep.repo; this.packed = dep.packed; this.file = dep.file; } } 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>(); private File scanning; private LaunchClassLoader loader = (LaunchClassLoader) DepLoader.class.getClassLoader(); 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(File file) { try { loader.addURL(file.toURI().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } private void deleteMod(File mod) { if (mod.delete()) { return; } try { 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(loader); 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."; logger.error(msg); if (!GraphicsEnvironment.isHeadless()) { JOptionPane.showMessageDialog(null, msg, "An update error has occured", JOptionPane.ERROR_MESSAGE); } System.exit(1); } } private void install(Dependency dep) { popupWindow = (JDialog) downloadMonitor.makeDialog(); if (!extract(dep)) { download(dep); } dep.existing = dep.file.filename; scanDepInfo(new File(v_modsDir, dep.existing)); } private boolean extract(Dependency dep) { if (dep.packed == null) { return false; } ZipFile zip = null; try { zip = new ZipFile(dep.source); ZipEntry libEntry = zip.getEntry(dep.packed + dep.file.filename); if (libEntry == null) { return false; } downloadMonitor.updateProgressString("Extracting file %s\n", dep.source.getPath() + '!' + libEntry.toString()); logger.info("Extracting file " + dep.source.getPath() + '!' + libEntry.toString()); download(zip.getInputStream(libEntry), (int) libEntry.getSize(), dep); downloadMonitor.updateProgressString("Extraction complete"); logger.info("Extraction complete"); } catch (Exception e) { installError(e, dep, "extraction"); } finally { try { if (zip != null) { zip.close(); } } catch (IOException e) { e.printStackTrace(); } } return true; } private void download(Dependency dep) { try { URL libDownload = new URL(dep.repo + dep.file.filename); downloadMonitor.updateProgressString("Downloading file %s", libDownload.toString()); logger.info("Downloading file " + libDownload.toString()); URLConnection connection = libDownload.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setRequestProperty("User-Agent", "" + owner + " Downloader"); download(connection.getInputStream(), connection.getContentLength(), dep); downloadMonitor.updateProgressString("Download complete"); logger.info("Download complete"); } catch (Exception e) { installError(e, dep, "download"); } } private void installError(Exception e, Dependency dep, String s) { if (downloadMonitor.shouldStopIt()) { logger.error("You have stopped the " + s + " before it could complete"); System.exit(1); } downloadMonitor.showErrorDialog(dep.file.filename, dep.repo + '/' + dep.file.filename); throw new RuntimeException(s + " error", e); } private void download(InputStream is, int sizeGuess, Dependency dep) throws Exception { File target = new File(v_modsDir, dep.file.filename); 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 read, fullLength = 0; downloadMonitor.resetProgress(sizeGuess); try { downloadMonitor.setPokeThread(Thread.currentThread()); byte[] buffer = new byte[1024]; while ((read = is.read(buffer)) >= 0) { downloadBuffer.put(buffer, 0, read); fullLength += read; 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"); } if (!target.exists()) { target.createNewFile(); } downloadBuffer.position(0); FileOutputStream fos = new FileOutputStream(target); fos.getChannel().write(downloadBuffer); fos.close(); } 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) { logger.info("Deleted old version " + f.getName()); deleteMod(f); return null; } if (cmp > 0) { logger.info("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()) { File file = new File(v_modsDir, dep.existing); if (!searchCoreMod(file) && dep.coreLib) { addClasspath(file); } } } /** * Looks for FMLCorePlugin attributes and adds to CoreModManager */ private boolean searchCoreMod(File coreMod) { JarFile jar = null; Attributes mfAttributes; try { jar = new JarFile(coreMod); if (jar.getManifest() == null) { return false; } ModAccessTransformer.addJar(jar); mfAttributes = jar.getManifest().getMainAttributes(); } catch (IOException ioe) { FMLRelaunchLog.log(Level.ERROR, ioe, "Unable to read the jar file %s - ignoring", coreMod.getName()); return false; } finally { try { if (jar != null) { jar.close(); } } catch (IOException ignored) { } } String fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin"); if (fmlCorePlugin == null) { return false; } addClasspath(coreMod); try { Class<CoreModManager> c = CoreModManager.class; if (!mfAttributes.containsKey(new Attributes.Name("FMLCorePluginContainsFMLMod"))) { FMLRelaunchLog.finer("Adding %s to the list of known coremods, it will not be examined again", coreMod.getName()); Field f_loadedCoremods = c.getDeclaredField("ignoredModFiles"); f_loadedCoremods.setAccessible(true); ((List) f_loadedCoremods.get(null)).add(coreMod.getName()); } else { FMLRelaunchLog.finer("Found FMLCorePluginContainsFMLMod marker in %s, it will be examined later for regular @Mod instances", coreMod.getName()); Field f_reparsedCoremods = c.getDeclaredField("candidateModFiles"); f_reparsedCoremods.setAccessible(true); ((List) f_reparsedCoremods.get(null)).add(coreMod.getName()); } Method m_loadCoreMod = c.getDeclaredMethod("loadCoreMod", LaunchClassLoader.class, String.class, File.class); m_loadCoreMod.setAccessible(true); m_loadCoreMod.invoke(null, loader, fmlCorePlugin, coreMod); } catch (Exception e) { throw new RuntimeException(e); } return true; } 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 && DepLoader.class.getResource("/" + dep.testClass.replace('.', '/') + ".class") == null) { install(dep); } } 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 { scanning = file; ZipFile zip = new ZipFile(file); ZipEntry e = zip.getEntry("dependencies.info"); if (e != null) { loadJSon(zip.getInputStream(e)); } zip.close(); } catch (Exception e) { logger.error("Failed to load dependencies.info from " + file.getName() + " as JSON", e); } } 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 repo = node.has("repo") ? node.get("repo").getAsString() : null; String packed = node.has("packed") ? node.get("packed").getAsString() : null; String testClass = node.get("class").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) { logger.error("Invalid filename pattern: " + node.get("pattern"), e); } 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(scanning, repo, packed, file, testClass, coreLib)); } private void addDep(Dependency newDep) { Dependency oldDep = depMap.get(newDep.file.name); if (oldDep == null) { depMap.put(newDep.file.name, newDep); depSet.add(newDep.file.name); return; } //combine newer info from newDep into oldDep oldDep.coreLib |= newDep.coreLib; int cmp = newDep.file.version.compareTo(oldDep.file.version); if (cmp == 1) { oldDep.set(newDep); } else if (cmp == 0) { if (oldDep.repo == null) { oldDep.repo = newDep.repo; } if (oldDep.packed == null) { oldDep.source = newDep.source; oldDep.packed = newDep.packed; } } } } 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; } }