From 968c01f1851019829edfc782310fa63e04e31f60 Mon Sep 17 00:00:00 2001 From: Frederik van der Els Date: Mon, 5 Feb 2024 16:57:35 +0100 Subject: [PATCH] Add the clisten command --- build.gradle | 2 + .../clientcommands/ClientCommands.java | 4 + .../earthcomputer/clientcommands/Configs.java | 8 + .../clientcommands/MappingsHelper.java | 226 ++++++ .../clientcommands/PacketDumper.java | 701 ++++++++++++++++++ .../clientcommands/ReflectionUtils.java | 19 + .../clientcommands/UnsafeUtils.java | 34 + .../clientcommands/command/ListenCommand.java | 267 +++++++ .../MojmapPacketClassArgumentType.java | 68 ++ .../mixin/MixinClientConnection.java | 34 + .../assets/clientcommands/lang/en_us.json | 11 + src/main/resources/mixins.clientcommands.json | 1 + 12 files changed, 1375 insertions(+) create mode 100644 src/main/java/net/earthcomputer/clientcommands/MappingsHelper.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/PacketDumper.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/ReflectionUtils.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/UnsafeUtils.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/command/ListenCommand.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/command/arguments/MojmapPacketClassArgumentType.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/mixin/MixinClientConnection.java diff --git a/build.gradle b/build.gradle index e8569a04c..1d826d41b 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,8 @@ dependencies { modRuntimeOnly('me.djtheredstoner:DevAuth-fabric:1.1.0') { exclude group: 'net.fabricmc', module: 'fabric-loader' } + + include api('net.fabricmc:mapping-io:0.5.1') } jar { diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index e6e5fb338..a62f10c98 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -14,6 +14,7 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.MinecraftVersion; import net.minecraft.client.MinecraftClient; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.network.PacketByteBuf; @@ -85,6 +86,8 @@ public void onInitializeClient() { new ModConfigBuilder("clientcommands", Configs.class).build(); ItemGroupCommand.registerItemGroups(); + + MappingsHelper.initMappings(MinecraftVersion.CURRENT.getName()); } private static Set getCommands(CommandDispatcher dispatcher) { @@ -160,6 +163,7 @@ public static void registerCommands(CommandDispatcher CrackRNGCommand.register(dispatcher); WeatherCommand.register(dispatcher); PluginsCommand.register(dispatcher); + ListenCommand.register(dispatcher); Calendar calendar = Calendar.getInstance(); boolean registerChatCommand = calendar.get(Calendar.MONTH) == Calendar.APRIL && calendar.get(Calendar.DAY_OF_MONTH) == 1; diff --git a/src/main/java/net/earthcomputer/clientcommands/Configs.java b/src/main/java/net/earthcomputer/clientcommands/Configs.java index da095dda8..8fde47958 100644 --- a/src/main/java/net/earthcomputer/clientcommands/Configs.java +++ b/src/main/java/net/earthcomputer/clientcommands/Configs.java @@ -113,4 +113,12 @@ public static void setMaxChorusItemThrows(int maxChorusItemThrows) { public static boolean conditionLessThan1_20() { return MultiVersionCompat.INSTANCE.getProtocolVersion() < MultiVersionCompat.V1_20; } + + @Config + public static PacketDumpMethod packetDumpMethod = PacketDumpMethod.REFLECTION; + + public enum PacketDumpMethod { + REFLECTION, + BYTE_BUF, + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/MappingsHelper.java b/src/main/java/net/earthcomputer/clientcommands/MappingsHelper.java new file mode 100644 index 000000000..7dcabd89d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/MappingsHelper.java @@ -0,0 +1,226 @@ +package net.earthcomputer.clientcommands; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collection; +import java.util.Optional; + +public class MappingsHelper { + + private static final MemoryMappingTree mojmapOfficial = new MemoryMappingTree(); + private static final int SRC_OFFICIAL = 0; + private static final int DEST_OFFICIAL = 0; + + private static final MemoryMappingTree officialIntermediaryNamed = new MemoryMappingTree(); + private static final int SRC_INTERMEDIARY = 0; + private static final int DEST_INTERMEDIARY = 0; + private static final int SRC_NAMED = 1; + private static final int DEST_NAMED = 1; + + private static final boolean IS_DEV_ENV = FabricLoader.getInstance().isDevelopmentEnvironment(); + + private static final HttpClient httpClient = HttpClient.newHttpClient(); + + public static void initMappings(String version) { + HttpRequest versionsRequest = HttpRequest.newBuilder() + .uri(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")) + .GET() + .timeout(Duration.ofSeconds(5)) + .build(); + httpClient.sendAsync(versionsRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(HttpResponse::body) + .thenAccept(versionsBody -> { + JsonObject versionsJson = JsonParser.parseString(versionsBody).getAsJsonObject(); + String versionUrl = versionsJson.getAsJsonArray("versions").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(v -> v.get("id").getAsString().equals(version)) + .map(v -> v.get("url").getAsString()) + .findAny().orElseThrow(); + + HttpRequest versionRequest = HttpRequest.newBuilder() + .uri(URI.create(versionUrl)) + .GET() + .timeout(Duration.ofSeconds(5)) + .build(); + httpClient.sendAsync(versionRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(HttpResponse::body) + .thenAccept(versionBody -> { + JsonObject versionJson = JsonParser.parseString(versionBody).getAsJsonObject(); + String mappingsUrl = versionJson + .getAsJsonObject("downloads") + .getAsJsonObject("client_mappings") + .get("url").getAsString(); + + HttpRequest mappingsRequest = HttpRequest.newBuilder() + .uri(URI.create(mappingsUrl)) + .GET() + .timeout(Duration.ofSeconds(5)) + .build(); + httpClient.sendAsync(mappingsRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(HttpResponse::body) + .thenApply(StringReader::new) + .thenAccept(reader -> { + try { + MappingReader.read(reader, MappingFormat.PROGUARD_FILE, mojmapOfficial); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + }); + }); + + try (InputStream stream = FabricLoader.class.getClassLoader().getResourceAsStream("mappings/mappings.tiny")) { + if (stream == null) { + throw new IOException("Could not find mappings.tiny"); + } + MappingReader.read(new InputStreamReader(stream), IS_DEV_ENV ? MappingFormat.TINY_2_FILE : MappingFormat.TINY_FILE, officialIntermediaryNamed); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Collection mojmapClasses() { + return mojmapOfficial.getClasses(); + } + + public static Optional mojmapToOfficial_class(String mojmapClass) { + MappingTree.ClassMapping officialClass = mojmapOfficial.getClass(mojmapClass); + if (officialClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(officialClass.getDstName(DEST_OFFICIAL)); + } + + public static Optional officialToMojmap_class(String officialClass) { + MappingTree.ClassMapping mojmapClass = mojmapOfficial.getClass(officialClass, SRC_OFFICIAL); + if (mojmapClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapClass.getSrcName()); + } + + public static Optional mojmapToNamed_class(String mojmapClass) { + Optional officialClass = mojmapToOfficial_class(mojmapClass); + if (officialClass.isEmpty()) { + return Optional.empty(); + } + MappingTree.ClassMapping namedClass = officialIntermediaryNamed.getClass(officialClass.get()); + if (namedClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(namedClass.getDstName(DEST_NAMED)); + } + + public static Optional namedToMojmap_class(String namedClass) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(namedClass, SRC_NAMED); + if (officialClass == null) { + return Optional.empty(); + } + MappingTree.ClassMapping mojmapClass = mojmapOfficial.getClass(officialClass.getSrcName(), SRC_OFFICIAL); + if (mojmapClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapClass.getSrcName()); + } + + public static Optional mojmapToIntermediary_class(String mojmapClass) { + Optional officialClass = mojmapToOfficial_class(mojmapClass); + if (officialClass.isEmpty()) { + return Optional.empty(); + } + MappingTree.ClassMapping intermediaryClass = officialIntermediaryNamed.getClass(officialClass.get()); + if (intermediaryClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(intermediaryClass.getDstName(DEST_INTERMEDIARY)); + } + + public static Optional intermediaryToMojmap_class(String intermediaryClass) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(intermediaryClass, SRC_INTERMEDIARY); + if (officialClass == null) { + return Optional.empty(); + } + MappingTree.ClassMapping mojmapClass = mojmapOfficial.getClass(officialClass.getSrcName(), SRC_OFFICIAL); + if (mojmapClass == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapClass.getSrcName()); + } + + public static Optional namedOrIntermediaryToMojmap_class(String namedOrIntermediaryClass) { + if (IS_DEV_ENV) { + return MappingsHelper.namedToMojmap_class(namedOrIntermediaryClass); + } + return MappingsHelper.intermediaryToMojmap_class(namedOrIntermediaryClass); + } + + public static Optional mojmapToNamedOrIntermediary_class(String mojmapClass) { + if (IS_DEV_ENV) { + return MappingsHelper.mojmapToNamed_class(mojmapClass); + } + return MappingsHelper.mojmapToIntermediary_class(mojmapClass); + } + + public static Optional officialToMojmap_field(String officialClass, String officialField) { + MappingTree.FieldMapping mojmapField = mojmapOfficial.getField(officialClass, officialField, null); + if (mojmapField == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapField.getSrcName()); + } + + public static Optional namedToMojmap_field(String namedClass, String namedField) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(namedClass, SRC_NAMED); + if (officialClass == null) { + return Optional.empty(); + } + MappingTree.FieldMapping officialField = officialIntermediaryNamed.getField(namedClass, namedField, null, SRC_NAMED); + if (officialField == null) { + return Optional.empty(); + } + MappingTree.FieldMapping mojmapField = mojmapOfficial.getField(officialClass.getSrcName(), officialField.getSrcName(), null, SRC_OFFICIAL); + if (mojmapField == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapField.getSrcName()); + } + + public static Optional intermediaryToMojmap_field(String intermediaryClass, String intermediaryField) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(intermediaryClass, SRC_INTERMEDIARY); + if (officialClass == null) { + return Optional.empty(); + } + MappingTree.FieldMapping officialField = officialIntermediaryNamed.getField(intermediaryClass, intermediaryField, null, SRC_INTERMEDIARY); + if (officialField == null) { + return Optional.empty(); + } + MappingTree.FieldMapping mojmapField = mojmapOfficial.getField(officialClass.getSrcName(), officialField.getSrcName(), null, SRC_OFFICIAL); + if (mojmapField == null) { + return Optional.empty(); + } + return Optional.ofNullable(mojmapField.getSrcName()); + } + + public static Optional namedOrIntermediaryToMojmap_field(String namedOrIntermediaryClass, String namedOrIntermediaryField) { + if (IS_DEV_ENV) { + return namedToMojmap_field(namedOrIntermediaryClass, namedOrIntermediaryField); + } + return intermediaryToMojmap_field(namedOrIntermediaryClass, namedOrIntermediaryField); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/PacketDumper.java b/src/main/java/net/earthcomputer/clientcommands/PacketDumper.java new file mode 100644 index 000000000..bd39eaca1 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/PacketDumper.java @@ -0,0 +1,701 @@ +package net.earthcomputer.clientcommands; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonWriter; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.authlib.yggdrasil.response.ProfileSearchResultsResponse; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import com.mojang.util.ByteBufferTypeAdapter; +import com.mojang.util.InstantTypeAdapter; +import com.mojang.util.UUIDTypeAdapter; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.EncoderException; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtElement; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.Packet; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.collection.IndexedIterable; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.*; +import org.apache.commons.io.function.IOBiConsumer; +import org.apache.commons.io.function.IORunnable; +import org.apache.commons.io.function.IOStream; +import org.apache.commons.io.function.Uncheck; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.charset.Charset; +import java.security.PublicKey; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; +import java.util.function.ToIntFunction; + +/** + * @author Gaming32 + */ +public class PacketDumper { + public static void dumpPacket(Packet packet, JsonWriter writer) throws IOException { + writer.beginArray(); + try { + packet.write(new PacketDumpByteBuf(writer)); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + writer.endArray(); + } + + public static class PacketDumpByteBuf extends PacketByteBuf { + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) + .registerTypeHierarchyAdapter(ByteBuffer.class, new ByteBufferTypeAdapter().nullSafe()) + .registerTypeAdapter(GameProfile.class, new GameProfile.Serializer()) + .registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()) + .registerTypeAdapter(ProfileSearchResultsResponse.class, new ProfileSearchResultsResponse.Serializer()) + .create(); + private static final DateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + + private final JsonWriter writer; + + public PacketDumpByteBuf(JsonWriter writer) { + super(Unpooled.buffer(0, 0)); + this.writer = writer; + } + + @Override + @SuppressWarnings("deprecation") + public PacketDumpByteBuf encode(DynamicOps ops, Codec codec, T value) { + return dump("withCodec", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + writer.name("encodedNbt").value(Util.getResult( + codec.encodeStart(ops, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ).toString()); + writer.name("encodedJson"); + GSON.toJson(Util.getResult( + codec.encodeStart(JsonOps.INSTANCE, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ), writer); + }); + } + + @Override + public void encodeAsJson(Codec codec, T value) { + dump("jsonWithCodec", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + writer.name("encodedJson"); + GSON.toJson(Util.getResult( + codec.encodeStart(JsonOps.INSTANCE, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ), writer); + }); + } + + @Override + public void writeRegistryValue(IndexedIterable indexedIterable, T value) { + dump("id", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + if (indexedIterable instanceof Registry registry) { + writer.name("registry").value(registry.getKey().getValue().toString()); + writer.name("valueKey").value(Objects.toString(registry.getId(value))); + } + writer.name("id").value(indexedIterable.getRawId(value)); + }); + } + + @Override + public void writeRegistryEntry(IndexedIterable> idMap, RegistryEntry value, PacketWriter directWriter) { + dump("idHolder", () -> { + writer.name("kind").value(value.getType().name()); + value.getKeyOrValue().ifLeft(key -> Uncheck.run(() -> { + writer.name("referenceKey").value(key.getValue().toString()); + writer.name("id").value(idMap.getRawId(value)); + })).ifRight(directValue -> Uncheck.run(() -> { + writer.name("directValue"); + dumpValue(directValue, directWriter); + })); + }); + } + + @Override + public void writeCollection(Collection collection, PacketWriter elementWriter) { + dump("collection", () -> { + writer.name("size").value(collection.size()); + writer.name("elements").beginArray(); + for (T element : collection) { + dumpValue(element, elementWriter); + } + writer.endArray(); + }); + } + + @Override + public void writeIntList(IntList intIdList) { + dump("intIdList", () -> { + writer.name("size").value(intIdList.size()); + writer.name("elements").beginArray(); + for (int value : intIdList) { + writer.value(value); + } + writer.endArray(); + }); + } + + @Override + public void writeMap(Map map, PacketWriter keyWriter, PacketWriter valueWriter) { + dump("map", () -> { + writer.name("size").value(map.size()); + writer.name("elements").beginArray(); + for (var entry : map.entrySet()) { + writer.beginObject(); + writer.name("key"); + dumpValue(entry.getKey(), keyWriter); + writer.name("value"); + dumpValue(entry.getValue(), valueWriter); + writer.endObject(); + } + writer.endArray(); + }); + } + + @Override + public > void writeEnumSet(EnumSet enumSet, Class enumClass) { + dump("enumSet", () -> { + writer.name("enumClass").value(MappingsHelper.namedOrIntermediaryToMojmap_class(enumClass.getName().replace('.', '/')).orElseThrow()); + writer.name("size").value(enumSet.size()); + writer.name("elements").beginArray(); + for (E element : enumSet) { + writer.value(element.name()); + } + writer.endArray(); + }); + } + + @Override + public void writeOptional(Optional optional, PacketWriter valueWriter) { + writeNullable("optional", optional.orElse(null), valueWriter); + } + + @Override + public void writeNullable(@Nullable T value, PacketWriter writer) { + writeNullable("nullable", value, writer); + } + + private void writeNullable(String type, T value, PacketWriter valueWriter) { + dump(type, () -> { + writer.name("present"); + if (value != null) { + writer.value(true); + writer.name("value"); + dumpValue(value, valueWriter); + } else { + writer.value(false); + } + }); + } + + @Override + public void writeEither(Either value, PacketWriter leftWriter, PacketWriter rightWriter) { + dump("either", () -> { + writer.name("either"); + value.ifLeft(left -> Uncheck.run(() -> { + writer.value("left"); + writer.name("value"); + dumpValue(left, leftWriter); + })).ifRight(right -> Uncheck.run(() -> { + writer.value("right"); + writer.name("value"); + dumpValue(right, rightWriter); + })); + }); + } + + @Override + public PacketDumpByteBuf writeByteArray(byte[] array) { + return dump("byteArray", () -> writer + .name("length").value(array.length) + .name("value").value(Base64.getEncoder().encodeToString(array)) + ); + } + + @Override + public PacketDumpByteBuf writeIntArray(int[] array) { + return dump("varIntArray", () -> { + writer.name("length").value(array.length); + writer.name("elements").beginArray(); + for (int element : array) { + writer.value(element); + } + writer.endArray(); + }); + } + + @Override + public PacketDumpByteBuf writeLongArray(long[] array) { + return dump("longArray", () -> { + writer.name("length").value(array.length); + writer.name("elements").beginArray(); + for (long element : array) { + writer.value(element); + } + writer.endArray(); + }); + } + + @Override + public PacketDumpByteBuf writeBlockPos(BlockPos pos) { + return dump("blockPos", () -> writer + .name("x").value(pos.getX()) + .name("y").value(pos.getY()) + .name("z").value(pos.getZ()) + ); + } + + @Override + public PacketDumpByteBuf writeChunkPos(ChunkPos chunkPos) { + return dump("chunkPos", () -> writer + .name("x").value(chunkPos.x) + .name("z").value(chunkPos.z) + ); + } + + @Override + public PacketDumpByteBuf writeChunkSectionPos(ChunkSectionPos chunkSectionPos) { + return dump("sectionPos", () -> writer + .name("x").value(chunkSectionPos.getSectionX()) + .name("y").value(chunkSectionPos.getSectionY()) + .name("z").value(chunkSectionPos.getSectionZ()) + ); + } + + @Override + public void writeGlobalPos(GlobalPos pos) { + dump("globalPos", () -> writer + .name("level").value(pos.getDimension().getValue().toString()) + .name("x").value(pos.getPos().getX()) + .name("y").value(pos.getPos().getY()) + .name("z").value(pos.getPos().getZ()) + ); + } + + @Override + public void writeVector3f(Vector3f vector3f) { + dump("vector3f", () -> writer + .name("x").value(vector3f.x) + .name("y").value(vector3f.y) + .name("z").value(vector3f.z) + ); + } + + @Override + public void writeQuaternionf(Quaternionf quaternion) { + dump("quaternion", () -> writer + .name("x").value(quaternion.x) + .name("y").value(quaternion.y) + .name("z").value(quaternion.z) + .name("w").value(quaternion.w) + ); + } + + @Override + public void writeVec3d(Vec3d vec3) { + dump("vec3", () -> writer + .name("x").value(vec3.x) + .name("y").value(vec3.y) + .name("z").value(vec3.z) + ); + } + + @Override + public PacketDumpByteBuf writeText(Text text) { + return dump("component", () -> { + writer.name("value"); + GSON.toJson(Text.Serialization.toJsonTree(text), writer); + }); + } + + @Override + public PacketDumpByteBuf writeEnumConstant(Enum value) { + return dump("enum", () -> writer + .name("enum").value(value.getDeclaringClass().getName()) + .name("value").value(value.name()) + ); + } + + @Override + public PacketDumpByteBuf encode(ToIntFunction idGetter, T value) { + return dump("byId", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + writer.name("id").value(idGetter.applyAsInt(value)); + }); + } + + @Override + public PacketDumpByteBuf writeUuid(UUID uuid) { + return dumpAsString("uuid", uuid); + } + + @Override + public PacketDumpByteBuf writeVarInt(int input) { + return dumpSimple("varInt", input, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeVarLong(long value) { + return dumpSimple("varLong", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeNbt(@Nullable NbtElement NbtElement) { + return dumpAsString("nbt", NbtElement); + } + + @Override + public PacketDumpByteBuf writeItemStack(ItemStack stack) { + return dump("item", () -> writer + .name("item").value(stack.getRegistryEntry().getKey().map(k -> k.getValue().toString()).orElse(null)) + .name("count").value(stack.getCount()) + .name("tag").value(Objects.toString(stack.getNbt())) + ); + } + + @Override + public PacketByteBuf writeString(String string) { + return dump("utf", () -> writer + .name("value").value(string) + ); + } + + @Override + public PacketDumpByteBuf writeString(String string, int maxLength) { + return dump("utf", () -> writer + .name("maxLength").value(maxLength) + .name("value").value(string) + ); + } + + @Override + public PacketDumpByteBuf writeIdentifier(Identifier identifier) { + return dumpAsString("resourceLocation", identifier); + } + + @Override + public void writeRegistryKey(RegistryKey registryKey) { + dump("resourceKey", () -> writer + .name("registry").value(registryKey.getRegistry().toString()) + .name("location").value(registryKey.getValue().toString()) + ); + } + + @Override + public PacketDumpByteBuf writeDate(Date time) { + return dumpSimple("date", ISO_8601.format(time), JsonWriter::value); + } + + @Override + public void writeInstant(Instant instant) { + dumpAsString("instant", instant); + } + + @Override + public PacketDumpByteBuf writePublicKey(PublicKey publicKey) { + return dump("publicKey", () -> writer + .name("encoded").value(Base64.getEncoder().encodeToString(publicKey.getEncoded())) + ); + } + + @Override + public void writeBlockHitResult(BlockHitResult result) { + dump("blockHitResult", () -> writer + .name("pos").beginObject() + .name("x").value(result.getBlockPos().getX()) + .name("y").value(result.getBlockPos().getY()) + .name("z").value(result.getBlockPos().getZ()).endObject() + .name("direction").value(result.getSide().getName()) + .name("offset").beginObject() + .name("x").value(result.getPos().x - result.getBlockPos().getX()) + .name("y").value(result.getPos().y - result.getBlockPos().getY()) + .name("z").value(result.getPos().z - result.getBlockPos().getZ()).endObject() + .name("isInside").value(result.isInsideBlock()) + ); + } + + @Override + public void writeBitSet(BitSet bitSet) { + dump("bitSet", () -> { + writer.name("bits").beginArray(); + IOStream.adapt(bitSet.stream().boxed()).forEach(writer::value); + writer.endArray(); + }); + } + + @Override + public void writeBitSet(BitSet bitSet, int size) { + dump("fixedBitSet", () -> { + writer.name("size").value(size); + writer.name("bits").beginArray(); + IOStream.adapt(bitSet.stream().boxed()).forEach(writer::value); + writer.endArray(); + }); + } + + @Override + public void writeGameProfile(GameProfile gameProfile) { + dump("gameProfile", () -> { + writer.name("value"); + GSON.toJson(gameProfile, GameProfile.class, writer); + }); + } + + @Override + public void writePropertyMap(PropertyMap gameProfileProperties) { + dump("gameProfileProperties", () -> { + writer.name("value"); + GSON.toJson(gameProfileProperties, PropertyMap.class, writer); + }); + } + + @Override + public void writeProperty(Property property) { + dump("property", () -> { + writer.name("name").value(property.name()); + writer.name("value").value(property.value()); + if (property.hasSignature()) { + writer.name("signature").value(property.signature()); + } + }); + } + + @Override + public PacketDumpByteBuf skipBytes(int length) { + return dump("skipBytes", () -> writer.name("length").value(length)); + } + + @Override + public PacketDumpByteBuf writeBoolean(boolean value) { + return dumpSimple("boolean", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeByte(int value) { + return dumpSimple("byte", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeShort(int value) { + return dumpSimple("short", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeShortLE(int value) { + return dumpSimple("shortLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeMedium(int value) { + return dumpSimple("medium", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeMediumLE(int value) { + return dumpSimple("mediumLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeInt(int value) { + return dumpSimple("int", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeIntLE(int value) { + return dumpSimple("intLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeLong(long value) { + return dumpSimple("long", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeLongLE(long value) { + return dumpSimple("longLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeChar(int value) { + return dumpSimple("char", Character.toString((char) value), JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeFloat(float value) { + return dumpSimple("float", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeFloatLE(float value) { + return dumpSimple("floatLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeDouble(double value) { + return dumpSimple("double", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeDoubleLE(double value) { + return dumpSimple("doubleLE", value, JsonWriter::value); + } + + @Override + public PacketDumpByteBuf writeBytes(ByteBuf source) { + return writeBytes(source, source.readableBytes()); + } + + @Override + public PacketDumpByteBuf writeBytes(ByteBuf source, int length) { + byte[] bytes = new byte[length]; + source.readBytes(bytes); + return dumpBytes(bytes); + } + + @Override + public PacketDumpByteBuf writeBytes(ByteBuf source, int sourceIndex, int length) { + byte[] bytes = new byte[length]; + source.getBytes(sourceIndex, bytes); + return dumpBytes(bytes); + } + + @Override + public PacketDumpByteBuf writeBytes(byte[] source) { + return dumpBytes(source); + } + + @Override + public PacketDumpByteBuf writeBytes(byte[] source, int sourceIndex, int length) { + return dumpBytes(Arrays.copyOfRange(source, sourceIndex, sourceIndex + length)); + } + + @Override + public PacketDumpByteBuf writeBytes(ByteBuffer source) { + byte[] bytes = new byte[source.remaining()]; + source.get(bytes); + return dumpBytes(bytes); + } + + @Override + public int writeBytes(InputStream inputStream, int i) throws IOException { + byte[] bytes = new byte[i]; + int read = inputStream.read(bytes); + dumpBytes(Arrays.copyOf(bytes, i)); + return read; + } + + @Override + public int writeBytes(ScatteringByteChannel scatteringByteChannel, int i) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(i); + int read = scatteringByteChannel.read(buffer); + buffer.flip(); + dumpBytes(Arrays.copyOfRange( + buffer.array(), + buffer.arrayOffset() + buffer.position(), + buffer.arrayOffset() + buffer.limit() + )); + return read; + } + + @Override + public int writeBytes(FileChannel fileChannel, long l, int i) throws IOException { + return writeBytes(fileChannel.position(l), i); + } + + private PacketDumpByteBuf dumpBytes(byte[] bytes) { + return dump("bytes", () -> writer + .name("length").value(bytes.length) + .name("value").value(Base64.getEncoder().encodeToString(bytes)) + ); + } + + @Override + public PacketDumpByteBuf writeZero(int length) { + return dump("zero", () -> writer.name("length").value(length)); + } + + @Override + public int writeCharSequence(CharSequence charSequence, Charset charset) { + String string = charSequence.toString(); + byte[] encoded = string.getBytes(charset); + dump("charSequence", () -> writer + .name("charset").value(charset.name()) + .name("value").value(string) + .name("encoded").value(Base64.getEncoder().encodeToString(encoded)) + ); + return encoded.length; + } + + private void dumpValueClass(Object value) throws IOException { + writer.name("valueClass"); + if (value != null) { + writer.value(MappingsHelper.namedOrIntermediaryToMojmap_class(value.getClass().getName().replace('.', '/')).orElseThrow()); + } else { + writer.nullValue(); + } + } + + private void dumpValue(T value, PacketWriter valueWriter) throws IOException { + writer.beginObject(); + dumpValueClass(value); + writer.name("fields").beginArray(); + valueWriter.accept(this, value); + writer.endArray(); + writer.endObject(); + } + + private PacketDumpByteBuf dumpAsString(String type, Object value) { + return dumpSimple(type, value != null ? value.toString() : null, JsonWriter::value); + } + + private PacketDumpByteBuf dumpSimple(String type, T value, IOBiConsumer valueWriter) { + return dump(type, () -> { + writer.name("value"); + valueWriter.accept(writer, value); + }); + } + + private PacketDumpByteBuf dump(String type, IORunnable dumper) { + Uncheck.run(() -> { + writer.beginObject(); + writer.name("type").value(type); + dumper.run(); + writer.endObject(); + }); + return this; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/ReflectionUtils.java b/src/main/java/net/earthcomputer/clientcommands/ReflectionUtils.java new file mode 100644 index 000000000..a7dfe86f7 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/ReflectionUtils.java @@ -0,0 +1,19 @@ +package net.earthcomputer.clientcommands; + +import java.lang.reflect.Field; +import java.util.stream.Stream; + +public class ReflectionUtils { + public static Stream getAllFields(Class clazz) { + Stream.Builder builder = Stream.builder(); + Class targetClass = clazz; + while (targetClass.getSuperclass() != null) { + Field[] fields = targetClass.getDeclaredFields(); + for (Field field : fields) { + builder.add(field); + } + targetClass = targetClass.getSuperclass(); + } + return builder.build(); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/UnsafeUtils.java b/src/main/java/net/earthcomputer/clientcommands/UnsafeUtils.java new file mode 100644 index 000000000..18b3c8d2a --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/UnsafeUtils.java @@ -0,0 +1,34 @@ +package net.earthcomputer.clientcommands; + +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +/** + * @author Gaming32 + */ +public class UnsafeUtils { + private static final Unsafe UNSAFE; + private static final MethodHandles.Lookup IMPL_LOOKUP; + + static { + try { + final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + UNSAFE = (Unsafe) unsafeField.get(null); + final Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + IMPL_LOOKUP = (MethodHandles.Lookup) UNSAFE.getObject(UNSAFE.staticFieldBase(implLookupField), UNSAFE.staticFieldOffset(implLookupField)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static Unsafe getUnsafe() { + return UNSAFE; + } + + public static MethodHandles.Lookup getImplLookup() { + return IMPL_LOOKUP; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ListenCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/ListenCommand.java new file mode 100644 index 000000000..3969bb1b1 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/ListenCommand.java @@ -0,0 +1,267 @@ +package net.earthcomputer.clientcommands.command; + +import com.google.gson.stream.JsonWriter; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; +import net.earthcomputer.clientcommands.*; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.network.NetworkSide; +import net.minecraft.network.packet.Packet; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.ChunkPos; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Array; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Modifier; +import java.time.Instant; +import java.util.*; + +import static net.earthcomputer.clientcommands.command.arguments.MojmapPacketClassArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class ListenCommand { + + private static final SimpleCommandExceptionType ALREADY_LISTENING_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.clisten.add.failed")); + private static final SimpleCommandExceptionType NOT_LISTENING_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.clisten.remove.failed")); + + private static final Set>> packets = new HashSet<>(); + + private static PacketCallback callback; + + private static final ThreadLocal> SEEN = ThreadLocal.withInitial(ReferenceOpenHashSet::new); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("clisten") + .then(literal("add") + .then(argument("packet", packet()) + .executes(ctx -> add(ctx.getSource(), getPacket(ctx, "packet"))))) + .then(literal("remove") + .then(argument("packet", packet()) + .executes(ctx -> remove(ctx.getSource(), getPacket(ctx, "packet"))))) + .then(literal("list") + .executes(ctx -> list(ctx.getSource()))) + .then(literal("clear") + .executes(ctx -> clear(ctx.getSource())))); + } + + private static int add(FabricClientCommandSource source, Class> packetClass) throws CommandSyntaxException { + if (!packets.add(packetClass)) { + throw ALREADY_LISTENING_EXCEPTION.create(); + } + + source.sendFeedback(Text.translatable("commands.clisten.add.success")); + + if (callback == null) { + callback = (packet, side) -> { + String packetClassName = packet.getClass().getName().replace('.', '/'); + Optional mojmapPacketName = MappingsHelper.namedOrIntermediaryToMojmap_class(packetClassName); + + String packetData; + Text packetDataPreview; + if (Configs.packetDumpMethod == Configs.PacketDumpMethod.BYTE_BUF) { + StringWriter writer = new StringWriter(); + try { + PacketDumper.dumpPacket(packet, new JsonWriter(writer)); + } catch (IOException e) { + e.printStackTrace(); + return; + } + packetData = writer.toString(); + packetDataPreview = Text.literal(packetData.replace("\u00a7", "\\u00a7")); + } else { + try { + packetDataPreview = serialize(packet); + packetData = packetDataPreview.getString(); + } catch (StackOverflowError e) { + e.printStackTrace(); + return; + } + } + + MutableText packetText = Text.literal(mojmapPacketName.orElseThrow().substring(MOJMAP_PACKET_PREFIX.length())).styled(s -> s + .withUnderline(true) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, packetDataPreview)) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, packetData))); + + switch (side) { + case CLIENTBOUND -> source.sendFeedback(Text.translatable("commands.clisten.receivedPacket", packetText)); + case SERVERBOUND -> source.sendFeedback(Text.translatable("commands.clisten.sentPacket", packetText)); + } + }; + } + + return Command.SINGLE_SUCCESS; + } + + private static int remove(FabricClientCommandSource source, Class> packetClass) throws CommandSyntaxException { + if (!packets.remove(packetClass)) { + throw NOT_LISTENING_EXCEPTION.create(); + } + + source.sendFeedback(Text.translatable("commands.clisten.remove.success")); + return Command.SINGLE_SUCCESS; + } + + private static int list(FabricClientCommandSource source) { + int amount = packets.size(); + if (amount == 0) { + source.sendFeedback(Text.translatable("commands.clisten.list.none")); + } else { + source.sendFeedback(Text.translatable("commands.clisten.list")); + packets.forEach(packetClass -> { + String packetClassName = packetClass.getName().replace('.', '/'); + Optional mojmapName = MappingsHelper.namedOrIntermediaryToMojmap_class(packetClassName); + source.sendFeedback(Text.literal(mojmapName.orElseThrow().substring(MOJMAP_PACKET_PREFIX.length()))); + }); + } + + return amount; + } + + private static int clear(FabricClientCommandSource source) { + int amount = packets.size(); + packets.clear(); + source.sendFeedback(Text.translatable("commands.clisten.clear")); + return amount; + } + + private static Text serialize(Object object) { + try { + if (SEEN.get().add(object)) { + return serializeInner(object); + } + return Text.empty(); + } finally { + SEEN.get().remove(object); + if (SEEN.get().isEmpty()) { + SEEN.remove(); + } + } + } + + private static Text serializeInner(Object object) { + if (object == null) { + return Text.literal("null"); + } + if (object instanceof Text text) { + return text; + } + if (object instanceof String string) { + return Text.literal(string); + } + if (object instanceof Number || object instanceof Boolean) { + return Text.literal(object.toString()); + } + if (object instanceof Optional optional) { + return optional.isPresent() ? serialize(optional.get()) : Text.literal("empty"); + } + if (object instanceof Date date) { + return Text.of(date); + } + if (object instanceof Instant instant) { + return Text.of(Date.from(instant)); + } + if (object instanceof UUID uuid) { + return Text.of(uuid); + } + if (object instanceof ChunkPos chunkPos) { + return Text.of(chunkPos); + } + if (object instanceof Identifier identifier) { + return Text.of(identifier); + } + if (object instanceof Message message) { + return Text.of(message); + } + if (object.getClass().isArray()) { + MutableText text = Text.literal("["); + int lengthMinusOne = Array.getLength(object) - 1; + if (lengthMinusOne < 0) { + return text.append("]"); + } + for (int i = 0; i < lengthMinusOne; i++) { + text.append(serialize(Array.get(object, i))).append(", "); + } + return text.append(serialize(Array.get(object, lengthMinusOne))).append("]"); + } + if (object instanceof Collection collection) { + MutableText text = Text.literal("["); + text.append(collection.stream().map(e -> serialize(e).copy()).reduce((l, r) -> l.append(", ").append(r)).orElse(Text.empty())); + return text.append("]"); + } + if (object instanceof Map map) { + MutableText text = Text.literal("{"); + text.append(map.entrySet().stream().map(e -> serialize(e.getKey()).copy().append("=").append(serialize(e.getValue()))).reduce((l, r) -> l.append(", ").append(r)).orElse(Text.empty())); + return text.append("}"); + } + if (object instanceof Registry registry) { + return Text.of(registry.getKey().getValue()); + } + if (object instanceof RegistryKey registryKey) { + MutableText text = Text.literal("{"); + text.append("registry=").append(serialize(registryKey.getRegistry())).append(", "); + text.append("value=").append(serialize(registryKey.getValue())); + return text.append("}"); + } + if (object instanceof RegistryEntry registryEntry) { + MutableText text = Text.literal("{"); + text.append("key=").append(serialize(registryEntry.getKey())).append(", "); + text.append("value=").append(serialize(registryEntry.value())); + return text.append("}"); + } + + String className = object.getClass().getName().replace(".", "/"); + String mojmapClassName = MappingsHelper.namedOrIntermediaryToMojmap_class(className).orElse(className); + mojmapClassName = mojmapClassName.substring(mojmapClassName.lastIndexOf('/') + 1); + + MutableText text = Text.literal(mojmapClassName + '{'); + text.append(ReflectionUtils.getAllFields(object.getClass()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .map(field -> { + String fieldName = field.getName(); + Optional mojmapFieldName = MappingsHelper.namedOrIntermediaryToMojmap_field(className, fieldName); + try { + field.setAccessible(true); + return Text.literal(mojmapFieldName.orElse(fieldName) + '=').append(serialize(field.get(object))); + } catch (InaccessibleObjectException | ReflectiveOperationException e) { + try { + VarHandle varHandle = UnsafeUtils.getImplLookup().findVarHandle(object.getClass(), fieldName, field.getType()); + return Text.literal(mojmapFieldName.orElse(fieldName) + '=').append(serialize(varHandle.get(object))); + } catch (ReflectiveOperationException ex) { + return Text.literal(mojmapFieldName.orElse(fieldName) + '=').append(Text.translatable("commands.clisten.packetError").formatted(Formatting.DARK_RED)); + } + } + }) + .reduce((l, r) -> l.append(", ").append(r)) + .orElse(Text.empty())); + return text.append("}"); + } + + public static void onPacket(Packet packet, NetworkSide side) { + if (!packets.contains(packet.getClass())) { + return; + } + callback.apply(packet, side); + } + + @FunctionalInterface + private interface PacketCallback { + void apply(Packet packet, NetworkSide side); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/MojmapPacketClassArgumentType.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/MojmapPacketClassArgumentType.java new file mode 100644 index 000000000..46fea57c7 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/MojmapPacketClassArgumentType.java @@ -0,0 +1,68 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.earthcomputer.clientcommands.MappingsHelper; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.minecraft.command.CommandSource; +import net.minecraft.network.packet.Packet; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class MojmapPacketClassArgumentType implements ArgumentType>> { + + private static final Collection EXAMPLES = Arrays.asList("ClientboundPlayerChatPacket", "ClientboundSystemChatMessage", "ServerboundContainerSlotStateChangedPacket"); + + public static final String MOJMAP_PACKET_PREFIX = "net/minecraft/network/protocol/game/"; + + private static final Set mojmapPackets = MappingsHelper.mojmapClasses().stream() + .map(MappingTreeView.ElementMappingView::getSrcName) + .filter(name -> name.startsWith(MOJMAP_PACKET_PREFIX) && name.endsWith("Packet")) + .map(name -> name.substring(MOJMAP_PACKET_PREFIX.length())) + .collect(Collectors.toSet()); + + public static MojmapPacketClassArgumentType packet() { + return new MojmapPacketClassArgumentType(); + } + + @SuppressWarnings("unchecked") + public static Class> getPacket(final CommandContext context, final String name) { + return (Class>) context.getArgument(name, Class.class); + } + + @Override + @SuppressWarnings("unchecked") + public Class> parse(StringReader reader) throws CommandSyntaxException { + String packet = MOJMAP_PACKET_PREFIX + reader.readString(); + Optional mojmapPacketName = MappingsHelper.mojmapToNamedOrIntermediary_class(packet); + if (mojmapPacketName.isEmpty()) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create(); + } + String packetClass = mojmapPacketName.get().replace('/', '.'); + try { + return (Class>) Class.forName(packetClass); + } catch (ReflectiveOperationException e) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create(); + } + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + return CommandSource.suggestMatching(mojmapPackets, builder); + } + + @Override + public Collection getExamples() { + return EXAMPLES; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/MixinClientConnection.java b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinClientConnection.java new file mode 100644 index 000000000..8cd3a04f0 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinClientConnection.java @@ -0,0 +1,34 @@ +package net.earthcomputer.clientcommands.mixin; + +import io.netty.channel.ChannelHandlerContext; +import net.earthcomputer.clientcommands.command.ListenCommand; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.NetworkSide; +import net.minecraft.network.PacketCallbacks; +import net.minecraft.network.packet.Packet; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientConnection.class) +public class MixinClientConnection { + @Shadow @Final private NetworkSide side; + + @Inject(method = "channelRead0(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/packet/Packet;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;handlePacket(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;)V")) + private void onPacketReceive(ChannelHandlerContext channelHandlerContext, Packet packet, CallbackInfo ci) { + if (this.side == NetworkSide.CLIENTBOUND) { + ListenCommand.onPacket(packet, NetworkSide.CLIENTBOUND); + } + } + + @Inject(method = "sendInternal", at = @At("HEAD")) + private void onPacketSend(Packet packet, @Nullable PacketCallbacks callbacks, boolean flush, CallbackInfo ci) { + if (this.side == NetworkSide.CLIENTBOUND) { + ListenCommand.onPacket(packet, NetworkSide.SERVERBOUND); + } + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 2366a582c..cca7244fe 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -137,6 +137,17 @@ "commands.ckit.list": "Available kits: %s", "commands.ckit.list.empty": "No available kits", + "commands.clisten.receivedPacket": "Received the following packet: %s", + "commands.clisten.sentPacket": "Sent the following packet: %s", + "commands.clisten.packetError": "ERROR", + "commands.clisten.add.success": "Successfully started listening to that packet", + "commands.clisten.add.failed": "Already listening to that packet", + "commands.clisten.remove.success": "No longer listening to that packet", + "commands.clisten.remove.failed": "Not listening to that packet", + "commands.clisten.list.none": "Not listening to any packets", + "commands.clisten.list": "Listening to the following packets:", + "commands.clisten.clear": "No longer listening to any packets", + "commands.cplayerinfo.ioException": "An error occurred", "commands.cplayerinfo.getNameHistory.success": "%s has had the following names: %s", diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 28be2c28a..1d62a7ae1 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -17,6 +17,7 @@ "MixinChatHud", "MixinChatScreen", "MixinClientCommandSource", + "MixinClientConnection", "MixinClientPlayerEntity", "MixinClientPlayerInteractionManager", "MixinClientPlayNetworkHandler",