diff --git a/build.gradle b/build.gradle index 20439f0f7..990f9db34 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,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 26efe2134..a4d947b63 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -7,6 +7,7 @@ import dev.xpple.betterconfig.api.ModConfigBuilder; import io.netty.buffer.Unpooled; import net.earthcomputer.clientcommands.command.*; +import net.earthcomputer.clientcommands.features.MappingsHelper; import net.earthcomputer.clientcommands.render.RenderQueue; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; @@ -85,6 +86,8 @@ public void onInitializeClient() { new ModConfigBuilder("clientcommands", Configs.class).build(); ItemGroupCommand.registerItemGroups(); + + MappingsHelper.load(); } private static Set getCommands(CommandDispatcher dispatcher) { @@ -161,6 +164,7 @@ public static void registerCommands(CommandDispatcher WeatherCommand.register(dispatcher); PluginsCommand.register(dispatcher); CGameModeCommand.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 8dc91b7b8..9c2822f43 100644 --- a/src/main/java/net/earthcomputer/clientcommands/Configs.java +++ b/src/main/java/net/earthcomputer/clientcommands/Configs.java @@ -113,4 +113,15 @@ 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, + } + + @Config + public static int maximumPacketFieldDepth = 10; } 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..512453983 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/ReflectionUtils.java @@ -0,0 +1,23 @@ +package net.earthcomputer.clientcommands; + +import java.lang.reflect.Field; +import java.util.stream.Stream; + +public final class ReflectionUtils { + + private 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..863252886 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/UnsafeUtils.java @@ -0,0 +1,54 @@ +package net.earthcomputer.clientcommands; + +import com.mojang.logging.LogUtils; +import net.minecraft.Util; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +/** + * @author Gaming32 + */ +public final class UnsafeUtils { + + private UnsafeUtils() { + } + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final @Nullable Unsafe UNSAFE = Util.make(() -> { + try { + final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + return (Unsafe) unsafeField.get(null); + } catch (Exception e) { + LOGGER.error("Could not access theUnsafe", e); + return null; + } + }); + + private static final @Nullable MethodHandles.Lookup IMPL_LOOKUP = Util.make(() -> { + try { + //noinspection ConstantValue + if (UNSAFE == null) { + return null; + } + final Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + return (MethodHandles.Lookup) UNSAFE.getObject(UNSAFE.staticFieldBase(implLookupField), UNSAFE.staticFieldOffset(implLookupField)); + } catch (Exception e) { + LOGGER.error("Could not access IMPL_LOOKUP", e); + return null; + } + }); + + public static @Nullable Unsafe getUnsafe() { + return UNSAFE; + } + + public static @Nullable 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..a08c6a3f5 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/ListenCommand.java @@ -0,0 +1,300 @@ +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 com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.ReflectionUtils; +import net.earthcomputer.clientcommands.UnsafeUtils; +import net.earthcomputer.clientcommands.features.MappingsHelper; +import net.earthcomputer.clientcommands.features.PacketDumper; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +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.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static net.earthcomputer.clientcommands.command.arguments.MojmapPacketClassArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class ListenCommand { + + private static volatile boolean isEnabled = true; + + public static void disable() { + isEnabled = false; + } + + private static final SimpleCommandExceptionType COMMAND_DISABLED_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.clisten.commandDisabled")); + private static final SimpleCommandExceptionType ALREADY_LISTENING_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.clisten.add.failed")); + private static final SimpleCommandExceptionType NOT_LISTENING_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.clisten.remove.failed")); + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final Set>> packets = new HashSet<>(); + + private static PacketCallback callback; + + 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 { + checkEnabled(); + if (!packets.add(packetClass)) { + throw ALREADY_LISTENING_EXCEPTION.create(); + } + + source.sendFeedback(Component.translatable("commands.clisten.add.success")); + + if (callback == null) { + callback = (packet, side) -> { + String packetData; + Component packetDataPreview; + if (Configs.packetDumpMethod == Configs.PacketDumpMethod.BYTE_BUF) { + StringWriter writer = new StringWriter(); + try { + PacketDumper.dumpPacket(packet, new JsonWriter(writer)); + } catch (IOException e) { + LOGGER.error("Could not dump packet", e); + return; + } + packetData = writer.toString(); + packetDataPreview = Component.literal(packetData.replace("\u00a7", "\\u00a7")); + } else { + try { + packetDataPreview = serialize(packet, new ReferenceOpenHashSet<>(), 0); + packetData = packetDataPreview.getString(); + } catch (StackOverflowError e) { + LOGGER.error("Could not serialize packet into a Component", e); + return; + } + } + + String packetClassName = packet.getClass().getName().replace('.', '/'); + String mojmapPacketName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(packetClassName), packetClassName); + mojmapPacketName = mojmapPacketName.substring(mojmapPacketName.lastIndexOf('/') + 1); + + MutableComponent packetComponent = Component.literal(mojmapPacketName).withStyle(s -> s + .withUnderlined(true) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, packetDataPreview)) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, packetData))); + + switch (side) { + case CLIENTBOUND -> source.sendFeedback(Component.translatable("commands.clisten.receivedPacket", packetComponent)); + case SERVERBOUND -> source.sendFeedback(Component.translatable("commands.clisten.sentPacket", packetComponent)); + } + }; + } + + return Command.SINGLE_SUCCESS; + } + + private static int remove(FabricClientCommandSource source, Class> packetClass) throws CommandSyntaxException { + checkEnabled(); + if (!packets.remove(packetClass)) { + throw NOT_LISTENING_EXCEPTION.create(); + } + + source.sendFeedback(Component.translatable("commands.clisten.remove.success")); + return Command.SINGLE_SUCCESS; + } + + private static int list(FabricClientCommandSource source) throws CommandSyntaxException { + checkEnabled(); + int amount = packets.size(); + if (amount == 0) { + source.sendFeedback(Component.translatable("commands.clisten.list.none")); + } else { + source.sendFeedback(Component.translatable("commands.clisten.list")); + packets.forEach(packetClass -> { + String packetClassName = packetClass.getName().replace('.', '/'); + String mojmapName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(packetClassName), packetClassName); + mojmapName = mojmapName.substring(mojmapName.lastIndexOf('/') + 1); + source.sendFeedback(Component.literal(mojmapName)); + }); + } + + return amount; + } + + private static int clear(FabricClientCommandSource source) throws CommandSyntaxException { + checkEnabled(); + int amount = packets.size(); + packets.clear(); + source.sendFeedback(Component.translatable("commands.clisten.clear")); + return amount; + } + + private static void checkEnabled() throws CommandSyntaxException { + if (!isEnabled) { + throw COMMAND_DISABLED_EXCEPTION.create(); + } + } + + private static Component serialize(Object object, Set seen, int depth) { + try { + if (depth <= Configs.maximumPacketFieldDepth && seen.add(object)) { + return serializeInner(object, seen, depth); + } + return Component.empty(); + } finally { + seen.remove(object); + } + } + + private static Component serializeInner(Object object, Set seen, int depth) { + if (object == null) { + return Component.literal("null"); + } + if (object instanceof Component component) { + return component; + } + if (object instanceof String string) { + return Component.literal(string); + } + if (object instanceof Number || object instanceof Boolean) { + return Component.literal(object.toString()); + } + if (object instanceof Optional optional) { + return optional.isPresent() ? serialize(optional.get(), seen, depth + 1) : Component.literal("empty"); + } + if (object instanceof Date date) { + return Component.translationArg(date); + } + if (object instanceof Instant instant) { + return Component.translationArg(Date.from(instant)); + } + if (object instanceof UUID uuid) { + return Component.translationArg(uuid); + } + if (object instanceof ChunkPos chunkPos) { + return Component.translationArg(chunkPos); + } + if (object instanceof ResourceLocation resourceLocation) { + return Component.translationArg(resourceLocation); + } + if (object instanceof Message message) { + return Component.translationArg(message); + } + if (object.getClass().isArray()) { + MutableComponent component = Component.literal("["); + int lengthMinusOne = Array.getLength(object) - 1; + if (lengthMinusOne < 0) { + return component.append("]"); + } + for (int i = 0; i < lengthMinusOne; i++) { + component.append(serialize(Array.get(object, i), seen, depth + 1)).append(", "); + } + return component.append(serialize(Array.get(object, lengthMinusOne), seen, depth + 1)).append("]"); + } + if (object instanceof Collection collection) { + MutableComponent component = Component.literal("["); + component.append(collection.stream().map(e -> serialize(e, seen, depth + 1).copy()).reduce((l, r) -> l.append(", ").append(r)).orElse(Component.empty())); + return component.append("]"); + } + if (object instanceof Map map) { + MutableComponent component = Component.literal("{"); + component.append(map.entrySet().stream().map(e -> serialize(e.getKey(), seen, depth + 1).copy().append("=").append(serialize(e.getValue(), seen, depth + 1))).reduce((l, r) -> l.append(", ").append(r)).orElse(Component.empty())); + return component.append("}"); + } + if (object instanceof Registry registry) { + return Component.translationArg(registry.key().location()); + } + if (object instanceof ResourceKey resourceKey) { + MutableComponent component = Component.literal("{"); + component.append("registry=").append(serialize(resourceKey.registry(), seen, depth + 1)).append(", "); + component.append("location=").append(serialize(resourceKey.location(), seen, depth + 1)); + return component.append("}"); + } + if (object instanceof Holder holder) { + MutableComponent component = Component.literal("{"); + component.append("kind=").append(serialize(holder.kind().name(), seen, depth + 1)).append(", "); + component.append("value=").append(serialize(holder.value(), seen, depth + 1)); + return component.append("}"); + } + + String className = object.getClass().getName().replace(".", "/"); + String mojmapClassName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(className), className); + mojmapClassName = mojmapClassName.substring(mojmapClassName.lastIndexOf('/') + 1); + + MutableComponent component = Component.literal(mojmapClassName + '{'); + component.append(ReflectionUtils.getAllFields(object.getClass()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .map(field -> { + String fieldName = field.getName(); + String mojmapFieldName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_field(className, fieldName), fieldName); + try { + field.setAccessible(true); + return Component.literal(mojmapFieldName + '=').append(serialize(field.get(object), seen, depth + 1)); + } catch (InaccessibleObjectException | ReflectiveOperationException e) { + try { + MethodHandles.Lookup implLookup = UnsafeUtils.getImplLookup(); + if (implLookup == null) { + return Component.literal(mojmapFieldName + '=').append(Component.translatable("commands.clisten.packetError").withStyle(ChatFormatting.DARK_RED)); + } + VarHandle varHandle = implLookup.findVarHandle(object.getClass(), fieldName, field.getType()); + return Component.literal(mojmapFieldName + '=').append(serialize(varHandle.get(object), seen, depth + 1)); + } catch (ReflectiveOperationException ex) { + return Component.literal(mojmapFieldName + '=').append(Component.translatable("commands.clisten.packetError").withStyle(ChatFormatting.DARK_RED)); + } + } + }) + .reduce((l, r) -> l.append(", ").append(r)) + .orElse(Component.empty())); + return component.append("}"); + } + + public static void onPacket(Packet packet, PacketFlow side) { + if (!packets.contains(packet.getClass())) { + return; + } + callback.apply(packet, side); + } + + @FunctionalInterface + private interface PacketCallback { + void apply(Packet packet, PacketFlow 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..0eea25638 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/MojmapPacketClassArgumentType.java @@ -0,0 +1,72 @@ +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.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.earthcomputer.clientcommands.features.MappingsHelper; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.Optionull; +import net.minecraft.Util; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class MojmapPacketClassArgumentType implements ArgumentType>> { + + private static final Collection EXAMPLES = Arrays.asList("ClientboundPlayerChatPacket", "ClientboundSystemChatMessage", "ServerboundContainerSlotStateChangedPacket"); + + private static final DynamicCommandExceptionType UNKNOWN_PACKET_EXCEPTION = new DynamicCommandExceptionType(packet -> Component.translatable("commands.clisten.unknownPacket", packet)); + + private static final Map>> mojmapPackets = Arrays.stream(ConnectionProtocol.values()) + .flatMap(connectionProtocol -> connectionProtocol.flows.values().stream()) + .flatMap(codecData -> codecData.packetSet.classToId.keySet().stream()) + .map(clazz -> Optionull.map(MappingsHelper.namedOrIntermediaryToMojmap_class(clazz.getName().replace('.', '/')), + packet -> Map.entry(packet.substring(packet.lastIndexOf('/') + 1), clazz))) + .filter(Objects::nonNull) + .distinct() + .collect(Util.toMap()); + + 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 + public Class> parse(StringReader reader) throws CommandSyntaxException { + final int start = reader.getCursor(); + while (reader.canRead() && (StringReader.isAllowedInUnquotedString(reader.peek()) || reader.peek() == '$' )) { + reader.skip(); + } + String packet = reader.getString().substring(start, reader.getCursor()); + Class> packetClass = mojmapPackets.get(packet); + if (packetClass == null) { + throw UNKNOWN_PACKET_EXCEPTION.create(packet); + } + return packetClass; + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + return SharedSuggestionProvider.suggest(mojmapPackets.keySet(), builder); + } + + @Override + public Collection getExamples() { + return EXAMPLES; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/MappingsHelper.java b/src/main/java/net/earthcomputer/clientcommands/features/MappingsHelper.java new file mode 100644 index 000000000..0319a6978 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/MappingsHelper.java @@ -0,0 +1,291 @@ +package net.earthcomputer.clientcommands.features; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.ClientCommands; +import net.earthcomputer.clientcommands.command.ListenCommand; +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 net.minecraft.DetectedVersion; +import net.minecraft.Optionull; +import net.minecraft.Util; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public final class MappingsHelper { + + private MappingsHelper() { + } + + public static void load() { + } + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final Path MAPPINGS_DIR = ClientCommands.configDir.resolve("mappings"); + + private static final boolean IS_DEV_ENV = FabricLoader.getInstance().isDevelopmentEnvironment(); + + static { + try { + Files.createDirectories(MAPPINGS_DIR); + } catch (IOException e) { + LOGGER.error("Failed to create mappings dir", e); + } + } + + private static final CompletableFuture mojmapOfficial = Util.make(() -> { + String version = DetectedVersion.BUILT_IN.getName(); + try (BufferedReader reader = Files.newBufferedReader(MAPPINGS_DIR.resolve(version + ".txt"))) { + MemoryMappingTree tree = new MemoryMappingTree(); + MappingReader.read(reader, MappingFormat.PROGUARD_FILE, tree); + return CompletableFuture.completedFuture(tree); + } catch (IOException e) { + HttpClient httpClient = HttpClient.newHttpClient(); + HttpRequest versionsRequest = HttpRequest.newBuilder() + .uri(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")) + .GET() + .timeout(Duration.ofSeconds(5)) + .build(); + return httpClient.sendAsync(versionsRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(HttpResponse::body) + .thenCompose(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(); + return httpClient.sendAsync(versionRequest, HttpResponse.BodyHandlers.ofString()); + }) + .whenComplete((result, exception) -> { + if (exception != null) { + ListenCommand.disable(); + } + }) + .thenApply(HttpResponse::body) + .thenCompose(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(); + return httpClient.sendAsync(mappingsRequest, HttpResponse.BodyHandlers.ofString()); + }) + .thenApply(HttpResponse::body) + .thenApply(body -> { + try (StringReader reader = new StringReader(body)) { + MemoryMappingTree tree = new MemoryMappingTree(); + MappingReader.read(reader, MappingFormat.PROGUARD_FILE, tree); + return tree; + } catch (IOException ex) { + LOGGER.error("Could not read ProGuard mappings file", ex); + ListenCommand.disable(); + throw new UncheckedIOException(ex); + } finally { + try (BufferedWriter writer = Files.newBufferedWriter(MAPPINGS_DIR.resolve(version + ".txt"), StandardOpenOption.CREATE)) { + writer.write(body); + } catch (IOException ex) { + LOGGER.error("Could not write ProGuard mappings file", ex); + } + } + }); + } + }); + private static final int SRC_OFFICIAL = 0; + private static final int DEST_OFFICIAL = 0; + + private static final MemoryMappingTree officialIntermediaryNamed = Util.make(() -> { + try (InputStream stream = FabricLoader.class.getClassLoader().getResourceAsStream("mappings/mappings.tiny")) { + if (stream == null) { + throw new IOException("Could not find mappings.tiny"); + } + MemoryMappingTree tree = new MemoryMappingTree(); + MappingReader.read(new InputStreamReader(stream), IS_DEV_ENV ? MappingFormat.TINY_2_FILE : MappingFormat.TINY_FILE, tree); + return tree; + } catch (IOException e) { + LOGGER.error("Could not read mappings.tiny", e); + ListenCommand.disable(); + return null; + } + }); + 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; + + public static @Nullable Collection mojmapClasses() { + return Optionull.map(getMojmapOfficial(), MemoryMappingTree::getClasses); + } + + public static @Nullable String mojmapToOfficial_class(String mojmapClass) { + MappingTree.ClassMapping officialClass = Optionull.map(getMojmapOfficial(), tree -> tree.getClass(mojmapClass)); + if (officialClass == null) { + return null; + } + return officialClass.getDstName(DEST_OFFICIAL); + } + + public static @Nullable String officialToMojmap_class(String officialClass) { + MappingTree.ClassMapping mojmapClass = Optionull.map(getMojmapOfficial(), tree -> tree.getClass(officialClass, SRC_OFFICIAL)); + if (mojmapClass == null) { + return null; + } + return mojmapClass.getSrcName(); + } + + public static @Nullable String mojmapToNamed_class(String mojmapClass) { + String officialClass = mojmapToOfficial_class(mojmapClass); + if (officialClass == null) { + return null; + } + MappingTree.ClassMapping namedClass = officialIntermediaryNamed.getClass(officialClass); + if (namedClass == null) { + return null; + } + return namedClass.getDstName(DEST_NAMED); + } + + public static @Nullable String namedToMojmap_class(String namedClass) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(namedClass, SRC_NAMED); + if (officialClass == null) { + return null; + } + MappingTree.ClassMapping mojmapClass = Optionull.map(getMojmapOfficial(), tree -> tree.getClass(officialClass.getSrcName(), SRC_OFFICIAL)); + if (mojmapClass == null) { + return null; + } + return mojmapClass.getSrcName(); + } + + public static @Nullable String mojmapToIntermediary_class(String mojmapClass) { + String officialClass = mojmapToOfficial_class(mojmapClass); + if (officialClass == null) { + return null; + } + MappingTree.ClassMapping intermediaryClass = officialIntermediaryNamed.getClass(officialClass); + if (intermediaryClass == null) { + return null; + } + return intermediaryClass.getDstName(DEST_INTERMEDIARY); + } + + public static @Nullable String intermediaryToMojmap_class(String intermediaryClass) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(intermediaryClass, SRC_INTERMEDIARY); + if (officialClass == null) { + return null; + } + MappingTree.ClassMapping mojmapClass = Optionull.map(getMojmapOfficial(), tree -> tree.getClass(officialClass.getSrcName(), SRC_OFFICIAL)); + if (mojmapClass == null) { + return null; + } + return mojmapClass.getSrcName(); + } + + public static @Nullable String namedOrIntermediaryToMojmap_class(String namedOrIntermediaryClass) { + if (IS_DEV_ENV) { + return MappingsHelper.namedToMojmap_class(namedOrIntermediaryClass); + } + return MappingsHelper.intermediaryToMojmap_class(namedOrIntermediaryClass); + } + + public static @Nullable String mojmapToNamedOrIntermediary_class(String mojmapClass) { + if (IS_DEV_ENV) { + return MappingsHelper.mojmapToNamed_class(mojmapClass); + } + return MappingsHelper.mojmapToIntermediary_class(mojmapClass); + } + + public static @Nullable String officialToMojmap_field(String officialClass, String officialField) { + MappingTree.FieldMapping mojmapField = Optionull.map(getMojmapOfficial(), tree -> tree.getField(officialClass, officialField, null)); + if (mojmapField == null) { + return null; + } + return mojmapField.getSrcName(); + } + + public static @Nullable String namedToMojmap_field(String namedClass, String namedField) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(namedClass, SRC_NAMED); + if (officialClass == null) { + return null; + } + MappingTree.FieldMapping officialField = officialIntermediaryNamed.getField(namedClass, namedField, null, SRC_NAMED); + if (officialField == null) { + return null; + } + MappingTree.FieldMapping mojmapField = Optionull.map(getMojmapOfficial(), tree -> tree.getField(officialClass.getSrcName(), officialField.getSrcName(), null, SRC_OFFICIAL)); + if (mojmapField == null) { + return null; + } + return mojmapField.getSrcName(); + } + + public static @Nullable String intermediaryToMojmap_field(String intermediaryClass, String intermediaryField) { + MappingTree.ClassMapping officialClass = officialIntermediaryNamed.getClass(intermediaryClass, SRC_INTERMEDIARY); + if (officialClass == null) { + return null; + } + MappingTree.FieldMapping officialField = officialIntermediaryNamed.getField(intermediaryClass, intermediaryField, null, SRC_INTERMEDIARY); + if (officialField == null) { + return null; + } + MappingTree.FieldMapping mojmapField = Optionull.map(getMojmapOfficial(), tree -> tree.getField(officialClass.getSrcName(), officialField.getSrcName(), null, SRC_OFFICIAL)); + if (mojmapField == null) { + return null; + } + return mojmapField.getSrcName(); + } + + public static @Nullable String namedOrIntermediaryToMojmap_field(String namedOrIntermediaryClass, String namedOrIntermediaryField) { + if (IS_DEV_ENV) { + return namedToMojmap_field(namedOrIntermediaryClass, namedOrIntermediaryField); + } + return intermediaryToMojmap_field(namedOrIntermediaryClass, namedOrIntermediaryField); + } + + private static MemoryMappingTree getMojmapOfficial() { + try { + return mojmapOfficial.get(); + } catch (ExecutionException | InterruptedException e) { + LOGGER.error("mojmap mappings were not available", e); + ListenCommand.disable(); + return null; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java b/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java new file mode 100644 index 000000000..2b29d3606 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java @@ -0,0 +1,725 @@ +package net.earthcomputer.clientcommands.features; + +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.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; +import net.minecraft.core.IdMap; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +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.NotNull; +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.Arrays; +import java.util.Base64; +import java.util.BitSet; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +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(); + } + + private static class PacketDumpByteBuf extends FriendlyByteBuf { + 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)); // Uses singleton EmptyByteBuf + this.writer = writer; + } + + @Override + @SuppressWarnings("deprecation") + public @NotNull PacketDumpByteBuf writeWithCodec(DynamicOps ops, Codec codec, T value) { + return dump("withCodec", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + writer.name("encodedNbt").value(Util.getOrThrow( + codec.encodeStart(ops, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ).toString()); + writer.name("encodedJson"); + GSON.toJson(Util.getOrThrow( + codec.encodeStart(JsonOps.INSTANCE, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ), writer); + }); + } + + @Override + public void writeJsonWithCodec(Codec codec, T value) { + dump("jsonWithCodec", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + writer.name("encodedJson"); + GSON.toJson(Util.getOrThrow( + codec.encodeStart(JsonOps.INSTANCE, value), + message -> new EncoderException("Failed to encode: " + message + " " + value) + ), writer); + }); + } + + @Override + public void writeId(IdMap idMap, T value) { + dump("id", () -> { + dumpValueClass(value); + writer.name("value").value(Objects.toString(value)); + if (idMap instanceof Registry registry) { + writer.name("registry").value(registry.key().location().toString()); + writer.name("valueKey").value(Objects.toString(registry.getKey(value))); + } + writer.name("id").value(idMap.getId(value)); + }); + } + + @Override + public void writeId(IdMap> idMap, Holder value, Writer directWriter) { + dump("idHolder", () -> { + writer.name("kind").value(value.kind().name()); + value.unwrap().ifLeft(key -> Uncheck.run(() -> { + writer.name("referenceKey").value(key.location().toString()); + writer.name("id").value(idMap.getId(value)); + })).ifRight(directValue -> Uncheck.run(() -> { + writer.name("directValue"); + dumpValue(directValue, directWriter); + })); + }); + } + + @Override + public void writeCollection(Collection collection, Writer elementWriter) { + dump("collection", () -> { + writer.name("size").value(collection.size()); + writer.name("elements").beginArray(); + for (final T element : collection) { + dumpValue(element, elementWriter); + } + writer.endArray(); + }); + } + + @Override + public void writeIntIdList(IntList intIdList) { + dump("intIdList", () -> { + writer.name("size").value(intIdList.size()); + writer.name("elements").beginArray(); + for (final int value : intIdList) { + writer.value(value); + } + writer.endArray(); + }); + } + + @Override + public void writeMap(Map map, Writer keyWriter, Writer valueWriter) { + dump("map", () -> { + writer.name("size").value(map.size()); + writer.name("elements").beginArray(); + for (final 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", () -> { + String className = enumClass.getName().replace('.', '/'); + String mojmapClassName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(className), className); + mojmapClassName = mojmapClassName.substring(mojmapClassName.lastIndexOf('/') + 1); + writer.name("enumClass").value(mojmapClassName); + writer.name("size").value(enumSet.size()); + writer.name("elements").beginArray(); + for (final E element : enumSet) { + writer.value(element.name()); + } + writer.endArray(); + }); + } + + @Override + public void writeOptional(Optional optional, Writer valueWriter) { + writeNullable("optional", optional.orElse(null), valueWriter); + } + + @Override + public void writeNullable(@Nullable T value, Writer writer) { + writeNullable("nullable", value, writer); + } + + private void writeNullable(String type, T value, Writer 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, Writer leftWriter, Writer 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 @NotNull PacketDumpByteBuf writeByteArray(byte[] array) { + return dump("byteArray", () -> writer + .name("length").value(array.length) + .name("value").value(Base64.getEncoder().encodeToString(array)) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeVarIntArray(int[] array) { + return dump("varIntArray", () -> { + writer.name("length").value(array.length); + writer.name("elements").beginArray(); + for (final int element : array) { + writer.value(element); + } + writer.endArray(); + }); + } + + @Override + public @NotNull PacketDumpByteBuf writeLongArray(long[] array) { + return dump("longArray", () -> { + writer.name("length").value(array.length); + writer.name("elements").beginArray(); + for (final long element : array) { + writer.value(element); + } + writer.endArray(); + }); + } + + @Override + public @NotNull 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 @NotNull PacketDumpByteBuf writeChunkPos(ChunkPos chunkPos) { + return dump("chunkPos", () -> writer + .name("x").value(chunkPos.x) + .name("z").value(chunkPos.z) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeSectionPos(SectionPos sectionPos) { + return dump("sectionPos", () -> writer + .name("x").value(sectionPos.x()) + .name("y").value(sectionPos.y()) + .name("z").value(sectionPos.z()) + ); + } + + @Override + public void writeGlobalPos(GlobalPos pos) { + dump("globalPos", () -> writer + .name("level").value(pos.dimension().location().toString()) + .name("x").value(pos.pos().getX()) + .name("y").value(pos.pos().getY()) + .name("z").value(pos.pos().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 writeQuaternion(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 writeVec3(Vec3 vec3) { + dump("vec3", () -> writer + .name("x").value(vec3.x) + .name("y").value(vec3.y) + .name("z").value(vec3.z) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeComponent(Component component) { + return dump("component", () -> { + writer.name("value"); + GSON.toJson(Component.Serializer.toJsonTree(component), writer); + }); + } + + @Override + public @NotNull PacketDumpByteBuf writeEnum(Enum value) { + return dump("enum", () -> { + String className = value.getDeclaringClass().getName().replace('.', '/'); + String mojmapClassName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(className), className); + mojmapClassName = mojmapClassName.substring(mojmapClassName.lastIndexOf('/') + 1); + writer + .name("enum").value(mojmapClassName) + .name("value").value(value.name()); + }); + } + + @Override + public @NotNull PacketDumpByteBuf writeById(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 @NotNull PacketDumpByteBuf writeUUID(UUID uuid) { + return dumpAsString("uuid", uuid); + } + + @Override + public @NotNull PacketDumpByteBuf writeVarInt(int input) { + return dumpSimple("varInt", input, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeVarLong(long value) { + return dumpSimple("varLong", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeNbt(@Nullable Tag tag) { + return dumpAsString("nbt", tag); + } + + @Override + public @NotNull PacketDumpByteBuf writeItem(ItemStack stack) { + return dump("item", () -> writer + .name("item").value(stack.getItemHolder().unwrapKey().map(k -> k.location().toString()).orElse(null)) + .name("count").value(stack.getCount()) + .name("tag").value(Objects.toString(stack.getTag())) + ); + } + + @Override + public @NotNull FriendlyByteBuf writeUtf(String string) { + return dump("utf", () -> writer + .name("value").value(string) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeUtf(String string, int maxLength) { + return dump("utf", () -> writer + .name("maxLength").value(maxLength) + .name("value").value(string) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeResourceLocation(ResourceLocation resourceLocation) { + return dumpAsString("resourceLocation", resourceLocation); + } + + @Override + public void writeResourceKey(ResourceKey resourceKey) { + dump("resourceKey", () -> writer + .name("registry").value(resourceKey.registry().toString()) + .name("location").value(resourceKey.location().toString()) + ); + } + + @Override + public @NotNull PacketDumpByteBuf writeDate(Date time) { + return dumpSimple("date", ISO_8601.format(time), JsonWriter::value); + } + + @Override + public void writeInstant(Instant instant) { + dumpAsString("instant", instant); + } + + @Override + public @NotNull 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.getDirection().getSerializedName()) + .name("offset").beginObject() + .name("x").value(result.getLocation().x - result.getBlockPos().getX()) + .name("y").value(result.getLocation().y - result.getBlockPos().getY()) + .name("z").value(result.getLocation().z - result.getBlockPos().getZ()) + .name("isInside").value(result.isInside()) + ); + } + + @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 writeFixedBitSet(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 writeGameProfileProperties(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 @NotNull PacketDumpByteBuf skipBytes(int length) { + return dump("skipBytes", () -> writer.name("length").value(length)); + } + + @Override + public @NotNull PacketDumpByteBuf writeBoolean(boolean value) { + return dumpSimple("boolean", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeByte(int value) { + return dumpSimple("byte", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeShort(int value) { + return dumpSimple("short", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeShortLE(int value) { + return dumpSimple("shortLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeMedium(int value) { + return dumpSimple("medium", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeMediumLE(int value) { + return dumpSimple("mediumLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeInt(int value) { + return dumpSimple("int", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeIntLE(int value) { + return dumpSimple("intLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeLong(long value) { + return dumpSimple("long", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeLongLE(long value) { + return dumpSimple("longLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeChar(int value) { + return dumpSimple("char", Character.toString((char)value), JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeFloat(float value) { + return dumpSimple("float", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeFloatLE(float value) { + return dumpSimple("floatLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeDouble(double value) { + return dumpSimple("double", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeDoubleLE(double value) { + return dumpSimple("doubleLE", value, JsonWriter::value); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(ByteBuf source) { + return writeBytes(source, source.readableBytes()); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(ByteBuf source, int length) { + final byte[] bytes = new byte[length]; + source.readBytes(bytes); + return dumpBytes(bytes); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(ByteBuf source, int sourceIndex, int length) { + final byte[] bytes = new byte[length]; + source.getBytes(sourceIndex, bytes); + return dumpBytes(bytes); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(byte[] source) { + return dumpBytes(source); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(byte[] source, int sourceIndex, int length) { + return dumpBytes(Arrays.copyOfRange(source, sourceIndex, sourceIndex + length)); + } + + @Override + public @NotNull PacketDumpByteBuf writeBytes(ByteBuffer source) { + final byte[] bytes = new byte[source.remaining()]; + source.get(bytes); + return dumpBytes(bytes); + } + + @Override + public int writeBytes(InputStream inputStream, int i) throws IOException { + final byte[] bytes = new byte[i]; + final int read = inputStream.read(bytes); + dumpBytes(Arrays.copyOf(bytes, i)); + return read; + } + + @Override + public int writeBytes(ScatteringByteChannel scatteringByteChannel, int i) throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(i); + final 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 @NotNull PacketDumpByteBuf writeZero(int length) { + return dump("zero", () -> writer.name("length").value(length)); + } + + @Override + public int writeCharSequence(CharSequence charSequence, Charset charset) { + final String string = charSequence.toString(); + final 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) { + String className = value.getClass().getName().replace('.', '/'); + String mojmapClassName = Objects.requireNonNullElse(MappingsHelper.namedOrIntermediaryToMojmap_class(className), className); + mojmapClassName = mojmapClassName.substring(mojmapClassName.lastIndexOf('/') + 1); + writer.value(mojmapClassName); + } else { + writer.nullValue(); + } + } + + private void dumpValue(T value, Writer 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/mixin/MixinConnection.java b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinConnection.java new file mode 100644 index 000000000..65536c75e --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinConnection.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.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +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(Connection.class) +public class MixinConnection { + @Shadow @Final private PacketFlow receiving; + + @Inject(method = "channelRead0(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/protocol/Packet;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;genericsFtw(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;)V")) + private void onPacketReceive(ChannelHandlerContext context, Packet packet, CallbackInfo ci) { + if (this.receiving == PacketFlow.CLIENTBOUND) { + ListenCommand.onPacket(packet, PacketFlow.CLIENTBOUND); + } + } + + @Inject(method = "doSendPacket", at = @At("HEAD")) + private void onPacketSend(Packet packet, @Nullable PacketSendListener sendListener, boolean flush, CallbackInfo ci) { + if (this.receiving == PacketFlow.CLIENTBOUND) { + ListenCommand.onPacket(packet, PacketFlow.SERVERBOUND); + } + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index a43bc2ef2..322be7ce1 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -142,6 +142,19 @@ "commands.ckit.list": "Available kits: %s", "commands.ckit.list.empty": "No available kits", + "commands.clisten.commandDisabled": "The command was disabled, check your logs", + "commands.clisten.unknownPacket": "Unknown packet %s", + "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/clientcommands.aw b/src/main/resources/clientcommands.aw index f29340aaf..c2bb3f5b4 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -12,3 +12,7 @@ accessible method net/minecraft/client/Minecraft openChatScreen (Ljava/lang/Stri accessible method net/minecraft/network/chat/HoverEvent (Lnet/minecraft/network/chat/HoverEvent$TypedHoverEvent;)V accessible class net/minecraft/network/chat/HoverEvent$TypedHoverEvent + +accessible field net/minecraft/network/ConnectionProtocol flows Ljava/util/Map; +accessible field net/minecraft/network/ConnectionProtocol$CodecData packetSet Lnet/minecraft/network/ConnectionProtocol$PacketSet; +accessible field net/minecraft/network/ConnectionProtocol$PacketSet classToId Lit/unimi/dsi/fastutil/objects/Object2IntMap; diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 3b6b2917d..d7e09a52c 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -22,6 +22,7 @@ "MixinClientPacketListener", "MixinClientPacketListenerHighPriority", "MixinClientSuggestionsProvider", + "MixinConnection", "MixinCraftingMenu", "MixinCreeperMushroomCowSheepAndSnowGolem", "MixinCrossbowItem",