Skip to content

Commit

Permalink
fix: workaround viabackwards sending client tick packets when not in …
Browse files Browse the repository at this point in the history
…the world
  • Loading branch information
ishland committed Dec 18, 2024
1 parent 5bdfc99 commit 178fa0a
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class AsyncChunkLoadUtil {

private static final ChunkTicketType<Unit> 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<OptionalChunk<Chunk>> scheduleChunkLoad(ServerWorld world, ChunkPos pos) {
return scheduleChunkLoadWithRadius(world, pos, 3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,50 @@
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;
import org.slf4j.Logger;
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 {
Expand All @@ -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<Unit> 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<Void> original) {
RegistryKey<World> 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<ServerPlayerEntity> 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<Packet<?>> packetConsumer, Operation<Void> 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<World> 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);
}
}

}

0 comments on commit 178fa0a

Please sign in to comment.