Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
46a5032
fabric: split into common / intermediary / official + 26.X aggregator
Axionize May 23, 2026
ff8662f
fabric: codex review #1 fixes — modid, MC range, JiJ, skip-worktree
Axionize May 23, 2026
2ef32fd
ci: bump CI JDK to 25 for fabric-official 26.X support
Axionize May 23, 2026
cbd2272
fabric-intermediary mcXXXX: add minecraft <26 upper bounds
Axionize May 23, 2026
fe7b6f3
fabric: erase MC types into fabric-common as Object-bridged surface
Axionize May 24, 2026
2d8b75d
fabric-official: wire PacketEventsMod entrypoint via fabric-common JiJ
Axionize May 24, 2026
0552275
fabric: re-export variant + common deps so consumers' POMs resolve
Axionize May 24, 2026
1390862
fabric-official: real Mojang-named FabricPlayerManager + ConnectionMixin
Axionize May 24, 2026
4af7ea4
fabric-official: ConnectionMixin param type must be BandwidthDebugMon…
Axionize May 24, 2026
b99c4b9
fabric: deduplicate JiJ — shared deps land once at aggregator level
Axionize May 24, 2026
79f1105
fabric: codex review fixes — tighten mc261 range, stub registry, rest…
Axionize May 24, 2026
1a09ba6
fabric: aggregator depends.minecraft → union to actually-supported ra…
Axionize May 24, 2026
40df24b
settings: restore upstream rootProject.name + workspace hook
Axionize May 24, 2026
0f9c92d
ci: bump release workflow JDK 21 → 25 to match fabric-official Gradle…
Axionize May 24, 2026
7c0447d
fabric-official: PlayerListMixin — fire UserLoginEvent on 26.X
Axionize May 24, 2026
32d3577
fix(26.X): correct serverbound PLAY packet ID ordering
Axionize May 25, 2026
1a5fbe9
fix(26.X): catch IOOBE in handleServerBoundPacket so events still fire
Axionize May 25, 2026
a36ca8e
fix(26.X): stop removing PE pipeline handlers on reinject — causes pl…
Axionize May 25, 2026
6094700
fix(26.X): catch IOOBE in handleClientBoundPacket too
Axionize May 25, 2026
ce37f25
fix(26.X): correct serverbound enum from bytecode instruction order
Axionize May 25, 2026
c42d404
chore: remove diagnostic printlns from 26.X pipeline code
Axionize May 25, 2026
33e5bb6
fix: widen mc1140 version range to match parent intermediary module
Axionize May 25, 2026
b9bc23c
review: address fabric-26.1.2-review feedback
Axionize May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/gradle-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
- name: Validate gradle wrapper
uses: gradle/actions/wrapper-validation@v5

- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'
cache: 'gradle'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ jobs:
- name: Validate gradle wrapper
uses: gradle/actions/wrapper-validation@v5

- name: Setup java 21
- name: Setup java 25
uses: actions/setup-java@v5
with:
java-version: 21
# fabric-official's LoomNoRemap setup needs Gradle on Java 25 because
# the MC 26.1.2 jar is Java 25 bytecode and Loom configures it during
# Gradle's config phase (before toolchain selection).
java-version: 25
distribution: temurin

- name: Prepare gradle
Expand Down
36 changes: 36 additions & 0 deletions fabric-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
packetevents.`library-conventions`
}

repositories {
maven("https://maven.fabricmc.net/")
maven("https://repo.viaversion.com/")
maven("https://repo.spongepowered.org/repository/maven-public/")
maven("https://jitpack.io")
}

dependencies {
compileOnly(libs.bundles.adventure)
compileOnly(project(":api", "shadow"))
compileOnly(project(":netty-common"))

compileOnly("net.fabricmc:fabric-loader:${rootProject.findProperty("loader_version") ?: "0.16.14"}")
compileOnly(libs.via.version)
compileOnly("org.slf4j:slf4j-api:2.0.16")
compileOnly("org.apache.logging.log4j:log4j-api:2.24.3")
// PacketEventsMixinManager extends a conditional-mixin base class, which itself
// extends Sponge Mixin's IMixinConfigPlugin.
compileOnly("com.github.Fallen-Breath.conditional-mixin:conditional-mixin-fabric:0.6.4")
compileOnly("org.spongepowered:mixin:0.8.7")
}

