Skip to content

Commit

Permalink
Yeet lectern cache (GeyserMC#4695)
Browse files Browse the repository at this point in the history
* attempt to yeet lectern cache

* yeet lecternutils usage

* properly update lecterns

* yeet accidental diff
  • Loading branch information
onebeastchris authored and XingLingQAQ committed Sep 25, 2024
1 parent bd5aeaa commit 9a9891a
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 391 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@

package org.geysermc.geyser.platform.mod.world;

import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minecraft.SharedConstants;
Expand All @@ -48,24 +43,19 @@
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BannerPatternLayers;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -121,94 +111,6 @@ public boolean hasOwnChunkCache() {
return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion();
}

@Override
public boolean shouldExpectLecternHandled(GeyserSession session) {
return true;
}

@Override
public void sendLecternData(GeyserSession session, int x, int z, List<BlockEntityInfo> blockEntityInfos) {
server.execute(() -> {
ServerPlayer player = getPlayer(session);
if (player == null) {
return;
}

//noinspection resource - level() is just a getter
LevelChunk chunk = player.level().getChunk(x, z);
final int chunkBlockX = x << 4;
final int chunkBlockZ = z << 4;
//noinspection ForLoopReplaceableByForEach - avoid constructing iterator
for (int i = 0; i < blockEntityInfos.size(); i++) {
BlockEntityInfo blockEntityInfo = blockEntityInfos.get(i);
BlockEntity blockEntity = chunk.getBlockEntity(new BlockPos(chunkBlockX + blockEntityInfo.getX(),
blockEntityInfo.getY(), chunkBlockZ + blockEntityInfo.getZ()));
sendLecternData(session, blockEntity, true);
}
});
}

@Override
public void sendLecternData(GeyserSession session, int x, int y, int z) {
server.execute(() -> {
ServerPlayer player = getPlayer(session);
if (player == null) {
return;
}
//noinspection resource - level() is just a getter
BlockEntity blockEntity = player.level().getBlockEntity(new BlockPos(x, y, z));
sendLecternData(session, blockEntity, false);
});
}

private void sendLecternData(GeyserSession session, BlockEntity blockEntity, boolean isChunkLoad) {
if (!(blockEntity instanceof LecternBlockEntity lectern)) {
return;
}

int x = blockEntity.getBlockPos().getX();
int y = blockEntity.getBlockPos().getY();
int z = blockEntity.getBlockPos().getZ();

if (!lectern.hasBook()) {
if (!isChunkLoad) {
BlockEntityUtils.updateBlockEntity(session, LecternUtils.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z));
}
return;
}

ItemStack book = lectern.getBook();
int pageCount = getPageCount(book);
boolean hasBookPages = pageCount > 0;
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x, y, z, hasBookPages ? pageCount : 1);
lecternTag.putInt("page", lectern.getPage() / 2);
NbtMapBuilder bookTag = NbtMap.builder()
.putByte("Count", (byte) book.getCount())
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:writable_book");
List<NbtMap> pages = new ArrayList<>(hasBookPages ? pageCount : 1);
if (hasBookPages) {
List<String> bookPages = getPages(book);
for (String page : bookPages) {
NbtMapBuilder pageBuilder = NbtMap.builder()
.putString("photoname", "")
.putString("text", page);
pages.add(pageBuilder.build());
}
} else {
// Empty page
NbtMapBuilder pageBuilder = NbtMap.builder()
.putString("photoname", "")
.putString("text", "");
pages.add(pageBuilder.build());
}

bookTag.putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, pages).build());
lecternTag.putCompound("book", bookTag.build());
NbtMap blockEntityTag = lecternTag.build();
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z));
}

@Override
public boolean hasPermission(GeyserSession session, String permission) {
ServerPlayer player = getPlayer(session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,22 @@

import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.erosion.bukkit.BukkitLecterns;
import org.geysermc.erosion.bukkit.BukkitUtils;
import org.geysermc.erosion.bukkit.PickBlockUtils;
import org.geysermc.erosion.bukkit.SchedulerUtils;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.GameRule;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

Expand All @@ -58,11 +51,9 @@
*/
public class GeyserSpigotWorldManager extends WorldManager {
private final Plugin plugin;
private final BukkitLecterns lecterns;

public GeyserSpigotWorldManager(Plugin plugin) {
this.plugin = plugin;
this.lecterns = new BukkitLecterns(plugin);
}

@Override
Expand Down Expand Up @@ -95,69 +86,6 @@ public boolean hasOwnChunkCache() {
return true;
}

@Override
public void sendLecternData(GeyserSession session, int x, int y, int z) {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return;
}

Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
// Run as a task to prevent async issues
SchedulerUtils.runTask(this.plugin, () -> sendLecternData(session, block, false), block);
}

public void sendLecternData(GeyserSession session, int x, int z, List<BlockEntityInfo> blockEntityInfos) {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return;
}
if (SchedulerUtils.FOLIA) {
Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z);
if (chunk == null) {
return;
}
Bukkit.getRegionScheduler().execute(this.plugin, bukkitPlayer.getWorld(), x, z, () ->
sendLecternData(session, chunk, blockEntityInfos));
} else {
Bukkit.getScheduler().runTask(this.plugin, () -> {
Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z);
if (chunk == null) {
return;
}
sendLecternData(session, chunk, blockEntityInfos);
});
}
}

