package me.modmuss50.optifabric.mod; import me.modmuss50.optifabric.patcher.ClassCache; import me.modmuss50.optifabric.patcher.LambadaRebuiler; import me.modmuss50.optifabric.patcher.PatchSplitter; import me.modmuss50.optifabric.patcher.RemapUtils; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.launch.common.FabricLauncher; import net.fabricmc.loader.launch.common.FabricLauncherBase; import net.fabricmc.loader.launch.common.MappingConfiguration; import net.fabricmc.loader.launch.knot.Knot; import net.fabricmc.loader.util.UrlConversionException; import net.fabricmc.loader.util.UrlUtil; import net.fabricmc.loader.util.mappings.TinyRemapperMappingsHelper; import net.fabricmc.mapping.reader.v2.TinyMetadata; import net.fabricmc.mapping.tree.ClassDef; import net.fabricmc.mapping.tree.TinyTree; import net.fabricmc.tinyremapper.IMappingProvider; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; import org.zeroturnaround.zip.ZipUtil; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class OptifineSetup { private File workingDir = new File(FabricLoader.getInstance().getGameDirectory(), ".optifine"); private File versionDir; private MappingConfiguration mappingConfiguration = new MappingConfiguration(); private FabricLauncher fabricLauncher = FabricLauncherBase.getLauncher(); public Pair<File, ClassCache> getRuntime() throws Throwable { if (!workingDir.exists()) { workingDir.mkdirs(); } File optifineModJar = OptifineVersion.findOptifineJar(); byte[] modHash = fileHash(optifineModJar); versionDir = new File(workingDir, OptifineVersion.version); if (!versionDir.exists()) { versionDir.mkdirs(); } File remappedJar = new File(versionDir, "Optifine-mapped.jar"); File optifinePatches = new File(versionDir, "Optifine.classes"); ClassCache classCache = null; if(remappedJar.exists() && optifinePatches.exists()){ classCache = ClassCache.read(optifinePatches); //Validate that the classCache found is for the same input jar if(!Arrays.equals(classCache.getHash(), modHash)){ System.out.println("Class cache is from a different optifine jar, deleting and re-generating"); classCache = null; optifinePatches.delete(); } } if (remappedJar.exists() && classCache != null) { System.out.println("Found existing patched optifine jar, using that"); return Pair.of(remappedJar, classCache); } if (OptifineVersion.jarType == OptifineVersion.JarType.OPTFINE_INSTALLER) { File optifineMod = new File(versionDir, "/Optifine-mod.jar"); if (!optifineMod.exists()) { OptifineInstaller.extract(optifineModJar, optifineMod, getMinecraftJar().toFile()); } optifineModJar = optifineMod; } System.out.println("Setting up optifine for the first time, this may take a few seconds."); //A jar without srgs File jarOfTheFree = new File(versionDir, "/Optifine-jarofthefree.jar"); List<String> srgs = new ArrayList<>(); System.out.println("De-Volderfiying jar"); //Find all the SRG named classes and remove them ZipUtil.iterate(optifineModJar, (in, zipEntry) -> { String name = zipEntry.getName(); if(name.startsWith("com/mojang/blaze3d/platform/")){ if(name.contains("$")){ String[] split = name.replace(".class", "").split("\\$"); if(split.length >= 2){ if(split[1].length() > 2){ srgs.add(name); } } } } if(name.startsWith("srg/") || name.startsWith("net/minecraft/")){ srgs.add(name); } }); if(jarOfTheFree.exists()){ jarOfTheFree.delete(); } ZipUtil.removeEntries(optifineModJar, srgs.toArray(new String[0]), jarOfTheFree); System.out.println("Building lambada fix mappings"); LambadaRebuiler rebuiler = new LambadaRebuiler(jarOfTheFree, getMinecraftJar().toFile()); rebuiler.buildLambadaMap(); System.out.println("Remapping optifine with fixed lambada names"); File lambadaFixJar = new File(versionDir, "/Optifine-lambadafix.jar"); RemapUtils.mapJar(lambadaFixJar.toPath(), jarOfTheFree.toPath(), rebuiler, getLibs()); remapOptifine(lambadaFixJar.toPath(), remappedJar); classCache = PatchSplitter.generateClassCache(remappedJar, optifinePatches, modHash); if(true){ //We are done, lets get rid of the stuff we no longer need lambadaFixJar.delete(); jarOfTheFree.delete(); if(OptifineVersion.jarType == OptifineVersion.JarType.OPTFINE_INSTALLER){ optifineModJar.delete(); } File extractedMappings = new File(versionDir, "mappings.tiny"); File fieldMappings = new File(versionDir, "mappings.full.tiny"); extractedMappings.delete(); fieldMappings.delete(); } boolean extractClasses = Boolean.parseBoolean(System.getProperty("optifabric.extract", "false")); if(extractClasses){ System.out.println("Extracting optifine classes"); File optifineClasses = new File(versionDir, "optifine-classes"); if(optifineClasses.exists()){ FileUtils.deleteDirectory(optifineClasses); } ZipUtil.unpack(remappedJar, optifineClasses); } return Pair.of(remappedJar, classCache); } private void remapOptifine(Path input, File remappedJar) throws Exception { String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace(); System.out.println("Remapping optifine to :" + namespace); List<Path> mcLibs = getLibs(); mcLibs.add(getMinecraftJar()); RemapUtils.mapJar(remappedJar.toPath(), input, createMappings("official", namespace), mcLibs); } //Optifine currently has two fields that match the same name as Yarn mappings, we'll rename Optifine's to something else IMappingProvider createMappings(String from, String to) { //In dev if (fabricLauncher.isDevelopment()) { try { File fullMappings = extractMappings(); return (out) -> { RemapUtils.getTinyRemapper(fullMappings, from, to).load(out); //TODO use the mappings API here to stop neededing to change this each version out.acceptField(new IMappingProvider.Member("dbq", "CLOUDS", "Ldbe;"), "CLOUDS_OF"); out.acceptField(new IMappingProvider.Member("dqr", "renderDistance", "I"), "renderDistance_OF"); }; } catch (Exception e) { throw new RuntimeException(e); } } //In prod TinyTree mappingsNew = new TinyTree() { private final TinyTree mappings = mappingConfiguration.getMappings(); @Override public TinyMetadata getMetadata() { return mappings.getMetadata(); } @Override public Map<String, ClassDef> getDefaultNamespaceClassMap() { return mappings.getDefaultNamespaceClassMap(); } @Override public Collection<ClassDef> getClasses() { return mappings.getClasses(); } }; return TinyRemapperMappingsHelper.create(mappingsNew, from, to); } //Gets the minecraft librarys List<Path> getLibs() { return fabricLauncher.getLoadTimeDependencies().stream().map(url -> { try { return UrlUtil.asPath(url); } catch (UrlConversionException e) { throw new RuntimeException(e); } }).filter(Files::exists).collect(Collectors.toList()); } //Gets the offical minecraft jar Path getMinecraftJar() throws FileNotFoundException { Optional<Path> entrypointResult = findFirstClass(Knot.class.getClassLoader(), Collections.singletonList("net.minecraft.client.main.Main")); if (!entrypointResult.isPresent()) { throw new RuntimeException("Failed to find minecraft jar"); } if (!Files.exists(entrypointResult.get())) { throw new RuntimeException("Failed to locate minecraft jar"); } if (fabricLauncher.isDevelopment()) { Path path = entrypointResult.get().getParent(); Path minecraftJar = path.resolve(String.format("minecraft-%s-client.jar", OptifineVersion.minecraftVersion)); //Lets hope you are using loom in dev if (!Files.exists(minecraftJar)) { return getNewMinecraftDevJar(); } return minecraftJar; } return entrypointResult.get(); } //Loom 0.2.7 fallback Path getNewMinecraftDevJar() throws FileNotFoundException { Optional<Path> entrypointResult = getSource(Knot.class.getClassLoader(), "mappings/mappings.tiny"); if (entrypointResult.isPresent()) { Path path = entrypointResult.get().getParent(); Path minecraftJar = path.resolve(String.format("minecraft-%s-client.jar", OptifineVersion.minecraftVersion)); //Lets hope you are using loom in dev if (Files.exists(minecraftJar)) { return minecraftJar; } } throw new FileNotFoundException("Could not find minecraft jar!"); } //Stolen from fabric loader static Optional<Path> findFirstClass(ClassLoader loader, List<String> classNames) { List<String> entrypointFilenames = classNames.stream().map((ep) -> ep.replace('.', '/') + ".class").collect(Collectors.toList()); for (int i = 0; i < entrypointFilenames.size(); i++) { String className = classNames.get(i); String classFilename = entrypointFilenames.get(i); Optional<Path> classSourcePath = getSource(loader, classFilename); if (classSourcePath.isPresent()) { return Optional.of(classSourcePath.get()); } } return Optional.empty(); } static Optional<Path> getSource(ClassLoader loader, String filename) { URL url; if ((url = loader.getResource(filename)) != null) { try { URL urlSource = UrlUtil.getSource(filename, url); Path classSourceFile = UrlUtil.asPath(urlSource); return Optional.of(classSourceFile); } catch (UrlConversionException e) { // TODO: Point to a logger e.printStackTrace(); } } return Optional.empty(); } //Extracts the devtime mappings out of yarn into a file File extractMappings() throws IOException { File extractedMappings = new File(versionDir, "mappings.tiny"); if (extractedMappings.exists()) { extractedMappings.delete(); } InputStream mappingStream = FabricLauncherBase.class.getClassLoader().getResourceAsStream("mappings/mappings.tiny"); FileUtils.copyInputStreamToFile(mappingStream, extractedMappings); if (!extractedMappings.exists()) { throw new RuntimeException("failed to extract mappings!"); } return extractedMappings; } byte[] fileHash(File input) throws IOException { try (InputStream is = new FileInputStream(input)) { return DigestUtils.md5(is); } } }