// Override library-conventions' release=8 — bridge code uses switch expressions
// and pattern-matching instanceof.
tasks.withType<JavaCompile> {
options.release = 17
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,9 @@
import io.github.retrooper.packetevents.loader.ChainLoadData;
import io.github.retrooper.packetevents.loader.ChainLoadEntryPoint;
import net.fabricmc.api.EnvType;
import com.github.retrooper.packetevents.protocol.PacketSide;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkSide;

import java.util.List;

Expand All @@ -39,21 +36,6 @@ public class PacketEventsMod implements PreLaunchEntrypoint, ModInitializer {
public static PacketEventsMod INSTANCE;
public static final String MOD_ID = "packetevents";


// isOurConnection() overloads are unused by our fork internally
// Methods kept for true ABI compatability with upstream PacketEvents
public static boolean isOurConnection(ClientConnection connection) {
return isOurConnection(connection.side);
}
public static boolean isOurConnection(NetworkSide flow) {
PacketSide connectionSide = switch (flow) {
case CLIENTBOUND -> PacketSide.CLIENT;
case SERVERBOUND -> PacketSide.SERVER;
};
PacketEventsAPI<?> api = PacketEvents.getAPI();
return api != null && api.getInjector().getPacketSide() == connectionSide;
}

@Override
public void onPreLaunch() {
INSTANCE = this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import io.github.retrooper.packetevents.handler.PacketEncoder;
import io.netty.channel.Channel;
import net.fabricmc.api.EnvType;
import net.minecraft.entity.player.PlayerEntity;

import static com.github.retrooper.packetevents.PacketEvents.DECODER_NAME;
import static com.github.retrooper.packetevents.PacketEvents.ENCODER_NAME;
Expand Down Expand Up @@ -81,8 +80,8 @@ public void setPlayer(Object channel, Object player) {
return; // this channel isn't injected by packetevents
}
Channel ch = (Channel) channel;
((PacketDecoder) ch.pipeline().get(DECODER_NAME)).player = (PlayerEntity) player;
((PacketEncoder) ch.pipeline().get(ENCODER_NAME)).player = (PlayerEntity) player;
((PacketDecoder) ch.pipeline().get(DECODER_NAME)).player = player;
((PacketEncoder) ch.pipeline().get(ENCODER_NAME)).player = player;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import net.minecraft.entity.player.PlayerEntity;
import org.jetbrains.annotations.ApiStatus;

import java.util.List;
Expand All @@ -38,7 +37,8 @@ public class PacketDecoder extends MessageToMessageDecoder<ByteBuf> {

private final PacketSide side;
public User user;
public PlayerEntity player;
// Platform-typed player: see PacketEncoder.player for the rationale.
public Object player;
private final boolean preViaVersion;

public PacketDecoder(PacketSide side, User user, boolean preViaVersion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

Expand All @@ -46,7 +44,9 @@ public class PacketEncoder extends ChannelOutboundHandlerAdapter {

private final PacketSide side;
public User user;
public PlayerEntity player;
// Platform-typed player: yarn ServerPlayerEntity on intermediary, Mojang ServerPlayer
// on official. Concrete-only handling is delegated to AbstractFabricPlayerManager.
public Object player;
private ChannelPromise promise;
private final boolean preViaVersion;

Expand Down Expand Up @@ -107,18 +107,17 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E
if (didWeCauseThis && (user == null || user.getEncoderState() != ConnectionState.HANDSHAKING)) {
if (PacketEvents.getAPI().getSettings().isKickOnPacketExceptionEnabled()) {
try {
if (user != null && player instanceof ServerPlayerEntity) {
if (user != null && player != null) {
WrapperPlayServerDisconnect disconnectPacket = new WrapperPlayServerDisconnect(
net.kyori.adventure.text.Component.text("Invalid packet")
);
user.sendPacket(disconnectPacket);
}
} catch (Exception ignored) {}
ctx.channel().close();
if (player instanceof ServerPlayerEntity serverPlayer) {
serverPlayer.getServer().execute(() -> {
FabricPacketEventsAPI.getServerAPI().getPlayerManager().disconnectPlayer(serverPlayer, "Invalid packet");
});
if (player != null) {
FabricPacketEventsAPI.getServerAPI().getPlayerManager()
.kickOnException(player, "Invalid packet");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import com.github.retrooper.packetevents.protocol.ConnectionState;
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
import io.github.retrooper.packetevents.impl.netty.manager.player.PlayerManagerAbstract;
import net.minecraft.network.chat.Component;
import net.minecraft.server.network.ServerPlayerEntity;
import org.jetbrains.annotations.NotNull;

public abstract class AbstractFabricPlayerManager extends PlayerManagerAbstract {
Expand Down Expand Up @@ -80,5 +78,20 @@ public void receivePacketSilently(Object player, PacketWrapper<?> wrapper) {
packetEventsAPI.getProtocolManager().receivePacketSilently(getChannel(player), wrapper);
}

public abstract void disconnectPlayer(ServerPlayerEntity serverPlayerEntity, String message);
/**
* Disconnect a player with the given message. The runtime object is the platform's
* server-player type (yarn {@code ServerPlayerEntity} on fabric-intermediary, Mojang
* {@code ServerPlayer} on fabric-official) — common code should never inspect it.
*/
public abstract void disconnectPlayer(@NotNull Object serverPlayer, @NotNull String message);

/**
* Called from the netty pipeline when a packet processing exception should kick the
* player. Concrete impls own the server-thread hop and the platform-typed cast; the
* default forwards directly to {@link #disconnectPlayer(Object, String)}, which is
* safe for hand-rolled tests but real impls should schedule onto the server thread.
*/
public void kickOnException(@NotNull Object serverPlayer, @NotNull String message) {
disconnectPlayer(serverPlayer, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,33 @@
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.server.network.ServerPlayerEntity;

import java.util.List;

public class FabricInjectionUtil {
private static final String VIA_DECODER_NAME = "via-decoder";
private static final String VIA_ENCODER_NAME = "via-encoder";

public static void injectAtPipelineBuilder(ChannelPipeline pipeline, NetworkSide flow) {
PacketSide pipelineSide = switch (flow) {
case CLIENTBOUND -> PacketSide.CLIENT;
case SERVERBOUND -> PacketSide.SERVER;
};

// pipelineSide is already PacketSide because each branch's mixin entrypoint converts
// from its native NetworkSide/PacketFlow enum before calling in. Keeping the enum
// out of fabric-common is what frees this class from yarn vs. Mojang chat.network.*.
public static void injectAtPipelineBuilder(ChannelPipeline pipeline, PacketSide pipelineSide) {
FabricPacketEventsAPI fabricPacketEventsAPI = FabricPacketEventsAPI.getAPI(pipelineSide);
fabricPacketEventsAPI.getLogManager().debug("Game connected!");

Channel channel = pipeline.channel();

// 26.X: configureSerialization fires for EVERY state transition (LOGIN,
// CONFIGURATION, PLAY). On the first call we create the User + fire
// UserConnectEvent. On subsequent calls we must NOT overwrite the User
// (it now has a name, UUID, and is keyed in PlayerDataManager). Instead
// delegate to reinjectPipelineHandlers which preserves the existing User.
User existing = fabricPacketEventsAPI.getProtocolManager().getUser(channel);
if (existing != null) {
reinjectPipelineHandlers(channel, pipelineSide);
return;
}

fabricPacketEventsAPI.getLogManager().debug("Game connected!");
User user = new User(channel, ConnectionState.HANDSHAKING,
null, new UserProfile(null, null));

Expand Down Expand Up @@ -352,18 +359,45 @@ private static String findLatestHandler(List<String> names, String... handlers)
return latest;
}

public static void fireUserLoginEvent(ServerPlayerEntity player) {
public static void reinjectPipelineHandlers(Channel channel, PacketSide side) {
FabricPacketEventsAPI api = FabricPacketEventsAPI.getAPI(side);
User user = api.getProtocolManager().getUser(channel);
if (user == null) return;

ChannelPipeline pipeline = channel.pipeline();

// 26.X: PE's state machine may not transition CONFIGURATION → PLAY
// automatically (CONFIGURATION_END_ACK not triggering the internal
// switch). Detect the PLAY transition by checking whether the pipeline
// now has "decoder" instead of "inbound_config" (MC uses "decoder"
// for PLAY and "inbound_config" for LOGIN/CONFIGURATION).
// 26.X: detect CONFIGURATION → PLAY transition. Only force when the
// User is in CONFIGURATION and the pipeline now has "decoder" (PLAY's
// handler) instead of "inbound_config" (CONFIGURATION's handler).
// Don't force on LOGIN → CONFIGURATION which also briefly shows "decoder".
boolean hasDecoder = pipeline.names().contains("decoder");
boolean hasInboundConfig = pipeline.names().contains("inbound_config");
if (hasDecoder && !hasInboundConfig
&& user.getConnectionState() == ConnectionState.CONFIGURATION) {
user.setConnectionState(ConnectionState.PLAY);
}

// Do NOT remove + re-add PE's handlers. They persist across MC's state
// transitions (configureSerialization replaces MC's "decoder"/"encoder"
// but not arbitrary handlers). Removing PE's handlers triggers
// handlerRemoved → PE interprets as disconnect → GrimPlayer eviction.
// The state fix above is all that's needed.
}

public static void fireUserLoginEvent(Object player) {
FabricPacketEventsAPI api = FabricPacketEventsAPI.getServerAPI();

User user = api.getPlayerManager().getUser(player);
if (user == null) {
Object channelObj = api.getPlayerManager().getChannel(player);

// Check if it's a fake connection
if (!FakeChannelUtil.isFakeChannel(channelObj) &&
(!api.isTerminated() || api.getSettings().isKickIfTerminated())) {
// Kick the player if they're not a fake player
// player.connection.disconnect(Component.literal("PacketEvents 2.0 failed to inject"));
FabricPacketEventsAPI.getServerAPI().getPlayerManager().disconnectPlayer(player, "PacketEvents failed to inject into a channel.");
}
return;
Expand Down
Loading
Loading