public abstract class ApplyMappings extends DefaultTask {
    private boolean javadocs = false;
    private boolean lambdas = true;

    public ApplyMappings() {
        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(s -> s.file("output.zip")));
    }

    @TaskAction
    public void apply() throws IOException {
        McpNames names = McpNames.load(getMappings().get().getAsFile());

        try (ZipFile zin = new ZipFile(getInput().get().getAsFile())) {
            try (FileOutputStream fos = new FileOutputStream(getOutput().get().getAsFile());
                 ZipOutputStream out = new ZipOutputStream(fos)) {
                zin.stream().forEach(e -> {
                    try {
                        out.putNextEntry(Utils.getStableEntry(e.getName()));
                        if (!e.getName().endsWith(".java")) {
                            IOUtils.copy(zin.getInputStream(e), out);
                        } else {
                            out.write(names.rename(zin.getInputStream(e), javadocs, lambdas).getBytes(StandardCharsets.UTF_8));
                        }
                        out.closeEntry();
                    } catch (IOException e1) {
                        throw new RuntimeException(e1);
                    }
                });
            }
        }
    }

    @InputFile
    public abstract RegularFileProperty getInput();

    @InputFile
    public abstract RegularFileProperty getMappings();

    @OutputFile
    public abstract RegularFileProperty getOutput();

    @Input
    public boolean getJavadocs() {
        return this.javadocs;
    }

    public void setJavadocs(boolean javadocs) {
        this.javadocs = javadocs;
    }

    @Input
    public boolean getLambdas() {
        return this.lambdas;
    }

    public void setLambdas(boolean lambdas) {
        this.lambdas = lambdas;
    }
} public abstract class ApplyRangeMap extends JarExec {
    public boolean annotate = false;
    public boolean keepImports = true;

    public ApplyRangeMap() {
        getTool().set(Utils.SRG2SOURCE);
        getArgs().addAll("--apply", "--input", "{input}", "--range", "{range}", "--srg", "{srg}", "--exc", "{exc}",
                "--output", "{output}", "--keepImports", "{keepImports}");
        setMinimumRuntimeJavaVersion(11);

        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(d -> d.file("output.zip")));
    }

    @Override
    protected List<String> filterArgs(List<String> args) {
        return replaceArgs(args, ImmutableMap.of(
                "{range}", getRangeMap().get().getAsFile(),
                "{output}", getOutput().get().getAsFile(),
                "{annotate}", annotate,
                "{keepImports}", keepImports
                ), ImmutableMap.of(
                "{input}", getSources().getFiles(),
                "{srg}", getSrgFiles().getFiles(),
                "{exc}", getExcFiles().getFiles()
                )
        );
    }

    @InputFiles
    public abstract ConfigurableFileCollection getSrgFiles();

    @InputFiles
    public abstract ConfigurableFileCollection getSources();

    @InputFiles
    public abstract ConfigurableFileCollection getExcFiles();

    @InputFile
    public abstract RegularFileProperty getRangeMap();

    @OutputFile
    public abstract RegularFileProperty getOutput();

    @Input
    public boolean getAnnotate() {
        return annotate;
    }

    public void setAnnotate(boolean value) {
        this.annotate = value;
    }

    @Input
    public boolean getKeepImports() {
        return keepImports;
    }

    public void setKeepImports(boolean value) {
        this.keepImports = value;
    }
} public abstract class DownloadAssets extends DefaultTask {
    public DownloadAssets() {
        getAssetRepository().convention("https://resources.download.minecraft.net/");
        getConcurrentDownloads().convention(8);
    }

    @TaskAction
    public void run() throws IOException, InterruptedException {
        AssetIndex index = Utils.loadJson(getIndex(), AssetIndex.class);
        List<String> keys = new ArrayList<>(index.objects.keySet());
        Collections.sort(keys);
        removeDuplicateRemotePaths(keys, index);

        File assetsPath = new File(Utils.getMCDir(), "/assets/objects");
        ExecutorService executorService = Executors.newFixedThreadPool(getConcurrentDownloads().get());
        CopyOnWriteArrayList<String> failedDownloads = new CopyOnWriteArrayList<>();
        String assetRepo = getAssetRepository().get();
        for (String key : keys) {
            Asset asset = index.objects.get(key);
            File target = Utils.getCache(getProject(), "assets", "objects", asset.getPath());
            if (!target.exists() || !HashFunction.SHA1.hash(target).equals(asset.hash)) {
                URL url = new URL(assetRepo + asset.getPath());
                Runnable copyURLtoFile = () -> {
                    try {
                        File localFile = FileUtils.getFile(assetsPath + File.separator + asset.getPath());
                        if (localFile.exists()) {
                            getProject().getLogger().lifecycle("Copying local object: " + asset.getPath() + " Asset: " + key);
                            FileUtils.copyFile(localFile, target);
                        } else {
                            getProject().getLogger().lifecycle("Downloading: " + url + " Asset: " + key);
                            FileUtils.copyURLToFile(url, target, 10_000, 5_000);
                        }
                        if (!HashFunction.SHA1.hash(target).equals(asset.hash)) {
                            failedDownloads.add(key);
                            Utils.delete(target);
                            getProject().getLogger().error("{} Hash failed.", key);
                        }
                    } catch (IOException e) {
                        failedDownloads.add(key);
                        getProject().getLogger().error("{} Failed.", key);
                        e.printStackTrace();
                    }
                };
                executorService.execute(copyURLtoFile);
            }
        }
        executorService.shutdown();
        executorService.awaitTermination(8, TimeUnit.HOURS);
        if (!failedDownloads.isEmpty()) {
            String errorMessage = "";
            for (String key : failedDownloads) {
                errorMessage += "Failed to get asset: " + key + "\n";
            }
            errorMessage += "Some assets failed to download or validate, try running the task again.";
            throw new RuntimeException(errorMessage);
        }
    }

    // Some keys may reference the same remote file. Remove these duplicates to prevent two threads
    // writing to the same file on disk.
    private static void removeDuplicateRemotePaths(List<String> keys, AssetIndex index) {
        Set<String> seen = new HashSet<>(keys.size());
        keys.removeIf(key -> !seen.add(index.objects.get(key).getPath()));
    }

    private File getIndex() throws IOException {
        VersionJson json = Utils.loadJson(getMeta().get().getAsFile(), VersionJson.class);
        File target = Utils.getCache(getProject(), "assets", "indexes", json.assetIndex.id + ".json");
        return Utils.updateDownload(getProject(), target, json.assetIndex);
    }

    @InputFile
    public abstract RegularFileProperty getMeta();

    /**
     * The Base URL that will be used to download Minecraft assets.
     * A trailing slash is required.
     */
    @Internal
    public abstract Property<String> getAssetRepository();

    /**
     * Defines how many threads will be used to download assets concurrently.
     */
    @Internal
    public abstract Property<Integer> getConcurrentDownloads();

    @OutputDirectory
    public File getOutput() {
        return Utils.getCache(getProject(), "assets");
    }

    private static class AssetIndex {
        Map<String, Asset> objects;
    }

    private static class Asset {
        String hash;

        public String getPath() {
            return hash.substring(0, 2) + '/' + hash;
        }
    }
} public abstract class DownloadMCMeta extends DefaultTask {
    // TODO: convert this into a property?
    private static final String MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
    private static final Gson GSON = new GsonBuilder().create();

    public DownloadMCMeta() {
        getManifest().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(s -> s.file("manifest.json")));
        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(s -> s.file("version.json")));
    }

    @TaskAction
    public void downloadMCMeta() throws IOException {
        try (InputStream manin = new URL(MANIFEST_URL).openStream()) {
            URL url = GSON.fromJson(new InputStreamReader(manin), ManifestJson.class).getUrl(getMCVersion().get());
            if (url != null) {
                FileUtils.copyURLToFile(url, getOutput().get().getAsFile());
            } else {
                throw new RuntimeException("Missing version from manifest: " + getMCVersion().get());
            }
        }
    }

    @Input
    public abstract Property<String> getMCVersion();

    // TODO: check for uses, remove if not used
    @Internal
    public abstract RegularFileProperty getManifest();

    @OutputFile
    public abstract RegularFileProperty getOutput();
} public abstract class DownloadMavenArtifact extends DefaultTask {
    private final Property<Artifact> artifact;
    private boolean changing = false;

    public DownloadMavenArtifact() {
        // We need to always ask, in case the file on the remote maven/local fake repo has changed.
        getOutputs().upToDateWhen(task -> false);

        this.artifact = getProject().getObjects().property(Artifact.class);
        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName())
                        .zip(getArtifact(), (d, a) -> d.file("output." + a.getExtension())));
    }

    @Internal
    public String getResolvedVersion() {
        return MavenArtifactDownloader.getVersion(getProject(), getArtifact().get().getDescriptor());
    }

    @Input
    public Property<Artifact> getArtifact() {
        return this.artifact;
    }

    public void setArtifact(String value) {
        getArtifact().set(Artifact.from(value));
    }

    @Input
    public boolean getChanging() {
        return changing;
    }

    public void setChanging(boolean value) {
        this.changing = value;
    }

    @OutputFile
    public abstract RegularFileProperty getOutput();

    @TaskAction
    public void run() throws IOException {
        File out = MavenArtifactDownloader.download(getProject(), getArtifact().get().getDescriptor(), getChanging());
        this.setDidWork(out != null && out.exists());

        File output = getOutput().get().getAsFile();
        if (FileUtils.contentEquals(out, output)) return;
        if (output.exists()) output.delete();
        if (!output.getParentFile().exists()) output.getParentFile().mkdirs();
        FileUtils.copyFile(out, output);
    }
} public abstract class ExtractExistingFiles extends DefaultTask {
    @TaskAction
    public void run() throws IOException {
        try (ZipFile zip = new ZipFile(getArchive().get().getAsFile())) {
            Enumeration<? extends ZipEntry> enu = zip.entries();
            while (enu.hasMoreElements()) {
                ZipEntry e = enu.nextElement();
                if (e.isDirectory()) continue;

                for (File target : getTargets()) {
                    File out = new File(target, e.getName());
                    if (!out.exists()) continue;
                    try (FileOutputStream fos = new FileOutputStream(out)) {
                        IOUtils.copy(zip.getInputStream(e), fos);
                    }
                }
            }
        }
    }

    @InputFile
    public abstract RegularFileProperty getArchive();

    @OutputDirectories
    public abstract ConfigurableFileCollection getTargets();
} public abstract class ExtractMCPData extends DefaultTask {
    private boolean allowEmpty = false;

    public ExtractMCPData() {
        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(s -> s.file("output.srg")));
        getKey().convention("mappings");
    }

    @TaskAction
    public void run() throws IOException {
        MCPConfigV2 cfg = MCPConfigV2.getFromArchive(getConfig().get().getAsFile());

        String key = getKey().get();
        File output = getOutput().get().getAsFile();
        try (ZipFile zip = new ZipFile(getConfig().get().getAsFile())) {
            String path = cfg.getData(key.split("/"));
            if (path == null && "statics".equals(key))
                path = "config/static_methods.txt";

            if (path == null) {
                error("Could not find data entry for '" + key + "'");
                return;
            }

            ZipEntry entry = zip.getEntry(path);
            if (entry == null) {
                error("Invalid config zip, Missing path '" + path + "'");
                return;
            }

            try (OutputStream out = new FileOutputStream(output)) {
                IOUtils.copy(zip.getInputStream(entry), out);
            }
        }

        if (cfg.isOfficial() && output.exists() && "mappings".equals(key)) {
            IMappingFile obfToSrg = IMappingFile.load(output);
            remapSrgClasses(getProject(), cfg, obfToSrg).write(output.toPath(), IMappingFile.Format.TSRG2, false);
        }
    }

    public static IMappingFile remapSrgClasses(Project project, MCPConfigV2 config, IMappingFile obfToSrg) throws IOException {
        String minecraftVersion = MinecraftRepo.getMCVersion(config.getVersion());
        File client = MavenArtifactDownloader.generate(project, "net.minecraft:client:" + minecraftVersion + ":mappings@txt", true);

        IMappingFile obfToOfficial = IMappingFile.load(client).reverse();

        return obfToSrg.rename(new IRenamer() {
            @Override
            public String rename(IMappingFile.IClass value) {
                return obfToOfficial.remapClass(value.getOriginal());
            }
        });
    }

    private void error(String message) throws IOException {
        if (!isAllowEmpty())
            throw new IllegalStateException(message);

        File outputFile = getOutput().get().getAsFile();
        if (outputFile.exists())
            outputFile.delete();

        outputFile.createNewFile();
    }

    @Input
    public abstract Property<String> getKey();

    @InputFile
    public abstract RegularFileProperty getConfig();

    @Input
    public boolean isAllowEmpty() {
        return allowEmpty;
    }

    public void setAllowEmpty(boolean allowEmpty) {
        this.allowEmpty = allowEmpty;
    }

    @OutputFile
    public abstract RegularFileProperty getOutput();
} public abstract class ExtractNatives extends DefaultTask {
    @TaskAction
    public void run() throws IOException {
        VersionJson json = Utils.loadJson(getMeta().get().getAsFile(), VersionJson.class);
        for (LibraryDownload lib : json.getNatives()) {
            File target = Utils.getCache(getProject(), "libraries", lib.path);
            Utils.updateDownload(getProject(), target, lib);
            Utils.extractZip(target, getOutput().get().getAsFile(), false);
        }
    }

    @InputFile
    public abstract RegularFileProperty getMeta();

    @OutputDirectory
    public abstract DirectoryProperty getOutput();
} public abstract class ExtractRangeMap extends JarExec {
    private boolean batch = true;

    public ExtractRangeMap() {
        getTool().set(Utils.SRG2SOURCE);
        getArgs().addAll("--extract", "--source-compatibility", "{compat}", "--output", "{output}", "--lib",
                "{library}", "--input", "{input}", "--batch", "{batched}");
        setMinimumRuntimeJavaVersion(11);

        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(d -> d.file("output.txt")));
        final JavaPluginExtension extension = getProject().getExtensions().findByType(JavaPluginExtension.class);
        if (extension != null && extension.getToolchain().getLanguageVersion().isPresent()) {
            int version = extension.getToolchain().getLanguageVersion().get().asInt();
            getSourceCompatibility().convention((version <= 8 ? "1." : "") + version);
        } else {
            getSourceCompatibility().convention("1.8");
        }
    }

    @Override
    protected List<String> filterArgs(List<String> args) {
        return replaceArgs(args, ImmutableMap.of(
                "{compat}", getSourceCompatibility().get(),
                "{output}", getOutput().get().getAsFile(),
                "{batched}", batch
                ), ImmutableMap.of(
                "{input}", getSources().getFiles(),
                "{library}", getDependencies().getFiles()
                )
        );
    }

    @InputFiles
    public abstract ConfigurableFileCollection getSources();

    @InputFiles
    public abstract ConfigurableFileCollection getDependencies();

    @OutputFile
    public abstract RegularFileProperty getOutput();

    @Input
    public abstract Property<String> getSourceCompatibility();

    @Input
    public boolean getBatch() {
        return this.batch;
    }

    public void setBatch(boolean value) {
        this.batch = value;
    }
} public abstract class BaseRepo implements ArtifactProvider<ArtifactIdentifier> {

    public static final boolean DEBUG = Boolean.getBoolean("fg.debugRepo");

    private final File cache;
    protected final Logger log;
    protected final String REPO_NAME = getClass().getSimpleName();

    protected BaseRepo(File cache, Logger log) {
        this.cache = cache;
        this.log = log;
    }

    protected File getCacheRoot() {
        return this.cache;
    }

    protected File cache(String... path) {
        return new File(getCacheRoot(), String.join(File.separator, path));
    }

    protected String clean(ArtifactIdentifier art) {
        return art.getGroup() + ":" + art.getName() + ":" + art.getVersion() + ":" + art.getClassifier() + "@" + art.getExtension();
    }

    protected void debug(String message) {
        if (DEBUG)
            this.log.lifecycle(message);
    }
    protected void info(String message) {
        this.log.lifecycle(message);
    }

    @Override
    public final Artifact getArtifact(ArtifactIdentifier artifact) {
        try {
            debug(REPO_NAME + " Request: " + clean(artifact));
            String[] pts  = artifact.getExtension().split("\\.");

            String desc = (artifact.getGroup() + ":" + artifact.getName() + ":" + artifact.getVersion() + ":" + artifact.getClassifier() + "@" + pts[0]).intern();
            File ret;
            synchronized (desc) {
                if (pts.length == 1)
                    ret = findFile(artifact);
                else // Call without the .md5/.sha extension.
                    ret = findFile(net.minecraftforge.gradle.common.util.Artifact.from(artifact.getGroup(), artifact.getName(), artifact.getVersion(), artifact.getClassifier(), pts[0]));
            }

            if (ret != null) {
                ArtifactType type = ArtifactType.OTHER;
                if (artifact.getClassifier() != null && artifact.getClassifier().endsWith("sources"))
                    type = ArtifactType.SOURCE;
                else if ("jar".equals(artifact.getExtension()))
                    type = ArtifactType.BINARY;

                if (pts.length == 1)
                    return StreamableArtifact.ofFile(artifact, type, ret);
                else if (pts.length == 2) {
                    File hash = new File(ret.getAbsolutePath() + "." + pts[1]);
                    if (hash.exists())
                        return StreamableArtifact.ofFile(artifact, type, hash);
                }
            }
            return Artifact.none();
        } catch (Throwable e) {
            //Catch everything so we don't error up in gradle and fuck up the internals so it never asks us for anything ever again!
            log.lifecycle("Error getting artifact: " + clean(artifact) + " from  " + REPO_NAME, e);
            return Artifact.none();
        }
    }

    @Nullable
    protected abstract File findFile(ArtifactIdentifier artifact) throws IOException;

    public static class Builder {
        private List<ArtifactProvider<ArtifactIdentifier>> repos = new ArrayList<>();
        public Builder add(@Nullable ArtifactProvider<ArtifactIdentifier> repo) {
            if (repo != null)
                repos.add(repo);
            return this;
        }

        public void attach(Project project) {
            int random = new Random().nextInt();
            File cache = Utils.getCache(project, "bundeled_repo");
            // Java 8's compiler doesn't allow the lambda to be a method reference, but Java 16 allows it
            // noinspection Convert2MethodRef to prevent IDEA from warning us about it
            GradleRepositoryAdapter.add(project.getRepositories(), "BUNDELED_" + random, cache,
                    SimpleRepository.of(ArtifactProviderBuilder.begin(ArtifactIdentifier.class).provide(
                            artifact -> repos.stream()
                                    .map(repo -> repo.getArtifact(artifact))
                                    .filter(s -> s.isPresent())
                                    .findFirst()
                                    .orElse(Artifact.none())
                    ))
            );
        }
    }

} public class EnvironmentChecks {
    public static final String ENABLE_CERTIFICATE_CHECK_VARIABLE = "net.minecraftforge.gradle.check.certs";
    public static final String ENABLE_GRADLE_CHECK_VARIABLE = "net.minecraftforge.gradle.check.gradle";
    public static final String ENABLE_JAVA_CHECK_VARIABLE = "net.minecraftforge.gradle.check.java";

    private static final boolean ENABLE_CERTIFICATE_CHECK = Boolean.parseBoolean(System.getProperty(ENABLE_CERTIFICATE_CHECK_VARIABLE, "true"));
    private static final boolean ENABLE_GRADLE_CHECK = Boolean.parseBoolean(System.getProperty(ENABLE_GRADLE_CHECK_VARIABLE, "true"));
    private static final boolean ENABLE_JAVA_CHECK = Boolean.parseBoolean(System.getProperty(ENABLE_JAVA_CHECK_VARIABLE, "true"));
    private static final Marker ENV_CHECK = MarkerFactory.getMarker("forgegradle.env_check");

    public static void checkJavaRange(@Nullable JavaVersionParser.JavaVersion minVersionInclusive, @Nullable JavaVersionParser.JavaVersion maxVersionExclusive) {
        checkRange("java", JavaVersionParser.getCurrentJavaVersion(), minVersionInclusive, maxVersionExclusive, "", "");
    }

    public static void checkGradleRange(@Nullable GradleVersion minVersionInclusive, @Nullable GradleVersion maxVersionExclusive) {
        checkRange("Gradle", GradleVersion.current(), minVersionInclusive, maxVersionExclusive,
                "\nNote: Upgrade your gradle version first before trying to switch to FG5.", "");
    }

    private static <T> void checkRange(String name, Comparable<T> current, @Nullable T minVersionInclusive, @Nullable T maxVersionExclusive, String additionalMin, String additionalMax) {
        if (minVersionInclusive != null && current.compareTo(minVersionInclusive) < 0) {
            throw new EnvironmentCheckFailedException(String.format("Found %s version %s. Minimum required is %s.%s", name, current, minVersionInclusive, additionalMin));
        }

        if (maxVersionExclusive != null && current.compareTo(maxVersionExclusive) >= 0) {
            throw new EnvironmentCheckFailedException(String.format("Found %s version %s. Versions %s and newer are not supported yet.%s", name, current, maxVersionExclusive, additionalMax));
        }
    }

    /**
     * Checks the current project environment, and throws an exception if not satisfied.
     *
     * Current environment checks:
     * <ul>
     *     <li>Java version is <em>1.8.0_101</em> or above (first JDK version to include Let's Encrypt certificates)</li>
     *     <li>Gradle version is <em>7.1</em> or above (minimum version required by ForgeGradle)</li>
     *     <li>Certificates for {@link Utils#FORGE_MAVEN} and {@link Utils#MOJANG_MAVEN} are valid (required repositories)</li>
     * </ul>
     *
     * @see #checkGradleRange(GradleVersion, GradleVersion)
     * @see #checkJavaRange(JavaVersionParser.JavaVersion, JavaVersionParser.JavaVersion)
     * @see #testServerConnection(String)
     */
    public static void checkEnvironment(Project project) {
        Logger logger = project.getLogger();
        if (ENABLE_JAVA_CHECK) {
            logger.debug(ENV_CHECK, "Checking Java version");
            checkJavaRange(
                    // Minimum must be update 101 as it's the first one to include Let's Encrypt certificates.
                    JavaVersionParser.parseJavaVersion("1.8.0_101"),
                    null
            );
        } else {
            logger.debug(ENV_CHECK, "Java version check disabled by system property");
        }

        if (ENABLE_GRADLE_CHECK) {
            logger.debug(ENV_CHECK, "Checking Gradle version");
            checkGradleRange(
                    GradleVersion.version("7.1"),
                    null
            );
        } else {
            logger.debug(ENV_CHECK, "Gradle version check disabled by system property");
        }

        if (ENABLE_CERTIFICATE_CHECK) {
            logger.debug(ENV_CHECK, "Checking server connections");
            testServerConnection(Utils.FORGE_MAVEN);
            testServerConnection(Utils.MOJANG_MAVEN);
        } else {
            logger.debug(ENV_CHECK, "Server connection check disabled by system property");
        }
    }

    private static void testServerConnection(String url) {
        try {
            HttpsURLConnection conn = (HttpsURLConnection)new URL(url).openConnection();
            conn.setRequestMethod("HEAD");
            conn.connect();
            conn.getResponseCode();
        } catch (SSLException e) {
            throw new EnvironmentCheckFailedException(String.format("Failed to validate certificate for host '%s'. "
                    + "To disable this check, re-run with '-D%s=false'.", url, ENABLE_CERTIFICATE_CHECK_VARIABLE), e);
        } catch (IOException e) {
            // Normal connection failed, not the point of this test so ignore
        }
    }

    /**
     * Exception thrown when an environment check fails.
     */
    static class EnvironmentCheckFailedException extends RuntimeException {
        EnvironmentCheckFailedException(String message) {
            super(message);
        }

        EnvironmentCheckFailedException(String message, Throwable cause) {
            super(message, cause);
        }
    }
} public class MinecraftRepo extends BaseRepo {
    private static MinecraftRepo INSTANCE;
    private static final String GROUP = "net.minecraft";
    public static final String MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
    public static final String CURRENT_OS = OS.getCurrent().getName();
    private static int CACHE_BUSTER = 1;
    private static final MinecraftVersion v1_14_4 = MinecraftVersion.from("1.14.4");
    private static final MinecraftVersion v1_18 = MinecraftVersion.from("1.18");
    public static final Pattern MCP_CONFIG_VERSION = Pattern.compile("\\d{8}\\.\\d{6}"); //Timestamp: YYYYMMDD.HHMMSS

    private final Project project;
    private final Repository repo;
    private final boolean offline;
    private MinecraftRepo(Project project, File cache, Logger log, boolean offline) {
        super(cache, log);
        this.project = project;
        this.repo = SimpleRepository.of(ArtifactProviderBuilder.begin(ArtifactIdentifier.class)
            .filter(ArtifactIdentifier.groupEquals(GROUP))
            .filter(ArtifactIdentifier.nameMatches("^(client|server)$"))
            .provide(this)
        );
        this.offline = offline;
    }

    private static MinecraftRepo getInstance(Project project) {
        if (INSTANCE == null)
            INSTANCE = new MinecraftRepo(project, Utils.getCache(project, "minecraft_repo"), project.getLogger(), project.getGradle().getStartParameter().isOffline());
        return INSTANCE;
    }

    public static void attach(Project project) {
        MinecraftRepo instance = getInstance(project);
        GradleRepositoryAdapter.add(project.getRepositories(), "MINECRAFT_DYNAMIC", instance.getCacheRoot(), instance.repo);
    }

    public static ArtifactProvider<ArtifactIdentifier> create(Project project) {
        return getInstance(project);
    }

    @Nullable
    protected String getMappings(String version) {
        if (!version.contains("_mapped_")) {
            return null;
        }
        return version.split("_mapped_")[0];
    }

    private HashStore commonCache(File file) throws IOException {
        HashStore ret = new HashStore(this.getCacheRoot()).load(new File(file.getAbsolutePath() + ".input"));
        ret.bust(CACHE_BUSTER);
        return ret;
    }

    @Override
    @Nullable
    public File findFile(ArtifactIdentifier artifact) throws IOException {
        String side = artifact.getName();

        if (!artifact.getGroup().equals(GROUP) || (!"client".equals(side) && !"server".equals(side)))
            return null;

        String version = artifact.getVersion();
        boolean forceStable = version.endsWith("-stable");
        if (forceStable)
            version = version.substring(0, version.length() - 7);
        String mappings = getMappings(version);
        if (mappings != null)
            return null; //We do not support mappings
        String classifier = artifact.getClassifier() == null ? "" : artifact.getClassifier();
        String ext = artifact.getExtension();

        File json = findVersion(getMCVersion(version));
        if (json == null)
            return null; //Not a vanilla version, MCP?

        debug("  " + REPO_NAME + " Request: " + artifact.getGroup() + ":" + side + ":" + version + ":" + classifier + "@" + ext);

        if ("pom".equals(ext)) {
            return null; //findPom(side, version, json); //MCP Repo will take care and add the extra libraries.
        } else if ("json".equals(ext)) {
            if ("".equals(classifier))
                return findVersion(getMCVersion(version));
        } else if ("jar".equals(ext)) {
            switch (classifier) {
                case "":         return findRaw(side, version, json);
                case "slim":     return findSlim(side, version, forceStable, json); //Deprecated - Use MCP Specific version
                case "data":     return findData(side, version, forceStable, json); //Deprecated - Use extra
                case "extra":    return findExtra(side, version, forceStable, json); //Deprecated - Use MCP Specific version
            }
        } else if ("txt".equals(ext)) {
            if ("mappings".equals(classifier))
                return findMappings(side, version, json);
        }
        return null;
    }

    private File findMcp(String version) throws IOException {
        Artifact mcp = Artifact.from("de.oceanlabs.mcp:mcp_config:" + version + "@zip");
        File zip = cache("versions", version, "mcp.zip");
        if (!zip.exists()) {
            FileUtils.copyFile(MavenArtifactDownloader.gradle(project, mcp.getDescriptor(), false), zip);
            Utils.updateHash(zip);
        }
        return zip;
    }

    @Nullable
    private File findMcpMappings(String version) throws IOException {
        File mcp = findMcp(version);
        if (mcp == null)
            return null;

        File mappings = cache("versions", version, "mcp_mappings.tsrg");
        HashStore cache = commonCache(cache("versions", version, "mcp_mappings.tsrg"));
        cache.add(mcp);

        if (!cache.isSame() || !mappings.exists()) {
            MCPWrapperSlim wrapper = new MCPWrapperSlim(mcp);
            wrapper.extractData(mappings, "mappings");
            cache.save();
            Utils.updateHash(mappings);
        }

        return mappings;
    }

    /**
     * This should ONLY ever be passed "mcversion" or "mcversion-mcpconfigversion".
     * Any other input is a MISTAKE and should NOT use this method.
     */
    public static String getMCVersion(String version) {
        int idx = version.lastIndexOf('-');
        if (idx != -1 && MCP_CONFIG_VERSION.matcher(version.substring(idx + 1)).matches()) {
            return version.substring(0, idx);
        }
        return version;
    }

    @Nullable
    private File findVersion(String version) throws IOException {
        File manifest = cache("versions/manifest.json");
        if (!DownloadUtils.downloadEtag(new URL(MANIFEST_URL), manifest, offline))
            return null;
        Utils.updateHash(manifest, HashFunction.SHA1);

        File json = cache("versions", version, "version.json");
        URL url =  Utils.loadJson(manifest, ManifestJson.class).getUrl(version);
        if (url == null)
            throw new RuntimeException("Missing version from manifest: " + version);

        if (!DownloadUtils.downloadEtag(url, json, offline))
            return null;
        Utils.updateHash(json, HashFunction.SHA1);
        return json;
    }

    @Nullable
    protected File findPom(String side, String version, File json) throws IOException {
        File pom = cache("versions", version, side + ".pom");
        HashStore cache = commonCache(cache("versions", version, side + ".pom"));

        if ("client".equals(side)) {
            cache.add(json);
        }

        //log("POM Cache: " + cache.isSame()  + " " + pom.exists());
        if (!cache.isSame() || !pom.exists()) {
            POMBuilder builder = new POMBuilder(GROUP, side, version);
            if ("client".equals(side)) {
                VersionJson meta = Utils.loadJson(json, VersionJson.class);
                for (VersionJson.Library lib : meta.libraries) {
                    if (lib.isAllowed()) {
                        if (lib.downloads.artifact != null)
                            builder.dependencies().add(lib.name, "compile");
                        if (lib.downloads.classifiers != null) {
                            if (lib.downloads.classifiers.containsKey("test")) {
                                builder.dependencies().add(lib.name, "test").withClassifier("test");
                            }
                            if (lib.natives != null && lib.natives.containsKey(CURRENT_OS) && !lib.getArtifact().getName().contains("java-objc-bridge")) {
                                builder.dependencies().add(lib.name, "runtime").withClassifier(lib.natives.get(CURRENT_OS));
                            }
                        }
                    }
                }
            }
            builder.dependencies().add(GROUP + ":" + side + ":" + version, "compile").withClassifier("extra");
            builder.dependencies().add("com.google.code.findbugs:jsr305:3.0.1", "compile"); //TODO: Pull this from MCPConfig.

            String ret = builder.tryBuild();
            if (ret == null) {
                return null;
            }
            FileUtils.writeByteArrayToFile(pom, ret.getBytes());
            cache.save();
            Utils.updateHash(pom);
        }

        return pom;
    }

    private File findMappings(String side, String version, File json_file) throws IOException {
        return findDownloadEntry(side + "_mappings", cache("versions", getMCVersion(version), side + "_mappings.txt"), getMCVersion(version), json_file);
    }

    private File findRaw(String side, String version, File json_file) throws IOException {
        String mcver = getMCVersion(version);
        File raw = findDownloadEntry(side, cache("versions", mcver, side + ".jar"), mcver, json_file);
        if (!"server".equals(side) || v1_18.compareTo(MinecraftVersion.from(mcver)) > 0)
            return raw;

        File extracted = cache("versions", mcver, side + "-extracted.jar");
        HashStore cache = commonCache(extracted)
                .add("raw", raw)
                .add("codever", "1");

        if (!cache.isSame() || !extracted.exists()) {
            BundlerUtils.extractMainJar(raw.toPath(), extracted.toPath());
            cache.save();
        }

        return extracted;
    }

    private File findDownloadEntry(String key, File target, String version, File json_file) throws IOException {
        VersionJson json = Utils.loadJson(json_file, VersionJson.class);
        if (json.downloads == null || !json.downloads.containsKey(key))
            throw new IllegalStateException(version + ".json missing download for " + key);

        Download dl = json.downloads.get(key);
        if (!target.exists() || !HashFunction.SHA1.hash(target).equals(dl.sha1)) {
            FileUtils.copyURLToFile(dl.url, target);
            Utils.updateHash(target, HashFunction.SHA1);
        }
        return target;
    }

    private File findExtra(String side, String version, boolean forceStable, File json) throws IOException {
        boolean stable = v1_14_4.compareTo(MinecraftVersion.from(getMCVersion(version))) < 0;
        File raw = findRaw(side, version, json);
        File mappings = findMcpMappings(version);
        File extra = cache("versions", version, side + "-extra" + (forceStable && !stable ? "-stable" : "") + ".jar");
        HashStore cache = commonCache(cache("versions", version, side + "-extra" + (forceStable && !stable ? "-stable" : "") + ".jar"))
                .add("raw", raw)
                .add("mappings", mappings)
                .add("codever", "1");

        if (!cache.isSame() || !extra.exists()) {
            splitJar(raw, mappings, extra, false, stable || forceStable);
            cache.save();
        }

        return extra;
    }

    private File findSlim(String side, String version, boolean forceStable, File json) throws IOException {
        boolean stable = v1_14_4.compareTo(MinecraftVersion.from(getMCVersion(version))) < 0;
        File raw = findRaw(side, version, json);
        File mappings = findMcpMappings(version);
        File extra = cache("versions", version, side + "-slim" + (forceStable && !stable ? "-stable" : "") + ".jar");
        HashStore cache = commonCache(cache("versions", version, side + "-slim" + (forceStable && !stable ? "-stable" : "") + ".jar"))
                .add("raw", raw)
                .add("mappings", mappings)
                .add("codever", "1");

        if (!cache.isSame() || !extra.exists()) {
            splitJar(raw, mappings, extra, true, stable || forceStable);
            cache.save();
        }


        return extra;
    }

    private static void splitJar(File raw, File mappings, File output, boolean slim, boolean stable) throws IOException {
        try (FileInputStream input = new FileInputStream(mappings)) {
            splitJar(raw, input, output, slim, stable);
        }
    }

    public static void splitJar(File raw, InputStream mappings, File output, boolean slim, boolean stable) throws IOException {
        try (ZipFile zin = new ZipFile(raw);
             FileOutputStream fos = new FileOutputStream(output);
             ZipOutputStream out = new ZipOutputStream(fos)) {

            Set<String> whitelist = IMappingFile.load(mappings).getClasses().stream()
                    .map(IMappingFile.IClass::getOriginal)
                    .collect(Collectors.toSet());

            for (Enumeration<? extends ZipEntry> entries = zin.entries(); entries.hasMoreElements();) {
                ZipEntry entry = entries.nextElement();
                String name = entry.getName();
                if (name.endsWith(".class")) {
                    boolean isNotch = whitelist.contains(name.substring(0, name.length() - 6 /*.class*/));
                    if (slim == isNotch) {
                        ZipEntry _new = Utils.getStableEntry(name, stable ? Utils.ZIPTIME : 0);
                        out.putNextEntry(_new);
                        try (InputStream ein = zin.getInputStream(entry)) {
                            IOUtils.copy(ein, out);
                        }
                        out.closeEntry();
                    }
                } else {
                    if (!slim) {
                        ZipEntry _new = Utils.getStableEntry(name, stable ? Utils.ZIPTIME : 0);
                        out.putNextEntry(_new);
                        try (InputStream ein = zin.getInputStream(entry)) {
                            IOUtils.copy(ein, out);
                        }
                        out.closeEntry();
                    }
                }
            }
        }
        Utils.updateHash(output);
    }

    private File findData(String side, String version, boolean forceStable, File json) throws IOException {
        boolean stable = v1_14_4.compareTo(MinecraftVersion.from(getMCVersion(version))) < 0 || forceStable;
        File raw = findRaw(side, version, json);
        File extra = cache("versions", version, side + "-data" + (forceStable && !stable ? "-stable" : "") + ".jar");
        HashStore cache = commonCache(cache("versions", version, side + "-data" + (forceStable && !stable ? "-stable" : "") + ".jar"))
                .add("raw", raw)
                .add("codever", "1");

        if (!cache.isSame() || !extra.exists()) {
            try (ZipFile zin = new ZipFile(raw);
                 FileOutputStream fos = new FileOutputStream(extra);
                 ZipOutputStream out = new ZipOutputStream(fos)) {

                for (Enumeration<? extends ZipEntry> entries = zin.entries(); entries.hasMoreElements();) {
                    ZipEntry entry = entries.nextElement();
                    String name = entry.getName();
                    if (!name.endsWith(".class")) {
                        ZipEntry _new = Utils.getStableEntry(name, stable || forceStable ? Utils.ZIPTIME : 0);
                        out.putNextEntry(_new);
                        try (InputStream ein = zin.getInputStream(entry)) {
                            IOUtils.copy(ein, out);
                        }
                        out.closeEntry();
                    }
                }
            }

            cache.save();
            Utils.updateHash(extra);
        }
        return extra;
    }


    private static class MCPWrapperSlim {
        private final File data;
        private final MCPConfigV1 config;
        public MCPWrapperSlim(File data) throws IOException {
            this.data = data;
            this.config = MCPConfigV2.getFromArchive(data);
        }

        public void extractData(File target, String... path) throws IOException {
            String name = config.getData(path);
            if (name == null)
                throw new IOException("Unknown MCP Entry: " + Joiner.on("/").join(path));

            try (ZipFile zip = new ZipFile(data)) {
                Utils.extractFile(zip, name, target);
            }
        }
    }
} public class MojangLicenseHelper {

    public static final String HIDE_LICENSE = "hideOfficialWarningUntilChanged";
    public static final String SHOW_LICENSE = "reshowOfficialWarning";

    public static void displayWarning(Project project, String channel, @Nullable String version, @Nullable String updateChannel, @Nullable String updateVersion) {
        displayWarning(project, channel, version);

        if (updateChannel == null || Objects.equals(channel, updateChannel)) return;

        displayWarning(project, updateChannel, updateVersion);
    }

    public static void displayWarning(Project project, String channel, @Nullable String version) {
        if ("official".equals(channel)) {
            Optional<String> license = version != null ? getOfficialLicense(project, version) : Optional.empty();
            Optional<String> licenseHash = license.map(HashFunction.SHA1::hash);

            if (license.isPresent() && isHidden(project, licenseHash.get())) return;

            String warning = getWarning(license.orElse(null));

            project.getLogger().warn(warning);
            license.map(s -> "WARNING: " + s).ifPresent(project.getLogger()::warn);
        }
    }

    public static void hide(Project project, String channel, String version) {
        if (!"official".equals(channel)) return;

        String hash = getOfficialLicense(project, version)
            .map(HashFunction.SHA1::hash)
            .orElseThrow(() -> new IllegalStateException("Could not get Mojang license text for " + version));

        Path accepted = getLicensePath(project, hash);

        if (Files.exists(accepted)) return;

        try {
            Utils.createEmpty(accepted.toFile());

            String warning = "WARNING: These warnings will not be shown again until the license changes "
                + "or the task `{TASK}` is run.";

            project.getLogger().warn(warning.replace("{TASK}", SHOW_LICENSE));
        } catch (IOException exception) {
            project.getLogger().error("Could not accept Mojang license", exception);
        }
    }

    public static void show(Project project, String channel, String version) {
        if (!"official".equals(channel)) return;

        String hash = getOfficialLicense(project, version)
            .map(HashFunction.SHA1::hash)
            .orElseThrow(() -> new IllegalStateException("Could not get Mojang license text for " + version));

        Path accepted = getLicensePath(project, hash);

        Utils.delete(accepted.toFile());
    }

    private static String getWarning(@Nullable String license) {
        String warning = "WARNING: "
            + "This project is configured to use the official obfuscation mappings provided by Mojang. "
            + "These mapping fall under their associated license, you should be fully aware of this license. "
            + "For the latest license text, refer {REFER}, or the reference copy here: "
            + "https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md"
            + ", You can hide this warning by running the `{TASK}` task";

        return warning
            .replace("{REFER}", license != null ? "below" : "to the mapping file itself")
            .replace("{TASK}", HIDE_LICENSE);
    }

    private static boolean isHidden(Project project, String hash) {
        return Files.exists(getLicensePath(project, hash));
    }

    private static Optional<String> getOfficialLicense(Project project, String version) {
        String minecraftVersion = MinecraftRepo.getMCVersion(version);
        String artifact = "net.minecraft:client:" + minecraftVersion + ":mappings@txt";

        File client = MavenArtifactDownloader.generate(project, artifact, true);

        if (client == null) return Optional.empty();

        try {
            return Optional.of(
                Files.lines(client.toPath())
                    .filter(line -> line.startsWith("#"))        // Only Comments
                    .map(l -> l.substring(1).trim())             // Remove initial #
                    .collect(Collectors.joining("\n"))   // Join via \n
            );
        } catch (IOException e) {
            e.printStackTrace();
        }

        return Optional.empty();
    }

    private static Path getLicensePath(Project project, String hash) {
        return new File(Utils.getCache(project, "licenses"), hash + ".marker").toPath();
    }
} public class Utils {
    private static final boolean ENABLE_FILTER_REPOS = Boolean.parseBoolean(System.getProperty("net.minecraftforge.gradle.filter_repos", "true"));

    public static final Gson GSON = new GsonBuilder()
        .registerTypeAdapter(MCPConfigV1.Step.class, new MCPConfigV1.Step.Deserializer())
        .registerTypeAdapter(VersionJson.Argument.class, new VersionJson.Argument.Deserializer())
        .setPrettyPrinting().create();
    static final int CACHE_TIMEOUT = 1000 * 60 * 60; //1 hour, Timeout used for version_manifest.json so we dont ping their server every request.
                                                          //manifest doesn't include sha1's so we use this for the per-version json as well.
    public static final String FORGE_MAVEN = "https://maven.minecraftforge.net/";
    public static final String MOJANG_MAVEN = "https://libraries.minecraft.net/";
    public static final String BINPATCHER =  "net.minecraftforge:binarypatcher:1.+:fatjar";
    public static final String ACCESSTRANSFORMER = "net.minecraftforge:accesstransformers:8.0.+:fatjar";
    public static final String SPECIALSOURCE = "net.md-5:SpecialSource:1.10.0:shaded";
    public static final String FART = "net.minecraftforge:ForgeAutoRenamingTool:0.1.+:all";
    public static final String SRG2SOURCE =  "net.minecraftforge:Srg2Source:8.+:fatjar";
    public static final String SIDESTRIPPER = "net.minecraftforge:mergetool:1.1.3:fatjar";
    public static final String INSTALLERTOOLS = "net.minecraftforge:installertools:1.2.8:fatjar";
    public static final long ZIPTIME = 628041600000L;
    public static final TimeZone GMT = TimeZone.getTimeZone("GMT");

    public static void extractFile(ZipFile zip, String name, File output) throws IOException {
        extractFile(zip, zip.getEntry(name), output);
    }

    public static void extractFile(ZipFile zip, ZipEntry entry, File output) throws IOException {
        File parent = output.getParentFile();
        if (!parent.exists())
            parent.mkdirs();

        try (InputStream stream = zip.getInputStream(entry)) {
            Files.copy(stream, output.toPath(), StandardCopyOption.REPLACE_EXISTING);
        }
    }

    public static void extractDirectory(Function<String, File> fileLocator, ZipFile zip, String directory) throws IOException {
        Enumeration<? extends ZipEntry> entries = zip.entries();
        while (entries.hasMoreElements()) {
            ZipEntry e = entries.nextElement();
            if (e.isDirectory()) continue;
            if (!e.getName().startsWith(directory)) continue;
            extractFile(zip, e, fileLocator.apply(e.getName()));
        }
    }

    public static Set<String> copyZipEntries(ZipOutputStream zout, ZipInputStream zin, Predicate<String> filter) throws IOException {
        Set<String> added = new HashSet<>();
        ZipEntry entry;
        while ((entry = zin.getNextEntry()) != null) {
            if (!filter.test(entry.getName())) continue;
            ZipEntry _new = new ZipEntry(entry.getName());
            _new.setTime(0); //SHOULD be the same time as the main entry, but NOOOO _new.setTime(entry.getTime()) throws DateTimeException, so you get 0, screw you!
            zout.putNextEntry(_new);
            IOUtils.copy(zin, zout);
            added.add(entry.getName());
        }
        return added;
    }

    public static byte[] base64DecodeStringList(List<String> strings) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        for (String string : strings) {
            bos.write(Base64.getDecoder().decode(string));
        }
        return bos.toByteArray();
    }

    public static File delete(File file) {
        if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
        if (file.exists()) file.delete();
        return file;
    }

    public static File createEmpty(File file) throws IOException {
        delete(file);
        file.createNewFile();
        return file;
    }

    public static Path getCacheBase(Project project) {
        File gradleUserHomeDir = project.getGradle().getGradleUserHomeDir();
        return Paths.get(gradleUserHomeDir.getPath(), "caches", "forge_gradle");
    }

    public static File getCache(Project project, String... tail) {
        return Paths.get(getCacheBase(project).toString(), tail).toFile();
    }

    public static void extractZip(File source, File target, boolean overwrite) throws IOException {
        extractZip(source, target, overwrite, false);
    }

    public static void extractZip(File source, File target, boolean overwrite, boolean deleteExtras) throws IOException {
        Set<File> extra = deleteExtras ? Files.walk(target.toPath()).filter(Files::isRegularFile).map(Path::toFile).collect(Collectors.toSet()) : new HashSet<>();

        try (ZipFile zip = new ZipFile(source)) {
            Enumeration<? extends ZipEntry> enu = zip.entries();
            while (enu.hasMoreElements()) {
                ZipEntry e = enu.nextElement();
                if (e.isDirectory()) continue;
                File out = new File(target, e.getName());
                File parent = out.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }
                extra.remove(out);

                if (out.exists()) {
                    if (!overwrite)
                        continue;

                    //Reading is fast, and prevents Disc wear, so check if it's equals before writing.
                    try (FileInputStream fis = new FileInputStream(out)){
                        if (IOUtils.contentEquals(zip.getInputStream(e), fis))
                            continue;
                    }
                }

                try (FileOutputStream fos = new FileOutputStream(out)) {
                    IOUtils.copy(zip.getInputStream(e), fos);
                }
            }
        }

        if (deleteExtras) {
            extra.forEach(File::delete);

            //Delete empty directories
            Files.walk(target.toPath())
            .filter(Files::isDirectory)
            .sorted(Comparator.reverseOrder())
            .map(Path::toFile)
            .filter(f -> f.list().length == 0)
            .forEach(File::delete);
        }
    }

    public static File updateDownload(Project project, File target, Download dl) throws IOException {
        if (!target.exists() || !HashFunction.SHA1.hash(target).equals(dl.sha1)) {
            project.getLogger().lifecycle("Downloading: " + dl.url);

            if (!target.getParentFile().exists()) {
                target.getParentFile().mkdirs();
            }

            FileUtils.copyURLToFile(dl.url, target);
        }
        return target;
    }

    public static <T> T loadJson(File target, Class<T> clz) throws IOException {
        try (InputStream in = new FileInputStream(target)) {
            return GSON.fromJson(new InputStreamReader(in), clz);
        }
    }
    public static <T> T loadJson(InputStream in, Class<T> clz) {
        return GSON.fromJson(new InputStreamReader(in), clz);
    }

    public static void updateHash(File target) throws IOException {
        updateHash(target, HashFunction.values());
    }
    public static void updateHash(File target, HashFunction... functions) throws IOException {
        for (HashFunction function : functions) {
            File cache = new File(target.getAbsolutePath() + "." + function.getExtension());
            if (target.exists()) {
                String hash = function.hash(target);
                Files.write(cache.toPath(), hash.getBytes());
            } else if (cache.exists()) {
                cache.delete();
            }
        }
    }

    public static void forZip(ZipFile zip, IOConsumer<ZipEntry> consumer) throws IOException {
        for (Enumeration<? extends ZipEntry> entries = zip.entries(); entries.hasMoreElements();) {
            consumer.accept(entries.nextElement());
        }
    }
    @FunctionalInterface
    public interface IOConsumer<T> {
        void accept(T value) throws IOException;
    }

    /**
     * Resolves the supplied object to a string.
     * If the input is null, this will return null.
     * Closures and Callables are called with no arguments.
     * Arrays use Arrays.toString().
     * File objects return their absolute paths.
     * All other objects have their toString run.
     * @param obj Object to resolve
     * @return resolved string
     */
    @SuppressWarnings("rawtypes")
    @Nullable
    public static String resolveString(@Nullable Object obj) {
        if (obj == null)
            return null;
        else if (obj instanceof String) // stop early if its the right type. no need to do more expensive checks
            return (String)obj;
        else if (obj instanceof Closure)
            return resolveString(((Closure)obj).call());// yes recursive.
        else if (obj instanceof Callable) {
            try {
                return resolveString(((Callable)obj).call());
            } catch (Exception e) {
                return null;
            }
        } else if (obj instanceof File)
            return ((File) obj).getAbsolutePath();
        else if (obj.getClass().isArray()) { // arrays
            if (obj instanceof Object[])
                return Arrays.toString(((Object[]) obj));
            else if (obj instanceof byte[])
                return Arrays.toString(((byte[]) obj));
            else if (obj instanceof char[])
                return Arrays.toString(((char[]) obj));
            else if (obj instanceof int[])
                return Arrays.toString(((int[]) obj));
            else if (obj instanceof float[])
                return Arrays.toString(((float[]) obj));
            else if (obj instanceof double[])
                return Arrays.toString(((double[]) obj));
            else if (obj instanceof long[])
                return Arrays.toString(((long[]) obj));
            else
                return obj.getClass().getSimpleName();
        }
        return obj.toString();
    }

    public static <T> T[] toArray(JsonArray array, Function<JsonElement, T> adapter, IntFunction<T[]> arrayFactory) {
        return StreamSupport.stream(array.spliterator(), false).map(adapter).toArray(arrayFactory);
    }

    public static byte[] getZipData(File file, String name) throws IOException {
        try (ZipFile zip = new ZipFile(file)) {
            ZipEntry entry = zip.getEntry(name);
            if (entry == null)
                throw new IOException("Zip Missing Entry: " + name + " File: " + file);

            return IOUtils.toByteArray(zip.getInputStream(entry));
        }
    }


    public static <T> T fromJson(InputStream stream, Class<T> classOfT) throws JsonSyntaxException, JsonIOException {
        return GSON.fromJson(new InputStreamReader(stream), classOfT);
    }
    public static <T> T fromJson(byte[] data, Class<T> classOfT) throws JsonSyntaxException, JsonIOException {
        return GSON.fromJson(new InputStreamReader(new ByteArrayInputStream(data)), classOfT);
    }

    @Nonnull
    public static String capitalize(@Nonnull final String toCapitalize) {
        return toCapitalize.length() > 1 ? toCapitalize.substring(0, 1).toUpperCase() + toCapitalize.substring(1) : toCapitalize;
    }

    public static Stream<String> lines(InputStream input) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        return reader.lines().onClose(() -> {
            try {
                reader.close();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

    public static ZipEntry getStableEntry(String name) {
        return getStableEntry(name, Utils.ZIPTIME);
    }

    public static ZipEntry getStableEntry(String name, long time) {
        TimeZone _default = TimeZone.getDefault();
        TimeZone.setDefault(GMT);
        ZipEntry ret = new ZipEntry(name);
        ret.setTime(time);
        TimeZone.setDefault(_default);
        return ret;
    }

    public static void createRunConfigTasks(final MinecraftExtension extension, final TaskProvider<ExtractNatives> extractNatives, final TaskProvider<?>... setupTasks) {
        List<TaskProvider<?>> setupTasksLst = new ArrayList<>(Arrays.asList(setupTasks));

        final TaskProvider<Task> prepareRuns = extension.getProject().getTasks().register("prepareRuns", Task.class, task -> {
            task.setGroup(RunConfig.RUNS_GROUP);
            task.dependsOn(extractNatives, setupTasksLst);
        });

        final TaskProvider<Task> makeSrcDirs = extension.getProject().getTasks().register("makeSrcDirs", Task.class, task ->
                task.doFirst(t -> {
                    final JavaPluginExtension java = task.getProject().getExtensions().getByType(JavaPluginExtension.class);

                    java.getSourceSets().forEach(s -> s.getAllSource()
                            .getSrcDirs().stream().filter(f -> !f.exists()).forEach(File::mkdirs));
                }));
        setupTasksLst.add(makeSrcDirs);

        extension.getRuns().forEach(RunConfig::mergeParents);

        // Create run configurations _AFTER_ all projects have evaluated so that _ALL_ run configs exist and have been configured
        extension.getProject().getGradle().projectsEvaluated(gradle -> {
            VersionJson json = null;

            try {
                json = Utils.loadJson(extractNatives.get().getMeta().get().getAsFile(), VersionJson.class);
            } catch (IOException ignored) {
            }

            List<String> additionalClientArgs = json != null ? json.getPlatformJvmArgs() : Collections.emptyList();

            extension.getRuns().forEach(RunConfig::mergeChildren);
            extension.getRuns().forEach(run -> RunConfigGenerator.createRunTask(run, extension.getProject(), prepareRuns, additionalClientArgs));

            EclipseHacks.doEclipseFixes(extension, extractNatives, setupTasksLst);

            RunConfigGenerator.createIDEGenRunsTasks(extension, prepareRuns, makeSrcDirs, additionalClientArgs);
        });
    }

    public static void addRepoFilters(Project project) {
        if (!ENABLE_FILTER_REPOS) return;

        if (project.getGradle().getStartParameter().getTaskNames().stream().anyMatch(t -> t.endsWith("DownloadSources"))) {
            // Only modify repos already present to fix issues with IntelliJ's download sources
            project.getRepositories().forEach(Utils::addMappedFilter);
        } else {
            // Modify Repos already present and when they get added
            project.getRepositories().all(Utils::addMappedFilter);
        }
    }

    private static void addMappedFilter(ArtifactRepository repository) {
        // Skip our "Fake" Repos that actually do provide the de-obfuscated Artifacts
        if (repository instanceof GradleRepositoryAdapter) return;

        // Exclude Artifacts that are being de-obfuscated via ForgeGradle (_mapped_ in version)
        repository.content(rcd -> rcd.excludeVersionByRegex(".*", ".*", ".*_mapped_.*"));
    }

    public static File getMCDir()
    {
        switch (VersionJson.OS.getCurrent()) {
            case OSX:
                return new File(System.getProperty("user.home") + "/Library/Application Support/minecraft");
            case WINDOWS:
                return new File(System.getenv("APPDATA") + "\\.minecraft");
            case LINUX:
            default:
                return new File(System.getProperty("user.home") + "/.minecraft");
        }
    }

    public static String replaceTokens(Map<String, ?> tokens, String value) {
        StringBuilder buf = new StringBuilder();

        for (int x = 0; x < value.length(); x++) {
            char c = value.charAt(x);
            if (c == '\\') {
                if (x == value.length() - 1)
                    throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
                buf.append(value.charAt(++x));
            } else if (c == '{' || c ==  '\'') {
                StringBuilder key = new StringBuilder();
                for (int y = x + 1; y <= value.length(); y++) {
                    if (y == value.length())
                        throw new IllegalArgumentException("Illegal pattern (Unclosed " + c + "): " + value);
                    char d = value.charAt(y);
                    if (d == '\\') {
                        if (y == value.length() - 1)
                            throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
                        key.append(value.charAt(++y));
                    } else if (c == '{' && d == '}') {
                        x = y;
                        break;
                    } else if (c == '\'' && d == '\'') {
                        x = y;
                        break;
                    } else
                        key.append(d);
                }
                if (c == '\'')
                    buf.append(key);
                else {
                    Object v = tokens.get(key.toString());
                    if (v instanceof Supplier)
                        v = ((Supplier<?>) v).get();

                    buf.append(v == null ? "{" + key + "}" : v);
                }
            } else {
                buf.append(c);
            }
        }

        return buf.toString();
    }
} public class VersionJson {
    @Nullable
    public Arguments arguments;
    public AssetIndex assetIndex;
    public String assets;
    @Nullable
    public Map<String, Download> downloads;
    public Library[] libraries;

    private List<LibraryDownload> _natives = null;

    public List<LibraryDownload> getNatives() {
        if (_natives == null) {
            _natives = new ArrayList<>();
            OS os = OS.getCurrent();
            for (Library lib : libraries) {
                if (lib.natives != null && lib.downloads.classifiers != null && lib.natives.containsKey(os.getName())) {
                    LibraryDownload l = lib.downloads.classifiers.get(lib.natives.get(os.getName()));
                    if (l != null) {
                        _natives.add(l);
                    }
                }
            }
        }
        return _natives;
    }

    public List<String> getPlatformJvmArgs() {
        if (arguments == null || arguments.jvm == null)
            return Collections.emptyList();

        return Stream.of(arguments.jvm).filter(arg -> arg.rules != null && arg.isAllowed()).
                flatMap(arg -> arg.value.stream()).
                map(s -> {
                    if (s.indexOf(' ') != -1)
                        return "\"" + s + "\"";
                    else
                        return s;
                }).collect(Collectors.toList());
    }

    public static class Arguments {
        public Argument[] game;
        @Nullable
        public Argument[] jvm;
    }

    public static class Argument extends RuledObject {
        public List<String> value;

        public Argument(@Nullable Rule[] rules, List<String> value) {
            this.rules = rules;
            this.value = value;
        }

        public static class Deserializer implements JsonDeserializer<VersionJson.Argument> {
            @Override
            public Argument deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                if (json.isJsonPrimitive()) {
                    return new Argument(null, Collections.singletonList(json.getAsString()));
                }

                JsonObject obj = json.getAsJsonObject();
                if (!obj.has("rules") || !obj.has("value"))
                    throw new JsonParseException("Error parsing arguments in version json. File is corrupt or its format has changed.");

                JsonElement val = obj.get("value");
                Rule[] rules = Utils.GSON.fromJson(obj.get("rules"), Rule[].class);
                @SuppressWarnings("unchecked")
                List<String> value = val.isJsonPrimitive() ? Collections.singletonList(val.getAsString()) : Utils.GSON.fromJson(val, List.class);

                return new Argument(rules, value);
            }
        }
    }

    public static class RuledObject {
        @Nullable
        public Rule[] rules;

        public boolean isAllowed() {
            if (rules != null) {
                for (Rule rule : rules) {
                    if (!rule.allowsAction()) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    public static class Rule {
        public String action;
        public OsCondition os;

        public boolean allowsAction() {
            return (os == null || os.platformMatches()) == action.equals("allow");
        }
    }

    public static class OsCondition {
        @Nullable
        public String name;
        @Nullable
        public String version;
        @Nullable
        public String arch;

        public boolean nameMatches() {
            return name == null || OS.getCurrent().getName().equals(name);
        }

        public boolean versionMatches() {
            return version == null || Pattern.compile(version).matcher(System.getProperty("os.version")).find();
        }

        public boolean archMatches() {
            return arch == null || Pattern.compile(arch).matcher(System.getProperty("os.arch")).find();
        }

        public boolean platformMatches() {
            return nameMatches() && versionMatches() && archMatches();
        }
    }

    public static class AssetIndex extends Download {
        public String id;
        public int totalSize;
    }

    public static class Download {
        public String sha1;
        public int size;
        public URL url;
    }

    public static class LibraryDownload extends Download {
        public String path;
    }

    public static class Downloads {
        @Nullable
        public Map<String, LibraryDownload> classifiers;
        @Nullable
        public LibraryDownload artifact;
    }

    public static class Library extends RuledObject {
        //Extract? rules?
        public String name;
        public Map<String, String> natives;
        public Downloads downloads;
        private Artifact _artifact;

        public Artifact getArtifact() {
            if (_artifact == null) {
                _artifact = Artifact.from(name);
            }
            return _artifact;
        }
    }

    public enum OS {
        WINDOWS("windows", "win"),
        LINUX("linux", "linux", "unix"),
        OSX("osx", "mac"),
        UNKNOWN("unknown");

        private final String name;
        private final String[] keys;

        OS(String name, String... keys) {
            this.name = name;
            this.keys = keys;
        }

        public String getName() {
            return this.name;
        }

        public static OS getCurrent() {
            String prop = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
            for (OS os : OS.values()) {
                for (String key : os.keys) {
                    if (prop.contains(key)) {
                        return os;
                    }
                }
            }
            return UNKNOWN;
        }
    }
} public class ChannelProvidersExtension {
    public static final String EXTENSION_NAME = "channelProviders";
    private final Map<String, ChannelProvider> providers = new ConcurrentHashMap<>();

    public ChannelProvidersExtension() {
        // Add the default providers
        addProvider(new OfficialChannelProvider());
        addProvider(new MCPChannelProvider());
    }

    /**
     * Returns an unmodifiable view of the provider map.
     *
     * @return an unmodifiable view of the provider map
     */
    public Map<String, ChannelProvider> getProviderMap() {
        return Collections.unmodifiableMap(providers);
    }

    /**
     * Retrieves a possibly-null {@link ChannelProvider} from the provider map using the {@code channel} key.
     *
     * @param channel the channel key to use for querying
     * @return a possibly-null {@link ChannelProvider} from the provider map
     */
    @Nullable
    public ChannelProvider getProvider(String channel) {
        return providers.get(channel);
    }

    /**
     * Adds a {@link ChannelProvider} to the provider map using all of its defined channels in {@link ChannelProvider#getChannels()}.
     *
     * @param provider The provider to add to the channel provider map
     * @throws IllegalArgumentException if a channel name from the provider is already registered to another provider
     */
    public void addProvider(ChannelProvider provider) {
        for (String channel : provider.getChannels()) {
            ChannelProvider previous = providers.get(channel);
            if (previous != null)
                throw new IllegalArgumentException(channel + " is already registered to " + previous + " containing channels " + previous.getChannels());
            providers.put(channel, provider);
        }
    }
} public class MCPRepo extends BaseRepo {
    private static MCPRepo INSTANCE = null;
    private static final String GROUP_MINECRAFT = "net.minecraft";
    private static final String NAMES_MINECRAFT = "^(client|server|joined|mappings_[a-z_]+)$";
    private static final String GROUP_MCP = "de.oceanlabs.mcp";
    private static final String NAMES_MCP = "^(mcp_config)$";
    private static final String STEP_MERGE = "merge"; //TODO: Design better way to get steps output, for now hardcode
    private static final String STEP_RENAME = "rename";

    //This is the artifact we expose that is a zip containing SRG->Official fields and methods.
    public static final String MAPPING_DEP = "net.minecraft:mappings_{CHANNEL}:{VERSION}@zip";
    public static String getMappingDep(String channel, String version) {
        return MAPPING_DEP.replace("{CHANNEL}", channel).replace("{VERSION}", version);
    }

    private final Project project;
    private final Repository repo;
    private final Map<String, MCPWrapper> wrappers = Maps.newHashMap();
    private final Map<String, McpNames> mapCache = new HashMap<>();

    private MCPRepo(Project project, File cache, Logger log) {
        super(cache, log);
        this.project = project;
        this.repo = SimpleRepository.of(ArtifactProviderBuilder.begin(ArtifactIdentifier.class)
            .provide(this)
        );
    }

    private static MCPRepo getInstance(Project project) {
        if (INSTANCE == null)
            INSTANCE = new MCPRepo(project, Utils.getCache(project, "mcp_repo"), project.getLogger());
        return INSTANCE;
    }
    public static void attach(Project project) {
        MCPRepo instance = getInstance(project);
        GradleRepositoryAdapter.add(project.getRepositories(), "MCP_DYNAMIC", instance.getCacheRoot(), instance.repo);
    }

    public static ArtifactProvider<ArtifactIdentifier> create(Project project) {
        return getInstance(project);
    }

    @Override
    protected File cache(String... path) {
        return super.cache(path);
    }

    File cacheMC(String side, String version, @Nullable String classifier, String ext) {
        if (classifier != null)
            return cache("net", "minecraft", side, version, side + '-' + version + '-' + classifier + '.' + ext);
        return cache("net", "minecraft", side, version, side + '-' + version + '.' + ext);
    }

    private File cacheMCP(String version, @Nullable String classifier, String ext) {
        if (classifier != null)
            return cache("de", "oceanlabs", "mcp", "mcp_config", version, "mcp_config-" + version + '-' + classifier + '.' + ext);
        return cache("de", "oceanlabs", "mcp", "mcp_config", version, "mcp_config-" + version + '.' + ext);
    }
    private File cacheMCP(String version) {
        return cache("de", "oceanlabs", "mcp", "mcp_config", version);
    }

    @Override
    public File findFile(ArtifactIdentifier artifact) throws IOException {
        String name = artifact.getName();
        String group = artifact.getGroup();

        if (group.equals(GROUP_MCP)) {
            if (!name.matches(NAMES_MCP))
                return null;
        } else if (group.equals(GROUP_MINECRAFT)) {
            if (!name.matches(NAMES_MINECRAFT))
                return null;
        } else
            return null;

        String version = artifact.getVersion();
        String classifier = artifact.getClassifier() == null ? "" : artifact.getClassifier();
        String ext = artifact.getExtension();

        debug("  " + REPO_NAME + " Request: " + artifact.getGroup() + ":" + name + ":" + version + ":" + classifier + "@" + ext);

        if (group.equals(GROUP_MINECRAFT)) {
            if (name.startsWith("mappings_")) {
                if ("zip".equals(ext)) {
                    return findNames(name.substring(9) + '_' + version);
                } else if ("pom".equals(ext)) {
                    return findEmptyPom(name, version);
                }
            } else if ("pom".equals(ext)) {
                return findPom(name, version);
            } else if ("jar".equals(ext)) {
                switch (classifier) {
                    case "":              return findRaw(name, version);
                    case "srg":           return findSrg(name, version);
                    case "extra":         return findExtra(name, version);
                }
            }
        } else if (group.equals(GROUP_MCP)) {
            /* Gradle fucks up caching for anything that isnt a zip or a jar, this is fucking annoying we can't do this.
            MappingFile.Format format = MappingFile.Format.get(ext);
            if (format != null) {
                classifier = classifier.replace('!', '.'); //We hack around finding the extension by using a invalid path character
                switch (classifier) {
                    case "obf-to-srg": return findRenames(classifier, format, version, false);
                    case "srg-to-obf": return findRenames(classifier, format, version, true);
                }
                if (classifier.startsWith("obf-to-")) return findRenames(classifier, format, version, classifier.substring(7), true, false);
                if (classifier.startsWith("srg-to-")) return findRenames(classifier, format, version, classifier.substring(7), false,  false);
                if (classifier.endsWith  ("-to-obf")) return findRenames(classifier, format, version, classifier.substring(0, classifier.length() - 7), true, true);
                if (classifier.endsWith  ("-to-srg")) return findRenames(classifier, format, version, classifier.substring(0, classifier.length() - 7), false, true);
            }
            */
        }
        return null;
    }

    HashStore commonHash(File mcp) {
        return new HashStore(this.getCacheRoot())
            .add("mcp", mcp);
    }

    @Nullable
    File getMCP(String version) {
        return MavenArtifactDownloader.manual(project, "de.oceanlabs.mcp:mcp_config:" + version + "@zip", false);
    }

    @Nullable
    private File findVersion(String version) throws IOException {
        File manifest = cache("versions", "manifest.json");
        if (!DownloadUtils.downloadEtag(new URL(MinecraftRepo.MANIFEST_URL), manifest, project.getGradle().getStartParameter().isOffline()))
            return null;
        Utils.updateHash(manifest);
        File json = cache("versions", version, "version.json");

        URL url =  Utils.loadJson(manifest, ManifestJson.class).getUrl(version);
        if (url == null)
            throw new RuntimeException("Missing version from manifest: " + version);

        if (!DownloadUtils.downloadEtag(url, json, project.getGradle().getStartParameter().isOffline()))
            return null;
        Utils.updateHash(json);
        return json;
    }

    @Nullable
    private File findPom(String side, String version) throws IOException {
        File mcp = getMCP(version);
        if (mcp == null)
            return null;

        File pom = cacheMC(side, version, null, "pom");
        debug("    Finding pom: " + pom);
        HashStore cache = commonHash(mcp).load(cacheMC(side, version, null, "pom.input"));
        File json = null;
        if (!"server".equals(side)) {
            json = findVersion(MinecraftRepo.getMCVersion(version));
            if (json == null) {
                project.getLogger().lifecycle("Could not make Minecraft POM. Missing version json");
                return null;
            }
            cache.add("json", json);
        }

        if (!cache.isSame() || !pom.exists()) {
            POMBuilder builder = new POMBuilder(GROUP_MINECRAFT, side, version);
            if (!"server".equals(side)) {
                VersionJson meta = Utils.loadJson(json, VersionJson.class);
                for (VersionJson.Library lib : meta.libraries) {
                    if (lib.isAllowed()) {
                        if (lib.downloads.artifact != null)
                            builder.dependencies().add(lib.name, "compile");
                        if (lib.downloads.classifiers != null) {
                            if (lib.downloads.classifiers.containsKey("test")) {
                                builder.dependencies().add(lib.name, "test").withClassifier("test");
                            }
                            if (lib.natives != null && lib.natives.containsKey(MinecraftRepo.CURRENT_OS) && !lib.getArtifact().getName().contains("java-objc-bridge")) {
                                builder.dependencies().add(lib.name, "runtime").withClassifier(lib.natives.get(MinecraftRepo.CURRENT_OS));
                            }
                        }
                    }
                }
                builder.dependencies().add("net.minecraft:client:" + version, "compile").withClassifier("extra");
                //builder.dependencies().add("net.minecraft:client:" + getMCVersion(version), "compile").withClassifier("data");
            } else {
                builder.dependencies().add("net.minecraft:server:" + version, "compile").withClassifier("extra");
                //builder.dependencies().add("net.minecraft:server:" + getMCVersion(version), "compile").withClassifier("data");
            }

            MCPWrapper wrapper = getWrapper(version, mcp);
            wrapper.getConfig().getLibraries(side).forEach(e -> builder.dependencies().add(e, "compile"));

            String ret = builder.tryBuild();
            if (ret == null)
                return null;
            FileUtils.writeByteArrayToFile(pom, ret.getBytes());
            cache.save();
            Utils.updateHash(pom, HashFunction.SHA1);
        }

        return pom;
    }

    @Nullable
    private File findRaw(String side, String version) throws IOException {
        if (!"joined".equals(side))
            return null; //MinecraftRepo provides these

        return findStepOutput(side, version, null, "jar", STEP_MERGE);
    }

    @Nullable
    private File findSrg(String side, String version) throws IOException {
        return findStepOutput(side, version, "srg", "jar", STEP_RENAME);
    }

    @Nullable
    private File findStepOutput(String side, String version, @Nullable String classifier, String ext, String step) throws IOException {
        File mcp = getMCP(version);
        if (mcp == null)
            return null;
        File raw = cacheMC(side, version, classifier, ext);
        debug("  Finding " + step + ": " + raw);
        HashStore cache = commonHash(mcp).load(cacheMC(side, version, classifier, ext + ".input"));

        if (!cache.isSame() || !raw.exists()) {
            MCPWrapper wrapper = getWrapper(version, mcp);
            MCPRuntime runtime = wrapper.getRuntime(project, side);
            try {
                File output = runtime.execute(log, step);
                FileUtils.copyFile(output, raw);
                cache.save();
                Utils.updateHash(raw, HashFunction.SHA1);
            } catch (IOException e) {
                throw e;
            } catch (Exception e) {
                e.printStackTrace();
                log.lifecycle(e.getMessage());
                if (e instanceof RuntimeException) throw (RuntimeException)e;
                throw new RuntimeException(e);
            }
        }
        return raw;
    }

    private synchronized MCPWrapper getWrapper(String version, File data) throws IOException {
        String hash = HashFunction.SHA1.hash(data);
        MCPWrapper ret = wrappers.get(version);
        if (ret == null  || !hash.equals(ret.getHash())) {
            ret = new MCPWrapper(hash, data, cacheMCP(version));
            wrappers.put(version, ret);
        }
        return ret;
    }

    @Nullable
    File findRenames(String classifier, IMappingFile.Format format, String version, boolean toObf) throws IOException {
        String ext = format.name().toLowerCase();
        //File names = findNames(version));
        File mcp = getMCP(version);
        if (mcp == null)
            return null;

        File file = cacheMCP(version, classifier, ext);
        debug("    Finding Renames: " + file);
        HashStore cache = commonHash(mcp).load(cacheMCP(version, classifier, ext + ".input"));

        if (!cache.isSame() || !file.exists()) {
            MCPWrapper wrapper = getWrapper(version, mcp);
            byte[] data = wrapper.getData("mappings");
            IMappingFile obf_to_srg = IMappingFile.load(new ByteArrayInputStream(data));
            obf_to_srg.write(file.toPath(), format, toObf);
            cache.save();
            Utils.updateHash(file, HashFunction.SHA1);
        }

        return file;
    }

    @Nullable
    private File findNames(String mapping) throws IOException {
        int idx = mapping.lastIndexOf('_');
        if (idx == -1) return null; //Invalid format
        String channel = mapping.substring(0, idx);
        String version = mapping.substring(idx + 1);

        ChannelProvidersExtension channelProviders = project.getExtensions().getByType(ChannelProvidersExtension.class);
        ChannelProvider provider = channelProviders.getProvider(channel);
        if (provider == null)
            throw new IllegalArgumentException("Unknown mapping provider: " + mapping + ", currently loaded: " + channelProviders.getProviderMap().keySet());
        return provider.getMappingsFile(this, project, channel, version);
    }

    private McpNames loadMCPNames(String name, File data) throws IOException {
        McpNames map = mapCache.get(name);
        String hash = HashFunction.SHA1.hash(data);
        if (map == null || !hash.equals(map.hash)) {
            map = McpNames.load(data);
            mapCache.put(name, map);
        }
        return map;
    }

    @SuppressWarnings("unused")
    @Nullable
    private File findRenames(String classifier, IMappingFile.Format format, String version, String mapping, boolean obf, boolean reverse) throws IOException {
        String ext = format.name().toLowerCase();
        File names = findNames(version);
        File mcp = getMCP(version);
        if (mcp == null || names == null)
            return null;

        File file = cacheMCP(version, classifier, ext);
        debug("    Finding Renames: " + file);
        HashStore cache = commonHash(mcp).load(cacheMCP(version, classifier, ext + ".input"));

        if (!cache.isSame() || !file.exists()) {
            MCPWrapper wrapper = getWrapper(version, mcp);
            byte[] data = wrapper.getData("mappings");
            IMappingFile input = IMappingFile.load(new ByteArrayInputStream(data)); //SRG->OBF
            if (!obf)
                input = input.reverse().chain(input); //SRG->OBF + OBF->SRG = SRG->SRG

            McpNames map = loadMCPNames(mapping, names);
            IMappingFile ret = input.rename(new IRenamer() {
                @Override
                public String rename(IField value) {
                    return map.rename(value.getMapped());
                }

                @Override
                public String rename(IMethod value) {
                    return map.rename(value.getMapped());
                }
            });

            ret.write(file.toPath(), format, reverse);
            cache.save();
            Utils.updateHash(file, HashFunction.SHA1);
        }

        return file;
    }

    @Nullable
    private File findExtra(String side, String version) throws IOException {
        File raw = findRaw(side, version);
        File mcp = getMCP(version);
        if (raw == null || mcp == null)
            return null;

        File extra = cacheMC(side, version, "extra", "jar");
        HashStore cache = commonHash(mcp).load(cacheMC(side, version, "extra", "jar.input"))
                .add("raw", raw)
                .add("mcp", mcp)
                .add("codever", "1");

        if (!cache.isSame() || !extra.exists()) {
            MCPWrapper wrapper = getWrapper(version, mcp);
            byte[] data = wrapper.getData("mappings");
            MinecraftRepo.splitJar(raw, new ByteArrayInputStream(data), extra, false, true);
            cache.save();
        }

        return extra;
    }

    protected static void writeCsv(String name, List<String[]> mappings, ZipOutputStream out) throws IOException {
        if (mappings.size() <= 1)
            return;
        out.putNextEntry(Utils.getStableEntry(name));
        try (CsvWriter writer = CsvWriter.builder().lineDelimiter(LineDelimiter.LF).build(new UncloseableOutputStreamWriter(out))) {
            mappings.forEach(writer::writeRow);
        }
        out.closeEntry();
    }

    private static class UncloseableOutputStreamWriter extends OutputStreamWriter {
        private UncloseableOutputStreamWriter(OutputStream out) {
            super(out);
        }

        @Override
        public void close() throws IOException {
            super.flush();
        }
    }

    @Nullable
    private File findEmptyPom(String side, String version) throws IOException {
        File pom = cacheMC(side, version, null, "pom");
        debug("    Finding pom: " + pom);
        HashStore cache = new HashStore(this.getCacheRoot()).load(cacheMC(side, version, null, "pom.input"));

        if (!cache.isSame() || !pom.exists()) {
            String ret = new POMBuilder(GROUP_MINECRAFT, side, version).tryBuild();
            if (ret == null)
                return null;
            FileUtils.writeByteArrayToFile(pom, ret.getBytes());
            cache.save();
            Utils.updateHash(pom, HashFunction.SHA1);
        }

        return pom;
    }

    @Override
    protected void debug(String message) {
        super.debug(message);
    }
} public abstract class DownloadMCPMappings extends DefaultTask {
    public DownloadMCPMappings() {
        getOutput().convention(getProject().getLayout().getBuildDirectory().file("mappings.zip"));
    }

    @TaskAction
    public void download() throws IOException {
        File out = getMappingFile();
        File output = getOutput().get().getAsFile();
        this.setDidWork(out.exists());
        if (FileUtils.contentEquals(out, output)) return;
        if (output.exists()) output.delete();
        if (output.getParentFile() != null && !output.getParentFile().exists()) output.getParentFile().mkdirs();
        FileUtils.copyFile(out, output);
    }

    private File getMappingFile() {
        String mappings = getMappings().get();
        int idx = mappings.lastIndexOf('_');
        if (idx == -1)
            throw new IllegalArgumentException("Invalid mapping string format, must be {channel}_{version}.");
        String channel = mappings.substring(0, idx);
        String version = mappings.substring(idx + 1);
        String artifact = MCPRepo.getMappingDep(channel, version);
        File ret = MavenArtifactDownloader.generate(getProject(), artifact, false);
        if (ret == null)
            throw new IllegalStateException("Failed to download mappings: " + artifact);
        return ret;
    }

    @Input
    public abstract Property<String> getMappings();

    @OutputFile
    public abstract RegularFileProperty getOutput();
} public abstract class GenerateSRG extends DefaultTask {
    private final Property<IMappingFile.Format> format;
    private boolean notch = false;
    private boolean reverse = false;

    public GenerateSRG() {
        this.format = getProject().getObjects().property(IMappingFile.Format.class)
                .convention(IMappingFile.Format.TSRG2);
        getOutput().convention(getProject().getLayout().getBuildDirectory().dir(getName()).map(f -> f.file("output.tsrg")));
    }

    @TaskAction
    public void apply() throws IOException {
        File names = findNames(getMappings().get());
        if (names == null)
            throw new IllegalStateException("Invalid mappings: " + getMappings() + " Could not find archive");

        IMappingFile input = IMappingFile.load(getSrg().get().getAsFile());
        if (!notch)
            input = input.reverse().chain(input); // Reverse makes SRG->OBF, chain makes SRG->SRG

        McpNames map = McpNames.load(names);
        IMappingFile ret = input.rename(new IRenamer() {
            @Override
            public String rename(IField value) {
                return map.rename(value.getMapped());
            }

            @Override
            public String rename(IMethod value) {
                return map.rename(value.getMapped());
            }
        });

        ret.write(getOutput().get().getAsFile().toPath(), getFormat().get(), reverse);
    }

    private File findNames(String mapping) {
        int idx = mapping.lastIndexOf('_');
        if (idx == -1) return null; //Invalid format
        String channel = mapping.substring(0, idx);
        String version = mapping.substring(idx + 1);
        String desc = MCPRepo.getMappingDep(channel, version);
        return MavenArtifactDownloader.generate(getProject(), desc, false);
    }

    @InputFile
    public abstract RegularFileProperty getSrg();

    @Input
    public abstract Property<String> getMappings();

    @Input
    public Property<IMappingFile.Format> getFormat() {
        return this.format;
    }

    public void setFormat(String value) {
        this.getFormat().set(IMappingFile.Format.valueOf(value));
    }

    @Input
    public boolean getNotch() {
        return this.notch;
    }

    public void setNotch(boolean value) {
        this.notch = value;
    }

    @Input
    public boolean getReverse() {
        return this.reverse;
    }

    public void setReverse(boolean value) {
        this.reverse = value;
    }

    @OutputFile
    public abstract RegularFileProperty getOutput();
} public abstract class RenameJarInPlace extends JarExec {
    private final Provider<Directory> workDir = getProject().getLayout().getBuildDirectory().dir(getName());
    private final Provider<RegularFile> temp = workDir.map(s -> s.file("output.jar"));

    public RenameJarInPlace() {
        getTool().set(Utils.SPECIALSOURCE);
        getArgs().addAll("--in-jar", "{input}", "--out-jar", "{output}", "--srg-in", "{mappings}", "--live");
        this.getOutputs().upToDateWhen(task -> false);
    }

    @Override
    protected List<String> filterArgs(List<String> args) {
        return replaceArgsMulti(args, ImmutableMap.of(
                "{input}", getInput().get().getAsFile(),
                "{output}", temp.get().getAsFile()
                ), ImmutableMultimap.<String, Object>builder()
                        .put("{mappings}", getMappings().get().getAsFile())
                        .putAll("{mappings}", getExtraMappings().getFiles()).build()
        );
    }

    @Override
    @TaskAction
    public void apply() throws IOException {
        File temp = this.temp.get().getAsFile();
        if (temp.getParentFile() != null && !temp.getParentFile().exists() && !temp.getParentFile().mkdirs()) {
            getProject().getLogger().warn("Could not create parent directories for temp dir '{}'", temp.getAbsolutePath());
        }

        super.apply();

        FileUtils.copyFile(temp, getInput().get().getAsFile());
    }

    // TODO: Make this a ConfigurableFileCollection? (then remove getExtraMappings())
    @InputFile
    public abstract RegularFileProperty getMappings();

    @Optional
    @InputFiles
    public abstract ConfigurableFileCollection getExtraMappings();

    @InputFile
    public abstract RegularFileProperty getInput();
} public class DeobfuscatingRepo extends BaseRepo {
    @SuppressWarnings("unused")
    private final Project project;

    //once resolved by gradle, will contain SRG-named artifacts for us to deobf
    private final Configuration origin;
    private ResolvedConfiguration resolvedOrigin;
    private final Deobfuscator deobfuscator;

    public DeobfuscatingRepo(Project project, Configuration origin, Deobfuscator deobfuscator) {
        super(Utils.getCache(project, "mod_remap_repo"), project.getLogger());
        this.project = project;
        this.origin = origin;
        this.deobfuscator = deobfuscator;
    }

    @Nullable
    private String getMappings(String version) {
        if (!version.contains("_mapped_"))
            return null;
        return version.split("_mapped_")[1];
    }

    @Override
    public File findFile(ArtifactIdentifier artifact) throws IOException {
        String version = artifact.getVersion();
        String mappings = getMappings(version);

        if (mappings == null)
            return null; //We only care about the remapped files, not orig

        version = version.substring(0, version.length() - (mappings.length() + "_mapped_".length()));
        String classifier = artifact.getClassifier() == null ? "" : artifact.getClassifier();

        Artifact unmappedArtifact = Artifact.from(artifact).withVersion(version);
        String ext = unmappedArtifact.getExtension();

        debug("  " + REPO_NAME + " Request: " + clean(artifact) + " Mapping: " + mappings);

        if ("pom".equals(ext)) {
            return findPom(unmappedArtifact, mappings);
        } else if ("jar".equals(ext)) {
            if ("sources".equals(classifier)) {
                return findSource(unmappedArtifact, mappings);
            }

            return findRaw(unmappedArtifact, mappings);
        } else {
            throw new RuntimeException("Invalid deobf dependency: " + artifact);
        }
    }

    @Nullable
    private File findPom(Artifact artifact, String mapping) throws IOException {
        Optional<File> orig = findArtifactFile(artifact);

        if (!orig.isPresent()) {
            return null;
        }

        File origFile = orig.get();

        return deobfuscator.deobfPom(origFile, mapping, getArtifactPath(artifact, mapping));
    }

    public ResolvedConfiguration getResolvedOrigin() {
        synchronized (origin) {
            if (resolvedOrigin == null) {
                resolvedOrigin = origin.getResolvedConfiguration();
            }

            return resolvedOrigin;
        }
    }

    private Optional<File> findArtifactFile(Artifact artifact) {
        Stream<ResolvedDependency> deps = getResolvedOrigin().getFirstLevelModuleDependencies(artifact.asDependencySpec()).stream();
        return deps.flatMap(
                d -> d.getModuleArtifacts().stream()
                        .filter(artifact.asArtifactMatcher())
        ).map(ResolvedArtifact::getFile).filter(File::exists).findAny();
    }

    @Nullable
    private File findRaw(Artifact artifact, String mapping) throws IOException {
        Optional<File> orig = findArtifactFile(artifact);
        if (!orig.isPresent()) {
            return null;
        }

        File origFile = orig.get();

        return deobfuscator.deobfBinary(origFile, mapping, getArtifactPath(artifact, mapping));
    }

    @Nullable
    private File findSource(Artifact artifact, String mapping) throws IOException {
        File origFile = MavenArtifactDownloader.manual(project, artifact.getDescriptor(), false);
        if (origFile == null) return null;

        return deobfuscator.deobfSources(origFile, mapping, getArtifactPath(artifact, mapping));
    }

    private String getArtifactPath(Artifact artifact, String mappings) {
        String newVersion = artifact.getVersion() + "_mapped_" + mappings;

        return artifact.withVersion(newVersion).getLocalPath();
    }
} public class Deobfuscator {
    private final Project project;
    private final File cacheRoot;

    private final DocumentBuilder xmlParser;
    private final XPath xPath;
    private final Transformer xmlTransformer;

    public Deobfuscator(Project project, File cacheRoot) {
        this.project = project;
        this.cacheRoot = cacheRoot;

        try {
            xPath = XPathFactory.newInstance().newXPath();
            xmlParser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            xmlTransformer = TransformerFactory.newInstance().newTransformer();
        } catch (ParserConfigurationException | TransformerConfigurationException e) {
            throw new RuntimeException("Error configuring XML parsers", e);
        }
    }

    public File deobfPom(File original, String mappings, String... cachePath) throws IOException {
        project.getLogger().debug("Updating POM file {} with mappings {}", original.getName(), mappings);

        File output = getCacheFile(cachePath);
        File input = new File(output.getParent(), output.getName() + ".input");

        HashStore cache = new HashStore()
                .load(input)
                .add("mappings", mappings)
                .add("orig", original);

        if (!cache.isSame() || !output.exists()) {
            try {
                Document pom = xmlParser.parse(original);
                NodeList versionNodes = (NodeList) xPath.compile("/*[local-name()=\"project\"]/*[local-name()=\"version\"]").evaluate(pom, XPathConstants.NODESET);
                if (versionNodes.getLength() > 0) {
                    versionNodes.item(0).setTextContent(versionNodes.item(0).getTextContent() + "_mapped_" + mappings);
                }

                xmlTransformer.transform(new DOMSource(pom), new StreamResult(output));
            } catch (IOException | SAXException | XPathExpressionException | TransformerException e) {
                project.getLogger().error("Error attempting to modify pom file " + original.getName(), e);
                return original;
            }

            Utils.updateHash(output, HashFunction.SHA1);
            cache.save();
        }

        return output;
    }

    @Nullable
    public File deobfBinary(File original, @Nullable String mappings, String... cachePath) throws IOException {
        project.getLogger().debug("Deobfuscating binary file {} with mappings {}", original.getName(), mappings);

        File names = findMapping(mappings);
        if (names == null || !names.exists()) {
            return null;
        }

        File output = getCacheFile(cachePath);
        File input = new File(output.getParent(), output.getName() + ".input");

        HashStore cache = new HashStore()
                .load(input)
                .add("names", names)
                .add("orig", original);

        if (!cache.isSame() || !output.exists()) {
            RenameJarSrg2Mcp rename = project.getTasks().create("_RenameSrg2Mcp_" + new Random().nextInt(), RenameJarSrg2Mcp.class);
            rename.getInput().set(original);
            rename.getOutput().set(output);
            rename.getMappings().set(names);
            rename.setSignatureRemoval(true);
            rename.apply();
            rename.setEnabled(false);

            Utils.updateHash(output, HashFunction.SHA1);
            cache.save();
        }

        return output;
    }

    @Nullable
    public File deobfSources(File original, @Nullable String mappings, String... cachePath) throws IOException {
        project.getLogger().debug("Deobfuscating sources file {} with mappings {}", original.getName(), mappings);

        File names = findMapping(mappings);
        if (names == null || !names.exists()) {
            return null;
        }

        File output = getCacheFile(cachePath);

        File input = new File(output.getParent(), output.getName() + ".input");

        HashStore cache = new HashStore()
                .load(input)
                .add("names", names)
                .add("orig", original);

        if (!cache.isSame() || !output.exists()) {
            McpNames map = McpNames.load(names);

            try (ZipInputStream zin = new ZipInputStream(new FileInputStream(original));
                 ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output))) {
                ZipEntry _old;
                while ((_old = zin.getNextEntry()) != null) {
                    zout.putNextEntry(Utils.getStableEntry(_old.getName()));

                    if (_old.getName().endsWith(".java")) {
                        String mapped = map.rename(zin, false);
                        IOUtils.write(mapped, zout, StandardCharsets.UTF_8);
                    } else {
                        IOUtils.copy(zin, zout);
                    }
                }
            }

            Utils.updateHash(output, HashFunction.SHA1);
            cache.save();
        }

        return output;
    }

    private File getCacheFile(String... cachePath) {
        File cacheFile = new File(cacheRoot, String.join(File.separator, cachePath));
        cacheFile.getParentFile().mkdirs();
        return cacheFile;
    }

    @Nullable
    private File findMapping(@Nullable String mapping) {
        if (mapping == null)
            return null;

        int idx = mapping.lastIndexOf('_');
        String channel = mapping.substring(0, idx);
        String version = mapping.substring(idx + 1);
        String desc = MCPRepo.getMappingDep(channel, version);
        return MavenArtifactDownloader.generate(project, desc, false);
    }
} public class DependencyRemapper {
    private final Project project;
    @SuppressWarnings("unused")
    private Deobfuscator deobfuscator;
    private final List<Consumer<String>> mappingListeners = new ArrayList<>();

    public DependencyRemapper(Project project, Deobfuscator deobfuscator) {
        this.project = project;
        this.deobfuscator = deobfuscator;
    }

    /*
     * Impl note: Gradle uses a lot of internal instanceof checking,
     * so it's more reliable to use the same classes gradle uses.
     *
     * Best way to do it is Dependency#copy. If that's not possible,
     * internal classes starting with Default are an option. It should be a last resort,
     * as that is a part of internal unstable APIs.
     */
    public Dependency remap(Dependency dependency) {
        if (dependency instanceof ExternalModuleDependency) {
            return remapExternalModule((ExternalModuleDependency) dependency);
        }

        if (dependency instanceof FileCollectionDependency) {
            project.getLogger().warn("files(...) dependencies are not deobfuscated. Use a flatDir repository instead: https://docs.gradle.org/current/userguide/repository_types.html#sec:flat_dir_resolver");
        }

        project.getLogger().warn("Cannot deobfuscate dependency of type {}, using obfuscated version!", dependency.getClass().getSimpleName());
        return dependency;
    }

    private ExternalModuleDependency remapExternalModule(ExternalModuleDependency dependency) {
        ExternalModuleDependency newDep = dependency.copy();
        mappingListeners.add(m -> newDep.version(v -> v.strictly(newDep.getVersion() + "_mapped_" + m)));
        return newDep;
    }

    public void attachMappings(String mappings) {
        mappingListeners.forEach(l -> l.accept(mappings));
    }

}
import net.minecraftforge.gradle.common.tasks.ApplyMappings;
import net.minecraftforge.gradle.common.tasks.ApplyRangeMap;
import net.minecraftforge.gradle.common.tasks.DownloadAssets;
import net.minecraftforge.gradle.common.tasks.DownloadMCMeta;
import net.minecraftforge.gradle.common.tasks.DownloadMavenArtifact;
import net.minecraftforge.gradle.common.tasks.ExtractExistingFiles;
import net.minecraftforge.gradle.common.tasks.ExtractMCPData;
import net.minecraftforge.gradle.common.tasks.ExtractNatives;
import net.minecraftforge.gradle.common.tasks.ExtractRangeMap;
import net.minecraftforge.gradle.common.util.BaseRepo;
import net.minecraftforge.gradle.common.util.EnvironmentChecks;
import net.minecraftforge.gradle.common.util.MinecraftRepo;
import net.minecraftforge.gradle.common.util.MojangLicenseHelper;
import net.minecraftforge.gradle.common.util.Utils;
import net.minecraftforge.gradle.common.util.VersionJson;
import net.minecraftforge.gradle.mcp.ChannelProvidersExtension;
import net.minecraftforge.gradle.mcp.MCPRepo;
import net.minecraftforge.gradle.mcp.tasks.DownloadMCPMappings;
import net.minecraftforge.gradle.mcp.tasks.GenerateSRG;
import net.minecraftforge.gradle.userdev.tasks.RenameJarInPlace;
import net.minecraftforge.gradle.userdev.util.DeobfuscatingRepo;
import net.minecraftforge.gradle.userdev.util.Deobfuscator;
import net.minecraftforge.gradle.userdev.util.DependencyRemapper;
import net.minecraftforge.srgutils.IMappingFile;
import org.apache.commons.lang3.StringUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository.MetadataSources;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.logging.Logger;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
/*
 * ForgeGradle
 * Copyright (C) 2018 Forge Development LLC
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
 * USA
 */

package net.minecraftforge.gradle.userdev;





public class UserDevPlugin implements Plugin<Project> {
    private static final String MINECRAFT = "minecraft";
    public static final String OBF = "__obfuscated";

    @Override
    public void apply(@Nonnull Project project) {
        EnvironmentChecks.checkEnvironment(project);
        Utils.addRepoFilters(project);

        final Logger logger = project.getLogger();
        final UserDevExtension extension = project.getExtensions().create(UserDevExtension.EXTENSION_NAME, UserDevExtension.class, project);
        project.getExtensions().create(ChannelProvidersExtension.EXTENSION_NAME, ChannelProvidersExtension.class);
        project.getPluginManager().apply(JavaPlugin.class);
        final File nativesFolder = project.file("build/natives/");

        final NamedDomainObjectContainer<RenameJarInPlace> reobfExtension = createReobfExtension(project);

        final Configuration minecraft = project.getConfigurations().create(MINECRAFT);
        project.getConfigurations().named(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME)
                .configure(c -> c.extendsFrom(minecraft));

        // Let gradle handle the downloading by giving it a configuration to dl. We'll focus on applying mappings to it.
        final Configuration internalObfConfiguration = project.getConfigurations().create(OBF);
        internalObfConfiguration.setDescription("Generated scope for obfuscated dependencies");

        // Create extension for dependency remapping
        // Can't create at top-level or put in `minecraft` ext due to configuration name conflict
        final Deobfuscator deobfuscator = new Deobfuscator(project, Utils.getCache(project, "deobf_dependencies"));
        final DependencyRemapper remapper = new DependencyRemapper(project, deobfuscator);
        project.getExtensions().create(DependencyManagementExtension.EXTENSION_NAME, DependencyManagementExtension.class, project, remapper);

        final TaskContainer tasks = project.getTasks();
        final TaskProvider<DownloadMavenArtifact> downloadMcpConfig = tasks.register("downloadMcpConfig", DownloadMavenArtifact.class);
        final TaskProvider<ExtractMCPData> extractSrg = tasks.register("extractSrg", ExtractMCPData.class);
        final TaskProvider<GenerateSRG> createSrgToMcp = tasks.register("createSrgToMcp", GenerateSRG.class);
        final TaskProvider<GenerateSRG> createMcpToSrg = tasks.register("createMcpToSrg", GenerateSRG.class);
        final TaskProvider<DownloadMCMeta> downloadMCMeta = tasks.register("downloadMCMeta", DownloadMCMeta.class);