From 178fa0ac667f2395c6c8bf6373ef3cb308417a67 Mon Sep 17 00:00:00 2001 From: ishland Date: Wed, 18 Dec 2024 23:24:35 +0800 Subject: [PATCH] fix: workaround viabackwards sending client tick packets when not in the world --- .../AsyncChunkLoadUtil.java | 2 +- ...ixinServerConfigurationNetworkHandler.java | 155 +++++++++++++++--- 2 files changed, 130 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/ishland/vmp/common/chunk/loading/async_chunks_on_player_login/AsyncChunkLoadUtil.java b/src/main/java/com/ishland/vmp/common/chunk/loading/async_chunks_on_player_login/AsyncChunkLoadUtil.java index de26baf..e6e4f9b 100644 --- a/src/main/java/com/ishland/vmp/common/chunk/loading/async_chunks_on_player_login/AsyncChunkLoadUtil.java +++ b/src/main/java/com/ishland/vmp/common/chunk/loading/async_chunks_on_player_login/AsyncChunkLoadUtil.java @@ -25,7 +25,7 @@ public class AsyncChunkLoadUtil { private static final ChunkTicketType ASYNC_CHUNK_LOAD = ChunkTicketType.create("vmp_async_chunk_load", (unit, unit2) -> 0); - private static final AsyncSemaphore SEMAPHORE = new FairAsyncSemaphore(6); + public static final AsyncSemaphore SEMAPHORE = new FairAsyncSemaphore(12); public static CompletableFuture> scheduleChunkLoad(ServerWorld world, ChunkPos pos) { return scheduleChunkLoadWithRadius(world, pos, 3); diff --git a/src/main/java/com/ishland/vmp/mixins/chunk/loading/async_chunk_on_player_login/MixinServerConfigurationNetworkHandler.java b/src/main/java/com/ishland/vmp/mixins/chunk/loading/async_chunk_on_player_login/MixinServerConfigurationNetworkHandler.java index b65c3d9..a71d509 100644 --- a/src/main/java/com/ishland/vmp/mixins/chunk/loading/async_chunk_on_player_login/MixinServerConfigurationNetworkHandler.java +++ b/src/main/java/com/ishland/vmp/mixins/chunk/loading/async_chunk_on_player_login/MixinServerConfigurationNetworkHandler.java @@ -3,23 +3,36 @@ import com.google.common.base.Stopwatch; import com.ishland.vmp.common.chunk.loading.async_chunks_on_player_login.AsyncChunkLoadUtil; import com.ishland.vmp.common.config.Config; +import com.ishland.vmp.mixins.access.IClientConnection; +import com.ishland.vmp.mixins.access.IServerChunkManager; +import com.ishland.vmp.mixins.access.IThreadedAnvilChunkStorage; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.authlib.GameProfile; import com.mojang.serialization.Dynamic; +import io.netty.channel.Channel; import net.minecraft.nbt.NbtOps; import net.minecraft.network.ClientConnection; +import net.minecraft.network.DisconnectionInfo; import net.minecraft.network.listener.ServerConfigurationPacketListener; import net.minecraft.network.listener.TickablePacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.c2s.common.SyncedClientOptions; +import net.minecraft.network.packet.s2c.common.DisconnectS2CPacket; import net.minecraft.registry.RegistryKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.JoinWorldTask; import net.minecraft.server.network.ServerCommonNetworkHandler; import net.minecraft.server.network.ServerConfigurationNetworkHandler; +import net.minecraft.server.network.ServerPlayerConfigurationTask; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ChunkTicketType; import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.math.BlockPos; +import net.minecraft.text.Text; +import net.minecraft.util.Unit; import net.minecraft.util.math.ChunkPos; import net.minecraft.world.World; import net.minecraft.world.dimension.DimensionType; @@ -27,7 +40,13 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; @Mixin(ServerConfigurationNetworkHandler.class) public abstract class MixinServerConfigurationNetworkHandler extends ServerCommonNetworkHandler implements ServerConfigurationPacketListener, TickablePacketListener { @@ -38,42 +57,126 @@ public abstract class MixinServerConfigurationNetworkHandler extends ServerCommo @Shadow @Final private GameProfile profile; + @Shadow private SyncedClientOptions syncedOptions; + + @Shadow @Final private static Text INVALID_PLAYER_DATA_TEXT; + @Unique + private static final ChunkTicketType VMP_PLAYER_ASYNC_CHUNKS = ChunkTicketType.create("vmp_player_async_chunk", (unit, unit2) -> 0); + + @Unique + private ChunkPos vmp$ticketHeld; + + @Unique + private ServerWorld vmp$ticketHeldWorld; + + @Unique + private ServerPlayerEntity vmp$heldPlayer; + public MixinServerConfigurationNetworkHandler(MinecraftServer server, ClientConnection connection, ConnectedClientData clientData) { super(server, connection, clientData); } - @WrapOperation(method = "onReady", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;onPlayerConnect(Lnet/minecraft/network/ClientConnection;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/server/network/ConnectedClientData;)V")) - private void wrapOnPlayerConnect(PlayerManager instance, ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, Operation original) { - RegistryKey registryKey = instance.loadPlayerData(player) - .flatMap(nbt -> DimensionType.worldFromDimensionNbt(new Dynamic<>(NbtOps.INSTANCE, nbt.get("Dimension"))).resultOrPartial(LOGGER::error)) - .orElse(World.OVERWORLD); - ServerWorld storedWorld = instance.getServer().getWorld(registryKey); - ServerWorld actualWorld; - if (storedWorld == null) { - LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", registryKey); - actualWorld = instance.getServer().getOverworld(); + @Inject(method = "onDisconnected", at = @At("RETURN")) + private void onDisconnect(DisconnectionInfo info, CallbackInfo ci) { + vmp$dropTicket(); + } + + @Unique + private void vmp$dropTicket() { + if (this.vmp$ticketHeld != null && this.vmp$ticketHeldWorld != null) { + ((IServerChunkManager) this.vmp$ticketHeldWorld.getChunkManager()).getTicketManager().removeTicketWithLevel(VMP_PLAYER_ASYNC_CHUNKS, this.vmp$ticketHeld, 31, Unit.INSTANCE); + this.vmp$ticketHeld = null; + this.vmp$ticketHeldWorld = null; + } + } + + @WrapOperation(method = "onReady", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;createPlayer(Lcom/mojang/authlib/GameProfile;Lnet/minecraft/network/packet/c2s/common/SyncedClientOptions;)Lnet/minecraft/server/network/ServerPlayerEntity;")) + private ServerPlayerEntity replacePlayer(PlayerManager instance, GameProfile profile, SyncedClientOptions syncedOptions, Operation original) { + if (this.vmp$heldPlayer != null) { + this.vmp$dropTicket(); + return this.vmp$heldPlayer; } else { - actualWorld = storedWorld; + return original.call(instance, profile, syncedOptions); } + } - Stopwatch timing = Stopwatch.createStarted(); - AsyncChunkLoadUtil.scheduleChunkLoad(actualWorld, new ChunkPos(player.getBlockPos())) - .thenRunAsync(() -> { - if (!this.isConnectionOpen()) { - return; - } + @WrapOperation(method = "pollTask", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerConfigurationTask;sendPacket(Ljava/util/function/Consumer;)V")) + private void delayJoinWorld(ServerPlayerConfigurationTask instance, Consumer> packetConsumer, Operation original) { + if (instance instanceof JoinWorldTask) { + PlayerManager playerManager = this.server.getPlayerManager(); + if (playerManager.getPlayer(this.profile.getId()) != null) { + this.disconnect(PlayerManager.DUPLICATE_LOGIN_TEXT); + return; + } - if (instance.getPlayer(this.profile.getId()) != null) { - this.disconnect(PlayerManager.DUPLICATE_LOGIN_TEXT); - return; - } + Text text = playerManager.checkCanJoin(this.connection.getAddress(), this.profile); + if (text != null) { + this.disconnect(text); + return; + } + + ServerPlayerEntity player = playerManager.createPlayer(this.profile, this.syncedOptions); + this.vmp$heldPlayer = player; - if (Config.SHOW_ASYNC_LOADING_MESSAGES) { - LOGGER.info("Async chunk loading for player {} completed after {}", profile.getName(), timing); + RegistryKey registryKey = playerManager.loadPlayerData(player) + .flatMap(nbt -> DimensionType.worldFromDimensionNbt(new Dynamic<>(NbtOps.INSTANCE, nbt.get("Dimension"))).resultOrPartial(LOGGER::error)) + .orElse(World.OVERWORLD); + ServerWorld storedWorld = playerManager.getServer().getWorld(registryKey); + ServerWorld actualWorld; + if (storedWorld == null) { + LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", registryKey); + actualWorld = playerManager.getServer().getOverworld(); + } else { + actualWorld = storedWorld; + } + + ChunkPos chunkPos = new ChunkPos(player.getBlockPos()); + this.vmp$dropTicket(); + this.vmp$ticketHeld = chunkPos; + this.vmp$ticketHeldWorld = actualWorld; + Stopwatch timing = Stopwatch.createStarted(); + AsyncChunkLoadUtil.SEMAPHORE.acquire().thenApplyAsync(unused -> { + try { + ((IServerChunkManager) actualWorld.getChunkManager()).getTicketManager().addTicketWithLevel(VMP_PLAYER_ASYNC_CHUNKS, chunkPos, 31, Unit.INSTANCE); + ((IServerChunkManager) actualWorld.getChunkManager()).invokeUpdateChunks(); + final ChunkHolder chunkHolder = ((IThreadedAnvilChunkStorage) actualWorld.getChunkManager().chunkLoadingManager).invokeGetCurrentChunkHolder(chunkPos.toLong()); + if (chunkHolder == null) { + throw new IllegalStateException("Chunk not there when requested"); } + return chunkHolder.getEntityTickingFuture().whenCompleteAsync((worldChunkOptionalChunk, throwable) -> { + if (Config.SHOW_ASYNC_LOADING_MESSAGES) { + LOGGER.info("Async chunk loading for player {} completed after {}", profile.getName(), timing); + } + Channel channel = ((IClientConnection) this.connection).getChannel(); + + if (channel == null || !channel.isOpen()) { + return; + } - original.call(instance, connection, player, clientData); - }, instance.getServer()); + try { + original.call(instance, packetConsumer); + } catch (Throwable t1) { + LOGGER.error("Couldn't place player in world", t1); + this.connection.send(new DisconnectS2CPacket(INVALID_PLAYER_DATA_TEXT)); + this.connection.disconnect(INVALID_PLAYER_DATA_TEXT); + } + }, ((IThreadedAnvilChunkStorage) actualWorld.getChunkManager().chunkLoadingManager).getMainThreadExecutor()); + } catch (Throwable t) { + LOGGER.warn("Failed to schedule chunkload for {} at {}", profile.getName(), chunkPos, t); + try { + original.call(instance, packetConsumer); + } catch (Throwable t1) { + LOGGER.error("Couldn't place player in world", t1); + this.connection.send(new DisconnectS2CPacket(INVALID_PLAYER_DATA_TEXT)); + this.connection.disconnect(INVALID_PLAYER_DATA_TEXT); + } + return CompletableFuture.completedFuture(null); + } + }, ((IThreadedAnvilChunkStorage) actualWorld.getChunkManager().chunkLoadingManager).getMainThreadExecutor()) + .whenComplete((completableFuture, throwable) -> AsyncChunkLoadUtil.SEMAPHORE.release()); + } else { + original.call(instance, packetConsumer); + } } }