From 34a71b8f82d1fe6896f2cb698d59c587f93fe831 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Sat, 2 Mar 2024 16:55:56 +0100 Subject: [PATCH] fix protocol info generation --- .gitignore | 3 +- build.gradle.kts | 35 +- .../java/com/mojang/authlib/GameProfile.java | 29 -- .../java/com/mojang/datafixers/util/Pair.java | 29 -- .../com/mojang/serialization/DynamicOps.java | 29 -- .../com/mojang/serialization/Keyable.java | 29 -- src/bridge/readme.txt | 2 - .../GeneratorCLIArguments.java | 6 +- .../GeneratorEntrypoint.java | 62 ++-- .../http/HttpFileDownloader.java | 5 + .../library/McLibraryLoader.java | 70 ++++ .../manifest/McManifestVersion.java | 19 +- .../manifest/McManifestVersionData.java} | 39 +- .../manifest/McManifestVersionFetcher.java | 9 +- .../manifest/McManifestVersionType.java} | 20 +- .../markdown/MarkdownGenerator.java | 6 +- .../protocol/ConnectionProtocolScanner.java | 83 ----- .../protocol/McClassNames.java | 7 +- .../{EnumSupport.java => McInit.java} | 28 +- .../protocol/McPacketFlow.java} | 21 +- .../protocol/McProtocolStates.java} | 39 +- .../protocol/McProtocolsDecoder.java | 341 ++++++++++++++++++ .../protocol/McUnboundProtocolBinder.java | 83 +++++ .../protocol/ProtocolInfoCollector.java | 116 +++--- .../protocolgenerator/remap/JarRemapper.java | 44 ++- 25 files changed, 785 insertions(+), 369 deletions(-) delete mode 100644 src/bridge/java/com/mojang/authlib/GameProfile.java delete mode 100644 src/bridge/java/com/mojang/datafixers/util/Pair.java delete mode 100644 src/bridge/java/com/mojang/serialization/DynamicOps.java delete mode 100644 src/bridge/java/com/mojang/serialization/Keyable.java delete mode 100644 src/bridge/readme.txt create mode 100644 src/main/java/dev/derklaro/protocolgenerator/library/McLibraryLoader.java rename src/{bridge/java/com/mojang/brigadier/suggestion/Suggestions.java => main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionData.java} (52%) rename src/{bridge/java/com/mojang/brigadier/Message.java => main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionType.java} (70%) delete mode 100644 src/main/java/dev/derklaro/protocolgenerator/protocol/ConnectionProtocolScanner.java rename src/main/java/dev/derklaro/protocolgenerator/protocol/{EnumSupport.java => McInit.java} (60%) rename src/{bridge/java/com/mojang/logging/LogUtils.java => main/java/dev/derklaro/protocolgenerator/protocol/McPacketFlow.java} (74%) rename src/{bridge/java/com/mojang/brigadier/arguments/ArgumentType.java => main/java/dev/derklaro/protocolgenerator/protocol/McProtocolStates.java} (53%) create mode 100644 src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolsDecoder.java create mode 100644 src/main/java/dev/derklaro/protocolgenerator/protocol/McUnboundProtocolBinder.java diff --git a/.gitignore b/.gitignore index d94ba17..76a40bc 100644 --- a/.gitignore +++ b/.gitignore @@ -70,8 +70,9 @@ atlassian-ide-plugin.xml # delombok */src/main/lombok -# temp test files +# stuff for generation only *.tmp +client_libs/ # final protocol output *.md diff --git a/build.gradle.kts b/build.gradle.kts index 9b0ec84..6a24a80 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ plugins { id("java") id("application") - id("com.diffplug.spotless") version "6.23.3" + id("com.diffplug.spotless") version "6.25.0" id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -34,7 +34,7 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() - maven("https://maven.fabricmc.net/") + maven("https://maven.minecraftforge.net/") } dependencies { @@ -44,18 +44,19 @@ dependencies { val guava = "33.0.0-jre" implementation("com.google.guava", "guava", guava) - val slf4j = "2.0.9" + val slf4j = "2.0.12" implementation("org.slf4j", "slf4j-api", slf4j) - val logback = "1.4.14" + val logback = "1.5.0" runtimeOnly("ch.qos.logback", "logback-classic", logback) - val jackson = "2.16.0" + val jackson = "2.16.1" implementation("com.fasterxml.jackson.core", "jackson-databind", jackson) implementation("com.fasterxml.jackson.datatype", "jackson-datatype-jsr310", jackson) - val enigma = "2.3.3" - implementation("cuchaz", "enigma", enigma) + // for updates check https://maven.minecraftforge.net/net/minecraftforge/ForgeAutoRenamingTool/maven-metadata.xml + val autoRenamingTool = "1.0.6" + implementation("net.minecraftforge", "ForgeAutoRenamingTool", autoRenamingTool) val argparse4j = "0.9.0" implementation("net.sourceforge.argparse4j", "argparse4j", argparse4j) @@ -63,19 +64,13 @@ dependencies { val reflexion = "1.8.0" implementation("dev.derklaro.reflexion", "reflexion", reflexion) - val fastutil = "8.5.12" // needed internally for minecraft, do not remove - runtimeOnly("it.unimi.dsi", "fastutil", fastutil) - - val joml = "1.10.5" // needed internally for minecraft, do not remove - runtimeOnly("org.joml", "joml", joml) - - val netty = "4.1.104.Final" // needed internally for minecraft, do not remove - runtimeOnly("io.netty", "netty-buffer", netty) - runtimeOnly("io.netty", "netty-handler", netty) - val lombok = "1.18.30" compileOnly("org.projectlombok", "lombok", lombok) annotationProcessor("org.projectlombok", "lombok", lombok) + + val asm = "9.6" + implementation("org.ow2.asm", "asm", asm) + implementation("org.ow2.asm", "asm-tree", asm) } tasks.withType().configureEach { @@ -86,12 +81,6 @@ tasks.withType().configureEach { options.isIncremental = true } -java { - sourceSets["main"].java { - srcDir("src/bridge/java") - } -} - tasks.shadowJar { archiveFileName.set("protocol-generator.jar") } diff --git a/src/bridge/java/com/mojang/authlib/GameProfile.java b/src/bridge/java/com/mojang/authlib/GameProfile.java deleted file mode 100644 index 26380d3..0000000 --- a/src/bridge/java/com/mojang/authlib/GameProfile.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 Pasqual K. and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.mojang.authlib; - -public class GameProfile { - -} diff --git a/src/bridge/java/com/mojang/datafixers/util/Pair.java b/src/bridge/java/com/mojang/datafixers/util/Pair.java deleted file mode 100644 index 0ede251..0000000 --- a/src/bridge/java/com/mojang/datafixers/util/Pair.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 Pasqual K. and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.mojang.datafixers.util; - -public class Pair { - -} diff --git a/src/bridge/java/com/mojang/serialization/DynamicOps.java b/src/bridge/java/com/mojang/serialization/DynamicOps.java deleted file mode 100644 index c645e84..0000000 --- a/src/bridge/java/com/mojang/serialization/DynamicOps.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 Pasqual K. and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.mojang.serialization; - -public interface DynamicOps { - -} diff --git a/src/bridge/java/com/mojang/serialization/Keyable.java b/src/bridge/java/com/mojang/serialization/Keyable.java deleted file mode 100644 index 228aa5f..0000000 --- a/src/bridge/java/com/mojang/serialization/Keyable.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 Pasqual K. and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.mojang.serialization; - -public interface Keyable { - -} diff --git a/src/bridge/readme.txt b/src/bridge/readme.txt deleted file mode 100644 index e5d2baf..0000000 --- a/src/bridge/readme.txt +++ /dev/null @@ -1,2 +0,0 @@ -This source set contains some files which are required in the runtime to init some classes required for dumping -the protocol which are not available via any maven repository. diff --git a/src/main/java/dev/derklaro/protocolgenerator/GeneratorCLIArguments.java b/src/main/java/dev/derklaro/protocolgenerator/GeneratorCLIArguments.java index 953d55d..56acb7d 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/GeneratorCLIArguments.java +++ b/src/main/java/dev/derklaro/protocolgenerator/GeneratorCLIArguments.java @@ -25,7 +25,7 @@ package dev.derklaro.protocolgenerator; import dev.derklaro.protocolgenerator.cli.CliArgParser; -import dev.derklaro.protocolgenerator.manifest.McManifestVersion; +import dev.derklaro.protocolgenerator.manifest.McManifestVersionType; import lombok.NonNull; import net.sourceforge.argparse4j.impl.Arguments; @@ -38,9 +38,9 @@ private GeneratorCLIArguments() { public static void registerDefaultArguments(@NonNull CliArgParser argParser) { // argument to set the version type to fetch argParser.registerArgument("-vt", "--version-type") - .setDefault(McManifestVersion.VersionType.LATEST) + .setDefault(McManifestVersionType.LATEST) .help("Sets the argument type to download and parse the protocol of") - .type(Arguments.caseInsensitiveEnumType(McManifestVersion.VersionType.class)); + .type(Arguments.caseInsensitiveEnumType(McManifestVersionType.class)); // the final output file name argParser.registerArgument("-of", "--output-file") diff --git a/src/main/java/dev/derklaro/protocolgenerator/GeneratorEntrypoint.java b/src/main/java/dev/derklaro/protocolgenerator/GeneratorEntrypoint.java index 2321735..5432b45 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/GeneratorEntrypoint.java +++ b/src/main/java/dev/derklaro/protocolgenerator/GeneratorEntrypoint.java @@ -27,8 +27,9 @@ import dev.derklaro.protocolgenerator.cli.CliArgParser; import dev.derklaro.protocolgenerator.gameversion.JarGameVersionParser; import dev.derklaro.protocolgenerator.http.HttpFileDownloader; -import dev.derklaro.protocolgenerator.manifest.McManifestVersion; +import dev.derklaro.protocolgenerator.library.McLibraryLoader; import dev.derklaro.protocolgenerator.manifest.McManifestVersionFetcher; +import dev.derklaro.protocolgenerator.manifest.McManifestVersionType; import dev.derklaro.protocolgenerator.manifest.McVersionDumper; import dev.derklaro.protocolgenerator.markdown.MarkdownFormatter; import dev.derklaro.protocolgenerator.markdown.MarkdownGenerator; @@ -38,12 +39,15 @@ import dev.derklaro.protocolgenerator.util.FileUtil; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import lombok.NonNull; public final class GeneratorEntrypoint { + private static final Path LIB_DIR_PATH = Path.of("client_libs"); private static final Path MAPPING_PATH = Path.of("mappings.tmp"); private static final Path CLIENT_JAR_PATH = Path.of("client.tmp"); private static final Path REMAPPED_JAR_PATH = Path.of("client_remapped.tmp"); @@ -59,10 +63,10 @@ public static void main(@NonNull String[] args) throws IOException { var clientVersions = versionFetcher.resolveMcVersions(); // search the requested version - McManifestVersion.VersionType versionType = cliNamespace.get("version_type"); + McManifestVersionType versionType = cliNamespace.get("version_type"); var latestVersionOfType = clientVersions .thenApply(versions -> versions.stream() - .filter(version -> versionType == McManifestVersion.VersionType.LATEST || version.type() == versionType) + .filter(version -> versionType == McManifestVersionType.LATEST || version.type() == versionType) .sorted() .findFirst()) .thenApply(optional -> { @@ -71,26 +75,42 @@ public static void main(@NonNull String[] args) throws IOException { return optional.orElseThrow(versionNotFoundExceptionSupplier); }); - // fetch the version data of the latest version - var versionTypeDownloads = latestVersionOfType - .thenCompose(versionFetcher::parseVersionData) - .thenApply(data -> data.get("downloads")) - .thenCompose(downloads -> { - // extract the client downloads - var clientUrl = downloads.get("client").get("url").asText(); - var clientMappings = downloads.get("client_mappings").get("url").asText(); - - // download both files - var clientDownload = HttpFileDownloader.downloadFile(clientUrl, CLIENT_JAR_PATH); - var clientMappingsDownload = HttpFileDownloader.downloadFile(clientMappings, MAPPING_PATH); - - // combine both futures - return CompletableFuture.allOf(clientDownload, clientMappingsDownload); - }); + // deserialize version data + var versionData = latestVersionOfType.thenCompose(versionFetcher::parseVersionData); + + // download the client and client mappings of the version + var versionTypeDownloads = versionData.thenCompose(data -> { + var clientDownloadInfo = data.fileDownloads().get("client"); + var mappingsDownloadInfo = data.fileDownloads().get("client_mappings"); + + // no null check here as we just require the downloads to present + // in all other cases we cannot proceed anyway + var clientDownload = HttpFileDownloader.downloadFile(clientDownloadInfo.downloadUrl(), CLIENT_JAR_PATH); + var clientMappingsDownload = HttpFileDownloader.downloadFile(mappingsDownloadInfo.downloadUrl(), MAPPING_PATH); + + // combine both futures + return CompletableFuture.allOf(clientDownload, clientMappingsDownload); + }); + + // download all required client libraries + var libraryLoader = new McLibraryLoader(LIB_DIR_PATH); + var libraryLoading = versionData.thenCompose(data -> { + List> libDownloadFutures = new ArrayList<>(); + for (var library : data.libraries()) { + var downloadFuture = libraryLoader.loadLibrary(library); + libDownloadFutures.add(downloadFuture); + } + + var futures = libDownloadFutures.toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures); + }); + + // combine the loading of the client and the loading of the required libraries + var downloadFuture = CompletableFuture.allOf(versionTypeDownloads, libraryLoading); // remap the client jar var remapper = new JarRemapper(CLIENT_JAR_PATH, MAPPING_PATH); - var remapOutput = versionTypeDownloads.thenApply(CatchingFunction.asJavaUtil(ignored -> { + var remapOutput = downloadFuture.thenApply(CatchingFunction.asJavaUtil(ignored -> { remapper.remap(REMAPPED_JAR_PATH); return null; }, "Unable to remap client")); @@ -106,7 +126,7 @@ public static void main(@NonNull String[] args) throws IOException { // collect the protocol information var protocolInfos = remapOutput .thenApply(CatchingFunction.asJavaUtil(ignored -> { - var protocolInfoGenerator = new ProtocolInfoCollector(REMAPPED_JAR_PATH); + var protocolInfoGenerator = new ProtocolInfoCollector(REMAPPED_JAR_PATH, libraryLoader.provideClassLoader()); return protocolInfoGenerator.collectAllPacketInfos(); }, "Unable to resolve packet information")) .join(); diff --git a/src/main/java/dev/derklaro/protocolgenerator/http/HttpFileDownloader.java b/src/main/java/dev/derklaro/protocolgenerator/http/HttpFileDownloader.java index 44493fc..aa73407 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/http/HttpFileDownloader.java +++ b/src/main/java/dev/derklaro/protocolgenerator/http/HttpFileDownloader.java @@ -46,6 +46,11 @@ private HttpFileDownloader() { return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) .thenApply(BodyParser.bodyExtractorIfOk()) .thenApply(CatchingFunction.asJavaUtil(stream -> { + var parent = filePath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + try (stream) { Files.copy(stream, filePath, StandardCopyOption.REPLACE_EXISTING); return null; diff --git a/src/main/java/dev/derklaro/protocolgenerator/library/McLibraryLoader.java b/src/main/java/dev/derklaro/protocolgenerator/library/McLibraryLoader.java new file mode 100644 index 0000000..cab7a92 --- /dev/null +++ b/src/main/java/dev/derklaro/protocolgenerator/library/McLibraryLoader.java @@ -0,0 +1,70 @@ +/* + * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package dev.derklaro.protocolgenerator.library; + +import dev.derklaro.protocolgenerator.http.HttpFileDownloader; +import dev.derklaro.protocolgenerator.manifest.McManifestVersionData; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.NonNull; + +public final class McLibraryLoader { + + private static final String LIB_ARTIFACT_DOWNLOAD_KEY = "artifact"; + + private final Path libraryDirectory; + private final List libraries = new ArrayList<>(); + + public McLibraryLoader(@NonNull Path libraryDirectory) { + this.libraryDirectory = libraryDirectory; + } + + public @NonNull CompletableFuture loadLibrary(@NonNull McManifestVersionData.Library library) { + var download = library.downloads().get(LIB_ARTIFACT_DOWNLOAD_KEY); + if (download != null) { + var localPath = this.libraryDirectory.resolve(download.pathName()); + return HttpFileDownloader.downloadFile(download.downloadUrl(), localPath).thenRun(() -> { + try { + var url = localPath.toUri().toURL(); + this.libraries.add(url); + } catch (IOException exception) { + throw new IllegalStateException("error converting path to url", exception); + } + }); + } else { + return CompletableFuture.completedFuture(null); + } + } + + public @NonNull ClassLoader provideClassLoader() { + var urls = this.libraries.toArray(URL[]::new); + return new URLClassLoader(urls, ClassLoader.getSystemClassLoader()); + } +} diff --git a/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersion.java b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersion.java index 278eedb..ec2a99c 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersion.java +++ b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersion.java @@ -24,15 +24,13 @@ package dev.derklaro.protocolgenerator.manifest; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import java.net.URI; import java.time.OffsetDateTime; import lombok.NonNull; public record McManifestVersion( @NonNull String id, - @NonNull VersionType type, + @NonNull McManifestVersionType type, @NonNull String url, @NonNull OffsetDateTime releaseTime, @NonNull String sha1 @@ -46,19 +44,4 @@ public record McManifestVersion( public int compareTo(@NonNull McManifestVersion other) { return other.releaseTime().compareTo(this.releaseTime()); } - - public enum VersionType { - - @JsonIgnore - LATEST, - - @JsonProperty("release") - RELEASE, - @JsonProperty("snapshot") - SNAPSHOT, - @JsonProperty("old_beta") - OLD_BETA, - @JsonProperty("old_alpha") - OLD_ALPHA - } } diff --git a/src/bridge/java/com/mojang/brigadier/suggestion/Suggestions.java b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionData.java similarity index 52% rename from src/bridge/java/com/mojang/brigadier/suggestion/Suggestions.java rename to src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionData.java index be1f946..8a0c6e1 100644 --- a/src/bridge/java/com/mojang/brigadier/suggestion/Suggestions.java +++ b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionData.java @@ -1,7 +1,7 @@ /* * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). * - * Copyright (c) 2023 Pasqual K. and contributors + * Copyright (c) 2024 Pasqual K. and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,41 @@ * THE SOFTWARE. */ -package com.mojang.brigadier.suggestion; +package dev.derklaro.protocolgenerator.manifest; -public class Suggestions { +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +public record McManifestVersionData( + @JsonProperty("id") String id, + @JsonProperty("type") McManifestVersionType type, + @JsonProperty("libraries") List libraries, + @JsonProperty("releaseTime") OffsetDateTime releaseTime, + @JsonProperty("minimumLauncherVersion") int minLauncherVersion, + @JsonProperty("downloads") Map fileDownloads +) { + + public record Library( + @JsonProperty("name") String id, + @JsonProperty("downloads") Map downloads + ) { + + } + + public record LibraryDownload( + @JsonProperty("path") String pathName, + @JsonProperty("sha1") String sha1, + @JsonProperty("url") String downloadUrl + ) { + + } + + public record FileDownload( + @JsonProperty("sha1") String sha1, + @JsonProperty("url") String downloadUrl + ) { + + } } diff --git a/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionFetcher.java b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionFetcher.java index ea74519..c39cd0c 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionFetcher.java +++ b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionFetcher.java @@ -25,7 +25,6 @@ package dev.derklaro.protocolgenerator.manifest; import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.type.TypeFactory; import dev.derklaro.protocolgenerator.http.BodyParser; import dev.derklaro.protocolgenerator.http.HttpClientProvider; @@ -43,6 +42,7 @@ public final class McManifestVersionFetcher { private static final URI VERSION_MANIFEST_URI = URI.create( "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); + private static final JavaType COLLECTION_MC_VERSION = TypeFactory.defaultInstance().constructCollectionType( Set.class, McManifestVersion.class); @@ -61,12 +61,15 @@ public final class McManifestVersionFetcher { } } - public @NonNull CompletableFuture parseVersionData(@NonNull McManifestVersion version) { + public @NonNull CompletableFuture parseVersionData(@NonNull McManifestVersion version) { try (var httpClient = HttpClientProvider.provideClient()) { var request = HttpRequest.newBuilder(version.uri()).build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) .thenApply(BodyParser.bodyExtractorIfOk()) - .thenApply(BodyParser.toJsonObject()); + .thenApply(BodyParser.toJsonObject()) + .thenApply(CatchingFunction.asJavaUtil( + jsonNode -> JacksonSupport.OBJECT_MAPPER.treeToValue(jsonNode, McManifestVersionData.class), + "Unable to parse mc manifest versions")); } } } diff --git a/src/bridge/java/com/mojang/brigadier/Message.java b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionType.java similarity index 70% rename from src/bridge/java/com/mojang/brigadier/Message.java rename to src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionType.java index 32ef646..dd11015 100644 --- a/src/bridge/java/com/mojang/brigadier/Message.java +++ b/src/main/java/dev/derklaro/protocolgenerator/manifest/McManifestVersionType.java @@ -1,7 +1,7 @@ /* * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). * - * Copyright (c) 2023 Pasqual K. and contributors + * Copyright (c) 2024 Pasqual K. and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,22 @@ * THE SOFTWARE. */ -package com.mojang.brigadier; +package dev.derklaro.protocolgenerator.manifest; -public interface Message { +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +public enum McManifestVersionType { + /* internally used, not actually supplied by minecraft manifest */ + @JsonIgnore + LATEST, + + @JsonProperty("release") + RELEASE, + @JsonProperty("snapshot") + SNAPSHOT, + @JsonProperty("old_beta") + OLD_BETA, + @JsonProperty("old_alpha") + OLD_ALPHA } diff --git a/src/main/java/dev/derklaro/protocolgenerator/markdown/MarkdownGenerator.java b/src/main/java/dev/derklaro/protocolgenerator/markdown/MarkdownGenerator.java index 997e677..61d0e7d 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/markdown/MarkdownGenerator.java +++ b/src/main/java/dev/derklaro/protocolgenerator/markdown/MarkdownGenerator.java @@ -57,9 +57,9 @@ public final class MarkdownGenerator { "Java Version", "Protocol Version", "World Version", - "Pack Resource Version", - "Pack Data Version", - "Built at (UTC)"); + "Resource Pack Version", + "Data Pack Version", + "Build Timestamp (UTC)"); public @NonNull Markdown generateProtocolMarkdown( @NonNull GameVersion currentGameVersion, diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/ConnectionProtocolScanner.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/ConnectionProtocolScanner.java deleted file mode 100644 index 63eda9f..0000000 --- a/src/main/java/dev/derklaro/protocolgenerator/protocol/ConnectionProtocolScanner.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 Pasqual K. and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package dev.derklaro.protocolgenerator.protocol; - -import dev.derklaro.reflexion.Reflexion; -import dev.derklaro.reflexion.matcher.MethodMatcher; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import lombok.NonNull; - -final class ConnectionProtocolScanner { - - private static final MethodMatcher GET_PACKETS_BY_ID_MATCHER = MethodMatcher.newMatcher() - .parameterCount(1) - .hasName("getPacketsByIds") - .derivedType(Method::getReturnType, Map.class); - - private final Object connectionProtocol; - - public ConnectionProtocolScanner(@NonNull Object connectionProtocol) { - this.connectionProtocol = connectionProtocol; - } - - public @NonNull Collection scanPackets( - @NonNull Object packetFlow, - @NonNull String sourceFlowName, - @NonNull String targetFlowName - ) { - // resolve the method to invoke to get the packets - var packetsByIdMethod = Reflexion.onBound(this.connectionProtocol) - .findMethod(GET_PACKETS_BY_ID_MATCHER) - .orElseThrow(() -> new IllegalStateException("Unknown: ConnectionProtocol#getPacketsByIds(PacketFlow): Map")); - - // get the packets of the connection protocol for the current packet flow - var packets = packetsByIdMethod.>>invokeWithArgs(packetFlow).getOrThrow(); - - // map each packet - List packetClassInfos = new LinkedList<>(); - for (var entry : packets.entrySet()) { - // normalize the packet class name - var normalizedName = entry.getValue().getSimpleName().replaceFirst(targetFlowName, ""); - var fullName = String.join(" ", normalizedName.split("(?=\\p{Lu})")); - - // build a packet class info - var packetClassInfo = new PacketClassInfo(entry.getKey(), fullName, sourceFlowName, targetFlowName); - packetClassInfos.add(packetClassInfo); - - // resolve all fields in the packet class - var fieldScanner = new PacketClassFieldScanner(entry.getValue(), packetClassInfo); - fieldScanner.scanAndRegisterClassFields(); - } - - // sort & return the collected class infos - Collections.sort(packetClassInfos); - return packetClassInfos; - } -} diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/McClassNames.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McClassNames.java index 3c78306..8c08e56 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/protocol/McClassNames.java +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McClassNames.java @@ -26,8 +26,11 @@ final class McClassNames { - public static final String PACKET_FLOW = "net.minecraft.network.protocol.PacketFlow"; - public static final String CONNECTION_PROTOCOL = "net.minecraft.network.ConnectionProtocol"; + public static final String BOOTSTRAP = "net.minecraft.server.Bootstrap"; + public static final String SHARED_CONSTANTS = "net.minecraft.SharedConstants"; + + public static final String REGISTRY_ACCESS = "net.minecraft.core.RegistryAccess"; + public static final String REGISTRY_FRIENDLY_BB = "net.minecraft.network.RegistryFriendlyByteBuf"; private McClassNames() { throw new UnsupportedOperationException(); diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/EnumSupport.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McInit.java similarity index 60% rename from src/main/java/dev/derklaro/protocolgenerator/protocol/EnumSupport.java rename to src/main/java/dev/derklaro/protocolgenerator/protocol/McInit.java index 13227f6..d71f4b1 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/protocol/EnumSupport.java +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McInit.java @@ -1,7 +1,7 @@ /* * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). * - * Copyright (c) 2023 Pasqual K. and contributors + * Copyright (c) 2024 Pasqual K. and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,25 +24,25 @@ package dev.derklaro.protocolgenerator.protocol; -import com.google.common.base.CaseFormat; -import dev.derklaro.reflexion.MethodAccessor; -import dev.derklaro.reflexion.Reflexion; import lombok.NonNull; -final class EnumSupport { +final class McInit { - private static final MethodAccessor ENUM_NAME_ACCESSOR = Reflexion.on(Enum.class).findMethod("name").orElseThrow(); + private final ClassLoader classLoader; - private EnumSupport() { - throw new UnsupportedOperationException(); + public McInit(@NonNull ClassLoader classLoader) { + this.classLoader = classLoader; } - public static @NonNull String resolveEnumConstantName(@NonNull Object enumConstant) { - return ENUM_NAME_ACCESSOR.invoke(enumConstant).getOrThrow(); - } + public void init() throws Exception { + // detect running version + var sharedConstantsClass = Class.forName(McClassNames.SHARED_CONSTANTS, true, this.classLoader); + var tryDetectVersionMethod = sharedConstantsClass.getMethod("tryDetectVersion"); + tryDetectVersionMethod.invoke(null); - public static @NonNull String normalizeEnumConstantName(@NonNull Object enumConstant) { - var enumConstantName = resolveEnumConstantName(enumConstant); - return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, enumConstantName); + // bootstrap registries + var bootstrapClass = Class.forName(McClassNames.BOOTSTRAP, true, this.classLoader); + var bootStrapMethod = bootstrapClass.getMethod("bootStrap"); + bootStrapMethod.invoke(null); } } diff --git a/src/bridge/java/com/mojang/logging/LogUtils.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McPacketFlow.java similarity index 74% rename from src/bridge/java/com/mojang/logging/LogUtils.java rename to src/main/java/dev/derklaro/protocolgenerator/protocol/McPacketFlow.java index 49bcf32..a3dfb46 100644 --- a/src/bridge/java/com/mojang/logging/LogUtils.java +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McPacketFlow.java @@ -1,7 +1,7 @@ /* * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). * - * Copyright (c) 2023 Pasqual K. and contributors + * Copyright (c) 2024 Pasqual K. and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,14 +22,21 @@ * THE SOFTWARE. */ -package com.mojang.logging; +package dev.derklaro.protocolgenerator.protocol; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.NonNull; -public class LogUtils { +enum McPacketFlow { - public static Logger getLogger() { - return LoggerFactory.getLogger("MCUnused"); + /* packets from server to client */ + CLIENTBOUND, + /* packets from client to server */ + SERVERBOUND; + + public @NonNull McPacketFlow sender() { + return switch (this) { + case CLIENTBOUND -> SERVERBOUND; + case SERVERBOUND -> CLIENTBOUND; + }; } } diff --git a/src/bridge/java/com/mojang/brigadier/arguments/ArgumentType.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolStates.java similarity index 53% rename from src/bridge/java/com/mojang/brigadier/arguments/ArgumentType.java rename to src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolStates.java index f4036db..729d091 100644 --- a/src/bridge/java/com/mojang/brigadier/arguments/ArgumentType.java +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolStates.java @@ -1,7 +1,7 @@ /* * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). * - * Copyright (c) 2023 Pasqual K. and contributors + * Copyright (c) 2024 Pasqual K. and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,41 @@ * THE SOFTWARE. */ -package com.mojang.brigadier.arguments; +package dev.derklaro.protocolgenerator.protocol; -public interface ArgumentType { +import lombok.NonNull; +enum McProtocolStates { + + /* state order here reflects into the field order of the final output */ + + HANDSHAKING("handshake"), + STATUS("status"), + LOGIN("login"), + CONFIGURATION("configuration"), + PLAY("game"); + + private static final String PKG_PREFIX_BASE = "net.minecraft.network.protocol.%s"; + + private final String pkg; + private final String displayName; + + McProtocolStates(@NonNull String name) { + this(name, String.format(PKG_PREFIX_BASE, name)); + } + + McProtocolStates(@NonNull String name, @NonNull String pkg) { + this.pkg = pkg; + + var firstChar = Character.toUpperCase(name.charAt(0)); + this.displayName = String.format("%c%s", firstChar, name.substring(1)); + } + + public @NonNull String displayName() { + return this.displayName; + } + + public @NonNull String protocolsClass() { + return String.format("%s.%sProtocols", this.pkg, this.displayName); + } } diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolsDecoder.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolsDecoder.java new file mode 100644 index 0000000..1c2ee2a --- /dev/null +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McProtocolsDecoder.java @@ -0,0 +1,341 @@ +/* + * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package dev.derklaro.protocolgenerator.protocol; + +import dev.derklaro.reflexion.Reflexion; +import dev.derklaro.reflexion.matcher.FieldMatcher; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; +import lombok.NonNull; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Handle; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +final class McProtocolsDecoder { + + private static final FieldMatcher PROTOCOL_INFO_MATCHER = FieldMatcher.newMatcher() + .hasModifier(Modifier.STATIC) + .and(field -> { + var name = field.getType().getCanonicalName(); + return name.endsWith(".ProtocolInfo") || name.endsWith(".ProtocolInfo.Unbound"); + }); + + private static final String STREAM_CODEC_DESC = "Lnet/minecraft/network/codec/StreamCodec;"; + private static final String PACKET_TYPE_DESC = "Lnet/minecraft/network/protocol/PacketType;"; + + private final Class targetClass; + private final ClassLoader mcClassLoader; + + public McProtocolsDecoder(@NonNull Class targetClass, @NonNull ClassLoader mcClassLoader) { + this.targetClass = targetClass; + this.mcClassLoader = mcClassLoader; + } + + public @NonNull Map>> decodeAssociatedPackets() throws Exception { + // decode the target class, find the static initializer & all static lambda methods + var decodedClass = this.decodeTargetClass(); + var clinit = this.findClinit(decodedClass); + var staticLambdas = this.resolveLambdaMethods(decodedClass); + if (clinit == null) { + return Map.of(); + } + + // there can be two calls: one for serverbound, one for clientbound + var firstInvokeDynamic = this.findNodeOfType(InvokeDynamicInsnNode.class, clinit, null); + var secondInvokeDynamic = this.findNodeOfType(InvokeDynamicInsnNode.class, clinit, firstInvokeDynamic); + + // resolve the protocol info (ProtocolInfoBuilder type -> Invocation Target for configuration) + var firstProtocolInfo = this.resolveProtocolInvocationInfo(clinit, firstInvokeDynamic); + var secondProtocolInfo = this.resolveProtocolInvocationInfo(clinit, secondInvokeDynamic); + + // do the actual type mapping for (flow -> (type -> class)) + Map>> result = new EnumMap<>(McPacketFlow.class); + this.doMapTypes(staticLambdas, firstProtocolInfo, result); + this.doMapTypes(staticLambdas, secondProtocolInfo, result); + + return result; + } + + public @NonNull Map> resolvePacketIds() { + Map> result = new EnumMap<>(McPacketFlow.class); + + // resolve the packet ids from the constants in the given class + var protocolInfoFields = Reflexion.on(this.targetClass).findFields(PROTOCOL_INFO_MATCHER); + for (var protocolInfoField : protocolInfoFields) { + var protocolInfo = protocolInfoField.getValue().get(); + if (McUnboundProtocolBinder.isUnbound(protocolInfo.getClass())) { + // need to bind protocol info first + var binder = new McUnboundProtocolBinder(protocolInfo, this.mcClassLoader); + protocolInfo = binder.bind(); + } + + // get codec and mc packet flow + var codec = Reflexion.onBound(protocolInfo) + .findMethod("codec") + .flatMap(accessor -> accessor.invoke().asOptional()) + .orElseThrow(); + var flow = Reflexion.onBound(protocolInfo) + .findMethod("flow") + .flatMap(accessor -> accessor.invoke().asOptional()) + .orElseThrow(); + + // try to convert the packet flow + var convertedFlow = this.resolveFlowFromBuilderType(flow.toString()); + if (convertedFlow != null) { + var typeToId = Reflexion.onBound(codec) + .findField("toId") + .flatMap(accessor -> accessor.>getValue().asOptional()) + .orElseThrow(); + result.put(convertedFlow, typeToId); + } + } + + return result; + } + + private void doMapTypes( + @NonNull Map lambdas, + @Nullable Map.Entry protocolInfo, + @NonNull Map>> target + ) throws Exception { + if (protocolInfo != null) { + var lambda = lambdas.get(protocolInfo.getValue()); + var flow = this.resolveFlowFromBuilderType(protocolInfo.getKey()); + if (flow != null && lambda != null) { + var packetTypeMappings = this.resolvePacketTypes(lambda); + target.put(flow, packetTypeMappings); + } + } + } + + private @NonNull Map> resolvePacketTypes(@NonNull MethodNode methodNode) throws Exception { + var instructions = methodNode.instructions; + Map> packetTypes = new IdentityHashMap<>(); + + var bundle = false; // bundle packets are registered differently + Class currentPacketClass = null; + + var current = instructions.getLast(); + while (current != null) { + var currentlyBundle = bundle; + switch (current) { + case FieldInsnNode fn when fn.desc.equals(STREAM_CODEC_DESC) -> + // first call must be the call to the codec in the packet class + currentPacketClass = this.loadClassByInternalName(fn.owner); + + case FieldInsnNode fn when fn.desc.equals(PACKET_TYPE_DESC) -> { + // second call must be to the packet type associated with the packet class + Objects.requireNonNull(currentPacketClass, "found packet type access without packet class before"); + + var typeHolderClass = this.loadClassByInternalName(fn.owner); + var typeField = typeHolderClass.getField(fn.name); + var packetType = typeField.get(null); + + packetTypes.put(packetType, currentPacketClass); + bundle = false; + currentPacketClass = null; + } + + case MethodInsnNode mn when mn.name.equals("withBundlePacket") -> + // special case: bundle packets are registered differently into the builder + // than normal packets, set this marker to notify other handlers + bundle = true; + + case MethodInsnNode mn when mn.name.equals("") && currentlyBundle -> { + // when a bundle is registered, a special packet construction is made to the packet that indicates + // to the client that a bundle starts. This is the packet we're recording here which needs to be + // registered separately as the packet type never shows up elsewhere after. + // The class has a type() method which we can use to get the packet type from the class + var bundleDelimiterClass = this.loadClassByInternalName(mn.owner); + var bundleDelimiterPacketType = this.findPacketTypeOfPacket(mn.owner); + packetTypes.put(bundleDelimiterPacketType, bundleDelimiterClass); + } + + case InvokeDynamicInsnNode idn when idn.name.equals("apply") && currentlyBundle -> { + // bundle packet type are registered with a function to construct the target packet type + // therefore the information about the target packet class is directly encoded into the + // target handle of the invokedynamic instruction, just resolve the class from that + var invokeHandle = this.findHandle(idn); + Objects.requireNonNull(invokeHandle, "unable to find target packet handle during bundle registration"); + currentPacketClass = this.loadClassByInternalName(invokeHandle.getOwner()); + } + + default -> { + // ignored node + } + } + + current = current.getPrevious(); + } + + return packetTypes; + } + + private @Nullable Map.Entry resolveProtocolInvocationInfo( + @NonNull MethodNode owner, + @Nullable InvokeDynamicInsnNode node + ) { + if (node == null) { + return null; + } + + // invoke target not found + var invokeTarget = this.findHandle(node); + if (invokeTarget == null) { + return null; + } + + // next token should be the invocation of the ProtocolInfoBuilder.xxx method + var invokingMethod = this.findNodeOfType(MethodInsnNode.class, owner, node); + if (invokingMethod == null) { + return null; + } + + // map method name and invoke target name + var methodName = invokingMethod.name; + var invokeTargetName = invokeTarget.getName(); + return Map.entry(methodName, invokeTargetName); + } + + private @NonNull Object findPacketTypeOfPacket(@NonNull String internalPacketClassName) throws Exception { + var classNode = this.decodeClassByInternalName(internalPacketClassName); + for (var method : classNode.methods) { + if (method.name.equals("type") && method.desc.startsWith("()")) { + var fieldInsnNode = this.findNodeOfType(FieldInsnNode.class, method, null); + if (fieldInsnNode != null) { + var typeHolderClass = this.loadClassByInternalName(fieldInsnNode.owner); + var typeField = typeHolderClass.getField(fieldInsnNode.name); + return typeField.get(null); + } + } + } + + throw new IllegalStateException("cannot resolve packet type from packet class " + internalPacketClassName); + } + + private @NonNull ClassNode decodeTargetClass() throws IOException { + var classFileName = this.targetClass.getName().replace('.', '/'); + return this.decodeClassByInternalName(classFileName); + } + + private @NonNull ClassNode decodeClassByInternalName(@NonNull String internalName) throws IOException { + try (var classFileStream = this.mcClassLoader.getResourceAsStream(internalName + ".class")) { + Objects.requireNonNull(classFileStream, "Class file not found: " + internalName); + var reader = new ClassReader(classFileStream); + + var classNode = new ClassNode(); + reader.accept(classNode, 0); + return classNode; + } + } + + private @Nullable MethodNode findClinit(@NonNull ClassNode classNode) { + for (var method : classNode.methods) { + if (method.name.equals("")) { + return method; + } + } + + return null; + } + + private @Nullable McPacketFlow resolveFlowFromBuilderType(@NonNull String name) { + var lower = name.toLowerCase(Locale.ROOT); + if (lower.contains("serverbound")) { + return McPacketFlow.SERVERBOUND; + } + if (lower.contains("clientbound")) { + return McPacketFlow.CLIENTBOUND; + } + + return null; + } + + private @NonNull Map resolveLambdaMethods(@NonNull ClassNode classNode) { + Map lambdaMethods = new HashMap<>(); + for (var method : classNode.methods) { + if (method.name.startsWith("lambda$static$")) { + lambdaMethods.put(method.name, method); + } + } + + return lambdaMethods; + } + + private @Nullable T findNodeOfType( + @NonNull Class type, + @NonNull MethodNode methodNode, + @Nullable AbstractInsnNode after + ) { + // either start from the top or from the next node of the insn we want to start after + AbstractInsnNode start; + if (after == null) { + start = methodNode.instructions.getFirst(); + } else { + start = after.getNext(); + } + + AbstractInsnNode current = start; + do { + // either because current.next is null or after.next was null + if (current == null) { + return null; + } + + if (current.getClass() == type) { + return type.cast(current); + } + + current = current.getNext(); + } while (true); + } + + private @Nullable Handle findHandle(@NonNull InvokeDynamicInsnNode node) { + for (var arg : node.bsmArgs) { + if (arg instanceof Handle handle) { + return handle; + } + } + return null; + } + + + private @NonNull Class loadClassByInternalName(@NonNull String internalName) throws Exception { + var javaClassName = internalName.replace('/', '.'); + return Class.forName(javaClassName, true, this.mcClassLoader); + } +} diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/McUnboundProtocolBinder.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/McUnboundProtocolBinder.java new file mode 100644 index 0000000..bc262c3 --- /dev/null +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/McUnboundProtocolBinder.java @@ -0,0 +1,83 @@ +/* + * This file is part of mc-protocol-generator, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package dev.derklaro.protocolgenerator.protocol; + +import dev.derklaro.reflexion.Reflexion; +import dev.derklaro.reflexion.matcher.MethodMatcher; +import lombok.NonNull; + +final class McUnboundProtocolBinder { + + private final Object protocolInfo; + private final ClassLoader mcClassLoader; + + public McUnboundProtocolBinder(@NonNull Object protocolInfo, @NonNull ClassLoader mcClassLoader) { + this.protocolInfo = protocolInfo; + this.mcClassLoader = mcClassLoader; + } + + public static boolean isUnbound(@NonNull Class clazz) { + // check if the given type directly is an unbound protocol info + var name = clazz.getName(); + if (name.endsWith(".ProtocolInfo$Unbound")) { + return true; + } + + // check if the class implements the unbound interface + var interfaces = clazz.getInterfaces(); + for (var implementingInterface : interfaces) { + if (isUnbound(implementingInterface)) { + return true; + } + } + + return false; + } + + public @NonNull Object bind() { + var emptyRegistry = Reflexion.find(McClassNames.REGISTRY_ACCESS, this.mcClassLoader) + .flatMap(reflexion -> reflexion.findField("EMPTY")) + .flatMap(accessor -> accessor.getValue().asOptional()) + .orElseThrow(); + + var registryFriendlyBB = Reflexion.find(McClassNames.REGISTRY_FRIENDLY_BB, this.mcClassLoader) + .flatMap(reflexion -> reflexion.findMethod(MethodMatcher.newMatcher() + .parameterCount(1) + .hasName("decorator") + .and(method -> { + var firstParamType = method.getParameterTypes()[0]; + return firstParamType.getName().equals(McClassNames.REGISTRY_ACCESS); + }))) + .flatMap(accessor -> accessor.invoke(null, emptyRegistry).asOptional()) + .orElseThrow(); + + return Reflexion.on(this.protocolInfo) + .findMethod(MethodMatcher.newMatcher() + .hasName("bind") + .parameterCount(1)) + .flatMap(accessor -> accessor.invoke(this.protocolInfo, registryFriendlyBB).asOptional()) + .orElseThrow(); + } +} diff --git a/src/main/java/dev/derklaro/protocolgenerator/protocol/ProtocolInfoCollector.java b/src/main/java/dev/derklaro/protocolgenerator/protocol/ProtocolInfoCollector.java index 7822bd8..4575588 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/protocol/ProtocolInfoCollector.java +++ b/src/main/java/dev/derklaro/protocolgenerator/protocol/ProtocolInfoCollector.java @@ -26,73 +26,99 @@ import com.google.common.collect.Table; import com.google.common.collect.Tables; -import dev.derklaro.reflexion.Reflexion; -import dev.derklaro.reflexion.matcher.MethodMatcher; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; import lombok.NonNull; public final class ProtocolInfoCollector { - private static final MethodMatcher OPPOSITE_FLOW_GETTER = MethodMatcher.newMatcher().hasName("getOpposite"); - private final Path remappedJarPath; + private final ClassLoader libraryClassLoader; - public ProtocolInfoCollector(@NonNull Path remappedJarPath) { + public ProtocolInfoCollector(@NonNull Path remappedJarPath, @NonNull ClassLoader libraryClassLoader) { this.remappedJarPath = remappedJarPath; - } - - private static @NonNull Object resolveOppositeFlow(@NonNull Object packetFlow) { - // get the method to resolve the opposite - var oppositeAccessor = Reflexion.onBound(packetFlow) - .findMethod(OPPOSITE_FLOW_GETTER) - .orElseThrow(() -> new IllegalStateException("Unknown: PacketFlow#getOpposite: PacketFlow")); - - // get the opposite flow - return oppositeAccessor.invoke().getOrThrow(); + this.libraryClassLoader = libraryClassLoader; } public @NonNull Table> collectAllPacketInfos() throws Exception { // build a class loader which is aware of the remapped jar var remappedJarUrl = this.remappedJarPath.toUri().toURL(); - var loader = new URLClassLoader(new URL[]{remappedJarUrl}, ClassLoader.getSystemClassLoader()); - - // resolve the connection protocol and packet flow class - var packetFlowClass = Class.forName(McClassNames.PACKET_FLOW, true, loader); - var connectionProtocolClass = Class.forName(McClassNames.CONNECTION_PROTOCOL, true, loader); + var loader = new URLClassLoader(new URL[]{remappedJarUrl}, this.libraryClassLoader); - // resolve all enum constants for later use - var packetFlows = packetFlowClass.getEnumConstants(); - var connectionProtocols = connectionProtocolClass.getEnumConstants(); + // important first step: initialize minecraft registries + var mcInitializer = new McInit(loader); + mcInitializer.init(); // collect all packets for all protocols & flows - Table> packetClassInfos = Tables.newCustomTable( - new LinkedHashMap<>(), - LinkedHashMap::new); - for (var connectionProtocol : connectionProtocols) { - // construct the protocol scanner once for the protocol - var protocolName = EnumSupport.resolveEnumConstantName(connectionProtocol); - var protocolScanner = new ConnectionProtocolScanner(connectionProtocol); - - for (var packetFlow : packetFlows) { - // get the original name and the normalized name of this flow - var flowName = EnumSupport.resolveEnumConstantName(packetFlow); - var normalizedFlowName = EnumSupport.normalizeEnumConstantName(packetFlow); - - // get the normalized name of the opposite flow - var oppositeFlow = resolveOppositeFlow(packetFlow); - var oppositeFlowName = EnumSupport.normalizeEnumConstantName(oppositeFlow); - - // get all packets - var packets = protocolScanner.scanPackets(packetFlow, oppositeFlowName, normalizedFlowName); - packetClassInfos.put(protocolName, flowName, packets); + // protocol -> flow -> info + Table> result + = Tables.newCustomTable(new LinkedHashMap<>(), LinkedHashMap::new); + for (var state : McProtocolStates.values()) { + // resolve packet information + var protocolsClass = Class.forName(state.protocolsClass(), true, loader); + var infoDecoder = new McProtocolsDecoder(protocolsClass, loader); + var typeToPacketClassPerFlow = infoDecoder.decodeAssociatedPackets(); + var typeToPacketIdPerFlow = infoDecoder.resolvePacketIds(); + + // register for each flow (if present) + for (var flow : McPacketFlow.values()) { + var idMapping = typeToPacketIdPerFlow.get(flow); + var classMapping = typeToPacketClassPerFlow.get(flow); + if (idMapping != null && classMapping != null) { + var idEntries = new ArrayList<>(idMapping.entrySet()); + idEntries.sort(Map.Entry.comparingByValue()); + + for (var entry : idEntries) { + var packetId = entry.getValue(); + var packetType = entry.getKey(); + var packetClass = classMapping.get(packetType); + Objects.requireNonNull(packetClass, "class for packet type missing " + packetType); + + // construct the base packet info + var externalName = this.externalizePacketClassName(flow, packetClass.getSimpleName()); + var baseInfo = new PacketClassInfo(packetId, externalName, flow.sender().name(), flow.name()); + var fieldScanner = new PacketClassFieldScanner(packetClass, baseInfo); + fieldScanner.scanAndRegisterClassFields(); + + // register the packet + var packetInfos = result.get(state.displayName(), flow.name()); + if (packetInfos == null) { + packetInfos = new LinkedList<>(); + result.put(state.displayName(), flow.name(), packetInfos); + } + packetInfos.add(baseInfo); + } + } } } - // return the created table - return packetClassInfos; + return result; + } + + /* example: ClientboundSetSimulationDistancePacket -> Set Simulation Distance */ + private @NonNull String externalizePacketClassName(@NonNull McPacketFlow flow, @NonNull String packetName) { + // remove the flow name from the packet class name + var lowerName = packetName.toLowerCase(Locale.ROOT); + var lowerFlowName = flow.name().toLowerCase(Locale.ROOT); + if (lowerName.startsWith(lowerFlowName)) { + packetName = packetName.substring(lowerFlowName.length()); + } + + // remove the packet suffix + if (lowerName.endsWith("packet")) { + packetName = packetName.substring(0, packetName.length() - 6); + } + + // insert space before each upper-camel char + var splitAtCamel = packetName.split("(?=\\p{Lu})"); + return String.join(" ", splitAtCamel); } } diff --git a/src/main/java/dev/derklaro/protocolgenerator/remap/JarRemapper.java b/src/main/java/dev/derklaro/protocolgenerator/remap/JarRemapper.java index 58c654e..38a7baf 100644 --- a/src/main/java/dev/derklaro/protocolgenerator/remap/JarRemapper.java +++ b/src/main/java/dev/derklaro/protocolgenerator/remap/JarRemapper.java @@ -24,18 +24,22 @@ package dev.derklaro.protocolgenerator.remap; -import cuchaz.enigma.Enigma; -import cuchaz.enigma.ProgressListener; -import cuchaz.enigma.classprovider.ClasspathClassProvider; -import cuchaz.enigma.translation.mapping.serde.MappingFormat; -import cuchaz.enigma.translation.mapping.serde.MappingParseException; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Consumer; import lombok.NonNull; +import net.minecraftforge.fart.api.IdentifierFixerConfig; +import net.minecraftforge.fart.api.Renamer; +import net.minecraftforge.fart.api.SignatureStripperConfig; +import net.minecraftforge.fart.api.SourceFixerConfig; +import net.minecraftforge.fart.api.Transformer; +import net.minecraftforge.srgutils.IMappingFile; public final class JarRemapper { - private static final ProgressListener PROGRESS_LISTENER = ProgressListener.none(); // todo maybe add a real progress listener? + private static final Consumer NO_OP_LOGGER = __ -> { + }; private final Path inputJarFile; private final Path mappingsPath; @@ -45,18 +49,20 @@ public JarRemapper(@NonNull Path inputJarFile, @NonNull Path mappingsPath) { this.mappingsPath = mappingsPath; } - public void remap(@NonNull Path outputPath) throws IOException, MappingParseException { - // read the jar file - var enigma = Enigma.create(); - var project = enigma.openJar(this.inputJarFile, new ClasspathClassProvider(), PROGRESS_LISTENER); - - // read and set the mappings - var saveParameters = enigma.getProfile().getMappingSaveParameters(); - var mappingTree = MappingFormat.PROGUARD.read(this.mappingsPath, PROGRESS_LISTENER, saveParameters); - project.setMappings(mappingTree); - - // export the jar file - var jarExport = project.exportRemappedJar(PROGRESS_LISTENER); - jarExport.write(outputPath, PROGRESS_LISTENER); + public void remap(@NonNull Path outputPath) throws IOException { + try (var mappingsStream = Files.newInputStream(this.mappingsPath)) { + var mappings = IMappingFile.load(mappingsStream).reverse(); + var renamerBuilder = Renamer.builder() + .logger(NO_OP_LOGGER) + .add(Transformer.renamerFactory(mappings, false)) + .add(Transformer.parameterAnnotationFixerFactory()) + .add(Transformer.recordFixerFactory()) + .add(Transformer.identifierFixerFactory(IdentifierFixerConfig.ALL)) + .add(Transformer.sourceFixerFactory(SourceFixerConfig.JAVA)) + .add(Transformer.signatureStripperFactory(SignatureStripperConfig.ALL)); + try (var renamer = renamerBuilder.build()) { + renamer.run(this.inputJarFile.toFile(), outputPath.toFile()); + } + } } }