diff --git a/api/build.gradle b/api/build.gradle index 4da2407c85..a3f80f92fb 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -3,10 +3,6 @@ repositories { maven { url 'https://repo.papermc.io/repository/maven-public/' } } -java { - disableAutoTargetJvm() -} - dependencies { compileOnly(adventureDependencies) diff --git a/build.gradle b/build.gradle index 110462ab8d..0118f05905 100644 --- a/build.gradle +++ b/build.gradle @@ -26,8 +26,10 @@ subprojects { "net.kyori:adventure-text-serializer-legacy:${adventureVersion}", "net.kyori:adventure-nbt:${adventureVersion}"] - java.sourceCompatibility = JavaVersion.VERSION_17 - java.targetCompatibility = JavaVersion.VERSION_1_8 + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + disableAutoTargetJvm() + } repositories { mavenLocal() diff --git a/settings.gradle b/settings.gradle index 2e3fbd3c59..cb02bc083e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include 'api' include 'netty-common' //Real modules include 'spigot' +include 'sponge' include 'bungeecord' include 'velocity' diff --git a/spigot/build.gradle b/spigot/build.gradle index 165fa4d4bd..5d53b73be3 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -4,9 +4,6 @@ repositories { maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } } -java.sourceCompatibility = JavaVersion.VERSION_1_8 -java.targetCompatibility = JavaVersion.VERSION_1_8 - shadowJar { relocate 'net.kyori.adventure.text.serializer.gson', 'io.github.retrooper.packetevents.adventure.serializer.gson' relocate 'net.kyori.adventure.text.serializer.legacy', 'io.github.retrooper.packetevents.adventure.serializer.legacy' diff --git a/sponge/build.gradle b/sponge/build.gradle new file mode 100644 index 0000000000..e71d944372 --- /dev/null +++ b/sponge/build.gradle @@ -0,0 +1,53 @@ +import org.spongepowered.gradle.plugin.config.PluginLoaders +import org.spongepowered.plugin.metadata.model.PluginDependency + +plugins { + id("org.spongepowered.gradle.plugin") version("2.0.2") +} + +repositories { + maven { url 'https://repo.spongepowered.org/repository/maven-public/' } // Sponge + maven { url 'https://repo.viaversion.com/' } +} + +sponge { + apiVersion("11.0.0-SNAPSHOT") + loader { + name(PluginLoaders.JAVA_PLAIN) + version("2.2.0") + } + + plugin("packetevents") { + displayName("PacketEvents") + entrypoint("io.github.retrooper.packetevents.sponge.PacketEventsPlugin") + license("GPL-3") + dependency("spongeapi") { + loadOrder(PluginDependency.LoadOrder.AFTER) + optional(false) + } + dependency("viaversion") { + version("*") + loadOrder(PluginDependency.LoadOrder.AFTER) + optional(true) + } + } +} + +shadowJar { + relocate 'net.kyori.adventure.text.serializer.gson', 'io.github.retrooper.packetevents.adventure.serializer.gson' + relocate 'net.kyori.adventure.text.serializer.legacy', 'io.github.retrooper.packetevents.adventure.serializer.legacy' + dependencies { + exclude(dependency('com.google.code.gson:gson:2.8.0')) + exclude(dependency('com.google.code.gson:gson:2.8.5')) + exclude(dependency('com.google.code.gson:gson:2.8.8')) + } +} + +dependencies { + compileOnly 'io.netty:netty-all:4.1.75.Final' + api project(':api') + api project(':netty-common') + api(adventureDependencies) + + compileOnly 'com.viaversion:viaversion:4.5.0' +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/InternalSpongeListener.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/InternalSpongeListener.java new file mode 100644 index 0000000000..bb38d5f372 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/InternalSpongeListener.java @@ -0,0 +1,52 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.github.retrooper.packetevents.sponge; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.FakeChannelUtil; +import io.github.retrooper.packetevents.sponge.injector.SpongeChannelInjector; +import net.kyori.adventure.text.Component; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.filter.Getter; +import org.spongepowered.api.event.network.ServerSideConnectionEvent; + +public class InternalSpongeListener { + + @Listener(order = Order.LATE) + public void onJoin(ServerSideConnectionEvent.Join event, @Getter("player") ServerPlayer player) { + SpongeChannelInjector injector = (SpongeChannelInjector) PacketEvents.getAPI().getInjector(); + + User user = PacketEvents.getAPI().getPlayerManager().getUser(player); + if (user == null) { + // We did not inject this user + Object channel = PacketEvents.getAPI().getPlayerManager().getChannel(player); + // Check if it is a fake connection... + if (!FakeChannelUtil.isFakeChannel(channel)) { + // Kick them, if they are not a fake player. + player.kick(Component.text("PacketEvents 2.0 failed to inject")); + } + return; + } + + // Set player object in the injectors + injector.updatePlayer(user, player); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/PacketEventsPlugin.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/PacketEventsPlugin.java new file mode 100644 index 0000000000..ad85c1edd0 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/PacketEventsPlugin.java @@ -0,0 +1,96 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; +import com.github.retrooper.packetevents.event.UserConnectEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; +import com.github.retrooper.packetevents.event.UserLoginEvent; +import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.util.TimeStampMode; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowItems; +import com.google.inject.Inject; +import io.github.retrooper.packetevents.sponge.factory.SpongePacketEventsBuilder; +import io.github.retrooper.packetevents.sponge.util.SpongeConversionUtil; +import org.spongepowered.api.Server; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.lifecycle.StartingEngineEvent; +import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; +import org.spongepowered.api.registry.RegistryTypes; +import org.spongepowered.plugin.PluginContainer; +import org.spongepowered.plugin.builtin.jvm.Plugin; + +@Plugin("packetevents") +public class PacketEventsPlugin { + + @Inject + private PluginContainer pluginContainer; + + @Listener + public void onServerStart(final StartingEngineEvent event) { + PacketEvents.setAPI(SpongePacketEventsBuilder.build(pluginContainer)); + PacketEvents.getAPI().load(); + + // Register your listeners + // TODO disable debug + PacketEvents.getAPI().getSettings().debug(true).bStats(true).checkForUpdates(true).timeStampMode(TimeStampMode.MILLIS).reEncodeByDefault(true); + PacketEvents.getAPI().init(); + + SimplePacketListenerAbstract listener = new SimplePacketListenerAbstract(PacketListenerPriority.HIGH) { + + // Testing ItemStack conversion, can be removed in future + @Override + public void onPacketPlaySend(PacketPlaySendEvent event) { + if (event.getPacketType() == PacketType.Play.Server.WINDOW_ITEMS) { + WrapperPlayServerWindowItems items = new WrapperPlayServerWindowItems(event); + for (ItemStack item : items.getItems()) { + org.spongepowered.api.item.inventory.ItemStack sponge = SpongeConversionUtil.toSpongeItemStack(item); + System.out.println(sponge.type().key(RegistryTypes.ITEM_TYPE).formatted()); + System.out.println(SpongeConversionUtil.fromSpongeItemStack(sponge).getType().getName().toString()); + } + } + } + + @Override + public void onUserConnect(UserConnectEvent event) { + PacketEvents.getAPI().getLogManager().debug("User: (host-name) " + event.getUser().getAddress().getHostString() + " connected..."); + } + + @Override + public void onUserLogin(UserLoginEvent event) { + PacketEvents.getAPI().getLogManager().debug("You logged in! User name: " + event.getUser().getProfile().getName()); + } + + @Override + public void onUserDisconnect(UserDisconnectEvent event) { + PacketEvents.getAPI().getLogManager().debug("User: (host-name) " + event.getUser().getAddress().getHostString() + " disconnected..."); + } + }; +// PacketEvents.getAPI().getEventManager().registerListener(listener); + } + + @Listener + public void onStopping(StoppingEngineEvent event) { + PacketEvents.getAPI().terminate(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/factory/SpongePacketEventsBuilder.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/factory/SpongePacketEventsBuilder.java new file mode 100644 index 0000000000..d1442c76c7 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/factory/SpongePacketEventsBuilder.java @@ -0,0 +1,227 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.factory; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.injector.ChannelInjector; +import com.github.retrooper.packetevents.manager.player.PlayerManager; +import com.github.retrooper.packetevents.manager.protocol.ProtocolManager; +import com.github.retrooper.packetevents.manager.server.ServerManager; +import com.github.retrooper.packetevents.netty.NettyManager; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.settings.PacketEventsSettings; +import com.github.retrooper.packetevents.util.LogManager; +import io.github.retrooper.packetevents.sponge.injector.SpongeChannelInjector; +import io.github.retrooper.packetevents.sponge.injector.connection.ServerConnectionInitializer; +import io.github.retrooper.packetevents.sponge.manager.protocol.ProtocolManagerImpl; +import io.github.retrooper.packetevents.sponge.util.viaversion.CustomPipelineUtil; +import io.github.retrooper.packetevents.sponge.manager.InternalSpongePacketListener; +import io.github.retrooper.packetevents.sponge.manager.player.PlayerManagerImpl; +import io.github.retrooper.packetevents.sponge.manager.server.ServerManagerImpl; +import io.github.retrooper.packetevents.sponge.netty.NettyManagerImpl; +import io.github.retrooper.packetevents.sponge.InternalSpongeListener; +import io.github.retrooper.packetevents.sponge.util.SpongeLogManager; +import io.github.retrooper.packetevents.sponge.util.SpongeReflectionUtil; +import io.github.retrooper.packetevents.sponge.util.viaversion.ViaVersionUtil; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.scheduler.Task; +import org.spongepowered.plugin.PluginContainer; + +public class SpongePacketEventsBuilder { + private static PacketEventsAPI API_INSTANCE; + + public static void clearBuildCache() { + API_INSTANCE = null; + } + + public static PacketEventsAPI build(PluginContainer plugin) { + if (API_INSTANCE == null) { + API_INSTANCE = buildNoCache(plugin); + } + return API_INSTANCE; + } + + public static PacketEventsAPI build(PluginContainer plugin, PacketEventsSettings settings) { + if (API_INSTANCE == null) { + API_INSTANCE = buildNoCache(plugin, settings); + } + return API_INSTANCE; + } + + public static PacketEventsAPI buildNoCache(PluginContainer plugin) { + return buildNoCache(plugin, new PacketEventsSettings()); + } + + public static PacketEventsAPI buildNoCache(PluginContainer plugin, PacketEventsSettings inSettings) { + return new PacketEventsAPI() { + private final PacketEventsSettings settings = inSettings; + private final ProtocolManager protocolManager = new ProtocolManagerImpl(); + private final ServerManager serverManager = new ServerManagerImpl(); + private final PlayerManager playerManager = new PlayerManagerImpl(); + private final NettyManager nettyManager = new NettyManagerImpl(); + private final SpongeChannelInjector injector = new SpongeChannelInjector(); + private final LogManager logManager = new SpongeLogManager(); + private boolean loaded; + private boolean initialized; + private boolean lateBind = false; + + @Override + public void load() { + if (loaded) return; + + //Resolve server version and cache + String id = plugin.metadata().id().toLowerCase(); + PacketEvents.IDENTIFIER = "pe-" + id; + PacketEvents.ENCODER_NAME = "pe-encoder-" + id; + PacketEvents.DECODER_NAME = "pe-decoder-" + id; + PacketEvents.CONNECTION_HANDLER_NAME = "pe-connection-handler-" + id; + PacketEvents.SERVER_CHANNEL_HANDLER_NAME = "pe-connection-initializer-" + id; + PacketEvents.TIMEOUT_HANDLER_NAME = "pe-timeout-handler-" + id; + try { + SpongeReflectionUtil.init(); + CustomPipelineUtil.init(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + + if (!PacketType.isPrepared()) { + PacketType.prepare(); + } + + // Server hasn't bound to the port yet. + lateBind = !injector.isServerBound(); + // If late-bind is enabled, we will inject a bit later. + if (!lateBind) { + injector.inject(); + } + + loaded = true; + + // Register internal packet listener (should be the first listener) + // This listener doesn't do any modifications to the packets, just reads data + getEventManager().registerListener(new InternalSpongePacketListener()); + } + + @Override + public boolean isLoaded() { + return loaded; + } + + @Override + public void init() { + //Load if we haven't loaded already + load(); + + if (initialized) return; + + if (settings.shouldCheckForUpdates()) { + getUpdateChecker().handleUpdateCheck(); + } + + if (settings.isbStatsEnabled()) { + // TODO: how do we do this? bStats wants @Inject but that won't work here. + // https://github.com/Bastian/bstats-metrics/blob/1.x.x/bstats-sponge/src/examples/java/ExamplePlugin.java + } + + Sponge.eventManager().registerListeners(plugin, new InternalSpongeListener()); + + if (lateBind) { + //If late-bind is enabled, we still need to inject (after all plugins enabled). + Runnable lateBindTask = () -> { + if (injector.isServerBound()) { + injector.inject(); + } + }; + + Sponge.server().scheduler().submit(Task.builder().plugin(plugin).execute(lateBindTask).build()); + } + + // Let people override this, at their own risk + if (!"true".equalsIgnoreCase(System.getenv("PE_IGNORE_INCOMPATIBILITY"))) { + // PacketEvents is now enabled, we can now check + ViaVersionUtil.checkIfViaIsPresent(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (initialized) { + // Uninject the injector if needed(depends on the injector implementation) + injector.uninject(); + for (User user : ProtocolManager.USERS.values()) { + ServerConnectionInitializer.destroyHandlers(user.getChannel()); + } + + // Unregister all listeners. Because if we attempt to reload, we will end up with duplicate listeners. + getEventManager().unregisterAllListeners(); + initialized = false; + } + } + + @Override + public PluginContainer getPlugin() { + return plugin; + } + + @Override + public ProtocolManager getProtocolManager() { + return protocolManager; + } + + @Override + public ServerManager getServerManager() { + return serverManager; + } + + @Override + public PlayerManager getPlayerManager() { + return playerManager; + } + + @Override + public PacketEventsSettings getSettings() { + return settings; + } + + @Override + public NettyManager getNettyManager() { + return nettyManager; + } + + @Override + public ChannelInjector getInjector() { + return injector; + } + + @Override + public LogManager getLogManager() { + return logManager; + } + }; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/SpongeChannelInjector.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/SpongeChannelInjector.java new file mode 100644 index 0000000000..4f06f781d7 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/SpongeChannelInjector.java @@ -0,0 +1,228 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.UserLoginEvent; +import com.github.retrooper.packetevents.injector.ChannelInjector; +import com.github.retrooper.packetevents.protocol.ConnectionState; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.reflection.ReflectionObject; +import io.github.retrooper.packetevents.sponge.injector.connection.ServerChannelHandler; +import io.github.retrooper.packetevents.sponge.injector.connection.ServerConnectionInitializer; +import io.github.retrooper.packetevents.sponge.injector.handlers.PacketEventsDecoder; +import io.github.retrooper.packetevents.sponge.injector.handlers.PacketEventsEncoder; +import io.github.retrooper.packetevents.sponge.util.InjectedList; +import io.github.retrooper.packetevents.sponge.util.SpongeReflectionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SpongeChannelInjector implements ChannelInjector { + //Channels that process connecting clients. + public final Set injectedConnectionChannels = new HashSet<>(); + public List networkManagers; + private int connectionChannelsListIndex = -1; + + public void updatePlayer(User user, Object player) { + PacketEvents.getAPI().getEventManager().callEvent(new UserLoginEvent(user, player)); + Object channel = user.getChannel(); + if (channel == null) { + channel = PacketEvents.getAPI().getPlayerManager().getChannel(player); + } + setPlayer(channel, player); + } + + @Override + public boolean isServerBound() { + //We want to check if the server has been bound to the port already. + Object serverConnection = SpongeReflectionUtil.getMinecraftServerConnectionInstance(); + if (serverConnection != null) { + ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection); + //There should only be 2 lists. + for (int i = 0; i < 2; i++) { + List list = reflectServerConnection.readList(i); + for (Object value : list) { + if (value instanceof ChannelFuture) { + connectionChannelsListIndex = i; + //Found the right list. + //It has connection channels, so the server has been bound. + return true; + } + } + } + } + return false; + } + + @Override + public void inject() { + Object serverConnection = SpongeReflectionUtil.getMinecraftServerConnectionInstance(); + if (serverConnection != null) { + ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection); + List connectionChannelFutures = reflectServerConnection.readList(connectionChannelsListIndex); + InjectedList wrappedList = new InjectedList<>(connectionChannelFutures, future -> { + //Each time a channel future is added, we run this. + //This is automatically also ran for the elements already added before we wrapped the list. + Channel channel = future.channel(); + //Inject into the server connection channel. + injectServerChannel(channel); + //Make sure to store it, so we can uninject later on. + injectedConnectionChannels.add(channel); + }); + + //Replace the list with our wrapped one. + reflectServerConnection.writeList(connectionChannelsListIndex, wrappedList); + + //Player channels might have been registered already. Let us add our handlers. We are a little late though. + if (networkManagers == null) { + networkManagers = SpongeReflectionUtil.getNetworkManagers(); + } + + synchronized (networkManagers) { + if (!networkManagers.isEmpty()) { + PacketEvents.getAPI().getLogManager().debug("Late bind not enabled, injecting into existing channel"); + } + + for (Object networkManager : networkManagers) { + ReflectionObject networkManagerWrapper = new ReflectionObject(networkManager); + Channel channel = networkManagerWrapper.readObject(0, Channel.class); + if (channel == null) { + continue; + } + try { + ServerConnectionInitializer.initChannel(channel, ConnectionState.PLAY); + } catch (Exception e) { + PacketEvents.getAPI().getLogManager().severe("Sponge injector failed to inject into an existing channel."); + e.printStackTrace(); + } + } + } + } + } + + @Override + public void uninject() { + //Uninject our connection handler from these connection channels. + for (Channel connectionChannel : injectedConnectionChannels) { + uninjectServerChannel(connectionChannel); + } + + injectedConnectionChannels.clear(); + Object serverConnection = SpongeReflectionUtil.getMinecraftServerConnectionInstance(); + if (serverConnection != null) { + ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection); + List connectionChannelFutures = reflectServerConnection.readList(connectionChannelsListIndex); + if (connectionChannelFutures instanceof InjectedList) { + //Let us unwrap this. We no longer want to listen to connecting channels. + reflectServerConnection.writeList(connectionChannelsListIndex, + ((InjectedList) connectionChannelFutures).originalList()); + } + } + } + + private void injectServerChannel(Channel serverChannel) { + ChannelPipeline pipeline = serverChannel.pipeline(); + ChannelHandler connectionHandler = pipeline.get(PacketEvents.CONNECTION_HANDLER_NAME); + if (connectionHandler != null) { + //Why does it already exist? Remove it. + pipeline.remove(PacketEvents.CONNECTION_HANDLER_NAME); + } + //Some forks add a handler which adds the other necessary vanilla handlers like (decoder, encoder, etc...) + else if (pipeline.get("MinecraftPipeline#0") != null) { + pipeline.addAfter("MinecraftPipeline#0", PacketEvents.CONNECTION_HANDLER_NAME, new ServerChannelHandler()); + } + //Otherwise, make sure we are first. + else { + pipeline.addFirst(PacketEvents.CONNECTION_HANDLER_NAME, new ServerChannelHandler()); + } + + if (networkManagers == null) { + networkManagers = SpongeReflectionUtil.getNetworkManagers(); + } + + //Make sure we handled all connected clients. + synchronized (networkManagers) { + for (Object networkManager : networkManagers) { + ReflectionObject networkManagerWrapper = new ReflectionObject(networkManager); + Channel channel = networkManagerWrapper.readObject(0, Channel.class); + if (channel.isOpen()) { + if (channel.localAddress().equals(serverChannel.localAddress())) { + channel.close(); + } + } + } + } + } + + private void uninjectServerChannel(Channel serverChannel) { + if (serverChannel.pipeline().get(PacketEvents.CONNECTION_HANDLER_NAME) != null) { + serverChannel.pipeline().remove(PacketEvents.CONNECTION_HANDLER_NAME); + } else { + PacketEvents.getAPI().getLogManager().warn("Failed to uninject server channel, handler not found"); + } + } + + @Override + public void updateUser(Object channel, User user) { + PacketEventsEncoder encoder = getEncoder((Channel) channel); + if (encoder != null) { + encoder.user = user; + } + + PacketEventsDecoder decoder = getDecoder((Channel) channel); + if (decoder != null) { + decoder.user = user; + } + } + + @Override + public void setPlayer(Object channel, Object player) { + PacketEventsEncoder encoder = getEncoder((Channel) channel); + if (encoder != null) { + encoder.player = (ServerPlayer) player; + } + + PacketEventsDecoder decoder = getDecoder((Channel) channel); + if (decoder != null) { + decoder.player = (ServerPlayer) player; + decoder.user.getProfile().setName(((ServerPlayer) player).name()); + decoder.user.getProfile().setUUID(((ServerPlayer) player).uniqueId()); + } + } + + private PacketEventsEncoder getEncoder(Channel channel) { + return (PacketEventsEncoder) channel.pipeline().get(PacketEvents.ENCODER_NAME); + } + + private PacketEventsDecoder getDecoder(Channel channel) { + return (PacketEventsDecoder) channel.pipeline().get(PacketEvents.DECODER_NAME); + } + + @Override + public boolean isProxy() { + return false; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/PreChannelInitializer.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/PreChannelInitializer.java new file mode 100644 index 0000000000..a10ea7906a --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/PreChannelInitializer.java @@ -0,0 +1,53 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector.connection; + +import com.github.retrooper.packetevents.protocol.ConnectionState; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +public class PreChannelInitializer extends ChannelInboundHandlerAdapter { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(io.netty.channel.ChannelInitializer.class); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + try { + ServerConnectionInitializer.initChannel(ctx.channel(), ConnectionState.HANDSHAKING); + } catch (Throwable t) { + exceptionCaught(ctx, t); + } finally { + ChannelPipeline pipeline = ctx.pipeline(); + if (pipeline.context(this) != null) { + pipeline.remove(this); + } + } + + ctx.pipeline().fireChannelRegistered(); + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { + PreChannelInitializer.logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), t); + ctx.close(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerChannelHandler.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerChannelHandler.java new file mode 100644 index 0000000000..becda9f58b --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerChannelHandler.java @@ -0,0 +1,34 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector.connection; + +import com.github.retrooper.packetevents.PacketEvents; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class ServerChannelHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = (Channel) msg; + channel.pipeline().addLast(PacketEvents.SERVER_CHANNEL_HANDLER_NAME, new PreChannelInitializer()); + super.channelRead(ctx, msg); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerConnectionInitializer.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerConnectionInitializer.java new file mode 100644 index 0000000000..8f00a1e0a1 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/connection/ServerConnectionInitializer.java @@ -0,0 +1,122 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector.connection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.UserConnectEvent; +import com.github.retrooper.packetevents.netty.channel.ChannelHelper; +import com.github.retrooper.packetevents.protocol.ConnectionState; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.util.FakeChannelUtil; +import com.github.retrooper.packetevents.util.PacketEventsImplHelper; +import io.github.retrooper.packetevents.sponge.injector.handlers.PacketEventsDecoder; +import io.github.retrooper.packetevents.sponge.injector.handlers.PacketEventsEncoder; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; + +import java.util.NoSuchElementException; + + +public class ServerConnectionInitializer { + + public static void initChannel(Object ch, ConnectionState connectionState) { + Channel channel = (Channel) ch; + if (FakeChannelUtil.isFakeChannel(channel)) { + return; + } + User user = new User(channel, connectionState, null, new UserProfile(null, null)); + + if (connectionState == ConnectionState.PLAY) { + // Player connected before ViaVersion init, therefore the player is server version (mostly true except 1.7 servers) + user.setClientVersion(PacketEvents.getAPI().getServerManager().getVersion().toClientVersion()); + PacketEvents.getAPI().getLogManager().warn("Late injection detected, we missed packets so some functionality may break!"); + } + + synchronized (channel) { + /* + * This is a rather rare one, BUT! + * If the plugin takes a while to initialize and handshakes/pings pile up, + * some may not be handled completely, thus, not having a 'splitter' ChannelHandler. + * We can, of course, wait for them to be handled, but this complexes the algorithm. + * Taken the above into account, here we just drop all unhandled connections. + */ + if (channel.pipeline().get("splitter") == null) { + channel.close(); + return; + } + + UserConnectEvent connectEvent = new UserConnectEvent(user); + PacketEvents.getAPI().getEventManager().callEvent(connectEvent); + if (connectEvent.isCancelled()) { + channel.unsafe().closeForcibly(); + return; + } + + relocateHandlers(channel, null, user); + + channel.closeFuture().addListener((ChannelFutureListener) future -> PacketEventsImplHelper.handleDisconnection(user.getChannel(), user.getUUID())); + PacketEvents.getAPI().getProtocolManager().setUser(channel, user); + } + } + + public static void destroyHandlers(Object ch) { + Channel channel = (Channel) ch; + if (channel.pipeline().get(PacketEvents.DECODER_NAME) != null) { + channel.pipeline().remove(PacketEvents.DECODER_NAME); + } else { + PacketEvents.getAPI().getLogger().warning("Could not find decoder handler in channel pipeline!"); + } + + if (channel.pipeline().get(PacketEvents.ENCODER_NAME) != null) { + channel.pipeline().remove(PacketEvents.ENCODER_NAME); + } else { + PacketEvents.getAPI().getLogger().warning("Could not find encoder handler in channel pipeline!"); + } + } + + public static void relocateHandlers(Channel ctx, PacketEventsDecoder decoder, User user) { + // Decoder == null means we haven't made handlers for the user yet + try { + ChannelHandler encoder; + if (decoder != null) { + // This patches a bug where PE 2.0 handlers keep jumping behind one another causing a stackoverflow + if (decoder.hasBeenRelocated) return; + // Make sure we only relocate because of compression once + decoder.hasBeenRelocated = true; + decoder = (PacketEventsDecoder) ctx.pipeline().remove(PacketEvents.DECODER_NAME); + encoder = ctx.pipeline().remove(PacketEvents.ENCODER_NAME); + decoder = new PacketEventsDecoder(decoder); + encoder = new PacketEventsEncoder(encoder); + } else { + encoder = new PacketEventsEncoder(user); + decoder = new PacketEventsDecoder(user); + } + // We are targeting the encoder and decoder since we don't want to target specific plugins + // (ProtocolSupport has changed its handler name in the past) + // I don't like the hacks required for compression but that's on vanilla, we can't fix it. + ctx.pipeline().addBefore("decoder", PacketEvents.DECODER_NAME, decoder); + ctx.pipeline().addBefore("encoder", PacketEvents.ENCODER_NAME, encoder); + } catch (NoSuchElementException ex) { + String handlers = ChannelHelper.pipelineHandlerNamesAsString(ctx); + throw new IllegalStateException("PacketEvents failed to add a decoder to the netty pipeline. Pipeline handlers: " + handlers, ex); + } + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsDecoder.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsDecoder.java new file mode 100644 index 0000000000..f36c1fb4c9 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsDecoder.java @@ -0,0 +1,82 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector.handlers; + +import com.github.retrooper.packetevents.exception.PacketProcessException; +import com.github.retrooper.packetevents.protocol.ConnectionState; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.ExceptionUtil; +import com.github.retrooper.packetevents.util.PacketEventsImplHelper; +import io.github.retrooper.packetevents.sponge.injector.connection.ServerConnectionInitializer; +import io.github.retrooper.packetevents.sponge.util.SpongeReflectionUtil; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.List; + +public class PacketEventsDecoder extends MessageToMessageDecoder { + + public User user; + public ServerPlayer player; + public boolean hasBeenRelocated; + + public PacketEventsDecoder(User user) { + this.user = user; + } + + public PacketEventsDecoder(PacketEventsDecoder decoder) { + user = decoder.user; + player = decoder.player; + hasBeenRelocated = decoder.hasBeenRelocated; + } + + public void read(ChannelHandlerContext ctx, ByteBuf input, List out) throws Exception { + PacketEventsImplHelper.handleServerBoundPacket(ctx.channel(), user, player, input, true); + out.add(input.retain()); + } + + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf buffer, List out) throws Exception { + if (buffer.isReadable()) { + read(ctx, buffer, out); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + //Check if the minecraft server will already print our exception for us. + //Don't print errors during handshake + if (ExceptionUtil.isException(cause, PacketProcessException.class) + && !SpongeReflectionUtil.isMinecraftServerInstanceDebugging() + && (user != null && user.getDecoderState() != ConnectionState.HANDSHAKING)) { + cause.printStackTrace(); + } + } + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, final Object event) throws Exception { + // Via changes the order of handlers in this event, so we must respond to Via changing their stuff + ServerConnectionInitializer.relocateHandlers(ctx.channel(), this, user); + super.userEventTriggered(ctx, event); + } + +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsEncoder.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsEncoder.java new file mode 100644 index 0000000000..8c086639c5 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/injector/handlers/PacketEventsEncoder.java @@ -0,0 +1,168 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.injector.handlers; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.exception.CancelPacketException; +import com.github.retrooper.packetevents.exception.InvalidDisconnectPacketSend; +import com.github.retrooper.packetevents.exception.PacketProcessException; +import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; +import com.github.retrooper.packetevents.protocol.ConnectionState; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.ExceptionUtil; +import com.github.retrooper.packetevents.util.PacketEventsImplHelper; +import io.github.retrooper.packetevents.sponge.injector.connection.ServerConnectionInitializer; +import io.github.retrooper.packetevents.sponge.util.viaversion.CustomPipelineUtil; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.MessageToMessageEncoder; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class PacketEventsEncoder extends MessageToMessageEncoder { + + public User user; + public ServerPlayer player; + private boolean handledCompression; + private ChannelPromise promise; + + public PacketEventsEncoder(User user) { + this.user = user; + } + + public PacketEventsEncoder(ChannelHandler encoder) { + user = ((PacketEventsEncoder) encoder).user; + player = ((PacketEventsEncoder) encoder).player; + handledCompression = ((PacketEventsEncoder) encoder).handledCompression; + promise = ((PacketEventsEncoder) encoder).promise; + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf byteBuf, List list) throws Exception { + boolean needsRecompression = !handledCompression && handleCompression(ctx, byteBuf); + handleClientBoundPacket(ctx.channel(), user, player, byteBuf, this.promise); + + if (needsRecompression) { + compress(ctx, byteBuf); + } + + // So apparently, this is how ViaVersion hacks around bungeecord not supporting sending empty packets + if (!ByteBufHelper.isReadable(byteBuf)) { + throw CancelPacketException.INSTANCE; + } + + list.add(byteBuf.retain()); + } + + private PacketSendEvent handleClientBoundPacket(Channel channel, User user, Object player, ByteBuf buffer, ChannelPromise promise) throws Exception { + PacketSendEvent packetSendEvent = PacketEventsImplHelper.handleClientBoundPacket(channel, user, player, buffer, true); + if (packetSendEvent.hasTasksAfterSend()) { + promise.addListener((p) -> { + for (Runnable task : packetSendEvent.getTasksAfterSend()) { + task.run(); + } + }); + } + return packetSendEvent; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + // We must restore the old promise (in case we are stacking promises such as sending packets on send event) + // If the old promise was successful, set it to null to avoid memory leaks. + ChannelPromise oldPromise = this.promise != null && !this.promise.isSuccess() ? this.promise : null; + promise.addListener(p -> this.promise = oldPromise); + + this.promise = promise; + super.write(ctx, msg, promise); + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // This is a terrible hack (to support bungee), I think we should use something other than a MessageToMessageEncoder + if (ExceptionUtil.isException(cause, CancelPacketException.class)) { + return; + } + // Ignore how mojang sends DISCONNECT packets in the wrong state + if (ExceptionUtil.isException(cause, InvalidDisconnectPacketSend.class)) { + return; + } + + boolean didWeCauseThis = ExceptionUtil.isException(cause, PacketProcessException.class); + + if (didWeCauseThis && user != null && user.getEncoderState() != ConnectionState.HANDSHAKING) { + // Ignore handshaking exceptions + cause.printStackTrace(); + return; + } + + super.exceptionCaught(ctx, cause); + } + + private void compress(ChannelHandlerContext ctx, ByteBuf input) throws InvocationTargetException { + ChannelHandler compressor = ctx.pipeline().get("compress"); + ByteBuf temp = ctx.alloc().buffer(); + try { + if (compressor != null) { + CustomPipelineUtil.callEncode(compressor, ctx, input, temp); + } + } finally { + input.clear().writeBytes(temp); + temp.release(); + } + } + + private void decompress(ChannelHandlerContext ctx, ByteBuf input, ByteBuf output) throws InvocationTargetException { + ChannelHandler decompressor = ctx.pipeline().get("decompress"); + if (decompressor != null) { + ByteBuf temp = (ByteBuf) CustomPipelineUtil.callDecode(decompressor, ctx, input).get(0); + try { + output.clear().writeBytes(temp); + } finally { + temp.release(); + } + } + } + + private boolean handleCompression(ChannelHandlerContext ctx, ByteBuf buffer) throws InvocationTargetException { + if (handledCompression) return false; + int compressIndex = ctx.pipeline().names().indexOf("compress"); + if (compressIndex == -1) return false; + handledCompression = true; + int peEncoderIndex = ctx.pipeline().names().indexOf(PacketEvents.ENCODER_NAME); + if (peEncoderIndex == -1) return false; + if (compressIndex > peEncoderIndex) { + //We are ahead of the decompression handler (they are added dynamically) so let us relocate. + //But first we need to compress the data and re-compress it after we do all our processing to avoid issues. + decompress(ctx, buffer, buffer); + //Let us relocate and no longer deal with compression. + PacketEventsDecoder decoder = (PacketEventsDecoder) ctx.pipeline().get(PacketEvents.DECODER_NAME); + ServerConnectionInitializer.relocateHandlers(ctx.channel(), decoder, user); + return true; + } + return false; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/InternalSpongePacketListener.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/InternalSpongePacketListener.java new file mode 100644 index 0000000000..a8691b06d7 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/InternalSpongePacketListener.java @@ -0,0 +1,47 @@ +package io.github.retrooper.packetevents.sponge.manager; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.manager.InternalPacketListener; +import com.github.retrooper.packetevents.protocol.ConnectionState; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.handshaking.client.WrapperHandshakingClientHandshake; +import io.github.retrooper.packetevents.sponge.util.viaversion.ViaVersionUtil; + +import java.net.InetSocketAddress; + +public class InternalSpongePacketListener extends InternalPacketListener { + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + User user = event.getUser(); + if (event.getPacketType() == PacketType.Handshaking.Client.HANDSHAKE) { + InetSocketAddress address = event.getSocketAddress(); + WrapperHandshakingClientHandshake handshake = new WrapperHandshakingClientHandshake(event); + ConnectionState nextState = handshake.getNextConnectionState(); + ClientVersion clientVersion = handshake.getClientVersion(); + + PacketEvents.getAPI().getLogManager().debug("Read handshake version for " + address.getHostString() + ":" + address.getPort() + " as " + clientVersion); + + if (ViaVersionUtil.isAvailable()) { + clientVersion = ClientVersion.getById(ViaVersionUtil.getProtocolVersion(user)); + PacketEvents.getAPI().getLogManager().debug("Read ViaVersion version for " + address.getHostString() + ":" + address.getPort() + " as " + clientVersion + " with UUID=" + user.getUUID()); + } + + if (clientVersion == ClientVersion.UNKNOWN) { + PacketEvents.getAPI().getLogManager().debug("Client version for " + address.getHostString() + ":" + address.getPort() + " is unknown!"); + } + + // Update client version for this event call (and user) + user.setClientVersion(clientVersion); + PacketEvents.getAPI().getLogManager().debug("Processed " + address.getHostString() + ":" + address.getPort() + "'s client version. Client Version: " + clientVersion.getReleaseName()); + + // Transition into LOGIN or STATUS connection state immediately, to remain in sync with vanilla + user.setConnectionState(nextState); + } else { + super.onPacketReceive(event); + } + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/player/PlayerManagerImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/player/PlayerManagerImpl.java new file mode 100644 index 0000000000..c5bf8eebc9 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/player/PlayerManagerImpl.java @@ -0,0 +1,87 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.github.retrooper.packetevents.sponge.manager.player; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.player.PlayerManager; +import com.github.retrooper.packetevents.manager.protocol.ProtocolManager; +import com.github.retrooper.packetevents.netty.channel.ChannelHelper; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.User; +import io.github.retrooper.packetevents.sponge.util.SpongeReflectionUtil; +import io.github.retrooper.packetevents.sponge.util.viaversion.ViaVersionUtil; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.UUID; + +public class PlayerManagerImpl implements PlayerManager { + @Override + public int getPing(@NotNull Object player) { + return ((ServerPlayer) player).connection().latency(); + } + + @Override + public @NotNull ClientVersion getClientVersion(@NotNull Object p) { + ServerPlayer player = (ServerPlayer) p; + User user = getUser(player); + if (user == null) return ClientVersion.UNKNOWN; + if (user.getClientVersion() == null) { + int protocolVersion; + if (ViaVersionUtil.isAvailable()) { + protocolVersion = ViaVersionUtil.getProtocolVersion(player); + PacketEvents.getAPI().getLogManager().debug("Requested ViaVersion for " + player.name() + "'s protocol version. Protocol version: " + protocolVersion); + } else { + //No protocol translation plugins available, the client must be the same version as the server. + protocolVersion = PacketEvents.getAPI().getServerManager().getVersion().getProtocolVersion(); + PacketEvents.getAPI().getLogManager().debug("No protocol translation plugins are available. We will assume " + user.getName() + "'s protocol version is the same as the server's protocol version. Protocol version: " + protocolVersion); + } + ClientVersion version = ClientVersion.getById(protocolVersion); + user.setClientVersion(version); + } + return user.getClientVersion(); + } + + @Override + public Object getChannel(@NotNull Object player) { + UUID uuid = ((ServerPlayer) player).uniqueId(); + Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(uuid); + if (channel == null) { + channel = SpongeReflectionUtil.getChannel((ServerPlayer) player); + // This is removed from the HashMap on channel close + // So if the channel is already closed, there will be a memory leak if we add an offline player + if (channel != null) { + synchronized (channel) { + if (ChannelHelper.isOpen(channel)) { + ProtocolManager.CHANNELS.put(uuid, channel); + } + } + } + } + return channel; + } + + @Override + public User getUser(@NotNull Object player) { + ServerPlayer p = (ServerPlayer) player; + Object channel = getChannel(p); + + if (channel == null) return null; + return PacketEvents.getAPI().getProtocolManager().getUser(channel); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/protocol/ProtocolManagerImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/protocol/ProtocolManagerImpl.java new file mode 100644 index 0000000000..807d0b2d7d --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/protocol/ProtocolManagerImpl.java @@ -0,0 +1,132 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.manager.protocol; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.protocol.ProtocolManager; +import com.github.retrooper.packetevents.netty.channel.ChannelHelper; +import com.github.retrooper.packetevents.protocol.ProtocolVersion; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.User; +import io.netty.buffer.ByteBuf; + +import java.util.List; + +public class ProtocolManagerImpl implements ProtocolManager { + private ProtocolVersion platformVersion; + + //TODO Implement + private ProtocolVersion resolveVersionNoCache() { + return ProtocolVersion.UNKNOWN; + } + + @Override + public ProtocolVersion getPlatformVersion() { + if (platformVersion == null) { + platformVersion = resolveVersionNoCache(); + } + return platformVersion; + } + + @Override + public void sendPacket(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + ChannelHelper.writeAndFlush(channel, byteBuf); + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public void sendPacketSilently(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + //Only call the encoders after ours in the pipeline. + //Here we do not need to retain when ProtocolSupport is present + ChannelHelper.writeAndFlushInContext(channel, PacketEvents.ENCODER_NAME, byteBuf); + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public void writePacket(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + //Write to all encoders. + ChannelHelper.write(channel, byteBuf); + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public void writePacketSilently(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + //Only call the encoders after ours in the pipeline + //Here we do not need to retain when ProtocolSupport is present + ChannelHelper.writeInContext(channel, PacketEvents.ENCODER_NAME, byteBuf); + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public void receivePacket(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + List handlerNames = ChannelHelper.pipelineHandlerNames(channel); + //Account for ViaVersion + if (handlerNames.contains("encoder")) { + ChannelHelper.fireChannelReadInContext(channel, "decoder", byteBuf); + } else if (handlerNames.contains("decompress")) { + //We will have to just skip through the minecraft server's decompression handler + ChannelHelper.fireChannelReadInContext(channel, "decompress", byteBuf); + } else { + if (handlerNames.contains("decrypt")) { + //We will have to just skip through the minecraft server's decryption handler + //We don't have to deal with decompressing, as that handler isn't currently in the pipeline + ChannelHelper.fireChannelReadInContext(channel, "decrypt", byteBuf); + } else { + //No decompressing nor decrypting handlers are present + //You cannot fill this buffer up with chunks of packets, + //since we skip the packet-splitter handler. + ChannelHelper.fireChannelReadInContext(channel, "splitter", byteBuf); + } + } + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public void receivePacketSilently(Object channel, Object byteBuf) { + if (ChannelHelper.isOpen(channel)) { + ChannelHelper.fireChannelReadInContext(channel, PacketEvents.DECODER_NAME, byteBuf); + } else { + ((ByteBuf) byteBuf).release(); + } + } + + @Override + public ClientVersion getClientVersion(Object channel) { + User user = getUser(channel); + if (user.getClientVersion() == null) { + return ClientVersion.UNKNOWN; + } + return user.getClientVersion(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/server/ServerManagerImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/server/ServerManagerImpl.java new file mode 100644 index 0000000000..b852cf8138 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/manager/server/ServerManagerImpl.java @@ -0,0 +1,67 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.manager.server; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerManager; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.util.PEVersion; +import org.spongepowered.api.Sponge; +import org.spongepowered.plugin.PluginContainer; + +public class ServerManagerImpl implements ServerManager { + + private ServerVersion serverVersion; + + private ServerVersion resolveVersionNoCache() { + PluginContainer plugin = (PluginContainer) PacketEvents.getAPI().getPlugin(); + String minecraftRelease = Sponge.platform().minecraftVersion().name(); + ServerVersion fallbackVersion = ServerVersion.getLatest(); + + // Our PEVersion class can parse this version and detect if it is a newer version than what is currently supported + // and account for that properly + PEVersion version = new PEVersion(minecraftRelease); + PEVersion latestVersion = new PEVersion(ServerVersion.getLatest().getReleaseName()); + if (version.isNewerThan(latestVersion)) { + //We do not support this version yet, so let us warn the user + plugin.logger().warn("[packetevents] We currently do not support the minecraft version " + + version + ", so things might break. PacketEvents will behave as if the minecraft version were " + + latestVersion + "!"); + return ServerVersion.getLatest(); + } + + for (final ServerVersion val : ServerVersion.reversedValues()) { + // For example "V_1_18" -> "1.18" + if (minecraftRelease.contains(val.getReleaseName())) { + return val; + } + } + + plugin.logger().warn("[packetevents] Your server software is preventing us from checking the server version. This is what we found: " + minecraftRelease + ". We will assume the server version is " + fallbackVersion.name() + "..."); + return fallbackVersion; + } + + @Override + public ServerVersion getVersion() { + if (serverVersion == null) { + serverVersion = resolveVersionNoCache(); + } + return serverVersion; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/NettyManagerImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/NettyManagerImpl.java new file mode 100644 index 0000000000..92f5e9ad40 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/NettyManagerImpl.java @@ -0,0 +1,48 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.netty; + +import com.github.retrooper.packetevents.netty.NettyManager; +import com.github.retrooper.packetevents.netty.buffer.ByteBufAllocationOperator; +import com.github.retrooper.packetevents.netty.buffer.ByteBufOperator; +import com.github.retrooper.packetevents.netty.channel.ChannelOperator; +import io.github.retrooper.packetevents.sponge.netty.buffer.ByteBufAllocationOperatorModernImpl; +import io.github.retrooper.packetevents.sponge.netty.buffer.ByteBufOperatorModernImpl; +import io.github.retrooper.packetevents.sponge.netty.channel.ChannelOperatorModernImpl; + +public class NettyManagerImpl implements NettyManager { + private static final ByteBufOperator BYTE_BUF_OPERATOR = new ByteBufOperatorModernImpl(); + private static final ByteBufAllocationOperator BYTE_BUF_ALLOCATION_OPERATOR = new ByteBufAllocationOperatorModernImpl(); + private static final ChannelOperator CHANNEL_OPERATOR = new ChannelOperatorModernImpl(); + + @Override + public ChannelOperator getChannelOperator() { + return CHANNEL_OPERATOR; + } + + @Override + public ByteBufOperator getByteBufOperator() { + return BYTE_BUF_OPERATOR; + } + + @Override + public ByteBufAllocationOperator getByteBufAllocationOperator() { + return BYTE_BUF_ALLOCATION_OPERATOR; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufAllocationOperatorModernImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufAllocationOperatorModernImpl.java new file mode 100644 index 0000000000..a288ed810e --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufAllocationOperatorModernImpl.java @@ -0,0 +1,49 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.netty.buffer; + +import com.github.retrooper.packetevents.netty.buffer.ByteBufAllocationOperator; +import io.netty.buffer.Unpooled; + +public class ByteBufAllocationOperatorModernImpl implements ByteBufAllocationOperator { + @Override + public Object wrappedBuffer(byte[] bytes) { + return Unpooled.wrappedBuffer(bytes); + } + + @Override + public Object copiedBuffer(byte[] bytes) { + return Unpooled.copiedBuffer(bytes); + } + + @Override + public Object buffer() { + return Unpooled.buffer(); + } + + @Override + public Object directBuffer() { + return Unpooled.directBuffer(); + } + + @Override + public Object compositeBuffer() { + return Unpooled.compositeBuffer(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufOperatorModernImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufOperatorModernImpl.java new file mode 100644 index 0000000000..c28920973b --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/buffer/ByteBufOperatorModernImpl.java @@ -0,0 +1,237 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.netty.buffer; + +import com.github.retrooper.packetevents.netty.buffer.ByteBufOperator; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.Charset; + +public class ByteBufOperatorModernImpl implements ByteBufOperator { + @Override + public int capacity(Object buffer) { + return ((ByteBuf)buffer).capacity(); + } + + @Override + public Object capacity(Object buffer, int capacity) { + return ((ByteBuf)buffer).capacity(capacity); + } + + @Override + public int readerIndex(Object buffer) { + return ((ByteBuf)buffer).readerIndex(); + } + + @Override + public Object readerIndex(Object buffer, int readerIndex) { + return ((ByteBuf)buffer).readerIndex(readerIndex); + } + + @Override + public int writerIndex(Object buffer) { + return ((ByteBuf)buffer).writerIndex(); + } + + @Override + public Object writerIndex(Object buffer, int writerIndex) { + return ((ByteBuf)buffer).writerIndex(writerIndex); + } + + @Override + public int readableBytes(Object buffer) { + return ((ByteBuf)buffer).readableBytes(); + } + + @Override + public int writableBytes(Object buffer) { + return ((ByteBuf)buffer).writableBytes(); + } + + @Override + public Object clear(Object buffer) { + return ((ByteBuf)buffer).clear(); + } + + @Override + public byte readByte(Object buffer) { + return ((ByteBuf)buffer).readByte(); + } + + @Override + public short readShort(Object buffer) { + return ((ByteBuf)buffer).readShort(); + } + + @Override + public int readInt(Object buffer) { + return ((ByteBuf)buffer).readInt(); + } + + @Override + public long readUnsignedInt(Object buffer) { + return ((ByteBuf)buffer).readUnsignedInt(); + } + + @Override + public long readLong(Object buffer) { + return ((ByteBuf)buffer).readLong(); + } + + + @Override + public void writeByte(Object buffer, int value) { + ((ByteBuf)buffer).writeByte(value); + } + + @Override + public void writeShort(Object buffer, int value) { + ((ByteBuf)buffer).writeShort(value); + } + + @Override + public void writeInt(Object buffer, int value) { + ((ByteBuf)buffer).writeInt(value); + } + + @Override + public void writeLong(Object buffer, long value) { + ((ByteBuf)buffer).writeLong(value); + } + + @Override + public Object getBytes(Object buffer, int index, byte[] destination) { + return ((ByteBuf)buffer).getBytes(index, destination); + } + + @Override + public short getUnsignedByte(Object buffer, int index) { + return ((ByteBuf)buffer).getUnsignedByte(index); + } + + @Override + public boolean isReadable(Object buffer) { + return ((ByteBuf)buffer).isReadable(); + } + + @Override + public Object copy(Object buffer) { + return ((ByteBuf)buffer).copy(); + } + + @Override + public Object duplicate(Object buffer) { + return ((ByteBuf)buffer).duplicate(); + } + + @Override + public boolean hasArray(Object buffer) { + return ((ByteBuf)buffer).hasArray(); + } + + @Override + public byte[] array(Object buffer) { + return ((ByteBuf)buffer).array(); + } + + @Override + public Object retain(Object buffer) { + return ((ByteBuf)buffer).retain(); + } + + @Override + public Object retainedDuplicate(Object buffer) { + return ((ByteBuf)buffer).duplicate().retain(); + } + + @Override + public Object readSlice(Object buffer, int length) { + return ((ByteBuf)buffer).readSlice(length); + } + + @Override + public Object readBytes(Object buffer, byte[] destination, int destinationIndex, int length) { + return ((ByteBuf)buffer).readBytes(destination, destinationIndex, length); + } + + @Override + public Object readBytes(Object buffer, int length) { + return ((ByteBuf)buffer).readBytes(length); + } + + @Override + public Object writeBytes(Object buffer, Object src) { + return ((ByteBuf)buffer).writeBytes((ByteBuf) src); + } + + @Override + public Object writeBytes(Object buffer, byte[] bytes) { + return ((ByteBuf)buffer).writeBytes(bytes); + } + + @Override + public Object writeBytes(Object buffer, byte[] bytes, int offset, int length) { + return ((ByteBuf)buffer).writeBytes(bytes, offset, length); + } + + @Override + public void readBytes(Object buffer, byte[] bytes) { + ((ByteBuf)buffer).readBytes(bytes); + } + + @Override + public boolean release(Object buffer) { + return ((ByteBuf)buffer).release(); + } + + @Override + public int refCnt(Object buffer) { + return ((ByteBuf)buffer).refCnt(); + } + + @Override + public Object skipBytes(Object buffer, int length) { + return ((ByteBuf)buffer).skipBytes(length); + } + + @Override + public String toString(Object buffer, int index, int length, Charset charset) { + return ((ByteBuf)buffer).toString(index, length, charset); + } + + @Override + public Object markReaderIndex(Object buffer) { + return ((ByteBuf)buffer).markReaderIndex(); + } + + @Override + public Object resetReaderIndex(Object buffer) { + return ((ByteBuf)buffer).resetReaderIndex(); + } + + @Override + public Object markWriterIndex(Object buffer) { + return ((ByteBuf)buffer).markWriterIndex(); + } + + @Override + public Object resetWriterIndex(Object buffer) { + return ((ByteBuf)buffer).resetWriterIndex(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/channel/ChannelOperatorModernImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/channel/ChannelOperatorModernImpl.java new file mode 100644 index 0000000000..a537322b07 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/netty/channel/ChannelOperatorModernImpl.java @@ -0,0 +1,117 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.netty.channel; + +import com.github.retrooper.packetevents.netty.channel.ChannelOperator; +import io.netty.channel.Channel; + +import java.net.SocketAddress; +import java.util.List; + +public class ChannelOperatorModernImpl implements ChannelOperator { + @Override + public SocketAddress remoteAddress(Object channel) { + return ((Channel) channel).remoteAddress(); + } + + @Override + public SocketAddress localAddress(Object channel) { + return ((Channel) channel).localAddress(); + } + + @Override + public boolean isOpen(Object channel) { + return ((Channel) channel).isOpen(); + } + + @Override + public Object close(Object channel) { + return ((Channel) channel).close(); + } + + @Override + public Object write(Object channel, Object buffer) { + return ((Channel) channel).write(buffer); + } + + @Override + public Object flush(Object channel) { + return ((Channel) channel).flush(); + } + + @Override + public Object writeAndFlush(Object channel, Object buffer) { + return ((Channel) channel).writeAndFlush(buffer); + } + + @Override + public Object fireChannelRead(Object channel, Object buffer) { + return ((Channel) channel).pipeline().fireChannelRead(buffer); + } + + @Override + public Object writeInContext(Object channel, String ctx, Object buffer) { + return ((Channel) channel).pipeline().context(ctx).write(buffer); + } + + @Override + public Object flushInContext(Object channel, String ctx) { + return ((Channel) channel).pipeline().context(ctx).flush(); + } + + @Override + public Object writeAndFlushInContext(Object channel, String ctx, Object buffer) { + return ((Channel) channel).pipeline().context(ctx).writeAndFlush(buffer); + } + + @Override + public Object getPipeline(Object channel) { + return ((Channel) channel).pipeline(); + } + + @Override + public Object fireChannelReadInContext(Object channel, String ctx, Object buffer) { + return ((Channel) channel).pipeline().context(ctx).fireChannelRead(buffer); + } + + @Override + public List pipelineHandlerNames(Object channel) { + return ((Channel) channel).pipeline().names(); + } + + @Override + public Object getPipelineHandler(Object channel, String name) { + return ((Channel) channel).pipeline().get(name); + } + + @Override + public Object getPipelineContext(Object channel, String name) { + return ((Channel) channel).pipeline().context(name); + } + + @Override + public void runInEventLoop(Object channel, Runnable runnable) { + ((Channel) channel).eventLoop().execute(runnable); + } + + @Override + public Object pooledByteBuf(Object o) { + return ((Channel) o).alloc().buffer(); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/InjectedList.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/InjectedList.java new file mode 100644 index 0000000000..f0cb947685 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/InjectedList.java @@ -0,0 +1,177 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Consumer; + +public class InjectedList implements List { + private final List originalList; + private final Consumer pushBackAction; + + public InjectedList(List originalList, Consumer pushBackAction) { + for (E key : originalList) { + pushBackAction.accept(key); + } + this.originalList = originalList; + this.pushBackAction = pushBackAction; + } + + public List originalList() { + return originalList; + } + + public Consumer pushBackAction() { + return pushBackAction; + } + + @Override + public synchronized boolean add(E e) { + pushBackAction.accept(e); + return originalList.add(e); + } + + @Override + public synchronized boolean addAll(@NotNull Collection c) { + for (E element : c) { + pushBackAction.accept(element); + } + return originalList.addAll(c); + } + + @Override + public synchronized boolean addAll(int index, @NotNull Collection c) { + for (E element : c) { + pushBackAction.accept(element); + } + return originalList.addAll(index, c); + } + + @Override + public synchronized void add(int index, E element) { + pushBackAction.accept(element); + originalList.add(index, element); + } + + @Override + public synchronized int size() { + return originalList.size(); + } + + @Override + public synchronized boolean isEmpty() { + return originalList.isEmpty(); + } + + @Override + public synchronized boolean contains(Object o) { + return originalList.contains(o); + } + + @NotNull + @Override + public synchronized Iterator iterator() { + return originalList.iterator(); + } + + @NotNull + @Override + public synchronized Object[] toArray() { + return originalList.toArray(); + } + + @NotNull + @Override + public synchronized T[] toArray(@NotNull T[] a) { + return originalList.toArray(a); + } + + @Override + public synchronized boolean remove(Object o) { + return originalList.remove(o); + } + + @Override + public synchronized boolean containsAll(@NotNull Collection c) { + return originalList.containsAll(c); + } + + @Override + public synchronized boolean removeAll(@NotNull Collection c) { + return originalList.removeAll(c); + } + + @Override + public synchronized boolean retainAll(@NotNull Collection c) { + return originalList.retainAll(c); + } + + @Override + public synchronized void clear() { + originalList.clear(); + } + + @Override + public synchronized E get(int index) { + return originalList.get(index); + } + + @Override + public synchronized E set(int index, E element) { + return originalList.set(index, element); + } + + @Override + public synchronized E remove(int index) { + return originalList.remove(index); + } + + @Override + public synchronized int indexOf(Object o) { + return originalList.indexOf(o); + } + + @Override + public synchronized int lastIndexOf(Object o) { + return originalList.lastIndexOf(o); + } + + @NotNull + @Override + public synchronized ListIterator listIterator() { + return originalList.listIterator(); + } + + @NotNull + @Override + public synchronized ListIterator listIterator(int index) { + return originalList.listIterator(index); + } + + @NotNull + @Override + public synchronized List subList(int fromIndex, int toIndex) { + return originalList.subList(fromIndex, toIndex); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeConversionUtil.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeConversionUtil.java new file mode 100644 index 0000000000..db5f503f24 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeConversionUtil.java @@ -0,0 +1,43 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; +import org.spongepowered.api.block.BlockState; + +public class SpongeConversionUtil { + + public static ItemStack fromSpongeItemStack(org.spongepowered.api.item.inventory.ItemStack itemStack) { + return SpongeReflectionUtil.decodeSpongeItemStack(itemStack); + } + + public static org.spongepowered.api.item.inventory.ItemStack toSpongeItemStack(ItemStack itemStack) { + return SpongeReflectionUtil.encodeSpongeItemStack(itemStack); + } + + public static WrappedBlockState fromSpongeBlockState(BlockState blockState) { + return WrappedBlockState.getByString(PacketEvents.getAPI().getServerManager().getVersion().toClientVersion(), blockState.asString()); + } + + public static BlockState toSpongeBlockState(WrappedBlockState blockState) { + return BlockState.fromString(blockState.toString()); + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeLogManager.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeLogManager.java new file mode 100644 index 0000000000..87c5990e78 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeLogManager.java @@ -0,0 +1,60 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.util.LogManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.api.Sponge; + +import java.util.logging.Level; + +public class SpongeLogManager extends LogManager { + + private static final Component PREFIX = Component.text("[packetevents]", NamedTextColor.AQUA).append(Component.text(" ", NamedTextColor.WHITE)); + + @Override + protected void log(Level level, @Nullable NamedTextColor color, String message) { + Sponge.systemSubject().sendMessage(PREFIX.append(Component.text(message, color))); + } + + @Override + public void info(String message) { + log(Level.INFO, NamedTextColor.WHITE, message); + } + + @Override + public void warn(final String message) { + log(Level.WARNING, NamedTextColor.YELLOW, message); + } + + @Override + public void severe(String message) { + log(Level.SEVERE, NamedTextColor.RED, message); + } + + @Override + public void debug(String message) { + if (PacketEvents.getAPI().getSettings().isDebugEnabled()) { + log(Level.FINE, NamedTextColor.GRAY, message); + } + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeReflectionUtil.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeReflectionUtil.java new file mode 100644 index 0000000000..16ca1f2932 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/SpongeReflectionUtil.java @@ -0,0 +1,319 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; +import com.github.retrooper.packetevents.netty.buffer.UnpooledByteBufAllocationHelper; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.util.reflection.Reflection; +import com.github.retrooper.packetevents.util.reflection.ReflectionObject; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.Channel; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.item.inventory.ItemStack; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +public final class SpongeReflectionUtil { + + public static ServerVersion VERSION; + + // Minecraft classes + public static Class MINECRAFT_SERVER_CLASS, NMS_PACKET_DATA_SERIALIZER_CLASS, NMS_ITEM_STACK_CLASS, + ENTITY_PLAYER_CLASS, + NMS_MINECRAFT_KEY_CLASS, + PLAYER_CONNECTION_CLASS, SERVER_COMMON_PACKETLISTENER_IMPL_CLASS, SERVER_CONNECTION_CLASS, NETWORK_MANAGER_CLASS, + NMS_NBT_COMPOUND_CLASS, NBT_COMPRESSION_STREAM_TOOLS_CLASS; + + // Fields + public static Field BYTE_BUF_IN_PACKET_DATA_SERIALIZER, NMS_MK_KEY_FIELD; + + // Methods + public static Method IS_DEBUGGING, + READ_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD, + WRITE_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD, + READ_NBT_FROM_STREAM_METHOD, WRITE_NBT_TO_STREAM_METHOD; + + // Constructors + private static Constructor NMS_PACKET_DATA_SERIALIZER_CONSTRUCTOR; + + private static Object MINECRAFT_SERVER_CONNECTION_INSTANCE; + + private static void initConstructors() { + try { + NMS_PACKET_DATA_SERIALIZER_CONSTRUCTOR = NMS_PACKET_DATA_SERIALIZER_CLASS.getConstructor(ByteBuf.class); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private static void initMethods() { + IS_DEBUGGING = Reflection.getMethod(MINECRAFT_SERVER_CLASS, "isDebugging", 0); + + READ_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD = Reflection.getMethod(NMS_PACKET_DATA_SERIALIZER_CLASS, NMS_ITEM_STACK_CLASS, 0); + WRITE_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD = Reflection.getMethod(NMS_PACKET_DATA_SERIALIZER_CLASS, NMS_PACKET_DATA_SERIALIZER_CLASS, 0, NMS_ITEM_STACK_CLASS); + + READ_NBT_FROM_STREAM_METHOD = Reflection.getMethod(NBT_COMPRESSION_STREAM_TOOLS_CLASS, 0, DataInputStream.class); + if (READ_NBT_FROM_STREAM_METHOD == null) { + READ_NBT_FROM_STREAM_METHOD = Reflection.getMethod(NBT_COMPRESSION_STREAM_TOOLS_CLASS, 0, DataInput.class); + } + WRITE_NBT_TO_STREAM_METHOD = Reflection.getMethod(NBT_COMPRESSION_STREAM_TOOLS_CLASS, 0, NMS_NBT_COMPOUND_CLASS, DataOutput.class); + } + + private static void initFields() { + BYTE_BUF_IN_PACKET_DATA_SERIALIZER = Reflection.getField(NMS_PACKET_DATA_SERIALIZER_CLASS, ByteBuf.class, 0, true); + NMS_MK_KEY_FIELD = Reflection.getField(NMS_MINECRAFT_KEY_CLASS, "key"); + } + + private static void initClasses() { + MINECRAFT_SERVER_CLASS = getServerClass("server.MinecraftServer"); + NMS_PACKET_DATA_SERIALIZER_CLASS = getServerClass("network.FriendlyByteBuf"); + NMS_ITEM_STACK_CLASS = getServerClass("world.item.ItemStack"); + NMS_MINECRAFT_KEY_CLASS = getServerClass("resources.ResourceLocation"); + + ENTITY_PLAYER_CLASS = getServerClass("server.level.ServerPlayer"); + + PLAYER_CONNECTION_CLASS = getServerClass("server.network.ServerGamePacketListenerImpl"); + + //Only on 1.20.2 + SERVER_COMMON_PACKETLISTENER_IMPL_CLASS = getServerClass("server.network.ServerCommonPacketListenerImpl"); + + SERVER_CONNECTION_CLASS = getServerClass("server.network.ServerConnectionListener"); + NETWORK_MANAGER_CLASS = getServerClass("network.Connection"); + + NMS_NBT_COMPOUND_CLASS = getServerClass("nbt.CompoundTag"); + NBT_COMPRESSION_STREAM_TOOLS_CLASS = getServerClass("nbt.NbtIo"); + } + + public static void init() { + VERSION = PacketEvents.getAPI().getServerManager().getVersion(); + + initClasses(); + initFields(); + initMethods(); + initConstructors(); + } + + @Nullable + public static Class getServerClass(String modern) { + return Reflection.getClassByNameWithoutException("net.minecraft." + modern); + } + + public static boolean isMinecraftServerInstanceDebugging() { + if (IS_DEBUGGING != null) { + try { + return (boolean) IS_DEBUGGING.invoke(Sponge.server()); + } catch (IllegalAccessException | InvocationTargetException e) { + IS_DEBUGGING = null; + return false; + } + } + return false; + } + + public static Object getMinecraftServerConnectionInstance() { + if (MINECRAFT_SERVER_CONNECTION_INSTANCE == null) { + try { + MINECRAFT_SERVER_CONNECTION_INSTANCE = Reflection.getField(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(Sponge.server()); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return MINECRAFT_SERVER_CONNECTION_INSTANCE; + } + + public static Class getNettyClass(String name) { + return Reflection.getClassByNameWithoutException("io.netty." + name); + } + + public static List getNetworkManagers() { + ReflectionObject serverConnectionWrapper = new ReflectionObject(getMinecraftServerConnectionInstance()); + for (int i = 0; true; i++) { + try { + List list = (List) serverConnectionWrapper.readObject(i, List.class); + for (Object obj : list) { + if (obj.getClass().isAssignableFrom(NETWORK_MANAGER_CLASS)) { + return (List) list; + } + } + } catch (Exception ex) { + break; + } + } + + return (List) serverConnectionWrapper.readObject(1, List.class); + } + + public static Object getNetworkManager(ServerPlayer player) { + Object playerConnection = getPlayerConnection(player); + if (playerConnection == null) { + return null; + } + Class playerConnectionClass = SERVER_COMMON_PACKETLISTENER_IMPL_CLASS != null ? + SERVER_COMMON_PACKETLISTENER_IMPL_CLASS : PLAYER_CONNECTION_CLASS; + ReflectionObject wrapper = new ReflectionObject(playerConnection, playerConnectionClass); + try { + return wrapper.readObject(0, NETWORK_MANAGER_CLASS); + } catch (Exception ex) { + //Support for some weird custom plugins. + try { + playerConnection = wrapper.read(0, PLAYER_CONNECTION_CLASS); + wrapper = new ReflectionObject(playerConnection, PLAYER_CONNECTION_CLASS); + return wrapper.readObject(0, NETWORK_MANAGER_CLASS); + } + catch (Exception ex2) { + //Print the original error! + ex.printStackTrace(); + } + } + return null; + } + + public static Object getChannel(ServerPlayer player) { + Object networkManager = getNetworkManager(player); + if (networkManager == null) { + return null; + } + ReflectionObject wrapper = new ReflectionObject(networkManager, NETWORK_MANAGER_CLASS); + return wrapper.readObject(0, Channel.class); + } + + public static Object getPlayerConnection(ServerPlayer player) { + ReflectionObject wrappedEntityPlayer = new ReflectionObject(player, ENTITY_PLAYER_CLASS); + return wrappedEntityPlayer.readObject(0, PLAYER_CONNECTION_CLASS); + } + + public static com.github.retrooper.packetevents.protocol.item.ItemStack decodeSpongeItemStack(ItemStack in) { + Object buffer = PooledByteBufAllocator.DEFAULT.buffer(); + //3 reflection calls + Object packetDataSerializer = createPacketDataSerializer(buffer); + writeNMSItemStackPacketDataSerializer(packetDataSerializer, in); + //No more reflection from here on. + PacketWrapper wrapper = PacketWrapper.createUniversalPacketWrapper(buffer); + com.github.retrooper.packetevents.protocol.item.ItemStack stack = wrapper.readItemStack(); + ByteBufHelper.release(buffer); + return stack; + } + + public static ItemStack encodeSpongeItemStack(com.github.retrooper.packetevents.protocol.item.ItemStack in) { + Object buffer = PooledByteBufAllocator.DEFAULT.buffer(); + PacketWrapper wrapper = PacketWrapper.createUniversalPacketWrapper(buffer); + wrapper.writeItemStack(in); + //3 reflection calls + Object packetDataSerializer = createPacketDataSerializer(wrapper.getBuffer()); + Object nmsItemStack = readNMSItemStackPacketDataSerializer(packetDataSerializer); + ItemStack stack = (ItemStack) nmsItemStack; + ByteBufHelper.release(buffer); + return stack; + } + + public static Object createPacketDataSerializer(Object byteBuf) { + try { + return NMS_PACKET_DATA_SERIALIZER_CONSTRUCTOR.newInstance(byteBuf); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + public static Object readNMSItemStackPacketDataSerializer(Object packetDataSerializer) { + try { + return READ_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD.invoke(packetDataSerializer); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + public static Object writeNMSItemStackPacketDataSerializer(Object packetDataSerializer, Object nmsItemStack) { + try { + return WRITE_ITEM_STACK_IN_PACKET_DATA_SERIALIZER_METHOD.invoke(packetDataSerializer, nmsItemStack); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + public static NBTCompound fromMinecraftNBT(Object nbtCompound) { + byte[] bytes; + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream stream = new DataOutputStream(byteStream)) { + writeNmsNbtToStream(nbtCompound, stream); + bytes = byteStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + Object buffer = UnpooledByteBufAllocationHelper.wrappedBuffer(bytes); + PacketWrapper wrapper = PacketWrapper.createUniversalPacketWrapper(buffer); + NBTCompound nbt = wrapper.readNBT(); + ByteBufHelper.release(buffer); + return nbt; + } + + public static Object toMinecraftNBT(NBTCompound nbtCompound) { + Object buffer = UnpooledByteBufAllocationHelper.buffer(); + PacketWrapper wrapper = PacketWrapper.createUniversalPacketWrapper(buffer); + wrapper.writeNBT(nbtCompound); + byte[] bytes = ByteBufHelper.copyBytes(buffer); + ByteBufHelper.release(buffer); + try (ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); + DataInputStream stream = new DataInputStream(byteStream)) { + return readNmsNbtFromStream(stream); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static void writeNmsNbtToStream(Object compound, DataOutput out) { + try { + WRITE_NBT_TO_STREAM_METHOD.invoke(null, compound, out); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + public static Object readNmsNbtFromStream(DataInputStream in) { + try { + return READ_NBT_FROM_STREAM_METHOD.invoke(null, in); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/CustomPipelineUtil.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/CustomPipelineUtil.java new file mode 100644 index 0000000000..b28ef9b847 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/CustomPipelineUtil.java @@ -0,0 +1,108 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util.viaversion; + +import io.github.retrooper.packetevents.sponge.util.SpongeReflectionUtil; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public class CustomPipelineUtil { + private static Method DECODE_METHOD; + private static Method ENCODE_METHOD; + private static Method MTM_DECODE; + private static Method MTM_ENCODE; + + public static void init() { + Class channelHandlerContextClass = SpongeReflectionUtil.getNettyClass("channel.ChannelHandlerContext"); + try { + DECODE_METHOD = ByteToMessageDecoder.class.getDeclaredMethod("decode", channelHandlerContextClass, ByteBuf.class, List.class); + DECODE_METHOD.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + try { + ENCODE_METHOD = MessageToByteEncoder.class.getDeclaredMethod("encode", channelHandlerContextClass, Object.class, ByteBuf.class); + ENCODE_METHOD.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + try { + Class messageToMessageDecoderClass = + SpongeReflectionUtil.getNettyClass("handler.codec.MessageToMessageDecoder"); + MTM_DECODE = messageToMessageDecoderClass.getDeclaredMethod("decode", channelHandlerContextClass, Object.class, List.class); + MTM_DECODE.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + + try { + Class messageToMessageEncoderClass = + SpongeReflectionUtil.getNettyClass("handler.codec.MessageToMessageEncoder"); + MTM_ENCODE = messageToMessageEncoderClass.getDeclaredMethod("encode", channelHandlerContextClass, Object.class, List.class); + MTM_ENCODE.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + + public static List callDecode(Object decoder, Object ctx, Object input) throws InvocationTargetException { + List output = new ArrayList<>(); + try { + CustomPipelineUtil.DECODE_METHOD.invoke(decoder, ctx, input, output); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return output; + } + + public static void callEncode(Object encoder, Object ctx, Object msg, Object output) throws InvocationTargetException { + try { + CustomPipelineUtil.ENCODE_METHOD.invoke(encoder, ctx, msg, output); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + public static List callMTMEncode(Object encoder, Object ctx, Object msg) { + List output = new ArrayList<>(); + try { + MTM_ENCODE.invoke(encoder, ctx, msg, output); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return output; + } + + public static List callMTMDecode(Object decoder, Object ctx, Object msg) throws InvocationTargetException { + List output = new ArrayList<>(); + try { + MTM_DECODE.invoke(decoder, ctx, msg, output); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return output; + } +} + diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessor.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessor.java new file mode 100644 index 0000000000..01e6f6cd83 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessor.java @@ -0,0 +1,32 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util.viaversion; + +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +public interface ViaVersionAccessor { + + int getProtocolVersion(ServerPlayer player); + + Class getUserConnectionClass(); + + Class getSpongeDecodeHandlerClass(); + + Class getSpongeEncodeHandlerClass(); +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessorImpl.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessorImpl.java new file mode 100644 index 0000000000..981cddaf5a --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionAccessorImpl.java @@ -0,0 +1,48 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util.viaversion; + +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.sponge.handlers.SpongeDecodeHandler; +import com.viaversion.viaversion.sponge.handlers.SpongeEncodeHandler; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +public class ViaVersionAccessorImpl implements ViaVersionAccessor { + + @Override + public int getProtocolVersion(ServerPlayer player) { + return Via.getAPI().getPlayerVersion(player); + } + + @Override + public Class getUserConnectionClass() { + return UserConnection.class; + } + + @Override + public Class getSpongeDecodeHandlerClass() { + return SpongeDecodeHandler.class; + } + + @Override + public Class getSpongeEncodeHandlerClass() { + return SpongeEncodeHandler.class; + } +} diff --git a/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionUtil.java b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionUtil.java new file mode 100644 index 0000000000..6a59cde760 --- /dev/null +++ b/sponge/src/main/java/io/github/retrooper/packetevents/sponge/util/viaversion/ViaVersionUtil.java @@ -0,0 +1,111 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.retrooper.packetevents.sponge.util.viaversion; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.netty.channel.ChannelHelper; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.reflection.Reflection; +import io.netty.channel.Channel; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.Optional; + +public class ViaVersionUtil { + private static ViaState available = ViaState.UNKNOWN; + private static ViaVersionAccessor viaVersionAccessor; + + private ViaVersionUtil() { + } + + private static void load() { + if (viaVersionAccessor == null) { + try { + Class.forName("com.viaversion.viaversion.api.Via"); + viaVersionAccessor = new ViaVersionAccessorImpl(); + } catch (Exception e) { + viaVersionAccessor = null; + } + } + } + + public static void checkIfViaIsPresent() { + boolean present = Sponge.pluginManager().plugin("viaversion").isPresent(); + System.out.println("via present? " + present); + available = present ? ViaState.ENABLED : ViaState.DISABLED; + } + + public static boolean isAvailable() { + if (available == ViaState.UNKNOWN) { // Plugins haven't loaded... let's refer to whether we have a class + return getViaVersionAccessor() != null; + } + return available == ViaState.ENABLED; + } + + public static ViaVersionAccessor getViaVersionAccessor() { + load(); + return viaVersionAccessor; + } + + public static int getProtocolVersion(User user) { + try { + if (user.getUUID() != null) { + Optional player = Sponge.server().player(user.getUUID()); + if (player.isPresent()) { + int version = getProtocolVersion(player.get()); + // -1 means via hasn't gotten join event yet + if (version != -1) return version; + } + } + + System.out.println(ChannelHelper.pipelineHandlerNamesAsString(user.getChannel())); + Object viaEncoder = ((Channel) user.getChannel()).pipeline().get("encoder"); + Object connection = Reflection.getField(viaEncoder.getClass(), "connection").get(viaEncoder); + Object protocolInfo = Reflection.getField(connection.getClass(), "protocolInfo").get(connection); + return (int) Reflection.getField(protocolInfo.getClass(), "protocolVersion").get(protocolInfo); + } catch (Exception e) { + PacketEvents.getAPI().getLogManager().warn("Unable to grab ViaVersion client version for player!"); + e.printStackTrace(); + return -1; + } + } + + public static int getProtocolVersion(ServerPlayer player) { + return getViaVersionAccessor().getProtocolVersion(player); + } + + public static Class getUserConnectionClass() { + return getViaVersionAccessor().getUserConnectionClass(); + } + + public static Class getSpongeDecodeHandlerClass() { + return getViaVersionAccessor().getSpongeDecodeHandlerClass(); + } + + public static Class getSpongeEncodeHandlerClass() { + return getViaVersionAccessor().getSpongeEncodeHandlerClass(); + } +} + +enum ViaState { + UNKNOWN, + DISABLED, + ENABLED +}