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 extends E> c) {
+ for (E element : c) {
+ pushBackAction.accept(element);
+ }
+ return originalList.addAll(c);
+ }
+
+ @Override
+ public synchronized boolean addAll(int index, @NotNull Collection extends E> 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
+}