private @Nullable Chunk getChunk(World world, int x, int z) {
if (!world.isChunkLoaded(x, z)) {
return null;
}
return world.getChunkAt(x, z);
}

private void sendLecternData(GeyserSession session, Chunk chunk, List<BlockEntityInfo> blockEntityInfos) {
//noinspection ForLoopReplaceableByForEach - avoid constructing Iterator
for (int i = 0; i < blockEntityInfos.size(); i++) {
BlockEntityInfo info = blockEntityInfos.get(i);
Block block = chunk.getBlock(info.getX(), info.getY(), info.getZ());
sendLecternData(session, block, true);
}
}

private void sendLecternData(GeyserSession session, Block block, boolean isChunkLoad) {
NbtMap blockEntityTag = this.lecterns.getLecternData(block, isChunkLoad);
if (blockEntityTag != null) {
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, BukkitUtils.getVector(block.getLocation()));
}
}

@Override
public boolean shouldExpectLecternHandled(GeyserSession session) {
return true;
}

public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
org.bukkit.GameRule<?> bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID());
if (bukkitGameRule == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,17 @@
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.geysermc.erosion.packet.backendbound.*;
import org.geysermc.erosion.packet.backendbound.BackendboundBatchBlockRequestPacket;
import org.geysermc.erosion.packet.backendbound.BackendboundBlockRequestPacket;
import org.geysermc.erosion.packet.backendbound.BackendboundPickBlockPacket;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo;

import java.util.List;
import java.util.concurrent.CompletableFuture;

public class GeyserWorldManager extends WorldManager {
Expand Down Expand Up @@ -92,51 +87,6 @@ public boolean hasOwnChunkCache() {
return false;
}

@Override
public void sendLecternData(GeyserSession session, int x, int z, List<BlockEntityInfo> blockEntityInfos) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
// No-op - don't send any additional information other than what the chunk has already sent
return;
}
List<Vector3i> vectors = new ObjectArrayList<>(blockEntityInfos.size());
//noinspection ForLoopReplaceableByForEach - avoid constructing iterator
for (int i = 0; i < blockEntityInfos.size(); i++) {
BlockEntityInfo info = blockEntityInfos.get(i);
vectors.add(Vector3i.from(info.getX(), info.getY(), info.getZ()));
}
erosionHandler.sendPacket(new BackendboundBatchBlockEntityPacket(x, z, vectors));
}

@Override
public void sendLecternData(GeyserSession session, int x, int y, int z) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler != null) {
erosionHandler.sendPacket(new BackendboundBlockEntityPacket(Vector3i.from(x, y, z)));
return;
}

// Without direct server access, we can't get lectern information on-the-fly.
// I should have set this up so it's only called when there is a book in the block state. - Camotoy
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x, y, z, 1);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:written_book")
.putCompound("tag", NbtMap.builder()
.putString("photoname", "")
.putString("text", "")
.build())
.build());
lecternTag.putInt("page", -1); // I'm surprisingly glad this exists - it forces Bedrock to stop reading immediately. Usually.
BlockEntityUtils.updateBlockEntity(session, lecternTag.build(), Vector3i.from(x, y, z));
}

@Override
public boolean shouldExpectLecternHandled(GeyserSession session) {
return session.getErosionHandler().isActive();
}

@Override
public void setGameRule(GeyserSession session, String name, Object value) {
super.setGameRule(session, name, value);
Expand Down
36 changes: 0 additions & 36 deletions core/src/main/java/org/geysermc/geyser/level/WorldManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,9 @@
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemCodecHelper;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo;
import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -118,40 +116,6 @@ public int[] getBlocksAt(GeyserSession session, BlockPositionIterator iter) {
*/
public abstract boolean hasOwnChunkCache();

/**
* Sigh. <br>
*
* So, on Java Edition, the lectern is an inventory. Java opens it and gets the contents of the book there.
* On Bedrock, the lectern contents are part of the block entity tag. Therefore, Bedrock expects to have the contents
* of the lectern ready and present in the world. If the contents are not there, it takes at least two clicks for the
* lectern to update the tag and then present itself. <br>
*
* We solve this problem by querying all loaded lecterns, where possible, and sending their information in a block entity
* tag.
* <p>
* Note that the lectern data may be sent asynchronously.
*
* @param session the session of the player
* @param x the x coordinate of the lectern
* @param y the y coordinate of the lectern
* @param z the z coordinate of the lectern
*/
public abstract void sendLecternData(GeyserSession session, int x, int y, int z);

/**
* {@link #sendLecternData(GeyserSession, int, int, int)} but batched for chunks.
*
* @param x chunk x
* @param z chunk z
* @param blockEntityInfos a list of coordinates (chunk local) to grab lecterns from.
*/
public abstract void sendLecternData(GeyserSession session, int x, int z, List<BlockEntityInfo> blockEntityInfos);

/**
* @return whether we should expect lectern data to update, or if we have to fall back on a workaround.
*/
public abstract boolean shouldExpectLecternHandled(GeyserSession session);

/**
* Updates a gamerule value on the Java server
*
Expand Down
Loading

0 comments on commit 9a9891a

Please sign in to comment.