package amidst.minecraft; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.Enumeration; import java.util.HashMap; import java.util.Stack; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import amidst.Options; import amidst.Util; import amidst.bytedata.ByteClass; import amidst.bytedata.ByteClass.AccessFlags; import amidst.bytedata.CCLongMatch; import amidst.bytedata.CCMethodPreset; import amidst.bytedata.CCMulti; import amidst.bytedata.CCPropertyPreset; import amidst.bytedata.CCRequire; import amidst.bytedata.CCStringMatch; import amidst.bytedata.CCWildcardByteSearch; import amidst.bytedata.ClassChecker; import amidst.json.JarLibrary; import amidst.json.JarProfile; import amidst.logging.Log; import amidst.version.VersionInfo; public class Minecraft { private static final int MAX_CLASSES = 128; private Class<?> mainClass; private URLClassLoader classLoader; private String versionID; private URL urlToJar; private File jarFile; private static ClassChecker[] classChecks = new ClassChecker[] { new CCWildcardByteSearch("IntCache", DeobfuscationData.intCache), new CCStringMatch("WorldType", "default_1_1"), new CCLongMatch("GenLayer", 1000L, 2001L, 2000L), new CCStringMatch("IntCache", ", tcache: "), (new ClassChecker() { @Override public void check(Minecraft m, ByteClass bClass) { if (bClass.fields.length != 3) return; int privateStatic = AccessFlags.PRIVATE | AccessFlags.STATIC; for (int i = 0; i < 3; i++) { if ((bClass.fields[i].accessFlags & privateStatic) != privateStatic) return; } if ((bClass.constructorCount == 0) && (bClass.methodCount == 6) && (bClass.searchForUtf("isDebugEnabled"))) { m.registerClass("BlockInit", bClass); isComplete = true; } } }), new CCRequire( new CCPropertyPreset( "WorldType", "a", "types", "b", "default", "c", "flat", "d", "largeBiomes", "e", "amplified", "g", "default_1_1", "f", "customized" ) , "WorldType"), new CCRequire( new CCMethodPreset( "BlockInit", "c()", "initialize" ) , "BlockInit"), new CCRequire( new CCMethodPreset( "GenLayer", "a(long, @WorldType)", "initializeAllBiomeGenerators", "a(long, @WorldType, String)", "initializeAllBiomeGeneratorsWithParams", "a(int, int, int, int)", "getInts" ) , "GenLayer"), new CCRequire(new CCMulti( new CCMethodPreset( "IntCache", "a(int)", "getIntCache", "a()", "resetIntCache", "b()", "getInformation" ), new CCPropertyPreset( "IntCache", "a", "intCacheSize", "b","freeSmallArrays", "c","inUseSmallArrays", "d","freeLargeArrays", "e","inUseLargeArrays" ) ), "IntCache") }; private HashMap<String, ByteClass> byteClassMap; private HashMap<String, MinecraftClass> nameMap; private HashMap<String, MinecraftClass> classMap; private Vector<String> byteClassNames; public String versionId; public VersionInfo version = VersionInfo.unknown; public Minecraft(File jarFile) throws MalformedURLException { this.jarFile = jarFile; byteClassNames = new Vector<String>(); byteClassMap = new HashMap<String, ByteClass>(MAX_CLASSES); urlToJar = jarFile.toURI().toURL(); Log.i("Reading minecraft.jar..."); if (!jarFile.exists()) Log.crash("Attempted to load jar file at: " + jarFile + " but it does not exist."); Stack<ByteClass> byteClassStack = new Stack<ByteClass>(); try { ZipFile jar = new ZipFile(jarFile); Enumeration<? extends ZipEntry> enu = jar.entries(); while (enu.hasMoreElements()) { ZipEntry entry = enu.nextElement(); String currentEntry = entry.getName(); String[] nameSplit = currentEntry.split("\\."); if (!entry.isDirectory() && (nameSplit.length == 2) && (nameSplit[0].indexOf('/') == -1) && nameSplit[1].equals("class")) { BufferedInputStream is = new BufferedInputStream(jar.getInputStream(entry)); if (is.available() < 8000) { // TODO: Double check that this filter won't mess anything up. byte[] classData = new byte[is.available()]; is.read(classData); is.close(); byteClassStack.push(new ByteClass(nameSplit[0], classData)); } } } jar.close(); Log.i("Jar load complete."); } catch (Exception e) { e.printStackTrace(); Log.crash(e, "Error extracting jar data."); } Log.i("Searching for classes..."); int checksRemaining = classChecks.length; Object[] byteClasses = byteClassStack.toArray(); boolean[] found = new boolean[byteClasses.length]; while (checksRemaining != 0) { for (int q = 0; q < classChecks.length; q++) { for (int i = 0; i < byteClasses.length; i++) { if (!found[q]) { classChecks[q].check(this, (ByteClass)byteClasses[i]); if (classChecks[q].isComplete) { Log.debug("Found: " + byteClasses[i] + " as " + classChecks[q].getName() + " | " + classChecks[q].getClass().getSimpleName()); found[q] = true; checksRemaining--; } // TODO: What is this line, and why is it commented //byteClassMap.put(classChecks[q].getName(), classFiles[i].getName().split("\\.")[0]); } } if (!found[q]) { classChecks[q].passes--; if (classChecks[q].passes == 0) { found[q] = true; checksRemaining--; } } } } Log.i("Class search complete."); Log.i("Generating version ID..."); use(); try { use(); if (classLoader.findResource("net/minecraft/client/Minecraft.class") != null) mainClass = classLoader.loadClass("net.minecraft.client.Minecraft"); else if (classLoader.findResource("net/minecraft/server/MinecraftServer.class") != null) mainClass = classLoader.loadClass("net.minecraft.server.MinecraftServer"); else throw new RuntimeException(); } catch (Exception e) { e.printStackTrace(); // TODO: Make this exception far less broad. Log.crash(e, "Attempted to load non-minecraft jar, or unable to locate starting point."); } String typeDump = ""; Field fields[] = null; try { fields = mainClass.getDeclaredFields(); } catch (NoClassDefFoundError e) { e.printStackTrace(); Log.crash(e, "Unable to find critical external class while loading.\nPlease ensure you have the correct Minecraft libraries installed."); } for (int i = 0; i < fields.length; i++) { String typeString = fields[i].getType().toString(); if (typeString.startsWith("class ") && !typeString.contains(".")) typeDump += typeString.substring(6); } versionId = typeDump; for (VersionInfo v : VersionInfo.values()) { if (versionId.equals(v.versionId)) { version = v; break; } } Log.i("Identified Minecraft [" + version.name() + "] with versionID of " + versionId); Log.i("Loading classes..."); nameMap = new HashMap<String, MinecraftClass>(); classMap = new HashMap<String, MinecraftClass>(); for (String name : byteClassNames) { ByteClass byteClass = byteClassMap.get(name); MinecraftClass minecraftClass = new MinecraftClass(name, byteClass.getClassName()); minecraftClass.load(this); nameMap.put(minecraftClass.getName(), minecraftClass); classMap.put(minecraftClass.getClassName(), minecraftClass); } for (MinecraftClass minecraftClass : nameMap.values()) { ByteClass byteClass = byteClassMap.get(minecraftClass.getName()); for (String[] property : byteClass.getProperties()) minecraftClass.addProperty(new MinecraftProperty(minecraftClass, property[1], property[0])); for (String[] method : byteClass.getMethods()) { String methodString = obfuscateStringClasses(method[0]); methodString = methodString.replaceAll(",INVALID", "").replaceAll("INVALID,","").replaceAll("INVALID", ""); String methodDeobfName = method[1]; String methodObfName = methodString.substring(0, methodString.indexOf('(')); String parameterString = methodString.substring(methodString.indexOf('(') + 1, methodString.indexOf(')')); if (parameterString.equals("")) { minecraftClass.addMethod(new MinecraftMethod(minecraftClass, methodDeobfName, methodObfName)); } else { String[] parameterClasses = parameterString.split(","); minecraftClass.addMethod(new MinecraftMethod(minecraftClass, methodDeobfName, methodObfName, parameterClasses)); } } for (String[] constructor : byteClass.getConstructors()) { String methodString = obfuscateStringClasses(constructor[0]).replaceAll(",INVALID", "").replaceAll("INVALID,","").replaceAll("INVALID", ""); String methodDeobfName = constructor[1]; String methodObfName = methodString.substring(0, methodString.indexOf('(')); String parameterString = methodString.substring(methodString.indexOf('(') + 1, methodString.indexOf(')')); if (parameterString.equals("")) { minecraftClass.addMethod(new MinecraftMethod(minecraftClass, methodDeobfName, methodObfName)); } else { String[] parameterClasses = parameterString.split(","); minecraftClass.addMethod(new MinecraftMethod(minecraftClass, methodDeobfName, methodObfName, parameterClasses)); } } } Log.i("Classes loaded."); Log.i("Minecraft load complete."); } private String obfuscateStringClasses(String inString) { inString = inString.replaceAll(" ", ""); Pattern cPattern = Pattern.compile("@[A-Za-z]+"); Matcher cMatcher = cPattern.matcher(inString); String tempOutput = inString; while (cMatcher.find()) { String match = inString.substring(cMatcher.start(), cMatcher.end()); ByteClass byteClass = getByteClass(match.substring(1)); if (byteClass != null) { tempOutput = tempOutput.replaceAll(match, byteClass.getClassName()); } else { tempOutput = tempOutput.replaceAll(match, "INVALID"); } cMatcher = cPattern.matcher(tempOutput); } return tempOutput; } public URL getPath() { return urlToJar; } private Stack<URL> getLibraries(File jsonFile) { Log.i("Loading libraries."); Stack<URL> libraries = new Stack<URL>(); JarProfile profile = null; try { profile = Util.readObject(jsonFile, JarProfile.class); } catch (IOException e) { Log.w("Invalid jar profile loaded. Library loading will be skipped. (Path: " + jsonFile + ")"); return libraries; } for (int i = 0; i < profile.libraries.size(); i++) { JarLibrary library = profile.libraries.get(i); if (library.isActive() && library.getFile() != null && library.getFile().exists()) { try { libraries.add(library.getFile().toURI().toURL()); Log.i("Found library: " + library.getFile()); } catch (MalformedURLException e) { Log.w("Unable to convert library file to URL with path: " + library.getFile()); e.printStackTrace(); } } else { Log.i("Skipping library: " + library.name); } } return libraries; } /* * This was the old search-and-add-all libraries method. This may still be useful * if the user doesn't have a json file, or mojang changes the format. * private Stack<URL> getLibraries(File path, Stack<URL> urls) { File[] files = path.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { getLibraries(files[i], urls); } else { try { Log.i("Found library: " + files[i]); urls.push(files[i].toURI().toURL()); } catch (MalformedURLException e) { e.printStackTrace(); } } } return urls; } */ public void use() { File librariesJson = Options.instance.minecraftJson == null ? new File(jarFile.getPath().replace(".jar", ".json")) : new File(Options.instance.minecraftJson); if (librariesJson.exists()) { Stack<URL> libraries = getLibraries(librariesJson); URL[] libraryArray = new URL[libraries.size() + 1]; libraries.toArray(libraryArray); libraryArray[libraries.size()] = urlToJar; classLoader = new URLClassLoader(libraryArray); } else { Log.i("Unable to find Minecraft library JSON at: " + librariesJson + ". Skipping."); classLoader = new URLClassLoader(new URL[] { urlToJar }); } Thread.currentThread().setContextClassLoader(classLoader); } public String getVersionID() { return versionID; } public MinecraftClass getClassByName(String name) { return nameMap.get(name); } public URLClassLoader getClassLoader() { return classLoader; } public Class<?> loadClass(String name) { try { return classLoader.loadClass(name); } catch (ClassNotFoundException e) { Log.crash(e, "Error loading a class (" + name + ")"); e.printStackTrace(); } return null; } public MinecraftClass getClassByType(String name) { return classMap.get(name); } public void registerClass(String publicName, ByteClass bClass) { if (byteClassMap.get(publicName)==null) { byteClassMap.put(publicName, bClass); byteClassNames.add(publicName); } } public ByteClass getByteClass(String name) { return byteClassMap.get(name); } public IMinecraftInterface createInterface() { return new LocalMinecraftInterface(this); } }