Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2025 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/

package com.velocitypowered.api.event.player;

import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;

/**
* Called when a player is marked as loaded by the client.
*
* <p>This event is fired when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
*
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout.
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
* @sinceMinecraft 1.21.4
* @since 3.4.0
*/
@Beta
public final class PlayerClientLoadedWorldEvent {

private final Player player;

public PlayerClientLoadedWorldEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}

public Player getPlayer() {
return player;
}

@Override
public String toString() {
return "PlayerClientLoadedWorldEvent{"
+ "player=" + player
+ '}';
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
*/
ServerInfo getServerInfo();

/**
* Returns whether the client notified this connection of having loaded the world.
*
* @return true if the client has loaded the world
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
* @sinceMinecraft 1.21.4
* @since 3.4.0
*/
boolean isClientLoaded();

/**
* Returns the player that this connection is associated with.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
Expand Down Expand Up @@ -200,6 +201,10 @@ default boolean handle(RespawnPacket packet) {
return false;
}

default boolean handle(ServerboundPlayerLoadedPacket packet) {
return false;
}

default boolean handle(ServerLoginPacket packet) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final VelocityServer server;
private @Nullable MinecraftConnection connection;
private boolean hasCompletedJoin = false;
private boolean clientLoaded = false; // 1.21.4+
private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>();
Expand Down Expand Up @@ -318,6 +319,15 @@ public void completeJoin() {
}
}

public void setClientLoaded(boolean clientLoaded) {
this.clientLoaded = clientLoaded;
}

@Override
public boolean isClientLoaded() {
return clientLoaded;
}

boolean isGracefulDisconnect() {
return gracefulDisconnect;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion;
Expand All @@ -48,6 +49,7 @@
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
Expand Down Expand Up @@ -195,6 +197,20 @@ public boolean handle(ClientSettingsPacket packet) {
return true; // will forward onto the server
}

@Override
public boolean handle(ServerboundPlayerLoadedPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
return true;
}
if (!serverConnection.isClientLoaded()) {
serverConnection.setClientLoaded(true);
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
}
return true;
}

@Override
public boolean handle(SessionPlayerCommandPacket packet) {
if (player.getCurrentServer().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
Expand Down Expand Up @@ -336,6 +337,11 @@ public enum StateRegistry {
map(0x11, MINECRAFT_1_20_5, false),
map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false));
serverbound.register(
ServerboundPlayerLoadedPacket.class,
() -> ServerboundPlayerLoadedPacket.INSTANCE,
map(0x2A, MINECRAFT_1_21_4, false),
map(0x2B, MINECRAFT_1_21_6, false));
serverbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2025 Velocity 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 <https://www.gnu.org/licenses/>.
*/

package com.velocitypowered.proxy.protocol.packet;

import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;

public class ServerboundPlayerLoadedPacket implements MinecraftPacket {

public static final ServerboundPlayerLoadedPacket INSTANCE = new ServerboundPlayerLoadedPacket();

private ServerboundPlayerLoadedPacket() {}

@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {}

@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {}

@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 0;
}

@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}

}