diff --git a/.gitignore b/.gitignore index a8e27a8a..1520d593 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,9 @@ build/ # Gradle Patch **/build/ +# Mapping generator outputs +/mapping/build/ + # Common working directory run/ diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 3fa5e45f..76459a34 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -77,9 +77,8 @@ publishing { } } -artifacts { - archives(javadocJar) - archives(sourcesJar) +tasks.named("assemble") { + dependsOn(javadocJar, sourcesJar) } @Suppress("UNCHECKED_CAST") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ae75e77..e1082e0a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ minecraft-velocity = "3.5.0-SNAPSHOT" # https://github.com/PaperMC/Velocity tool-commons-io = "2.6" # https://github.com/apache/commons-io tool-fastutil = "8.5.11" # https://github.com/vigna/fastutil/ tool-google-guava = "28.0-jre" # https://github.com/google/guava +tool-google-gson = "2.13.2" # https://github.com/google/gson tool-netty = "4.1.86.Final" # https://github.com/netty/netty tool-spotbugs-annotations = "4.7.3" # https://github.com/spotbugs/spotbugs @@ -27,6 +28,7 @@ minecraft-velocity-proxy = { module = "com.velocitypowered:velocity-proxy", vers tool-commons-io = { module = "commons-io:commons-io", version.ref = "tool-commons-io" } tool-fastutil = { module = "it.unimi.dsi:fastutil-core", version.ref = "tool-fastutil" } tool-google-guava = { module = "com.google.guava:guava", version.ref = "tool-google-guava" } +tool-google-gson = { module = "com.google.code.gson:gson", version.ref = "tool-google-gson" } tool-netty-codec = { module = "io.netty:netty-codec", version.ref = "tool-netty" } tool-netty-handler = { module = "io.netty:netty-handler", version.ref = "tool-netty" } tool-spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "tool-spotbugs-annotations" } diff --git a/mapping/README.md b/mapping/README.md new file mode 100644 index 00000000..0f0ef75c --- /dev/null +++ b/mapping/README.md @@ -0,0 +1,17 @@ +# mapping module + +This module generates `mapping/*.json` resources at build time using Java code. + +## Main tasks + +- `generateMappings`: downloads the Mojang version manifest and the required `server.jar` files, runs the data generator, and produces mapping resources. +- `mappingResourcesJar`: packages the generated `mapping/**` resources as a dedicated artifact consumed by the `plugin` module. + +## Resource source + +Seed and fallback files are stored in `src/main/seeds/mapping/`. + +## Consumption + +The `plugin` module consumes this module's resource artifact through the `mappingResourcesElements` configuration and merges it into the final plugin package during `processResources`. + diff --git a/mapping/build.gradle.kts b/mapping/build.gradle.kts new file mode 100644 index 00000000..73369be2 --- /dev/null +++ b/mapping/build.gradle.kts @@ -0,0 +1,83 @@ +@file:Suppress("UnstableApiUsage") + +import net.minecraftforge.licenser.LicenseExtension +import org.gradle.api.attributes.Usage +import org.gradle.api.tasks.JavaExec + +plugins { + java +} + +tasks.withType { + options.release.set(21) + options.encoding = "UTF-8" +} + +dependencies { + implementation(libs.tool.commons.io) + implementation(libs.tool.google.guava) + implementation(libs.tool.google.gson) + + compileOnly(libs.tool.spotbugs.annotations) +} + +extensions.configure { + setHeader(rootProject.file("HEADER.txt")) +} + +val generatedMappingsDir = layout.buildDirectory.dir("generated/resources/mapping") +val minecraftCacheDir = layout.buildDirectory.dir("minecraft") +val seedsDir = layout.projectDirectory.dir("src/main/seeds") +val manifestUrl = providers.gradleProperty("manifestUrl") +val cacheValidMillis = providers.gradleProperty("cacheValidMillis") +val gameVersion = providers.gradleProperty("gameVersion") + +val generateMappings by tasks.registering(JavaExec::class) { + group = LifecycleBasePlugin.BUILD_GROUP + description = "Generates Minecraft mapping resources for the plugin module." + + dependsOn(tasks.named("compileJava")) + + mainClass.set("net.elytrium.limboapi.mapping.MappingGeneratorMain") + classpath = sourceSets.main.get().runtimeClasspath + + inputs.dir(seedsDir) + inputs.property("manifestUrl", manifestUrl) + inputs.property("cacheValidMillis", cacheValidMillis) + inputs.property("gameVersion", gameVersion) + inputs.property("cacheRefreshBucket", cacheValidMillis.map { System.currentTimeMillis() / it.toLong() }) + outputs.dir(generatedMappingsDir) + + doFirst { + args = listOf( + generatedMappingsDir.get().asFile.absolutePath, + minecraftCacheDir.get().asFile.absolutePath, + seedsDir.asFile.absolutePath, + manifestUrl.get(), + cacheValidMillis.get(), + ) + } +} + +val mappingResourcesJar by tasks.registering(Jar::class) { + group = LifecycleBasePlugin.BUILD_GROUP + description = "Packages generated mapping resources for consumption by the plugin module." + + archiveClassifier.set("resources") + dependsOn(generateMappings) + from(generatedMappingsDir) +} + +val mappingResourcesElements by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + } +} + +artifacts { + add(mappingResourcesElements.name, mappingResourcesJar) +} + diff --git a/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGenerator.java b/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGenerator.java new file mode 100644 index 00000000..c0b5fbd6 --- /dev/null +++ b/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGenerator.java @@ -0,0 +1,928 @@ +/* + * Copyright (C) 2021 - 2025 Elytrium + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package net.elytrium.limboapi.mapping; + +import com.google.common.hash.Hashing; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; + +public final class MappingGenerator { + + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + private static final Gson PRETTY_GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + private static final int JSON_WALK_DEPTH = 32; + private static final int DOWNLOAD_ATTEMPTS = 4; + private static final int DOWNLOAD_CONNECT_TIMEOUT_MILLIS = 30_000; + private static final int DOWNLOAD_READ_TIMEOUT_MILLIS = 300_000; + private static final long DOWNLOAD_RETRY_DELAY_MILLIS = 2_000L; + + private final Path outputDirectory; + private final Path dataDirectory; + private final Path seedsDirectory; + private final Path versionManifestFile; + private final String manifestUrl; + private final long cacheValidMillis; + + public MappingGenerator(Path outputDirectory, Path dataDirectory, Path seedsDirectory, String manifestUrl, long cacheValidMillis) { + this.outputDirectory = outputDirectory; + this.dataDirectory = dataDirectory; + this.seedsDirectory = seedsDirectory; + this.versionManifestFile = dataDirectory.resolve("manifest.json"); + this.manifestUrl = manifestUrl; + this.cacheValidMillis = cacheValidMillis; + } + + public void generateAll() throws IOException, InterruptedException { + Files.createDirectories(this.outputDirectory); + Files.createDirectories(this.dataDirectory); + + this.downloadManifest(); + + Path targetDir = this.outputDirectory.resolve("mapping"); + Files.createDirectories(targetDir); + + System.out.println("> Generating Minecraft data..."); + + List supportedVersions = this.getSupportedGenerationVersions(); + MinecraftVersion latestGeneratedVersion = supportedVersions.get(supportedVersions.size() - 1); + + Map generated = supportedVersions.stream() + .collect(Collectors.toMap( + version -> version, + version -> this.uncheckedGenerateData(version), + (left, right) -> left, + LinkedHashMap::new + )); + + Map blockReports = new LinkedHashMap<>(); + for (Map.Entry entry : generated.entrySet()) { + blockReports.put(entry.getKey(), this.readJsonObject(entry.getValue().resolve("reports/blocks.json"))); + } + + this.generateBlockMappings(targetDir, blockReports, latestGeneratedVersion); + + Map registriesReports = new LinkedHashMap<>(); + for (Map.Entry entry : generated.entrySet()) { + if (entry.getKey().isAtLeast(MinecraftVersion.MINECRAFT_1_14)) { + registriesReports.put(entry.getKey(), this.readJsonObject(entry.getValue().resolve("reports/registries.json"))); + } + } + + this.generateRegistryMappings(targetDir, registriesReports); + + Map tagDirectories = new LinkedHashMap<>(); + for (Map.Entry entry : generated.entrySet()) { + tagDirectories.put(entry.getKey(), entry.getValue().resolve("data/minecraft/tags")); + } + + this.generateTags(targetDir, tagDirectories); + } + + private List getSupportedGenerationVersions() { + int currentJavaFeature = Runtime.version().feature(); + List supported = Arrays.stream(MinecraftVersion.values()) + .filter(version -> version.isAtLeast(MinecraftVersion.MINECRAFT_1_13)) + .filter(version -> this.isSupportedOnCurrentJava(version, currentJavaFeature)) + .toList(); + + if (supported.isEmpty()) { + throw new IllegalStateException("No Minecraft versions are runnable on Java " + currentJavaFeature); + } + + MinecraftVersion newestKnownVersion = MinecraftVersion.MAXIMUM_VERSION; + MinecraftVersion newestRunnableVersion = supported.get(supported.size() - 1); + if (newestRunnableVersion != newestKnownVersion) { + System.out.println("> Skipping versions newer than " + newestRunnableVersion.getVersionName() + + " because the current Java runtime is " + currentJavaFeature + "."); + } + + return supported; + } + + private boolean isSupportedOnCurrentJava(MinecraftVersion version, int currentJavaFeature) { + if (version == MinecraftVersion.MINECRAFT_26_1) { + return currentJavaFeature >= 25; + } + + return true; + } + + private Path uncheckedGenerateData(MinecraftVersion version) { + try { + return this.generateData(version); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while generating data for " + version.getVersionName(), e); + } + } + + private void downloadManifest() throws IOException { + System.out.println("> Downloading version manifest..."); + Path manifestParent = Objects.requireNonNull(this.versionManifestFile.getParent(), "manifest file has no parent"); + Files.createDirectories(manifestParent); + if (this.checkIsCacheValid(this.versionManifestFile)) { + this.downloadToFileWithRetries(new URL(this.manifestUrl), this.versionManifestFile, "version manifest"); + } + } + + private boolean checkIsCacheValid(Path file) { + if (Files.exists(file)) { + try { + long age = System.currentTimeMillis() - Files.getLastModifiedTime(file).toMillis(); + if (age < this.cacheValidMillis) { + System.out.println("> Found cached " + file.getFileName()); + return false; + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to check cache for " + file, e); + } + } + + return true; + } + + private Path downloadVersionManifest(String version) throws IOException { + System.out.println("> Downloading " + version + " manifest..."); + + JsonObject manifest = this.readJsonObject(this.versionManifestFile); + JsonObject resolved = null; + for (JsonElement element : manifest.getAsJsonArray("versions")) { + JsonObject candidate = element.getAsJsonObject(); + if (version.equals(candidate.get("id").getAsString())) { + resolved = candidate; + break; + } + } + + if (resolved == null) { + throw new IllegalStateException("Couldn't find version: " + version); + } + + Path output = this.dataDirectory.resolve(version).resolve("manifest.json"); + Path outputParent = Objects.requireNonNull(output.getParent(), "version manifest output has no parent"); + Files.createDirectories(outputParent); + this.downloadToFileWithRetries(new URL(resolved.get("url").getAsString()), output, version + " manifest"); + return output; + } + + private Path getGeneratedCache(MinecraftVersion version) { + Path generated = this.dataDirectory.resolve(version.getVersionName()).resolve("generated"); + Path registryOrItems = version.isAtLeast(MinecraftVersion.MINECRAFT_1_14) + ? generated.resolve("reports/registries.json") + : generated.resolve("reports/items.json"); + + if (Files.exists(generated.resolve("reports/blocks.json")) + && Files.exists(registryOrItems) + && Files.exists(generated.resolve("data/minecraft/tags"))) { + return generated; + } + + return null; + } + + private static boolean validateServer(Path file, String expected) throws IOException { + if (file == null || !Files.exists(file)) { + return false; + } + + String hash = com.google.common.io.Files.asByteSource(file.toFile()).hash(Hashing.sha1()).toString(); + return hash.equals(expected); + } + + private Path getServerJar(String version) throws IOException { + Path manifestFile = this.downloadVersionManifest(version); + JsonObject manifest = this.readJsonObject(manifestFile); + + JsonObject server = manifest.getAsJsonObject("downloads").getAsJsonObject("server"); + Path jarFile = this.dataDirectory.resolve(version).resolve("server.jar"); + if (!validateServer(jarFile, server.get("sha1").getAsString())) { + System.out.println("> Downloading " + version + " server..."); + Path jarParent = Objects.requireNonNull(jarFile.getParent(), "server jar path has no parent"); + Files.createDirectories(jarParent); + + IOException lastException = null; + for (int attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; ++attempt) { + try { + this.downloadToFileWithRetries(URI.create(server.get("url").getAsString()).toURL(), jarFile, version + " server"); + if (validateServer(jarFile, server.get("sha1").getAsString())) { + return jarFile; + } + + Files.deleteIfExists(jarFile); + lastException = new IOException("Downloaded server jar checksum mismatch for " + version); + } catch (IOException e) { + Files.deleteIfExists(jarFile); + lastException = e; + } + + if (attempt < DOWNLOAD_ATTEMPTS) { + System.out.println("> Retrying " + version + " server download checksum validation (attempt " + + (attempt + 1) + "/" + DOWNLOAD_ATTEMPTS + ")..."); + this.sleepBeforeRetry(); + } + } + + throw Objects.requireNonNullElseGet(lastException, + () -> new IOException("Failed to download server jar for " + version)); + } + + return jarFile; + } + + private void downloadToFileWithRetries(URL url, Path target, String label) throws IOException { + Files.createDirectories(Objects.requireNonNull(target.getParent())); + Path tempFile = target.resolveSibling(target.getFileName() + ".part"); + + IOException lastException = null; + for (int attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; ++attempt) { + try { + Files.deleteIfExists(tempFile); + FileUtils.copyURLToFile(url, tempFile.toFile(), DOWNLOAD_CONNECT_TIMEOUT_MILLIS, DOWNLOAD_READ_TIMEOUT_MILLIS); + Files.move(tempFile, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + return; + } catch (IOException e) { + lastException = e; + Files.deleteIfExists(tempFile); + + if (attempt < DOWNLOAD_ATTEMPTS) { + System.out.println("> Download failed for " + label + ", retrying (attempt " + (attempt + 1) + + "/" + DOWNLOAD_ATTEMPTS + "): " + e.getMessage()); + this.sleepBeforeRetry(); + } + } + } + + throw new IOException("Failed to download " + label + " after " + DOWNLOAD_ATTEMPTS + " attempts", lastException); + } + + private void sleepBeforeRetry() throws IOException { + try { + Thread.sleep(DOWNLOAD_RETRY_DELAY_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting to retry download", e); + } + } + + private Path generateData(MinecraftVersion version) throws IOException, InterruptedException { + Path cache = this.getGeneratedCache(version); + if (cache != null) { + return cache; + } + + Path jarFile = this.getServerJar(version.getVersionName()); + Path parent = Objects.requireNonNull(jarFile.getParent(), "server jar path has no parent"); + Path targetDir = parent.resolve("generated"); + + FileUtils.deleteDirectory(targetDir.toFile()); + + List commandLine = new ArrayList<>(); + commandLine.add(this.resolveJavaExecutable().toString()); + if (version.isAtLeast(MinecraftVersion.MINECRAFT_1_18)) { + commandLine.add("-DbundlerMainClass=net.minecraft.data.Main"); + commandLine.add("-jar"); + commandLine.add(jarFile.toString()); + } else { + commandLine.add("-cp"); + commandLine.add(jarFile.toString()); + commandLine.add("net.minecraft.data.Main"); + } + commandLine.add("--reports"); + commandLine.add("--server"); + + Process process = new ProcessBuilder(commandLine) + .directory(parent.toFile()) + .inheritIO() + .start(); + + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IllegalStateException("Minecraft data generation failed for " + version.getVersionName() + " with exit code " + exitCode); + } + + Files.deleteIfExists(jarFile); + FileUtils.deleteDirectory(parent.resolve("logs").toFile()); + FileUtils.deleteDirectory(parent.resolve("libraries").toFile()); + FileUtils.deleteDirectory(parent.resolve("versions").toFile()); + + Files.deleteIfExists(targetDir.resolve("reports/commands.json")); + + FileUtils.deleteDirectory(targetDir.resolve(".cache").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("reports/biome_parameters").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("reports/biomes").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("reports/worldgen").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("reports/minecraft/components/item").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/datapacks").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/advancements").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/advancement").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/recipes").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/recipe").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/loot_tables").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/loot_table").toFile()); + FileUtils.deleteDirectory(targetDir.resolve("data/minecraft/worldgen").toFile()); + + this.minifyJsonFiles(parent); + + return targetDir; + } + + private void generateBlockMappings(Path targetDir, Map blockReports, + MinecraftVersion latestGeneratedVersion) throws IOException { + Path defaultBlockPropertiesFile = targetDir.resolve("defaultblockproperties.json"); + Path blockStatesFile = targetDir.resolve("blockstates.json"); + Path blockStatesMappingFile = targetDir.resolve("blockstates_mapping.json"); + Path legacyBlocksFile = targetDir.resolve("legacyblocks.json"); + + if (this.checkIsCacheValid(defaultBlockPropertiesFile) + || this.checkIsCacheValid(blockStatesFile) + || this.checkIsCacheValid(blockStatesMappingFile) + || this.checkIsCacheValid(legacyBlocksFile)) { + System.out.println("> Generating default block properties..."); + + Map>> defaultProperties = new LinkedHashMap<>(); + for (Map.Entry entry : blockReports.entrySet()) { + defaultProperties.put(entry.getKey(), getDefaultProperties(entry.getValue())); + } + + this.writePrettyJson(defaultBlockPropertiesFile, toJsonObjectOfStringMaps(defaultProperties.get(latestGeneratedVersion))); + + System.out.println("> Generating blockstates..."); + + Map> mappings = loadLegacyMapping(this.seedsDirectory.resolve("mapping/legacyblockmapping.json")); + for (Map.Entry entry : blockReports.entrySet()) { + mappings.put(entry.getKey(), getBlockMappings(entry.getValue(), defaultProperties.get(entry.getKey()))); + } + + Map blocks = mappings.get(latestGeneratedVersion); + this.writePrettyJson(blockStatesFile, toJsonObjectFromIntegerMap(sortByIntegerValue(blocks))); + + System.out.println("> Generating blockstates mapping..."); + + Map> fallbackMapping = loadFallbackMapping(this.seedsDirectory.resolve("mapping/fallbackdata.json")); + LinkedHashMap blockStateMappingJson = new LinkedHashMap<>(); + for (Map.Entry blockEntry : sortByIntegerValue(blocks).entrySet()) { + LinkedHashMap blockMapping = new LinkedHashMap<>(); + int lastId = -1; + for (MinecraftVersion version : MinecraftVersion.values()) { + int id = getBlockId(blockEntry.getKey(), mappings, defaultProperties, fallbackMapping, version, latestGeneratedVersion); + if (lastId != id) { + blockMapping.put(version.getVersionName(), String.valueOf(id)); + lastId = id; + } + } + + blockStateMappingJson.put(String.valueOf(blockEntry.getValue()), toJsonObject(blockMapping)); + } + this.writePrettyJson(blockStatesMappingFile, toJsonObjectOfJsonObjects(blockStateMappingJson)); + + System.out.println("> Generating legacy blocks..."); + + JsonObject legacyData = this.readJsonObject(this.seedsDirectory.resolve("mapping/legacyblocks.json")); + LinkedHashMap resolvedLegacyData = new LinkedHashMap<>(); + for (Map.Entry entry : legacyData.entrySet()) { + int modernId = getBlockId(entry.getValue().getAsString(), mappings, defaultProperties, fallbackMapping, + latestGeneratedVersion, latestGeneratedVersion); + resolvedLegacyData.put(entry.getKey(), String.valueOf(modernId)); + } + this.writePrettyJson(legacyBlocksFile, toJsonObject(resolvedLegacyData)); + } + } + + private void generateRegistryMappings(Path targetDir, Map registriesReports) throws IOException { + this.generateRegistryMapping("item", targetDir, registriesReports.entrySet().stream() + .filter(entry -> MinecraftVersion.WORLD_VERSIONS.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (left, right) -> left, LinkedHashMap::new))); + + this.generateRegistryMapping("block", targetDir, registriesReports); + + this.generateRegistryMapping("data_component_type", targetDir, registriesReports.entrySet().stream() + .filter(entry -> entry.getKey().isAtLeast(MinecraftVersion.MINECRAFT_1_20_5)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (left, right) -> left, LinkedHashMap::new))); + + Path blockEntitiesMappingFile = targetDir.resolve("blockentities_mapping.json"); + if (this.checkIsCacheValid(blockEntitiesMappingFile)) { + System.out.println("> Generating blockentities mapping..."); + + LinkedHashMap> blockEntities = new LinkedHashMap<>(); + JsonObject legacyMappings = this.readJsonObject(this.seedsDirectory.resolve("mapping/legacy_blockentities_mapping.json")); + for (Map.Entry entry : legacyMappings.entrySet()) { + blockEntities.put(entry.getKey(), toLinkedStringMap(entry.getValue().getAsJsonObject())); + } + + for (Map.Entry entry : registriesReports.entrySet()) { + if (!entry.getKey().isAtLeast(MinecraftVersion.MINECRAFT_1_19)) { + continue; + } + + JsonObject values = entry.getValue().getAsJsonObject("minecraft:block_entity_type").getAsJsonObject("entries"); + for (Map.Entry valueEntry : values.entrySet()) { + int id = valueEntry.getValue().getAsJsonObject().get("protocol_id").getAsInt(); + blockEntities.computeIfAbsent(valueEntry.getKey(), ignored -> new LinkedHashMap<>()) + .put(entry.getKey().getVersionName(), String.valueOf(id)); + } + } + + this.writePrettyJson(blockEntitiesMappingFile, toJsonObjectOfStringMaps(sortRegistryMapping(blockEntities))); + } + } + + private void generateRegistryMapping(String target, Path targetDir, Map registriesReports) throws IOException { + Path targetFile = targetDir.resolve(target + "s.json"); + Path targetMappingFile = targetDir.resolve(target + "s_mapping.json"); + if (this.checkIsCacheValid(targetFile) || this.checkIsCacheValid(targetMappingFile)) { + System.out.println("> Generating " + target + "s..."); + + Map> idMap = new LinkedHashMap<>(); + for (Map.Entry entry : registriesReports.entrySet()) { + JsonObject entries = entry.getValue().getAsJsonObject("minecraft:" + target).getAsJsonObject("entries"); + LinkedHashMap ids = new LinkedHashMap<>(); + for (Map.Entry valueEntry : entries.entrySet()) { + ids.put(valueEntry.getKey(), String.valueOf(valueEntry.getValue().getAsJsonObject().get("protocol_id").getAsInt())); + } + idMap.put(entry.getKey(), ids); + } + + Map modernIds = idMap.entrySet().stream() + .max(Map.Entry.comparingByKey()) + .orElseThrow(() -> new IllegalStateException("No registry reports found for " + target)) + .getValue(); + + this.writePrettyJson(targetFile, toJsonObjectFromIntegerMap(sortByIntegerValue(parseIntegerValues(modernIds)))); + + System.out.println("> Generating " + target + "s mapping..."); + + LinkedHashMap> mapping = new LinkedHashMap<>(); + JsonObject legacyMapping = this.readJsonObject(this.seedsDirectory.resolve("mapping/legacy_" + target + "s_mapping.json")); + for (Map.Entry entry : legacyMapping.entrySet()) { + String modernId = modernIds.get(entry.getKey()); + if (modernId == null) { + throw new IllegalStateException("No modern id found for " + entry.getKey()); + } + + mapping.put(modernId, toLinkedStringMap(entry.getValue().getAsJsonObject())); + } + + for (Map.Entry> versionEntry : idMap.entrySet()) { + for (Map.Entry idEntry : versionEntry.getValue().entrySet()) { + if (!modernIds.containsKey(idEntry.getKey())) { + continue; + } + + mapping.computeIfAbsent(modernIds.get(idEntry.getKey()), ignored -> new LinkedHashMap<>()) + .put(versionEntry.getKey().getVersionName(), idEntry.getValue()); + } + } + + LinkedHashMap> sortedMapping = sortRegistryMapping(mapping); + this.writePrettyJson(targetMappingFile, toJsonObjectOfStringMaps(sortNestedMapByNumericKey(sortedMapping))); + } + } + + private void generateTags(Path targetDir, Map tagDirectories) throws IOException { + Path tagsFile = targetDir.resolve("tags.json"); + if (this.checkIsCacheValid(tagsFile)) { + System.out.println("> Generating tags..."); + + JsonObject tagTypesJson = this.readJsonObject(this.seedsDirectory.resolve("mapping/tag_types.json")); + Map tagTypes = toLinkedStringMap(tagTypesJson.getAsJsonObject("tag_types")); + Set supportedTagTypes = new HashSet<>(); + for (JsonElement element : tagTypesJson.getAsJsonArray("supported_tag_types")) { + supportedTagTypes.add(element.getAsString()); + } + + Map>>> allTags = new LinkedHashMap<>(); + for (Map.Entry entry : tagDirectories.entrySet()) { + allTags.put(entry.getKey(), getTags(entry.getValue(), tagTypes)); + } + + Map>> mergedTags = new LinkedHashMap<>(); + for (Map>> tags : allTags.values()) { + for (Map.Entry>> typeEntry : tags.entrySet()) { + if (!supportedTagTypes.contains(typeEntry.getKey())) { + continue; + } + + Map> mergedTypeTags = mergedTags.computeIfAbsent(typeEntry.getKey(), ignored -> new HashMap<>()); + for (Map.Entry> tagEntry : typeEntry.getValue().entrySet()) { + mergedTypeTags.computeIfAbsent(tagEntry.getKey(), ignored -> new HashSet<>()).addAll(tagEntry.getValue()); + } + } + } + + LinkedHashMap mergedTagsJson = new LinkedHashMap<>(); + mergedTags.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(typeEntry -> { + JsonObject typeJson = new JsonObject(); + typeEntry.getValue().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(tagEntry -> { + List values = new ArrayList<>(tagEntry.getValue()); + Collections.sort(values); + typeJson.add(tagEntry.getKey(), toJsonArray(values)); + }); + mergedTagsJson.put(typeEntry.getKey(), typeJson); + }); + + this.writePrettyJson(tagsFile, toJsonObjectOfJsonObjects(mergedTagsJson)); + } + } + + private static Map> getDefaultProperties(JsonObject data) { + LinkedHashMap> defaultProperties = new LinkedHashMap<>(); + + for (Map.Entry entry : data.entrySet()) { + JsonObject block = entry.getValue().getAsJsonObject(); + if (!block.has("properties")) { + continue; + } + + for (JsonElement stateElement : block.getAsJsonArray("states")) { + JsonObject blockState = stateElement.getAsJsonObject(); + if (!blockState.has("default") || !blockState.get("default").getAsBoolean()) { + continue; + } + + defaultProperties.put(entry.getKey(), new TreeMap<>(toLinkedStringMap(blockState.getAsJsonObject("properties")))); + break; + } + } + + return defaultProperties; + } + + private static Map> loadFallbackMapping(Path file) throws IOException { + JsonObject mappings = readJsonObjectStatic(file); + LinkedHashMap> fallback = new LinkedHashMap<>(); + for (MinecraftVersion version : MinecraftVersion.values()) { + JsonObject value = mappings.has(version.name()) ? mappings.getAsJsonObject(version.name()) : new JsonObject(); + fallback.put(version, toLinkedStringMap(value)); + } + + return fallback; + } + + private static Map> loadLegacyMapping(Path file) throws IOException { + JsonObject mappings = readJsonObjectStatic(file); + LinkedHashMap> result = new LinkedHashMap<>(); + for (Map.Entry entry : mappings.entrySet()) { + LinkedHashMap values = new LinkedHashMap<>(); + for (Map.Entry valueEntry : entry.getValue().getAsJsonObject().entrySet()) { + values.put(valueEntry.getKey(), valueEntry.getValue().getAsInt()); + } + result.put(MinecraftVersion.valueOf(entry.getKey()), values); + } + + return result; + } + + private static int getBlockId( + String block, + Map> mappings, + Map>> properties, + Map> fallback, + MinecraftVersion version, + MinecraftVersion latestGeneratedVersion + ) { + MinecraftVersion lookupVersion = mappings.containsKey(version) ? version : latestGeneratedVersion; + + Map> defaultProperties = lookupVersion.isAtLeast(MinecraftVersion.MINECRAFT_1_13) + ? properties.get(lookupVersion) + : properties.get(MinecraftVersion.MINECRAFT_1_18_2); + + String[] split = block.split("\\[", 2); + String noArgBlock = split[0]; + + MinecraftVersion fallbackVersion = latestGeneratedVersion; + while (fallbackVersion != lookupVersion) { + fallbackVersion = fallbackVersion.previous(); + noArgBlock = fallback.getOrDefault(fallbackVersion, Map.of()).getOrDefault(noArgBlock, noArgBlock); + } + + Map blockProperties = defaultProperties.get(noArgBlock); + String targetBlockId; + if (blockProperties == null) { + targetBlockId = noArgBlock; + } else { + TreeMap currentProperties = new TreeMap<>(blockProperties); + if (split.length > 1) { + String propertyText = split[1].replace("]", ""); + if (!propertyText.isBlank()) { + for (String argument : propertyText.split(",")) { + String[] parts = argument.split("=", 2); + if (parts.length == 2 && currentProperties.containsKey(parts[0])) { + currentProperties.put(parts[0], parts[1]); + } + } + } + } + + targetBlockId = formatStateId(noArgBlock, currentProperties); + } + + Integer id = mappings.get(lookupVersion).get(targetBlockId); + if (id == null && blockProperties != null) { + id = mappings.get(lookupVersion).get(formatStateId(noArgBlock, new TreeMap<>(blockProperties))); + } + + if (id == null) { + System.err.println("No " + version.getVersionName() + " fallback data for " + noArgBlock + ", replacing with minecraft:stone"); + return 1; + } + + return id; + } + + private static Map getBlockMappings(JsonObject data, Map> defaultPropertiesMap) { + LinkedHashMap mapping = new LinkedHashMap<>(); + + for (Map.Entry entry : data.entrySet()) { + String blockId = entry.getKey(); + JsonObject blockData = entry.getValue().getAsJsonObject(); + for (JsonElement stateElement : blockData.getAsJsonArray("states")) { + JsonObject blockState = stateElement.getAsJsonObject(); + int protocolId = blockState.get("id").getAsInt(); + + if (blockState.has("properties")) { + TreeMap properties = new TreeMap<>(defaultPropertiesMap.getOrDefault(blockId, Map.of())); + properties.putAll(toLinkedStringMap(blockState.getAsJsonObject("properties"))); + mapping.put(formatStateId(blockId, properties), protocolId); + } else { + mapping.put(blockId, protocolId); + } + } + } + + return mapping; + } + + private static LinkedHashMap> sortRegistryMapping(Map> mapping) { + Comparator> comparator = Comparator.comparing(entry -> { + if (entry.getKey().contains(".")) { + return MinecraftVersion.fromVersionName(entry.getKey()); + } + + return MinecraftVersion.MINIMUM_VERSION; + }); + + LinkedHashMap> sorted = new LinkedHashMap<>(); + mapping.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> { + LinkedHashMap sortedValues = entry.getValue().entrySet().stream() + .sorted(comparator) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (left, right) -> left, + LinkedHashMap::new + )); + sorted.put(entry.getKey(), sortedValues); + }); + + return sorted; + } + + private static Map>> getTags(Path tagDir, Map tagTypes) throws IOException { + LinkedHashMap>> tags = new LinkedHashMap<>(); + + for (Map.Entry tagTypeEntry : tagTypes.entrySet()) { + Path directory = tagDir.resolve(tagTypeEntry.getKey()); + if (!Files.exists(directory)) { + continue; + } + + Map> typeTags = new HashMap<>(); + try (Stream pathStream = Files.walk(directory)) { + for (Path file : pathStream.filter(Files::isRegularFile).toList()) { + JsonObject json = readJsonObjectStatic(file); + List values = new ArrayList<>(); + for (JsonElement element : json.getAsJsonArray("values")) { + values.add(element.getAsString()); + } + + Path relativePath = directory.relativize(file); + String name = FilenameUtils.removeExtension(relativePath.toString()).replace('\\', '/'); + typeTags.put("minecraft:" + name, values); + } + } + + boolean flatten = false; + while (!flatten) { + flatten = true; + Map> tempTags = new HashMap<>(); + for (Map.Entry> tagEntry : typeTags.entrySet()) { + List newTags = new ArrayList<>(); + for (String currentTag : tagEntry.getValue()) { + if (currentTag.startsWith("#")) { + newTags.addAll(typeTags.getOrDefault(currentTag.substring(1), List.of())); + flatten = false; + } else { + newTags.add(currentTag); + } + } + tempTags.put(tagEntry.getKey(), newTags); + } + typeTags = tempTags; + } + + tags.put(tagTypeEntry.getValue(), typeTags); + } + + return tags; + } + + private void minifyJsonFiles(Path root) throws IOException { + List jsonFiles; + try (Stream pathStream = Files.walk(root, JSON_WALK_DEPTH)) { + jsonFiles = pathStream + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".json")) + .toList(); + } + + for (Path jsonFile : jsonFiles) { + JsonElement json = this.readJson(jsonFile); + Files.writeString( + jsonFile, + GSON.toJson(json), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); + } + } + + private Path resolveJavaExecutable() { + String executable = System.getProperty("os.name").toLowerCase().contains("win") ? "java.exe" : "java"; + return Path.of(System.getProperty("java.home"), "bin", executable); + } + + private JsonElement readJson(Path file) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + return JsonParser.parseReader(reader); + } + } + + private JsonObject readJsonObject(Path file) throws IOException { + return readJsonObjectStatic(file); + } + + private void writePrettyJson(Path file, JsonElement json) throws IOException { + Files.createDirectories(Objects.requireNonNull(file.getParent())); + Files.writeString( + file, + PRETTY_GSON.toJson(json), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); + } + + private static JsonObject readJsonObjectStatic(Path file) throws IOException { + try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + return JsonParser.parseReader(reader).getAsJsonObject(); + } + } + + private static LinkedHashMap sortByIntegerValue(Map input) { + return input.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (left, right) -> left, + LinkedHashMap::new + )); + } + + private static LinkedHashMap> sortNestedMapByNumericKey(Map> input) { + return input.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> Integer.parseInt(entry.getKey()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (left, right) -> left, + LinkedHashMap::new + )); + } + + private static LinkedHashMap parseIntegerValues(Map input) { + LinkedHashMap result = new LinkedHashMap<>(); + for (Map.Entry entry : input.entrySet()) { + result.put(entry.getKey(), Integer.parseInt(entry.getValue())); + } + return result; + } + + private static String formatStateId(String blockId, Map properties) { + if (properties.isEmpty()) { + return blockId; + } + + return blockId + "[" + properties.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining(",")) + "]"; + } + + private static LinkedHashMap toLinkedStringMap(JsonObject object) { + LinkedHashMap result = new LinkedHashMap<>(); + for (Map.Entry entry : object.entrySet()) { + result.put(entry.getKey(), entry.getValue().getAsString()); + } + return result; + } + + private static JsonObject toJsonObject(Map values) { + JsonObject result = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + result.addProperty(entry.getKey(), entry.getValue()); + } + return result; + } + + private static JsonObject toJsonObjectFromIntegerMap(Map values) { + JsonObject result = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + result.addProperty(entry.getKey(), String.valueOf(entry.getValue())); + } + return result; + } + + private static JsonObject toJsonObjectOfStringMaps(Map> values) { + JsonObject result = new JsonObject(); + for (Map.Entry> entry : values.entrySet()) { + result.add(entry.getKey(), toJsonObject(entry.getValue())); + } + return result; + } + + private static JsonObject toJsonObjectOfJsonObjects(Map values) { + JsonObject result = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + result.add(entry.getKey(), entry.getValue()); + } + return result; + } + + private static JsonArray toJsonArray(Collection values) { + JsonArray result = new JsonArray(); + for (String value : values) { + result.add(value); + } + return result; + } +} + + diff --git a/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGeneratorMain.java b/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGeneratorMain.java new file mode 100644 index 00000000..df99861c --- /dev/null +++ b/mapping/src/main/java/net/elytrium/limboapi/mapping/MappingGeneratorMain.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 - 2025 Elytrium + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package net.elytrium.limboapi.mapping; + +import java.nio.file.Path; + +public final class MappingGeneratorMain { + + private MappingGeneratorMain() { + } + + public static void main(String[] args) throws Exception { + if (args.length != 5) { + throw new IllegalArgumentException("Expected arguments: "); + } + + MappingGenerator generator = new MappingGenerator( + Path.of(args[0]), + Path.of(args[1]), + Path.of(args[2]), + args[3], + Long.parseLong(args[4]) + ); + generator.generateAll(); + } +} + diff --git a/mapping/src/main/java/net/elytrium/limboapi/mapping/MinecraftVersion.java b/mapping/src/main/java/net/elytrium/limboapi/mapping/MinecraftVersion.java new file mode 100644 index 00000000..a00f8652 --- /dev/null +++ b/mapping/src/main/java/net/elytrium/limboapi/mapping/MinecraftVersion.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 - 2025 Elytrium + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package net.elytrium.limboapi.mapping; + +import java.util.List; + +public enum MinecraftVersion { + MINECRAFT_1_7_2(4), + MINECRAFT_1_7_6(5), + MINECRAFT_1_8(47), + MINECRAFT_1_9(107), + MINECRAFT_1_9_1(108), + MINECRAFT_1_9_2(109), + MINECRAFT_1_9_4(110), + MINECRAFT_1_10(210), + MINECRAFT_1_11(315), + MINECRAFT_1_11_1(316), + MINECRAFT_1_12(335), + MINECRAFT_1_12_1(338), + MINECRAFT_1_12_2(340), + MINECRAFT_1_13(393), + MINECRAFT_1_13_1(401), + MINECRAFT_1_13_2(404), + MINECRAFT_1_14(477), + MINECRAFT_1_14_1(480), + MINECRAFT_1_14_2(485), + MINECRAFT_1_14_3(490), + MINECRAFT_1_14_4(498), + MINECRAFT_1_15(573), + MINECRAFT_1_15_1(575), + MINECRAFT_1_15_2(578), + MINECRAFT_1_16(735), + MINECRAFT_1_16_1(736), + MINECRAFT_1_16_2(751), + MINECRAFT_1_16_3(753), + MINECRAFT_1_16_4(754), + MINECRAFT_1_17(755), + MINECRAFT_1_17_1(756), + MINECRAFT_1_18(757), + MINECRAFT_1_18_2(758), + MINECRAFT_1_19(759), + MINECRAFT_1_19_1(760), + MINECRAFT_1_19_3(761), + MINECRAFT_1_19_4(762), + MINECRAFT_1_20(763), + MINECRAFT_1_20_2(764), + MINECRAFT_1_20_3(765), + MINECRAFT_1_20_5(766), + MINECRAFT_1_21(767), + MINECRAFT_1_21_2(768), + MINECRAFT_1_21_4(769), + MINECRAFT_1_21_5(770), + MINECRAFT_1_21_6(771), + MINECRAFT_1_21_7(772), + MINECRAFT_1_21_9(773), + MINECRAFT_1_21_11(774), + MINECRAFT_26_1(774); + + public static final List WORLD_VERSIONS = List.of( + MINECRAFT_1_13, + MINECRAFT_1_13_2, + MINECRAFT_1_14, + MINECRAFT_1_15, + MINECRAFT_1_16, + MINECRAFT_1_16_2, + MINECRAFT_1_17, + MINECRAFT_1_19, + MINECRAFT_1_19_3, + MINECRAFT_1_19_4, + MINECRAFT_1_20, + MINECRAFT_1_20_3, + MINECRAFT_1_20_5, + MINECRAFT_1_21_2, + MINECRAFT_1_21_4, + MINECRAFT_1_21_5, + MINECRAFT_1_21_6, + MINECRAFT_1_21_7, + MINECRAFT_1_21_9, + MINECRAFT_1_21_11, + MINECRAFT_26_1 + ); + + public static final MinecraftVersion MINIMUM_VERSION = MINECRAFT_1_7_2; + public static final MinecraftVersion MAXIMUM_VERSION = values()[values().length - 1]; + + private final String versionName = name().substring(10).replace('_', '.'); + private final int protocolVersion; + + MinecraftVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public static MinecraftVersion fromVersionName(String name) { + return valueOf("MINECRAFT_" + name.replace('.', '_')); + } + + public int getProtocolVersion() { + return this.protocolVersion; + } + + public String getVersionName() { + return this.versionName; + } + + public boolean isAtLeast(MinecraftVersion other) { + return this.compareTo(other) >= 0; + } + + public MinecraftVersion previous() { + if (this.ordinal() == 0) { + throw new IllegalStateException("No previous version for " + this); + } + + return values()[this.ordinal() - 1]; + } +} + diff --git a/plugin/mapping/fallbackdata.json b/mapping/src/main/seeds/mapping/fallbackdata.json old mode 100755 new mode 100644 similarity index 100% rename from plugin/mapping/fallbackdata.json rename to mapping/src/main/seeds/mapping/fallbackdata.json diff --git a/plugin/mapping/legacy_blockentities_mapping.json b/mapping/src/main/seeds/mapping/legacy_blockentities_mapping.json similarity index 100% rename from plugin/mapping/legacy_blockentities_mapping.json rename to mapping/src/main/seeds/mapping/legacy_blockentities_mapping.json diff --git a/plugin/mapping/legacy_blocks_mapping.json b/mapping/src/main/seeds/mapping/legacy_blocks_mapping.json similarity index 100% rename from plugin/mapping/legacy_blocks_mapping.json rename to mapping/src/main/seeds/mapping/legacy_blocks_mapping.json diff --git a/plugin/mapping/legacy_data_component_types_mapping.json b/mapping/src/main/seeds/mapping/legacy_data_component_types_mapping.json similarity index 100% rename from plugin/mapping/legacy_data_component_types_mapping.json rename to mapping/src/main/seeds/mapping/legacy_data_component_types_mapping.json diff --git a/plugin/mapping/legacy_items_mapping.json b/mapping/src/main/seeds/mapping/legacy_items_mapping.json similarity index 100% rename from plugin/mapping/legacy_items_mapping.json rename to mapping/src/main/seeds/mapping/legacy_items_mapping.json diff --git a/plugin/mapping/legacyblockmapping.json b/mapping/src/main/seeds/mapping/legacyblockmapping.json old mode 100755 new mode 100644 similarity index 100% rename from plugin/mapping/legacyblockmapping.json rename to mapping/src/main/seeds/mapping/legacyblockmapping.json diff --git a/plugin/mapping/legacyblocks.json b/mapping/src/main/seeds/mapping/legacyblocks.json similarity index 100% rename from plugin/mapping/legacyblocks.json rename to mapping/src/main/seeds/mapping/legacyblocks.json diff --git a/plugin/mapping/tag_types.json b/mapping/src/main/seeds/mapping/tag_types.json similarity index 100% rename from plugin/mapping/tag_types.json rename to mapping/src/main/seeds/mapping/tag_types.json diff --git a/plugin/build.gradle b/plugin/build.gradle index e6801031..fba4b960 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,12 +1,5 @@ //file:noinspection GroovyAssignabilityCheck -buildscript() { - dependencies() { - classpath(libs.tool.commons.io) - classpath(libs.tool.google.guava) - } -} - plugins() { id("java") alias(libs.plugins.gradle.shadow) @@ -17,6 +10,13 @@ compileJava() { getOptions().setEncoding("UTF-8") } +configurations { + mappingResources { + canBeConsumed = false + canBeResolved = true + } +} + dependencies() { implementation(project(":api")) implementation(libs.elytrium.commons.config) @@ -38,6 +38,17 @@ dependencies() { implementation(libs.minecraft.bstats.velocity) compileOnly(libs.tool.spotbugs.annotations) + + mappingResources(project(path: ":mapping", configuration: "mappingResourcesElements")) +} + +processResources { + dependsOn(configurations.mappingResources) + from({ + configurations.mappingResources.collect { zipTree(it) } + }) { + include("mapping/**") + } } shadowJar() { @@ -78,676 +89,14 @@ license() { header = rootProject.file("HEADER.txt") } +def plainJarPath = layout.buildDirectory.file("libs/${project.name}-${project.version}.jar") + tasks.register("finalize") { doLast { - file("build/libs/${project.name}-${project.version}.jar").delete() + plainJarPath.get().asFile.delete() } } assemble.dependsOn(shadowJar) build.finalizedBy(finalize) -import groovy.io.FileType -import groovy.json.JsonOutput -import groovy.json.JsonSlurper -import org.apache.commons.io.FilenameUtils -import org.apache.commons.io.FileUtils -import com.google.common.hash.Hashing -import com.google.common.io.Files - -import java.nio.file.Path -import java.util.function.Function -import java.util.stream.Collectors - -enum MinecraftVersion { - MINECRAFT_1_7_2(4), - MINECRAFT_1_7_6(5), - MINECRAFT_1_8(47), - MINECRAFT_1_9(107), - MINECRAFT_1_9_1(108), - MINECRAFT_1_9_2(109), - MINECRAFT_1_9_4(110), - MINECRAFT_1_10(210), - MINECRAFT_1_11(315), - MINECRAFT_1_11_1(316), - MINECRAFT_1_12(335), - MINECRAFT_1_12_1(338), - MINECRAFT_1_12_2(340), - MINECRAFT_1_13(393), - MINECRAFT_1_13_1(401), - MINECRAFT_1_13_2(404), - MINECRAFT_1_14(477), - MINECRAFT_1_14_1(480), - MINECRAFT_1_14_2(485), - MINECRAFT_1_14_3(490), - MINECRAFT_1_14_4(498), - MINECRAFT_1_15(573), - MINECRAFT_1_15_1(575), - MINECRAFT_1_15_2(578), - MINECRAFT_1_16(735), - MINECRAFT_1_16_1(736), - MINECRAFT_1_16_2(751), - MINECRAFT_1_16_3(753), - MINECRAFT_1_16_4(754), - MINECRAFT_1_17(755), - MINECRAFT_1_17_1(756), - MINECRAFT_1_18(757), - MINECRAFT_1_18_2(758), - MINECRAFT_1_19(759), - MINECRAFT_1_19_1(760), - MINECRAFT_1_19_3(761), - MINECRAFT_1_19_4(762), - MINECRAFT_1_20(763), - MINECRAFT_1_20_2(764), - MINECRAFT_1_20_3(765), - MINECRAFT_1_20_5(766), - MINECRAFT_1_21(767), - MINECRAFT_1_21_2(768), - MINECRAFT_1_21_4(769), - MINECRAFT_1_21_5(770), - MINECRAFT_1_21_6(771), - MINECRAFT_1_21_7(772), - MINECRAFT_1_21_9(773), - MINECRAFT_1_21_11(774), - MINECRAFT_26_1(774) - - public static final List WORLD_VERSIONS = List.of( - MINECRAFT_1_13, - MINECRAFT_1_13_2, - MINECRAFT_1_14, - MINECRAFT_1_15, - MINECRAFT_1_16, - MINECRAFT_1_16_2, - MINECRAFT_1_17, - MINECRAFT_1_19, - MINECRAFT_1_19_3, - MINECRAFT_1_19_4, - MINECRAFT_1_20, - MINECRAFT_1_20_3, - MINECRAFT_1_20_5, - MINECRAFT_1_21_2, - MINECRAFT_1_21_4, - MINECRAFT_1_21_5, - MINECRAFT_1_21_6, - MINECRAFT_1_21_7, - MINECRAFT_1_21_9, - MINECRAFT_1_21_11, - MINECRAFT_26_1 - ) - - public static final MinecraftVersion MINIMUM_VERSION = MINECRAFT_1_7_2 - public static final MinecraftVersion MAXIMUM_VERSION = values()[values().length - 1] - - static MinecraftVersion fromVersionName(String name) { - return valueOf("MINECRAFT_" + name.replace('.', '_')) - } - - // Cache version name to reduce memory usage in general - final String versionName = this.toString().substring(10).replace('_', '.') - final int protocolVersion - - MinecraftVersion(int protocolVersion) { - this.protocolVersion = protocolVersion - } - - int getProtocolVersion() { - return this.protocolVersion - } - - String getVersionName() { - return this.versionName - } -} - -project.ext.dataDirectory = new File(this.getLayout().getBuildDirectory().get().getAsFile(), "minecraft") -project.ext.generatedDir = new File(this.getLayout().getBuildDirectory().get().getAsFile(), "generated/minecraft") -project.ext.versionManifestFile = new File(dataDirectory, "manifest.json") - -sourceSets { - main { - resources { - srcDirs += generatedDir - } - } -} - -tasks.register("downloadManifest") { - this.println("> Downloading version manifest...") - versionManifestFile.getParentFile().mkdirs() - if (checkIsCacheValid(versionManifestFile)) { - FileUtils.copyURLToFile(new URL(manifestUrl), versionManifestFile) - } -} - -boolean checkIsCacheValid(File file) { - if (file.exists() && System.currentTimeMillis() - file.lastModified() < Long.parseLong(cacheValidMillis)) { - println("> Found cached " + file.getName()) - return false - } - - return true -} -File downloadVersionManifest(String version) { - this.println("> Downloading ${version} manifest...") - - Object manifest = new JsonSlurper().parse(versionManifestFile) - def optional = manifest.versions.stream().filter({ it.id == version }).findFirst() - if (optional.empty()) { - throw new RuntimeException("Couldn't find version: ${version}") - } - - File output = new File(dataDirectory, "${version}/manifest.json") - output.getParentFile().mkdirs() - FileUtils.copyURLToFile(new URL(optional.get().url), output) - return output -} - -@SuppressWarnings('GrMethodMayBeStatic') -File getGeneratedCache(MinecraftVersion version) { - File generated = new File(dataDirectory, "${version.getVersionName()}/generated") - return new File(generated, "reports/blocks.json").exists() - && new File(generated, "reports/${version >= MinecraftVersion.MINECRAFT_1_14 ? "registries" : "items"}.json").exists() - && new File(generated, "data/minecraft/tags").exists() - ? generated : null -} - -static boolean validateServer(File file, String expected) { - if (file == null || !file.exists()) { - return false - } - - def hash = Files.asByteSource(file).hash(Hashing.sha1()) - StringBuilder hashBuilder = new StringBuilder() - hash.asBytes().each({hashBuilder.append(Integer.toString((it & 0xFF) + 0x100, 16).substring(1))}) - return hashBuilder.toString() == expected -} - -File getServerJar(String version) { - File manifestFile = this.downloadVersionManifest(version) - Object manifest = new JsonSlurper().parse(manifestFile) - - File jarFile = new File(dataDirectory, "${version}/server.jar") - if (!validateServer(jarFile, manifest.downloads.server.sha1)) { - this.println("> Downloading ${version} server...") - jarFile.getParentFile().mkdirs() - FileUtils.copyURLToFile(new URL(manifest.downloads.server.url), jarFile) - } - - return jarFile -} - -File generateData(MinecraftVersion version) { - File cache = getGeneratedCache(version) - if (cache != null) { - return cache - } - - File jarFile = this.getServerJar(version.getVersionName()) - File parent = jarFile.getParentFile() - File targetDir = new File(parent, "generated") - - try { - FileUtils.deleteDirectory(targetDir) - } catch (IOException ignored) { - // Ignored. - } - - String command - if (version >= MinecraftVersion.MINECRAFT_1_18) { - command = "\"%s\" -DbundlerMainClass=net.minecraft.data.Main -jar \"${jarFile.getAbsolutePath()}\" --reports --server" - } else { - command = "\"%s\" -cp \"${jarFile.getAbsolutePath()}\" net.minecraft.data.Main --reports --server" - } - - List commandLine; - if (System.getProperty("os.name").toLowerCase().contains("win")) { - File java = new File(System.getProperty("java.home"), "bin/java.exe") - commandLine = ["cmd", "/c", String.format(command, java)] - } else { - File java = new File(System.getProperty("java.home"), "bin/java") - commandLine = ["bash", "-c", String.format(command, java)] - } - commandLine.execute([], parent).waitFor() - - // Remove/compact files, reduces disk usage from ~2.9gb to ~92mb (or ~9.5mb on a compressed filesystem) - jarFile.delete() - FileUtils.deleteDirectory(new File(parent, "logs")) - FileUtils.deleteDirectory(new File(parent, "libraries")) - FileUtils.deleteDirectory(new File(parent, "versions")) - - new File(targetDir, "reports/commands.json").delete() - - FileUtils.deleteDirectory(new File(targetDir, ".cache")) - FileUtils.deleteDirectory(new File(targetDir, "reports/biome_parameters")) - FileUtils.deleteDirectory(new File(targetDir, "reports/biomes")) - FileUtils.deleteDirectory(new File(targetDir, "reports/worldgen")) - FileUtils.deleteDirectory(new File(targetDir, "reports/minecraft/components/item")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/datapacks")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/advancements")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/advancement")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/recipes")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/recipe")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/loot_tables")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/loot_table")) - FileUtils.deleteDirectory(new File(targetDir, "data/minecraft/worldgen")) - - java.nio.file.Files.walk(parent.toPath(), 32).forEach { it -> - if (it.fileName.toString().endsWith(".json")) { - java.nio.file.Files.writeString(it, JsonOutput.toJson(new JsonSlurper().parse(it))) - } - } - - return targetDir -} - -static Map> getDefaultProperties(Object data) { - Map> defaultProperties = new HashMap<>() - - data.forEach({ key, block -> - if (!block.containsKey("properties")) { - return - } - - for (Object blockState : block.states) { - if (!blockState.containsKey("default") || !blockState.default) { - continue - } - - Map properties = blockState["properties"] - defaultProperties.put(key, properties) - break - } - }) - - return defaultProperties -} - -static Map> loadFallbackMapping(File file) { - Object map = new JsonSlurper().parse(file) - return MinecraftVersion.values().collectEntries({ version -> - [version, map.getOrDefault(version.toString(), Collections.emptyMap())] - }) -} - -static Map> loadLegacyMapping(File file) { - return new JsonSlurper().parse(file).collectEntries({ version, mapping -> - [MinecraftVersion.valueOf(version), mapping.collectEntries({ block, id -> - [block, Integer.parseInt(id)] - })] - }) -} - -static int getBlockID(String block, - Map> mappings, - Map>> properties, - Map> fallback, - MinecraftVersion version) { - Map> defaultProperties - if (version >= MinecraftVersion.MINECRAFT_1_13) { - defaultProperties = properties[version] - } else { - defaultProperties = properties[MinecraftVersion.MINECRAFT_1_18_2] - } - - String[] split = block.split("\\[") - String noArgBlock = split[0] - - MinecraftVersion fallbackVersion = MinecraftVersion.MAXIMUM_VERSION - while (fallbackVersion != version) { - --fallbackVersion - noArgBlock = fallback[fallbackVersion].getOrDefault(noArgBlock, noArgBlock) - } - - Map blockProperties = defaultProperties[noArgBlock] - String targetBlockID - if (blockProperties == null) { - targetBlockID = noArgBlock - } else { - Map currentProperties = new TreeMap<>(blockProperties) - if (split.length > 1) { - String[] args = split[1].split(",") - Map input = Arrays.stream(args) - .map(arg -> arg.replace("]", "").split("=")) - .collect(Collectors.toMap(parts -> parts[0], parts -> parts[1])) - - input.forEach({ key, value -> - if (currentProperties.containsKey(key)) { - currentProperties.put(key, value) - } - }) - } - - targetBlockID = noArgBlock + Arrays.toString( - currentProperties.collect({ k, v -> k + "=" + v }).toArray() - ).replace(" ", "") - } - - Integer id = mappings[version][targetBlockID] - if (id == null && blockProperties != null) { - targetBlockID = noArgBlock + Arrays.toString( - new TreeMap<>(blockProperties).collect({ k, v -> k + "=" + v }).toArray() - ).replace(" ", "") - id = mappings[version][targetBlockID] - } - - if (id == null) { - System.err.println("No ${version.getVersionName()} fallback data for ${noArgBlock}, replacing with minecraft:stone") - id = 1 - } - - return id -} - -static Map getBlockMappings(Object data, Map> defaultPropertiesMap) { - Map mapping = new HashMap<>() - - data.forEach({ blockID, blockData -> - for (Object blockState : blockData.states) { - int protocolID = blockState.id - - if (blockState.containsKey("properties")) { - Map stateProperties = blockState["properties"] - Map properties = new TreeMap<>( - defaultPropertiesMap.getOrDefault(blockID, Collections.emptyMap())) - - properties.putAll(stateProperties) - - String stateID = blockID + Arrays.toString( - properties.collect({ k, v -> k + "=" + v }).toArray() - ).replace(" ", "") - - mapping.put(stateID, protocolID) - } else { - mapping.put(blockID, protocolID) - } - } - }) - - return mapping -} - -void generateBlockMappings(File targetDir, Map blockReports) { - File defaultBlockPropertiesFile = new File(targetDir, "defaultblockproperties.json") - File blockStatesFile = new File(targetDir, "blockstates.json") - File blockStatesMappingFile = new File(targetDir, "blockstates_mapping.json") - File legacyBlocksFile = new File(targetDir, "legacyblocks.json") - - if (checkIsCacheValid(defaultBlockPropertiesFile) || checkIsCacheValid(blockStatesFile) - || checkIsCacheValid(blockStatesMappingFile) || checkIsCacheValid(legacyBlocksFile)) { - this.println("> Generating default block properties...") - - Map>> defaultProperties = - blockReports.collectEntries({ version, report -> - [version, getDefaultProperties(report)] - }) - - defaultBlockPropertiesFile.write(JsonOutput.prettyPrint( - JsonOutput.toJson(defaultProperties[MinecraftVersion.MAXIMUM_VERSION].sort())), "UTF-8") - - this.println("> Generating blockstates...") - - Map> mappings = loadLegacyMapping( - new File(this.getProjectDir(), "mapping/legacyblockmapping.json")) - - blockReports.forEach({ version, report -> - mappings.put(version, getBlockMappings(report, defaultProperties[version])) - }) - - Map blocks = mappings[MinecraftVersion.MAXIMUM_VERSION] - - blockStatesFile.write( - JsonOutput.prettyPrint(JsonOutput.toJson( - blocks.sort(Map.Entry::getValue) - .collectEntries({ k, v -> [k, String.valueOf(v)] }) - )), "UTF-8") - - - - this.println("> Generating blockstates mapping...") - - Map> fallbackMapping = loadFallbackMapping( - new File(this.getProjectDir(), "mapping/fallbackdata.json")) - - Map> blockstateMapping = new LinkedHashMap<>() - blocks.sort(Map.Entry::getValue) - .forEach({ block, modernID -> - Map blockMapping = new LinkedHashMap<>() - - int lastID = -1 - for (MinecraftVersion version : MinecraftVersion.values()) { - int id = getBlockID(block, mappings, defaultProperties, fallbackMapping, version) - if (lastID != id) { - blockMapping.put(version.getVersionName(), String.valueOf(lastID = id)) - } - } - - blockstateMapping.put(String.valueOf(modernID), blockMapping) - }) - - blockStatesMappingFile.write( - JsonOutput.prettyPrint(JsonOutput.toJson(blockstateMapping)), "UTF-8") - - this.println("> Generating legacy blocks...") - - Map legacyData = new JsonSlurper().parse( - new File(this.getProjectDir(), "mapping/legacyblocks.json")) - - legacyData = legacyData.collectEntries({ legacy, modern -> - [legacy, String.valueOf(getBlockID(modern, mappings, defaultProperties, fallbackMapping, MinecraftVersion.MAXIMUM_VERSION))] - }) - - legacyBlocksFile.write( - JsonOutput.prettyPrint(JsonOutput.toJson(legacyData)), "UTF-8") - } -} - -static Map> sortRegistryMapping(Map> mapping) { - return mapping.collectEntries({ modernID, map -> - [modernID, map.sort({ - if (it.getKey().contains(".")) { - return MinecraftVersion.fromVersionName(it.getKey()) - } else { - return MinecraftVersion.MINIMUM_VERSION - } - })] - }).sort() -} - -void generateRegistryMapping(String target, File targetDir, Map registriesReports) { - File targetFile = new File(targetDir, "${target}s.json"); - File targetMappingFile = new File(targetDir, "${target}s_mapping.json"); - if (checkIsCacheValid(targetFile) || checkIsCacheValid(targetMappingFile)) { - this.println("> Generating ${target}s...") - - Map> idMap = - registriesReports.collectEntries({ version, registry -> - Object entries = registry["minecraft:${target}"].entries - return [version, entries.collectEntries({ name, id -> [name, String.valueOf(id["protocol_id"])] })] - }) - - Map modernIDs = Collections.max(idMap.entrySet(), Map.Entry.comparingByKey()).getValue() - - targetFile.write(JsonOutput.prettyPrint( - JsonOutput.toJson(modernIDs.sort({Integer.parseInt(it.getValue()) }))), "UTF-8") - - this.println("> Generating ${target}s mapping...") - - Map> mapping = new JsonSlurper() - .parse(new File(this.getProjectDir(), "mapping/legacy_${target}s_mapping.json")) - .collectEntries({ key, value -> { - if (modernIDs[key] == null) { - throw new IllegalStateException("No modern id found for $key") - } - - return [modernIDs[key], value] - } }) - - idMap.forEach({ version, ids -> - ids.forEach({ key, id -> - if (!modernIDs.containsKey(key)) { - return - } - - mapping.computeIfAbsent(modernIDs[key], _ -> new LinkedHashMap<>()).put(version.getVersionName(), id) - }) - }) - - mapping = sortRegistryMapping(mapping) - targetMappingFile.write(JsonOutput.prettyPrint( - JsonOutput.toJson(mapping.sort({ Integer.parseInt(it.getKey()) }))), "UTF-8") - } -} - -void generateRegistryMappings(File targetDir, Map registriesReports) { - this.generateRegistryMapping("item", targetDir, registriesReports - .findAll({ e -> MinecraftVersion.WORLD_VERSIONS.contains(e.getKey()) })) - this.generateRegistryMapping("block", targetDir, registriesReports) - this.generateRegistryMapping("data_component_type", targetDir, registriesReports - .findAll({ e -> e.getKey() >= MinecraftVersion.MINECRAFT_1_20_5 })) - - File blockEntitiesMappingFile = new File(targetDir, "blockentities_mapping.json"); - - if (checkIsCacheValid(blockEntitiesMappingFile)) { - this.println("> Generating blockentities mapping...") - - Map> blockentities = new JsonSlurper() - .parse(new File(this.getProjectDir(), "mapping/legacy_blockentities_mapping.json")) - - registriesReports.forEach({ version, registries -> - if (version < MinecraftVersion.MINECRAFT_1_19) { - return - } - - registries["minecraft:block_entity_type"].entries.forEach({ key, value -> - int id = value.protocol_id - blockentities.computeIfAbsent(key, _ -> new LinkedHashMap<>()) - .put(version.getVersionName(), String.valueOf(id)) - }) - }) - - blockentities = sortRegistryMapping(blockentities) - blockEntitiesMappingFile.write( - JsonOutput.prettyPrint(JsonOutput.toJson(blockentities)), "UTF-8") - } -} - -static Map>> getTags(File tagDir, Map tagTypes) { - Map>> tags = new LinkedHashMap<>() - - tagTypes.forEach({ directory, key -> - File directoryFile = new File(tagDir, directory) - if (!directoryFile.exists()) { - return - } - - Map> typeTags = new HashMap<>() - Map> tempTags = new HashMap<>() - - directoryFile.eachFileRecurse(FileType.FILES, { file -> - List values = new JsonSlurper().parse(file).values - Path relativePath = directoryFile.toPath().relativize(file.toPath()) - String name = FilenameUtils.removeExtension(relativePath.toString()).replace(File.separatorChar, '/' as char) - typeTags.put("minecraft:" + name, values) - }) - - boolean flatten = false - while (!flatten) { - flatten = true - - typeTags.forEach({ name, currentTags -> - List newTags = new ArrayList<>() - currentTags.forEach({ currentTag -> - if (currentTag.startsWith("#")) { - newTags.addAll(typeTags.get(currentTag.substring(1))) - flatten = false - } else { - newTags.add(currentTag) - } - }) - - tempTags.put(name, newTags) - }) - - typeTags = tempTags - tempTags = new HashMap<>() - } - - tags.put(key, typeTags) - }) - - return tags -} - -void generateTags(File targetDir, Map tagDirs) { - File tagsFile = new File(targetDir, "tags.json"); - if (checkIsCacheValid(tagsFile)) { - this.println("> Generating tags...") - - Map tagTypes = new JsonSlurper().parse(new File(getProjectDir(), "mapping/tag_types.json")) - - Map>>> allTags = - tagDirs.collectEntries({ version, dir -> - [version, getTags(dir, tagTypes.tag_types)] - }) - - Map>> mergedTags = new LinkedHashMap<>() - - allTags.forEach({ version, tags -> - tags.forEach({ type, typeTags -> { - Map> mergedTypeTags = mergedTags.computeIfAbsent(type, _ -> new HashMap<>()) - typeTags.forEach({ name, values -> - Set mergedValues = mergedTypeTags.computeIfAbsent(name, _ -> new HashSet<>()) - if (!tagTypes.supported_tag_types.contains(type)) { - return - } - - mergedValues.addAll(values) - }) - }}) - }) - - mergedTags = mergedTags.collectEntries({ type, typeTags -> - [type, typeTags.collectEntries({ name, values -> - [name, values.sort()] - }).sort()] - }) - - tagsFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(mergedTags)), "UTF-8") - } -} - -tasks.register("generateMappings") { - dependsOn(downloadManifest) - - File targetDir = new File(generatedDir, "mapping") - targetDir.mkdirs() - - this.println("> Generating Minecraft data...") - - Map generated = Arrays.stream(MinecraftVersion.values()) - .dropWhile({ it < MinecraftVersion.MINECRAFT_1_13 }) - .collect(Collectors.toMap(Function.identity(), this::generateData)) - - Map blockReports = generated.collectEntries({ version, directory -> - [version, new JsonSlurper().parse(new File(directory, "reports/blocks.json"))] - }) - - this.generateBlockMappings(targetDir, blockReports) - - Map registriesReports = generated - .findAll({ it.getKey() >= MinecraftVersion.MINECRAFT_1_14 }) - .collectEntries({ version, directory -> - [version, new JsonSlurper().parse(new File(directory, "reports/registries.json"))] - }) - - this.generateRegistryMappings(targetDir, registriesReports) - - Map tags = generated - .collectEntries({ version, directory -> - [version, new File(directory, "data/minecraft/tags")] - }) - - this.generateTags(targetDir, tags) -} - -processResources.dependsOn(generateMappings) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e35a202..a5c3912a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,4 +17,5 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "limboapi" include("api") +include("mapping") include("plugin")