Fixed glitched chunks when teleporting a player + divided teleportation count by 2 during first join

This commit is contained in:
themode 2020-11-22 13:56:36 +01:00
parent 5d8b59c012
commit acc013be6a
6 changed files with 134 additions and 78 deletions

View File

@ -28,6 +28,7 @@ import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.entity.EntityUtils;
import net.minestom.server.utils.player.PlayerUtils;
@ -229,31 +230,36 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* {@link Instance#hasEnabledAutoChunkLoad()} returns true.
*
* @param position the teleport position
* @param chunks the chunk indexes to load before teleporting the entity,
* indexes are from {@link ChunkUtils#getChunkIndex(int, int)},
* can be null or empty to only load the chunk at {@code position}
* @param callback the optional callback executed, even if auto chunk is not enabled
* @throws IllegalStateException if you try to teleport an entity before settings its instance
*/
public void teleport(@NotNull Position position, @Nullable Runnable callback) {
public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) {
Check.notNull(position, "Teleport position cannot be null");
Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!");
final Runnable runnable = () -> {
if (!this.position.isSimilar(position)) {
refreshPosition(position.getX(), position.getY(), position.getZ());
}
if (!this.position.hasSimilarView(position)) {
refreshView(position.getYaw(), position.getPitch());
}
final ChunkCallback endCallback = (chunk) -> {
refreshPosition(position.getX(), position.getY(), position.getZ());
refreshView(position.getYaw(), position.getPitch());
sendSynchronization();
OptionalCallback.execute(callback);
};
if (instance.hasEnabledAutoChunkLoad()) {
instance.loadChunk(position, chunk -> runnable.run());
if (chunks == null || chunks.length == 0) {
instance.loadOptionalChunk(position, endCallback);
} else {
runnable.run();
ChunkUtils.optionalLoadAll(instance, chunks, null, endCallback);
}
}
public void teleport(@NotNull Position position, @Nullable Runnable callback) {
teleport(position, null, callback);
}
public void teleport(@NotNull Position position) {
teleport(position, null);
}
@ -1020,13 +1026,14 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
final Instance instance = getInstance();
if (instance != null) {
// Needed to refresh the client chunks when connecting for the first time
final boolean forceUpdate = this instanceof Player && getAliveTicks() == 0;
final Chunk lastChunk = instance.getChunkAt(lastX, lastZ);
final Chunk newChunk = instance.getChunkAt(x, z);
if (lastChunk != null && newChunk != null && lastChunk != newChunk) {
synchronized (instance) {
instance.removeEntityFromChunk(this, lastChunk);
instance.addEntityToChunk(this, newChunk);
}
if (forceUpdate || (lastChunk != null && newChunk != null && lastChunk != newChunk)) {
instance.switchEntityChunk(this, lastChunk, newChunk);
if (this instanceof Player) {
// Refresh player view
final Player player = (Player) this;

View File

@ -698,55 +698,39 @@ public class Player extends LivingEntity implements CommandSender {
// true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance)
final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
// true if the player needs every chunk around its position
final boolean needChunkLoad = !firstSpawn || firstSpawn &&
ChunkUtils.getChunkCoordinate((int) getRespawnPoint().getX()) == 0 &&
ChunkUtils.getChunkCoordinate((int) getRespawnPoint().getZ()) == 0;
if (needWorldRefresh && needChunkLoad) {
if (needWorldRefresh) {
// Remove all previous viewable chunks (from the previous instance)
for (Chunk viewableChunk : viewableChunks) {
viewableChunk.removeViewer(this);
}
// Send the new dimension
if (this.instance != null) {
final DimensionType instanceDimensionType = instance.getDimensionType();
if (dimensionType != instanceDimensionType)
sendDimension(instanceDimensionType);
}
// Load all the required chunks
final Position pos = firstSpawn ? getRespawnPoint() : position;
final long[] visibleChunks = ChunkUtils.getChunksInRange(pos, getChunkRange());
final long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange());
final int length = visibleChunks.length;
AtomicInteger counter = new AtomicInteger(0);
for (long visibleChunk : visibleChunks) {
final int chunkX = ChunkUtils.getChunkCoordX(visibleChunk);
final int chunkZ = ChunkUtils.getChunkCoordZ(visibleChunk);
final ChunkCallback callback = (chunk) -> {
if (chunk != null) {
chunk.addViewer(this);
if (chunk.getChunkX() == ChunkUtils.getChunkCoordinate((int) getPosition().getX()) &&
chunk.getChunkZ() == ChunkUtils.getChunkCoordinate((int) getPosition().getZ())) {
updateViewPosition(chunk);
}
final ChunkCallback eachCallback = chunk -> {
if (chunk != null) {
final int chunkX = ChunkUtils.getChunkCoordinate((int) pos.getX());
final int chunkZ = ChunkUtils.getChunkCoordinate((int) pos.getZ());
if (chunk.getChunkX() == chunkX &&
chunk.getChunkZ() == chunkZ) {
updateViewPosition(chunkX, chunkZ);
}
final boolean isLast = counter.get() == length - 1;
if (isLast) {
// This is the last chunk to be loaded , spawn player
spawnPlayer(instance, firstSpawn);
} else {
// Increment the counter of current loaded chunks
counter.incrementAndGet();
}
};
}
};
final ChunkCallback endCallback = chunk -> {
// This is the last chunk to be loaded , spawn player
spawnPlayer(instance, firstSpawn);
};
ChunkUtils.optionalLoadAll(instance, visibleChunks, eachCallback, endCallback);
// WARNING: if auto load is disabled and no chunks are loaded beforehand, player will be stuck.
instance.loadOptionalChunk(chunkX, chunkZ, callback);
}
} else if (!needChunkLoad) {
// The player always believe that his position is 0;0 so this is a pretty hacky fix
instance.loadOptionalChunk(0, 0, chunk -> spawnPlayer(instance, true));
} else {
// The player already has the good version of all the chunks.
// We just need to refresh his entity viewing list and add him to the instance
@ -763,7 +747,7 @@ public class Player extends LivingEntity implements CommandSender {
*
* @param firstSpawn true if this is the player first spawn
*/
private void spawnPlayer(Instance instance, boolean firstSpawn) {
private void spawnPlayer(@NotNull Instance instance, boolean firstSpawn) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance);
@ -772,7 +756,6 @@ public class Player extends LivingEntity implements CommandSender {
teleport(getRespawnPoint());
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
callEvent(PlayerSpawnEvent.class, spawnEvent);
}
@ -1550,7 +1533,7 @@ public class Player extends LivingEntity implements CommandSender {
}
// Update client render distance
updateViewPosition(newChunk);
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
// Load new chunks
for (int index : newChunks) {
@ -1615,13 +1598,19 @@ public class Player extends LivingEntity implements CommandSender {
}
@Override
public void teleport(@NotNull Position position, @Nullable Runnable callback) {
super.teleport(position, () -> {
public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) {
super.teleport(position, chunks, () -> {
updatePlayerPosition();
OptionalCallback.execute(callback);
});
}
@Override
public void teleport(@NotNull Position position, @Nullable Runnable callback) {
final long[] chunks = ChunkUtils.getChunksInRange(position, getChunkRange());
teleport(position, chunks, callback);
}
@Override
public void teleport(@NotNull Position position) {
teleport(position, null);
@ -1953,12 +1942,13 @@ public class Player extends LivingEntity implements CommandSender {
/**
* Sends a {@link UpdateViewPositionPacket} to the player.
*
* @param chunk the chunk to update the view
* @param chunkX the chunk X
* @param chunkZ the chunk Z
*/
public void updateViewPosition(@NotNull Chunk chunk) {
public void updateViewPosition(int chunkX, int chunkZ) {
UpdateViewPositionPacket updateViewPositionPacket = new UpdateViewPositionPacket();
updateViewPositionPacket.chunkX = chunk.getChunkX();
updateViewPositionPacket.chunkZ = chunk.getChunkZ();
updateViewPositionPacket.chunkX = chunkX;
updateViewPositionPacket.chunkZ = chunkZ;
playerConnection.sendPacket(updateViewPositionPacket);
}

View File

@ -7,13 +7,13 @@ import io.netty.handler.codec.MessageToByteEncoder;
import javax.crypto.Cipher;
public class Encrypter extends MessageToByteEncoder<ByteBuf> {
private final CipherBase cipher;
private final CipherBase cipher;
public Encrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
public Encrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception {
this.cipher.encrypt(byteBufIn, byteBufOut);
}
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception {
this.cipher.encrypt(byteBufIn, byteBufOut);
}
}

View File

@ -409,13 +409,17 @@ public abstract class Chunk implements Viewable, DataContainer {
public boolean addViewer(@NotNull Player player) {
final boolean result = this.viewers.add(player);
// Send the chunk data & light packets to the player
sendChunk(player);
// Add to the viewable chunks set
player.getViewableChunks().add(this);
PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ);
player.callEvent(PlayerChunkLoadEvent.class, playerChunkLoadEvent);
if (result) {
// Send the chunk data & light packets to the player
sendChunk(player);
PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ);
player.callEvent(PlayerChunkLoadEvent.class, playerChunkLoadEvent);
}
return result;
}
@ -433,8 +437,11 @@ public abstract class Chunk implements Viewable, DataContainer {
// Remove from the viewable chunks set
player.getViewableChunks().remove(this);
PlayerChunkUnloadEvent playerChunkUnloadEvent = new PlayerChunkUnloadEvent(player, chunkX, chunkZ);
player.callEvent(PlayerChunkUnloadEvent.class, playerChunkUnloadEvent);
if (result) {
PlayerChunkUnloadEvent playerChunkUnloadEvent = new PlayerChunkUnloadEvent(player, chunkX, chunkZ);
player.callEvent(PlayerChunkUnloadEvent.class, playerChunkUnloadEvent);
}
return result;
}
@ -464,9 +471,6 @@ public abstract class Chunk implements Viewable, DataContainer {
// Only send loaded chunk
if (!isLoaded())
return;
// Only send chunk to netty client (because it sends raw ByteBuf buffer)
if (!PlayerUtils.isNettyClient(player))
return;
final PlayerConnection playerConnection = player.getPlayerConnection();

View File

@ -893,6 +893,19 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
});
}
/**
* Synchronized method to execute {@link #removeEntityFromChunk(Entity, Chunk)}
* and {@link #addEntityToChunk(Entity, Chunk)} simultaneously.
*
* @param entity the entity to change its chunk
* @param lastChunk the last entity chunk
* @param newChunk the new entity chunk
*/
public synchronized void switchEntityChunk(@NotNull Entity entity, @NotNull Chunk lastChunk, @NotNull Chunk newChunk) {
removeEntityFromChunk(entity, lastChunk);
addEntityToChunk(entity, newChunk);
}
/**
* Adds the specified {@link Entity} to the instance entities cache.
* <p>

View File

@ -7,15 +7,57 @@ import net.minestom.server.instance.Instance;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.callback.OptionalCallback;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicInteger;
public final class ChunkUtils {
private ChunkUtils() {
}
/**
* Executes {@link Instance#loadOptionalChunk(int, int, ChunkCallback)} for the array of chunks {@code chunks}
* with multiple callbacks, {@code eachCallback} which is executed each time a new chunk is loaded and
* {@code endCallback} when all the chunks in the array have been loaded.
* <p>
* Be aware that {@link Instance#loadOptionalChunk(int, int, ChunkCallback)} can give a null chunk in the callback
* if {@link Instance#hasEnabledAutoChunkLoad()} returns false and the chunk is not already loaded.
*
* @param instance the instance to load the chunks from
* @param chunks the chunks to loaded, long value from {@link #getChunkIndex(int, int)}
* @param eachCallback the optional callback when a chunk get loaded
* @param endCallback the optional callback when all the chunks have been loaded
*/
public static void optionalLoadAll(@NotNull Instance instance, @NotNull long[] chunks,
@Nullable ChunkCallback eachCallback, @Nullable ChunkCallback endCallback) {
final int length = chunks.length;
AtomicInteger counter = new AtomicInteger(0);
for (long visibleChunk : chunks) {
final int chunkX = ChunkUtils.getChunkCoordX(visibleChunk);
final int chunkZ = ChunkUtils.getChunkCoordZ(visibleChunk);
final ChunkCallback callback = (chunk) -> {
OptionalCallback.execute(eachCallback, chunk);
final boolean isLast = counter.get() == length - 1;
if (isLast) {
// This is the last chunk to be loaded , spawn player
OptionalCallback.execute(endCallback, chunk);
} else {
// Increment the counter of current loaded chunks
counter.incrementAndGet();
}
};
// WARNING: if auto load is disabled and no chunks are loaded beforehand, player will be stuck.
instance.loadOptionalChunk(chunkX, chunkZ, callback);
}
}
/**
* Gets if a chunk is loaded.
*