Player Loaded World API (#1541)
Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com> Co-authored-by: Wouter Gritter <wouter@gritter.nl>
This commit is contained in:
co-authored by
Emil
Wouter Gritter
parent
a08972749b
commit
e11584ba35
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2026 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 once per {@link com.velocitypowered.api.proxy.ServerConnection}
|
||||||
|
* 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 nor respawning.
|
||||||
|
* 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 4.1.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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package com.velocitypowered.api.proxy;
|
package com.velocitypowered.api.proxy;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
|
|||||||
*/
|
*/
|
||||||
ServerInfo getServerInfo();
|
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 4.1.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
boolean isClientLoaded();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the player that this connection is associated with.
|
* Returns the player that this connection is associated with.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
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.SetCompressionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||||
@@ -200,6 +201,10 @@ public interface MinecraftSessionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean handle(ServerboundPlayerLoadedPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default boolean handle(ServerLoginPacket packet) {
|
default boolean handle(ServerLoginPacket packet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable MinecraftConnection connection;
|
private @Nullable MinecraftConnection connection;
|
||||||
private boolean hasCompletedJoin = false;
|
private boolean hasCompletedJoin = false;
|
||||||
|
private boolean clientLoaded = false; // 1.21.4+
|
||||||
private boolean gracefulDisconnect = false;
|
private boolean gracefulDisconnect = false;
|
||||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||||
private final Map<Long, Long> pendingPings = new HashMap<>();
|
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||||
@@ -317,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setClientLoaded(boolean clientLoaded) {
|
||||||
|
this.clientLoaded = clientLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClientLoaded() {
|
||||||
|
return clientLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isGracefulDisconnect() {
|
boolean isGracefulDisconnect() {
|
||||||
return gracefulDisconnect;
|
return gracefulDisconnect;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -26,6 +26,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
|||||||
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
|
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
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.TabCompleteEvent;
|
||||||
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
@@ -50,6 +51,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
|
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
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.TabCompleteRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
||||||
@@ -241,6 +243,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true; // will forward onto the server
|
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
|
@Override
|
||||||
public boolean handle(SessionPlayerCommandPacket packet) {
|
public boolean handle(SessionPlayerCommandPacket packet) {
|
||||||
if (player.getCurrentServer().isEmpty()) {
|
if (player.getCurrentServer().isEmpty()) {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
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.SetCompressionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||||
@@ -344,6 +345,12 @@ public enum StateRegistry {
|
|||||||
map(0x13, MINECRAFT_1_21_2, false),
|
map(0x13, MINECRAFT_1_21_2, false),
|
||||||
map(0x14, MINECRAFT_1_21_6, false),
|
map(0x14, MINECRAFT_1_21_6, false),
|
||||||
map(0x15, MINECRAFT_26_1, false));
|
map(0x15, MINECRAFT_26_1, false));
|
||||||
|
serverbound.register(
|
||||||
|
ServerboundPlayerLoadedPacket.class,
|
||||||
|
() -> ServerboundPlayerLoadedPacket.INSTANCE,
|
||||||
|
map(0x2A, MINECRAFT_1_21_4, false),
|
||||||
|
map(0x2B, MINECRAFT_1_21_6, false),
|
||||||
|
map(0x2C, MINECRAFT_26_1, false));
|
||||||
serverbound.register(
|
serverbound.register(
|
||||||
PluginMessagePacket.class,
|
PluginMessagePacket.class,
|
||||||
PluginMessagePacket::new,
|
PluginMessagePacket::new,
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user