Folia support and preparations for future changes

This commit is contained in:
Camotoy 2023-03-30 15:44:55 -04:00
parent cd80ee893c
commit e2535108e6
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
36 changed files with 999 additions and 422 deletions

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.platform.fabric.world;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
@ -40,16 +41,16 @@ import net.minecraft.world.item.WrittenBookItem;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.chunk.LevelChunk;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class GeyserFabricWorldManager extends GeyserWorldManager {
private final MinecraftServer server;
@ -59,69 +60,91 @@ public class GeyserFabricWorldManager extends GeyserWorldManager {
}
@Override
public boolean shouldExpectLecternHandled() {
public boolean shouldExpectLecternHandled(GeyserSession session) {
return true;
}
@Override
public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) {
Runnable lecternGet = () -> {
// Mostly a reimplementation of Spigot lectern support
public void sendLecternData(GeyserSession session, int x, int z, List<BlockEntityInfo> blockEntityInfos) {
server.execute(() -> {
ServerPlayer player = getPlayer(session);
if (player != null) {
BlockEntity blockEntity = player.level.getBlockEntity(new BlockPos(x, y, z));
if (!(blockEntity instanceof LecternBlockEntity lectern)) {
return;
}
if (!lectern.hasBook()) {
if (!isChunkLoad) {
BlockEntityUtils.updateBlockEntity(session, LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z));
}
return;
}
ItemStack book = lectern.getBook();
int pageCount = WrittenBookItem.getPageCount(book);
boolean hasBookPages = pageCount > 0;
NbtMapBuilder lecternTag = LecternInventoryTranslator.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 && WritableBookItem.makeSureTagIsValid(book.getTag())) {
ListTag listTag = book.getTag().getList("pages", 8);
for (int i = 0; i < listTag.size(); i++) {
String page = listTag.getString(i);
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));
if (player == null) {
return;
}
};
if (isChunkLoad) {
// Hacky hacks to allow lectern loading to be delayed
session.scheduleInEventLoop(() -> server.execute(lecternGet), 1, TimeUnit.SECONDS);
} else {
server.execute(lecternGet);
LevelChunk chunk = player.getLevel().getChunk(x, z);
final int chunkBlockX = x << 4;
final int chunkBlockZ = z << 4;
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;
}
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;
}
return LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build();
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 = WrittenBookItem.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 && WritableBookItem.makeSureTagIsValid(book.getTag())) {
ListTag listTag = book.getTag().getList("pages", 8);
for (int i = 0; i < listTag.size(); i++) {
String page = listTag.getString(i);
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

View file

@ -1,5 +1,8 @@
dependencies {
api(projects.core)
api(libs.erosion.bukkit.common) {
isTransitive = false
}
implementation(libs.adapters.spigot)
@ -7,8 +10,8 @@ dependencies {
implementation(libs.adventure.text.serializer.bungeecord)
// Both paper-api and paper-mojangapi only provide Java 17 versions for 1.19
compileOnly(libs.paper.api) {
// Both folia-api and paper-mojangapi only provide Java 17 versions for 1.19
compileOnly(libs.folia.api) {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}

View file

@ -59,13 +59,13 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
// runtime because we still have to shade in our own Adventure class. For now.
PaperServerListPingEvent event;
if (OLD_CONSTRUCTOR != null) {
// Approximately pre-1.19
// 1.19, removed in 1.19.4
event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress),
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
} else {
event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
Bukkit.getMotd(), Bukkit.shouldSendChatPreviews(), Bukkit.getOnlinePlayers().size(),
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
}
Bukkit.getPluginManager().callEvent(event);

View file

@ -66,7 +66,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
private static class GeyserPingEvent extends ServerListPingEvent {
public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) {
super(address, motd, Bukkit.shouldSendChatPreviews(), numPlayers, maxPlayers);
super("", address, motd, numPlayers, maxPlayers);
}
@Override

View file

@ -60,16 +60,16 @@ public final class ReflectedNames {
}
static Constructor<PaperServerListPingEvent> getOldPaperPingConstructor() {
if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class,
if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class,
int.class, String.class, int.class, CachedServerIcon.class) != null) {
// @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers,
// @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
// @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
// New constructor is present
return null;
}
// @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
// @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers,
// @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, int.class,
return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class, int.class,
String.class, int.class, CachedServerIcon.class);
}

View file

@ -27,8 +27,8 @@ package org.geysermc.geyser.platform.spigot.world;
import com.github.steveice10.mc.protocol.data.game.level.block.value.PistonValueType;
import com.nukkitx.math.vector.Vector3i;
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
@ -85,7 +85,7 @@ public class GeyserPistonListener implements Listener {
PistonValueType type = isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING;
boolean sticky = event.isSticky();
Object2IntMap<Vector3i> attachedBlocks = new Object2IntOpenHashMap<>();
Object2IntMap<Vector3i> attachedBlocks = new Object2IntArrayMap<>();
boolean blocksFilled = false;
for (Map.Entry<UUID, GeyserSession> entry : geyser.getSessionManager().getSessions().entrySet()) {

View file

@ -25,33 +25,28 @@
package org.geysermc.geyser.platform.spigot.world.manager;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.block.*;
import org.bukkit.block.banner.Pattern;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.Plugin;
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.level.GameRule;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.nbt.BannerTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@ -60,9 +55,11 @@ import java.util.concurrent.CompletableFuture;
*/
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
@ -81,6 +78,12 @@ public class GeyserSpigotWorldManager extends WorldManager {
}
public int getBlockNetworkId(Block block) {
if (SchedulerUtils.FOLIA && !Bukkit.isOwnedByCurrentRegion(block)) {
// Terrible behavior, but this is basically what's always been happening behind the scenes anyway.
CompletableFuture<String> blockData = new CompletableFuture<>();
Bukkit.getRegionScheduler().execute(this.plugin, block.getLocation(), () -> blockData.complete(block.getBlockData().getAsString()));
return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(blockData.join(), BlockStateValues.JAVA_AIR_ID);
}
return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(block.getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID);
}
@ -90,71 +93,64 @@ public class GeyserSpigotWorldManager extends WorldManager {
}
@Override
public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) {
// Run as a task to prevent async issues
Runnable lecternInfoGet = () -> {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return;
}
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
if (!(block.getState() instanceof Lectern lectern)) {
session.getGeyser().getLogger().error("Lectern expected at: " + Vector3i.from(x, y, z).toString() + " but was not! " + block.toString());
return;
}
ItemStack itemStack = lectern.getInventory().getItem(0);
if (itemStack == null || !(itemStack.getItemMeta() instanceof BookMeta bookMeta)) {
if (!isChunkLoad) {
// We need to update the lectern since it's not going to be updated otherwise
BlockEntityUtils.updateBlockEntity(session, LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z));
}
// We don't care; return
return;
}
// On the count: allow the book to show/open even there are no pages. We know there is a book here, after all, and this matches Java behavior
boolean hasBookPages = bookMeta.getPageCount() > 0;
NbtMapBuilder lecternTag = LecternInventoryTranslator.getBaseLecternTag(x, y, z, hasBookPages ? bookMeta.getPageCount() : 1);
lecternTag.putInt("page", lectern.getPage() / 2);
NbtMapBuilder bookTag = NbtMap.builder()
.putByte("Count", (byte) itemStack.getAmount())
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:writable_book");
List<NbtMap> pages = new ArrayList<>(bookMeta.getPageCount());
if (hasBookPages) {
for (String page : bookMeta.getPages()) {
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));
};
if (isChunkLoad) {
// Delay to ensure the chunk is sent first, and then the lectern data
Bukkit.getScheduler().runTaskLater(this.plugin, lecternInfoGet, 5);
} else {
Bukkit.getScheduler().runTask(this.plugin, lecternInfoGet);
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 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) {
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()));
}
return LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(); // Will be updated later
}
@Override
public boolean shouldExpectLecternHandled() {
public boolean shouldExpectLecternHandled(GeyserSession session) {
return true;
}
@ -184,42 +180,18 @@ public class GeyserSpigotWorldManager extends WorldManager {
@Override
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
CompletableFuture<@Nullable CompoundTag> future = new CompletableFuture<>();
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
future.complete(null);
return future;
}
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
// Paper 1.19.3 complains about async access otherwise.
// java.lang.IllegalStateException: Tile is null, asynchronous access?
Bukkit.getScheduler().runTask(this.plugin, () -> {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
future.complete(null);
return;
}
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
BlockState state = block.getState();
if (state instanceof Banner banner) {
ListTag list = new ListTag("Patterns");
for (int i = 0; i < banner.numberOfPatterns(); i++) {
Pattern pattern = banner.getPattern(i);
list.add(BannerTranslator.getJavaPatternTag(pattern.getPattern().getIdentifier(), pattern.getColor().ordinal()));
}
CompoundTag root = addToBlockEntityTag(list);
future.complete(root);
return;
}
future.complete(null);
});
SchedulerUtils.runTask(this.plugin, () -> future.complete(PickBlockUtils.pickBlock(block)), block);
return future;
}
private CompoundTag addToBlockEntityTag(Tag tag) {
CompoundTag compoundTag = new CompoundTag("");
CompoundTag blockEntityTag = new CompoundTag("BlockEntityTag");
blockEntityTag.put(tag);
compoundTag.put(blockEntityTag);
return compoundTag;
}
/**
* This should be set to true if we are post-1.13 but before the latest version, and we should convert the old block state id
* to the current one.

View file

@ -5,6 +5,7 @@ website: ${url}
version: ${version}
softdepend: ["ViaVersion", "floodgate"]
api-version: 1.13
folia-supported: true
commands:
geyser:
description: The main command for Geyser.

View file

@ -49,6 +49,10 @@ dependencies {
// Adventure text serialization
api(libs.bundles.adventure)
api(libs.erosion.common) {
isTransitive = false
}
// Test
testImplementation(libs.junit)

View file

@ -50,6 +50,7 @@ import org.geysermc.api.Geyser;
import org.geysermc.common.PlatformType;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.erosion.packet.Packets;
import org.geysermc.floodgate.crypto.AesCipher;
import org.geysermc.floodgate.crypto.AesKeyProducer;
import org.geysermc.floodgate.crypto.Base64Topping;
@ -67,6 +68,7 @@ import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus;
import org.geysermc.geyser.extension.GeyserExtensionManager;
import org.geysermc.geyser.level.WorldManager;
@ -140,6 +142,8 @@ public class GeyserImpl implements GeyserApi {
private FloodgateSkinUploader skinUploader;
private NewsHandler newsHandler;
private UnixSocketClientListener erosionUnixListener;
private volatile boolean shuttingDown = false;
private ScheduledExecutorService scheduledThread;
@ -293,6 +297,14 @@ public class GeyserImpl implements GeyserApi {
this.newsHandler = new NewsHandler(BRANCH, this.buildNumber());
Packets.initGeyser();
if (Epoll.isAvailable()) {
this.erosionUnixListener = new UnixSocketClientListener();
} else {
logger.debug("Epoll is not available; Erosion's Unix socket handling will not work.");
}
CooldownUtils.setDefaultShowCooldown(config.getShowCooldown());
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
@ -570,6 +582,10 @@ public class GeyserImpl implements GeyserApi {
newsHandler.shutdown();
this.commandManager().getCommands().clear();
if (this.erosionUnixListener != null) {
this.erosionUnixListener.close();
}
ResourcePack.PACKS.clear();
this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus));

View file

@ -30,12 +30,11 @@ import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
import lombok.Getter;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.level.block.BlockPositionIterator;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.util.BlockUtils;

View file

@ -40,11 +40,12 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class ItemEntity extends ThrowableEntity {
protected ItemData item;
private int waterLevel = -1;
private CompletableFuture<Integer> waterLevel = CompletableFuture.completedFuture(-1);
public ItemEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@ -111,15 +112,15 @@ public class ItemEntity extends ThrowableEntity {
@Override
protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
float offset = definition.offset();
if (waterLevel == 0) { // Item is in a full block of water
if (waterLevel.join() == 0) { // Item is in a full block of water
// Move the item entity down so it doesn't float above the water
offset = -definition.offset();
}
super.moveAbsoluteImmediate(position.add(0, offset, 0), 0, 0, 0, isOnGround, teleported);
this.position = position;
int block = session.getGeyser().getWorldManager().getBlockAt(session, position.toInt());
waterLevel = BlockStateValues.getWaterLevel(block);
waterLevel = session.getGeyser().getWorldManager().getBlockAtAsync(session, position.getFloorX(), position.getFloorY(), position.getFloorZ())
.thenApply(BlockStateValues::getWaterLevel);
}
@Override
@ -144,6 +145,6 @@ public class ItemEntity extends ThrowableEntity {
@Override
protected boolean isInWater() {
return waterLevel != -1;
return waterLevel.join() != -1;
}
}

View file

@ -34,12 +34,13 @@ import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class SquidEntity extends WaterEntity implements Tickable {
private float targetPitch;
private float targetYaw;
private boolean inWater;
private CompletableFuture<Boolean> inWater = CompletableFuture.completedFuture(Boolean.FALSE);
public SquidEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@ -50,7 +51,7 @@ public class SquidEntity extends WaterEntity implements Tickable {
boolean pitchChanged;
boolean yawChanged;
float oldPitch = pitch;
if (inWater) {
if (inWater.join()) {
float oldYaw = yaw;
pitch += (targetPitch - pitch) * 0.1f;
yaw += (targetYaw - yaw) * 0.1f;
@ -93,7 +94,7 @@ public class SquidEntity extends WaterEntity implements Tickable {
@Override
public void setYaw(float yaw) {
// Let the Java server control yaw when the squid is out of water
if (!inWater) {
if (!inWater.join()) {
this.yaw = yaw;
}
}
@ -127,10 +128,10 @@ public class SquidEntity extends WaterEntity implements Tickable {
private void checkInWater() {
if (getFlag(EntityFlag.RIDING)) {
inWater = false;
inWater = CompletableFuture.completedFuture(false);
} else {
int block = session.getGeyser().getWorldManager().getBlockAt(session, position.toInt());
inWater = BlockStateValues.getWaterLevel(block) != -1;
inWater = session.getGeyser().getWorldManager().getBlockAtAsync(session, position.toInt())
.thenApply(block -> BlockStateValues.getWaterLevel(block) != -1);
}
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.erosion;
import org.geysermc.erosion.packet.geyserbound.*;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Nullable;
public abstract class AbstractGeyserboundPacketHandler implements GeyserboundPacketHandler {
protected final GeyserSession session;
public AbstractGeyserboundPacketHandler(GeyserSession session) {
this.session = session;
}
@Override
public void handleBatchBlockId(GeyserboundBatchBlockIdPacket packet) {
illegalPacket(packet);
}
@Override
public void handleBlockEntity(GeyserboundBlockEntityPacket packet) {
illegalPacket(packet);
}
@Override
public void handleBlockId(GeyserboundBlockIdPacket packet) {
illegalPacket(packet);
}
@Override
public void handleBlockLookupFail(GeyserboundBlockLookupFailPacket packet) {
illegalPacket(packet);
}
@Override
public void handleBlockPlace(GeyserboundBlockPlacePacket packet) {
illegalPacket(packet);
}
@Override
public void handlePistonEvent(GeyserboundPistonEventPacket packet) {
illegalPacket(packet);
}
@Override
public void handlePickBlock(GeyserboundPickBlockPacket packet) {
illegalPacket(packet);
}
/**
* Is this handler actually listening to any packets?
*/
public abstract boolean isActive();
@Nullable
public abstract GeyserboundPacketHandlerImpl getAsActive();
public void close() {
}
protected final void illegalPacket(GeyserboundPacket packet) {
session.getGeyser().getLogger().warning("Illegal packet sent from backend server! " + packet);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.erosion;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import org.geysermc.erosion.Constants;
import org.geysermc.erosion.packet.ErosionPacketSender;
import org.geysermc.erosion.packet.Packets;
import org.geysermc.erosion.packet.backendbound.BackendboundPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.PluginMessageUtils;
import java.io.IOException;
public record GeyserErosionPacketSender(GeyserSession session) implements ErosionPacketSender<BackendboundPacket> {
@Override
public void sendPacket(BackendboundPacket packet) {
ByteBuf buf = Unpooled.buffer();
try {
Packets.encode(buf, packet);
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
PluginMessageUtils.sendMessage(session, Constants.PLUGIN_MESSAGE, bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
buf.release();
}
}
@Override
public void setChannel(Channel channel) {
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.erosion;
import io.netty.channel.Channel;
import org.geysermc.erosion.netty.NettyPacketSender;
import org.geysermc.erosion.packet.ErosionPacketHandler;
import org.geysermc.erosion.packet.geyserbound.GeyserboundHandshakePacket;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Nullable;
public final class GeyserboundHandshakePacketHandler extends AbstractGeyserboundPacketHandler {
public GeyserboundHandshakePacketHandler(GeyserSession session) {
super(session);
}
@Override
public void handleHandshake(GeyserboundHandshakePacket packet) {
boolean useTcp = packet.getTransportType().getSocketAddress() == null;
GeyserboundPacketHandlerImpl handler = new GeyserboundPacketHandlerImpl(session, useTcp ? new GeyserErosionPacketSender(session) : new NettyPacketSender<>());
session.setErosionHandler(handler);
if (!useTcp) {
if (session.getGeyser().getErosionUnixListener() == null) {
session.disconnect("Erosion configurations using Unix socket handling are not supported on this hardware!");
return;
}
session.getGeyser().getErosionUnixListener().createClient(handler, packet.getTransportType().getSocketAddress());
} else {
handler.onConnect();
}
session.ensureInEventLoop(() -> session.getChunkCache().clear());
}
@Override
public boolean isActive() {
return false;
}
@Override
public @Nullable GeyserboundPacketHandlerImpl getAsActive() {
return null;
}
@Override
public ErosionPacketHandler setChannel(Channel channel) {
return null;
}
}

View file

@ -0,0 +1,209 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.erosion;
import com.github.steveice10.mc.protocol.data.game.level.block.value.PistonValueType;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
import io.netty.channel.Channel;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrays;
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.erosion.packet.ErosionPacketHandler;
import org.geysermc.erosion.packet.ErosionPacketSender;
import org.geysermc.erosion.packet.backendbound.BackendboundInitializePacket;
import org.geysermc.erosion.packet.backendbound.BackendboundPacket;
import org.geysermc.erosion.packet.geyserbound.*;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.PistonCache;
import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity;
import org.geysermc.geyser.util.BlockEntityUtils;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacketHandler {
private final ErosionPacketSender<BackendboundPacket> packetSender;
@Setter
private CompletableFuture<Integer> pendingLookup = null;
@Getter
private final Int2ObjectMap<CompletableFuture<Integer>> asyncPendingLookups = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>(4));
@Setter
private CompletableFuture<int[]> pendingBatchLookup = null;
@Setter
private CompletableFuture<CompoundTag> pickBlockLookup = null;
private final AtomicInteger nextTransactionId = new AtomicInteger(1);
public GeyserboundPacketHandlerImpl(GeyserSession session, ErosionPacketSender<BackendboundPacket> packetSender) {
super(session);
this.packetSender = packetSender;
}
@Override
public void handleBatchBlockId(GeyserboundBatchBlockIdPacket packet) {
if (this.pendingBatchLookup != null) {
this.pendingBatchLookup.complete(packet.getBlocks());
} else {
session.getGeyser().getLogger().warning("Batch block ID packet received with no future to complete.");
}
}
@Override
public void handleBlockEntity(GeyserboundBlockEntityPacket packet) {
NbtMap nbt = packet.getNbt();
BlockEntityUtils.updateBlockEntity(session, nbt, Vector3i.from(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")));
}
@Override
public void handleBlockId(GeyserboundBlockIdPacket packet) {
if (packet.getTransactionId() == 0) {
if (this.pendingLookup != null) {
this.pendingLookup.complete(packet.getBlockId());
return;
}
}
CompletableFuture<Integer> future = this.asyncPendingLookups.remove(packet.getTransactionId());
if (future != null) {
future.complete(packet.getBlockId());
return;
}
session.getGeyser().getLogger().warning("Block ID packet received with no future to complete.");
}
@Override
public void handleBlockLookupFail(GeyserboundBlockLookupFailPacket packet) {
if (packet.getTransactionId() == 0) {
if (this.pendingBatchLookup != null) {
this.pendingBatchLookup.complete(null);
return;
}
}
int transactionId = packet.getTransactionId() - 1;
if (transactionId == 0) {
if (this.pendingLookup != null) {
this.pendingLookup.complete(0);
}
}
CompletableFuture<Integer> future = this.asyncPendingLookups.remove(transactionId);
if (future != null) {
future.complete(BlockStateValues.JAVA_AIR_ID);
}
}
@Override
public void handleBlockPlace(GeyserboundBlockPlacePacket packet) {
LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket();
placeBlockSoundPacket.setSound(SoundEvent.PLACE);
placeBlockSoundPacket.setPosition(packet.getPos().toFloat());
placeBlockSoundPacket.setBabySound(false);
placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(packet.getBlockId()));
placeBlockSoundPacket.setIdentifier(":");
session.sendUpstreamPacket(placeBlockSoundPacket);
session.setLastBlockPlacePosition(null);
session.setLastBlockPlacedId(null);
}
@Override
public void handlePickBlock(GeyserboundPickBlockPacket packet) {
if (this.pickBlockLookup != null) {
this.pickBlockLookup.complete(packet.getTag());
}
}
@Override
public void handlePistonEvent(GeyserboundPistonEventPacket packet) {
Direction orientation = BlockStateValues.getPistonOrientation(packet.getBlockId());
Vector3i position = packet.getPos();
boolean isExtend = packet.isExtend();
var stream = packet.getAttachedBlocks()
.object2IntEntrySet()
.stream()
.filter(entry -> BlockStateValues.canPistonMoveBlock(entry.getIntValue(), isExtend));
Object2IntMap<Vector3i> attachedBlocks = new Object2IntArrayMap<>();
stream.forEach(entry -> attachedBlocks.put(entry.getKey(), entry.getIntValue()));
session.executeInEventLoop(() -> {
PistonCache pistonCache = session.getPistonCache();
PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos ->
new PistonBlockEntity(session, position, orientation, packet.isSticky(), !isExtend));
blockEntity.setAction(isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING, attachedBlocks);
});
}
@Override
public void handleHandshake(GeyserboundHandshakePacket packet) {
this.close();
var handler = new GeyserboundHandshakePacketHandler(this.session);
session.setErosionHandler(handler);
handler.handleHandshake(packet);
}
@Override
public boolean isActive() {
return true;
}
@Override
public GeyserboundPacketHandlerImpl getAsActive() {
return this;
}
@Override
public void onConnect() {
sendPacket(new BackendboundInitializePacket(session.getPlayerEntity().getUuid(), GameProtocol.getJavaProtocolVersion()));
}
public void sendPacket(BackendboundPacket packet) {
this.packetSender.sendPacket(packet);
}
public void close() {
this.packetSender.close();
}
public int getNextTransactionId() {
return nextTransactionId.getAndIncrement();
}
@Override
public ErosionPacketHandler setChannel(Channel channel) {
this.packetSender.setChannel(channel);
return this;
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.erosion;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollDomainSocketChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import org.geysermc.erosion.netty.impl.AbstractUnixSocketListener;
import org.geysermc.erosion.packet.geyserbound.GeyserboundPacketHandler;
import java.net.SocketAddress;
public final class UnixSocketClientListener extends AbstractUnixSocketListener {
private EventLoopGroup eventLoopGroup;
public void initializeEventLoopGroup() {
if (this.eventLoopGroup == null) {
this.eventLoopGroup = new EpollEventLoopGroup();
}
}
public void createClient(GeyserboundPacketHandler handler, SocketAddress address) {
initializeEventLoopGroup();
(new Bootstrap()
.channel(EpollDomainSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
initPipeline(ch, handler);
}
})
.group(this.eventLoopGroup.next())
.connect(address))
.syncUninterruptibly()
.channel();
}
@Override
public void close() {
if (this.eventLoopGroup != null) {
this.eventLoopGroup.shutdownGracefully();
}
}
}

View file

@ -25,19 +25,63 @@
package org.geysermc.geyser.level;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.geysermc.erosion.packet.backendbound.*;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class GeyserWorldManager extends WorldManager {
private final Object2ObjectMap<String, String> gameruleCache = new Object2ObjectOpenHashMap<>();
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
return session.getChunkCache().getBlockAt(x, y, z);
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return session.getChunkCache().getBlockAt(x, y, z);
}
CompletableFuture<Integer> future = new CompletableFuture<>(); // Boxes
erosionHandler.setPendingLookup(future);
erosionHandler.sendPacket(new BackendboundBlockRequestPacket(0, Vector3i.from(x, y, z)));
return future.join();
}
@Override
public CompletableFuture<Integer> getBlockAtAsync(GeyserSession session, int x, int y, int z) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getBlockAtAsync(session, x, y, z);
}
CompletableFuture<Integer> future = new CompletableFuture<>(); // Boxes
int transactionId = erosionHandler.getNextTransactionId();
erosionHandler.getAsyncPendingLookups().put(transactionId, future);
erosionHandler.sendPacket(new BackendboundBlockRequestPacket(transactionId, Vector3i.from(x, y, z)));
return future;
}
@Override
public int[] getBlocksAt(GeyserSession session, BlockPositionIterator iter) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getBlocksAt(session, iter);
}
CompletableFuture<int[]> future = new CompletableFuture<>();
erosionHandler.setPendingBatchLookup(future);
erosionHandler.sendPacket(new BackendboundBatchBlockRequestPacket(iter));
return future.join();
}
@Override
@ -47,10 +91,31 @@ public class GeyserWorldManager extends WorldManager {
}
@Override
public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) {
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());
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 = LecternInventoryTranslator.getBaseLecternTag(x, y, z, 1);
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x, y, z, 1);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
@ -61,12 +126,12 @@ public class GeyserWorldManager extends WorldManager {
.build())
.build());
lecternTag.putInt("page", -1); // I'm surprisingly glad this exists - it forces Bedrock to stop reading immediately. Usually.
return lecternTag.build();
BlockEntityUtils.updateBlockEntity(session, lecternTag.build(), Vector3i.from(x, y, z));
}
@Override
public boolean shouldExpectLecternHandled() {
return false;
public boolean shouldExpectLecternHandled(GeyserSession session) {
return session.getErosionHandler().isActive();
}
@Override
@ -99,4 +164,17 @@ public class GeyserWorldManager extends WorldManager {
public boolean hasPermission(GeyserSession session, String permission) {
return false;
}
@Nonnull
@Override
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getPickItemNbt(session, x, y, z, addNbtData);
}
CompletableFuture<CompoundTag> future = new CompletableFuture<>();
erosionHandler.setPickBlockLookup(future);
erosionHandler.sendPacket(new BackendboundPickBlockPacket(Vector3i.from(x, y, z)));
return future;
}
}

View file

@ -26,14 +26,16 @@
package org.geysermc.geyser.level;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
@ -68,6 +70,23 @@ public abstract class WorldManager {
*/
public abstract int getBlockAt(GeyserSession session, int x, int y, int z);
public final CompletableFuture<Integer> getBlockAtAsync(GeyserSession session, Vector3i vector) {
return this.getBlockAtAsync(session, vector.getX(), vector.getY(), vector.getZ());
}
public CompletableFuture<Integer> getBlockAtAsync(GeyserSession session, int x, int y, int z) {
return CompletableFuture.completedFuture(this.getBlockAt(session, x, y, z));
}
public int[] getBlocksAt(GeyserSession session, BlockPositionIterator iter) {
int[] blocks = new int[iter.getMaxIterations()];
for (; iter.hasNext(); iter.next()) {
int networkId = this.getBlockAt(session, iter.getX(), iter.getY(), iter.getZ());
blocks[iter.getIteration()] = networkId;
}
return blocks;
}
/**
* Checks whether or not this world manager requires a separate chunk cache/has access to more block data than the chunk cache.
* <p>
@ -89,20 +108,28 @@ public abstract class WorldManager {
* We solve this problem by querying all loaded lecterns, where possible, and sending their information in a block entity
* tag.
*
* 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
* @param isChunkLoad if this is called during a chunk load or not. Changes behavior in certain instances.
* @return the Bedrock lectern block entity tag. This may not be the exact block entity tag - for example, Spigot's
* block handled must be done on the server thread, so we send the tag manually there.
*/
public abstract NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad);
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();
public abstract boolean shouldExpectLecternHandled(GeyserSession session);
/**
* Updates a gamerule value on the Java server

View file

@ -1,80 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.level.block;
import com.nukkitx.network.util.Preconditions;
public class BlockPositionIterator {
private final int minX;
private final int minY;
private final int minZ;
private final int sizeX;
private final int sizeZ;
private int i = 0;
private final int maxI;
public BlockPositionIterator(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
Preconditions.checkArgument(maxX >= minX, "maxX is not greater than or equal to minX");
Preconditions.checkArgument(maxY >= minY, "maxY is not greater than or equal to minY");
Preconditions.checkArgument(maxZ >= minZ, "maxZ is not greater than or equal to minZ");
this.minX = minX;
this.minY = minY;
this.minZ = minZ;
this.sizeX = maxX - minX + 1;
int sizeY = maxY - minY + 1;
this.sizeZ = maxZ - minZ + 1;
this.maxI = sizeX * sizeY * sizeZ;
}
public boolean hasNext() {
return i < maxI;
}
public void next() {
// Iterate in zxy order
i++;
}
public void reset() {
i = 0;
}
public int getX() {
return ((i / sizeZ) % sizeX) + minX;
}
public int getY() {
return (i / sizeZ / sizeX) + minY;
}
public int getZ() {
return (i % sizeZ) + minZ;
}
}

View file

@ -33,10 +33,10 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.level.block.BlockPositionIterator;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.PistonCache;
@ -215,7 +215,7 @@ public class CollisionManager {
int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand));
int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand);
return new BlockPositionIterator(minCollisionX, minCollisionY, minCollisionZ, maxCollisionX, maxCollisionY, maxCollisionZ);
return BlockPositionIterator.fromMinMax(minCollisionX, minCollisionY, minCollisionZ, maxCollisionX, maxCollisionY, maxCollisionZ);
}
public BlockPositionIterator playerCollidableBlocksIterator() {
@ -235,8 +235,9 @@ public class CollisionManager {
// Used when correction code needs to be run before the main correction
BlockPositionIterator iter = session.getCollisionManager().playerCollidableBlocksIterator();
for (; iter.hasNext(); iter.next()) {
BlockCollision blockCollision = BlockUtils.getCollisionAt(session, iter.getX(), iter.getY(), iter.getZ());
int[] blocks = session.getGeyser().getWorldManager().getBlocksAt(session, iter);
for (iter.reset(); iter.hasNext(); iter.next()) {
BlockCollision blockCollision = BlockUtils.getCollision(blocks[iter.getIteration()]);
if (blockCollision != null) {
blockCollision.beforeCorrectPosition(iter.getX(), iter.getY(), iter.getZ(), playerBoundingBox);
}
@ -244,7 +245,7 @@ public class CollisionManager {
// Main correction code
for (iter.reset(); iter.hasNext(); iter.next()) {
BlockCollision blockCollision = BlockUtils.getCollisionAt(session, iter.getX(), iter.getY(), iter.getZ());
BlockCollision blockCollision = BlockUtils.getCollision(blocks[iter.getIteration()]);
if (blockCollision != null) {
if (!blockCollision.correctPosition(session, iter.getX(), iter.getY(), iter.getZ(), playerBoundingBox)) {
return false;

View file

@ -102,13 +102,17 @@ public class ResourcePack {
pack.sha256 = FileUtils.calculateSHA256(file);
Stream<? extends ZipEntry> stream = null;
try {
ZipFile zip = new ZipFile(file);
stream = zip.stream();
try (ZipFile zip = new ZipFile(file);
Stream<? extends ZipEntry> stream = zip.stream()) {
stream.forEach((x) -> {
if (x.getName().contains("manifest.json")) {
String name = x.getName();
if (name.length() >= 80) {
GeyserImpl.getInstance().getLogger().warning("The resource pack " + file.getName()
+ " has a file in it that meets or exceeds 80 characters in its path (" + name
+ ", " + name.length() + " characters long). This will cause problems on some Bedrock platforms." +
" Please rename it to be shorter, or reduce the amount of folders needed to get to the file.");
}
if (name.contains("manifest.json")) {
try {
ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);
// Sometimes a pack_manifest file is present and not in a valid format,
@ -133,10 +137,6 @@ public class ResourcePack {
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", file.getName()));
e.printStackTrace();
} finally {
if (stream != null) {
stream.close();
}
}
}
}

View file

@ -112,6 +112,8 @@ import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.Tickable;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.erosion.AbstractGeyserboundPacketHandler;
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
@ -136,6 +138,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.LoginEncryptionUtils;
import org.jetbrains.annotations.NotNull;
import java.net.ConnectException;
import java.net.InetSocketAddress;
@ -168,6 +171,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private JsonNode certChainData;
@NotNull
@Setter
private AbstractGeyserboundPacketHandler erosionHandler;
@Accessors(fluent = true)
@Setter
private RemoteServer remoteServer;
@ -255,7 +262,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Stores a list of all lectern locations and their block entity tags.
* See {@link WorldManager#getLecternDataAt(GeyserSession, int, int, int, boolean)}
* See {@link WorldManager#sendLecternData(GeyserSession, int, int, int)}
* for more information.
*/
private final Set<Vector3i> lecternCache;
@ -551,6 +558,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.upstream = new UpstreamSession(bedrockServerSession);
this.eventLoop = eventLoop;
this.erosionHandler = new GeyserboundHandshakePacketHandler(this);
this.advancementsCache = new AdvancementsCache(this);
this.bookEditCache = new BookEditCache(this);
this.chunkCache = new ChunkCache(this);
@ -579,7 +588,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.spawned = false;
this.loggedIn = false;
if (geyser.getWorldManager().shouldExpectLecternHandled()) {
if (geyser.getWorldManager().shouldExpectLecternHandled(this)) {
// Unneeded on these platforms
this.lecternCache = null;
} else {
@ -1088,6 +1097,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
tickThread.cancel(false);
}
erosionHandler.close();
closed = true;
}

View file

@ -35,6 +35,7 @@ import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.LecternContainer;
@ -110,13 +111,13 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
Vector3i position = session.getLastInteractionBlockPosition();
// If shouldExpectLecternHandled returns true, this is already handled for us
// shouldRefresh means that we should boot out the client on our side because their lectern GUI isn't updated yet
boolean shouldRefresh = !session.getGeyser().getWorldManager().shouldExpectLecternHandled() && !session.getLecternCache().contains(position);
boolean shouldRefresh = !session.getGeyser().getWorldManager().shouldExpectLecternHandled(session) && !session.getLecternCache().contains(position);
NbtMap blockEntityTag;
if (tag != null) {
int pagesSize = ((ListTag) tag.get("pages")).size();
ItemData itemData = book.getItemData(session);
NbtMapBuilder lecternTag = getBaseLecternTag(position.getX(), position.getY(), position.getZ(), pagesSize);
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), pagesSize);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) itemData.getCount())
.putShort("Damage", (short) 0)
@ -127,7 +128,7 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
blockEntityTag = lecternTag.build();
} else {
// There is *a* book here, but... no NBT.
NbtMapBuilder lecternTag = getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 1);
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 1);
NbtMapBuilder bookTag = NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
@ -162,20 +163,4 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) {
return new LecternContainer(name, windowId, this.size, containerType, playerInventory);
}
public static NbtMapBuilder getBaseLecternTag(int x, int y, int z, int totalPages) {
NbtMapBuilder builder = NbtMap.builder()
.putInt("x", x)
.putInt("y", y)
.putInt("z", z)
.putString("id", "Lectern");
if (totalPages != 0) {
builder.putByte("hasBook", (byte) 1);
builder.putInt("totalPages", totalPages);
} else {
// Not usually needed, but helps with kicking out Bedrock players from reading the UI
builder.putByte("hasBook", (byte) 0);
}
return builder;
}
}

View file

@ -40,6 +40,8 @@ import javax.annotation.Nonnull;
import java.util.*;
import java.util.stream.Collectors;
import static org.geysermc.erosion.util.BannerUtils.getJavaPatternTag;
@ItemRemapper
public class BannerTranslator extends NbtItemStackTranslator {
/**
@ -66,15 +68,6 @@ public class BannerTranslator extends NbtItemStackTranslator {
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 15));
}
public static CompoundTag getJavaPatternTag(String pattern, int color) {
StringTag patternType = new StringTag("Pattern", pattern);
IntTag colorTag = new IntTag("Color", color);
CompoundTag tag = new CompoundTag("");
tag.put(patternType);
tag.put(colorTag);
return tag;
}
public BannerTranslator() {
appliedItems = Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getItems())
.filter(entry -> entry.getJavaIdentifier().endsWith("banner"))
@ -117,7 +110,7 @@ public class BannerTranslator extends NbtItemStackTranslator {
* @return The Java edition format pattern nbt
*/
public static CompoundTag getJavaBannerPattern(NbtMap pattern) {
return BannerTranslator.getJavaPatternTag(pattern.getString("Pattern"), 15 - pattern.getInt("Color"));
return getJavaPatternTag(pattern.getString("Pattern"), 15 - pattern.getInt("Color"));
}
/**

View file

@ -224,7 +224,7 @@ public class PistonBlockEntity {
int blockId = session.getGeyser().getWorldManager().getBlockAt(session, blockInFront);
if (BlockStateValues.isPistonHead(blockId)) {
ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront);
} else if (session.getGeyser().getPlatformType() == PlatformType.SPIGOT && blockId == BlockStateValues.JAVA_AIR_ID) {
} else if ((session.getGeyser().getPlatformType() == PlatformType.SPIGOT || session.getErosionHandler().isActive()) && blockId == BlockStateValues.JAVA_AIR_ID) {
// Spigot removes the piston head from the cache, but we need to send the block update ourselves
ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront);
}

View file

@ -71,30 +71,28 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator<BlockPic
boolean addNbtData = packet.isAddUserData() && blockMapping.isBlockEntity(); // Holding down CTRL
if (BlockStateValues.getBannerColor(blockToPick) != -1 || addNbtData) {
session.getGeyser().getWorldManager().getPickItemNbt(session, vector.getX(), vector.getY(), vector.getZ(), addNbtData)
.whenComplete((tag, ex) -> {
.whenComplete((tag, ex) -> session.ensureInEventLoop(() -> {
if (tag == null) {
pickItem(session, blockMapping);
return;
}
session.ensureInEventLoop(() -> {
if (addNbtData) {
ListTag lore = new ListTag("Lore");
lore.add(new StringTag("", "\"(+NBT)\""));
CompoundTag display = tag.get("display");
if (display == null) {
display = new CompoundTag("display");
tag.put(display);
}
display.put(lore);
if (addNbtData) {
ListTag lore = new ListTag("Lore");
lore.add(new StringTag("", "\"(+NBT)\""));
CompoundTag display = tag.get("display");
if (display == null) {
display = new CompoundTag("display");
tag.put(display);
}
// I don't really like this... I'd rather get an ID from the block mapping I think
ItemMapping mapping = session.getItemMappings().getMapping(blockMapping.getPickItem());
display.put(lore);
}
// I don't really like this... I'd rather get an ID from the block mapping I think
ItemMapping mapping = session.getItemMappings().getMapping(blockMapping.getPickItem());
ItemStack itemStack = new ItemStack(mapping.getJavaId(), 1, tag);
InventoryUtils.findOrCreateItem(session, itemStack);
});
});
ItemStack itemStack = new ItemStack(mapping.getJavaId(), 1, tag);
InventoryUtils.findOrCreateItem(session, itemStack);
}));
return;
}

View file

@ -35,10 +35,13 @@ import io.netty.buffer.Unpooled;
import org.geysermc.cumulus.Forms;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormType;
import org.geysermc.erosion.Constants;
import org.geysermc.erosion.packet.ErosionPacket;
import org.geysermc.erosion.packet.Packets;
import org.geysermc.erosion.packet.geyserbound.GeyserboundPacket;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -51,82 +54,96 @@ public class JavaCustomPayloadTranslator extends PacketTranslator<ClientboundCus
@Override
public void translate(GeyserSession session, ClientboundCustomPayloadPacket packet) {
// The only plugin messages it has to listen for are Floodgate plugin messages
if (session.remoteServer().authType() != AuthType.FLOODGATE) {
String channel = packet.getChannel();
if (channel.equals(Constants.PLUGIN_MESSAGE)) {
ByteBuf buf = Unpooled.wrappedBuffer(packet.getData());
ErosionPacket<?> erosionPacket = Packets.decode(buf);
((GeyserboundPacket) erosionPacket).handle(session.getErosionHandler());
return;
}
String channel = packet.getChannel();
if (channel.equals(PluginMessageChannels.FORM)) {
byte[] data = packet.getData();
session.ensureInEventLoop(() -> {
byte[] data = packet.getData();
// receive: first byte is form type, second and third are the id, remaining is the form data
// respond: first and second byte id, remaining is form response data
// receive: first byte is form type, second and third are the id, remaining is the form data
// respond: first and second byte id, remaining is form response data
FormType type = FormType.fromOrdinal(data[0]);
if (type == null) {
throw new NullPointerException("Got type " + data[0] + " which isn't a valid form type!");
}
String dataString = new String(data, 3, data.length - 3, Charsets.UTF_8);
Form form = Forms.fromJson(dataString, type, (ignored, response) -> {
byte[] finalData;
if (response == null) {
// Response data can be null as of 1.19.20 (same behaviour as empty response data)
// Only need to send the form id
finalData = new byte[]{data[1], data[2]};
} else {
byte[] raw = response.getBytes(StandardCharsets.UTF_8);
finalData = new byte[raw.length + 2];
finalData[0] = data[1];
finalData[1] = data[2];
System.arraycopy(raw, 0, finalData, 2, raw.length);
FormType type = FormType.fromOrdinal(data[0]);
if (type == null) {
throw new NullPointerException("Got type " + data[0] + " which isn't a valid form type!");
}
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(channel, finalData));
String dataString = new String(data, 3, data.length - 3, Charsets.UTF_8);
Form form = Forms.fromJson(dataString, type, (ignored, response) -> {
byte[] finalData;
if (response == null) {
// Response data can be null as of 1.19.20 (same behaviour as empty response data)
// Only need to send the form id
finalData = new byte[]{data[1], data[2]};
} else {
byte[] raw = response.getBytes(StandardCharsets.UTF_8);
finalData = new byte[raw.length + 2];
finalData[0] = data[1];
finalData[1] = data[2];
System.arraycopy(raw, 0, finalData, 2, raw.length);
}
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(channel, finalData));
});
session.sendForm(form);
});
session.sendForm(form);
} else if (channel.equals(PluginMessageChannels.TRANSFER)) {
byte[] data = packet.getData();
session.ensureInEventLoop(() -> {
byte[] data = packet.getData();
// port (4 bytes), address (remaining data)
if (data.length < 5) {
throw new NullPointerException("Transfer data should be at least 5 bytes long");
}
// port (4 bytes), address (remaining data)
if (data.length < 5) {
throw new NullPointerException("Transfer data should be at least 5 bytes long");
}
int port = data[0] << 24 | (data[1] & 0xFF) << 16 | (data[2] & 0xFF) << 8 | data[3] & 0xFF;
String address = new String(data, 4, data.length - 4);
int port = data[0] << 24 | (data[1] & 0xFF) << 16 | (data[2] & 0xFF) << 8 | data[3] & 0xFF;
String address = new String(data, 4, data.length - 4);
if (logger.isDebug()) {
logger.info("Transferring client to: " + address + ":" + port);
}
if (logger.isDebug()) {
logger.info("Transferring client to: " + address + ":" + port);
}
TransferPacket transferPacket = new TransferPacket();
transferPacket.setAddress(address);
transferPacket.setPort(port);
session.sendUpstreamPacket(transferPacket);
TransferPacket transferPacket = new TransferPacket();
transferPacket.setAddress(address);
transferPacket.setPort(port);
session.sendUpstreamPacket(transferPacket);
});
} else if (channel.equals(PluginMessageChannels.PACKET)) {
logger.debug("A packet has been sent using the Floodgate api");
byte[] data = packet.getData();
session.ensureInEventLoop(() -> {
logger.debug("A packet has been sent using the Floodgate api");
byte[] data = packet.getData();
// packet id, packet data
if (data.length < 2) {
throw new IllegalStateException("Packet data should be at least 2 bytes long");
}
// packet id, packet data
if (data.length < 2) {
throw new IllegalStateException("Packet data should be at least 2 bytes long");
}
int packetId = data[0] & 0xFF;
ByteBuf packetData = Unpooled.wrappedBuffer(data, 1, data.length - 1);
int packetId = data[0] & 0xFF;
ByteBuf packetData = Unpooled.wrappedBuffer(data, 1, data.length - 1);
var toSend = new UnknownPacket();
toSend.setPacketId(packetId);
toSend.setPayload(packetData);
var toSend = new UnknownPacket();
toSend.setPacketId(packetId);
toSend.setPayload(packetData);
session.sendUpstreamPacket(toSend);
session.sendUpstreamPacket(toSend);
});
}
}
@Override
public boolean shouldExecuteInEventLoop() {
// For Erosion packets
return false;
}
}

View file

@ -36,6 +36,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.TextDecoration;
@ -57,6 +58,11 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
SessionPlayerEntity entity = session.getPlayerEntity();
entity.setEntityId(packet.getEntityId());
if (session.getErosionHandler().isActive()) {
session.getErosionHandler().close();
session.setErosionHandler(new GeyserboundHandshakePacketHandler(session));
}
Map<String, JavaDimension> dimensions = session.getDimensions();
dimensions.clear();
@ -129,6 +135,10 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
// TODO don't send two packets
// if (true) {
// session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:register", Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8)));
// }
// register the plugin messaging channels used in Floodgate
if (session.remoteServer().authType() == AuthType.FLOODGATE) {
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:register", PluginMessageChannels.getFloodgateRegisterData()));

View file

@ -58,15 +58,16 @@ public class JavaBlockEventTranslator extends PacketTranslator<ClientboundBlockE
blockEventPacket.setEventType(1);
session.sendUpstreamPacket(blockEventPacket);
} else if (packet.getValue() instanceof NoteBlockValue) {
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, position);
blockEventPacket.setEventData(BlockStateValues.getNoteblockPitch(blockState));
session.sendUpstreamPacket(blockEventPacket);
session.getGeyser().getWorldManager().getBlockAtAsync(session, position).thenAccept(blockState -> {
blockEventPacket.setEventData(BlockStateValues.getNoteblockPitch(blockState));
session.sendUpstreamPacket(blockEventPacket);
});
} else if (packet.getValue() instanceof PistonValue pistonValue) {
PistonValueType action = (PistonValueType) packet.getType();
Direction direction = Direction.fromPistonValue(pistonValue.getDirection());
PistonCache pistonCache = session.getPistonCache();
if (session.getGeyser().getPlatformType() == PlatformType.SPIGOT) {
if (session.getGeyser().getPlatformType() == PlatformType.SPIGOT || session.getErosionHandler().isActive()) {
// Mostly handled in the GeyserPistonEvents class
// Retracting sticky pistons is an exception, since the event is not called on Spigot from 1.13.2 - 1.17.1
// See https://github.com/PaperMC/Paper/blob/6fa1983e9ce177a4a412d5b950fd978620174777/patches/server/0304-Fire-BlockPistonRetractEvent-for-all-empty-pistons.patch

View file

@ -43,7 +43,7 @@ public class JavaBlockUpdateTranslator extends PacketTranslator<ClientboundBlock
public void translate(GeyserSession session, ClientboundBlockUpdatePacket packet) {
Vector3i pos = packet.getEntry().getPosition();
boolean updatePlacement = session.getGeyser().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event
session.getGeyser().getWorldManager().getBlockAt(session, pos) != packet.getEntry().getBlock();
!session.getErosionHandler().isActive() && session.getGeyser().getWorldManager().getBlockAt(session, pos) != packet.getEntry().getBlock();
session.getWorldCache().updateServerCorrectBlockState(pos, packet.getEntry().getBlock());
if (updatePlacement) {
this.checkPlace(session, packet);

View file

@ -52,7 +52,7 @@ public class JavaForgetLevelChunkTranslator extends PacketTranslator<Clientbound
}
removedSkulls.forEach(session.getSkullCache()::removeSkull);
if (!session.getGeyser().getWorldManager().shouldExpectLecternHandled()) {
if (!session.getGeyser().getWorldManager().shouldExpectLecternHandled(session)) {
// Do the same thing with lecterns
Iterator<Vector3i> iterator = session.getLecternCache().iterator();
while (iterator.hasNext()) {

View file

@ -38,6 +38,7 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NBTOutputStream;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtUtils;
import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
import io.netty.buffer.ByteBuf;
@ -48,6 +49,7 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.block.BlockStateValues;
@ -94,6 +96,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
final BlockEntityInfo[] blockEntities = packet.getBlockEntities();
final List<NbtMap> bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length);
final List<BlockEntityInfo> lecterns = new ObjectArrayList<>();
BitSet waterloggedPaletteIds = new BitSet();
BitSet bedrockOnlyBlockEntityIds = new BitSet();
@ -240,7 +243,9 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
sections[bedrockSectionY] = new GeyserChunkSection(layers);
}
session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks);
if (!session.getErosionHandler().isActive()) {
session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks);
}
final int chunkBlockX = packet.getX() << 4;
final int chunkBlockZ = packet.getZ() << 4;
@ -262,8 +267,15 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
if (type == BlockEntityType.LECTERN && BlockStateValues.getLecternBookStates().get(blockState)) {
// If getLecternBookStates is false, let's just treat it like a normal block entity
bedrockBlockEntities.add(session.getGeyser().getWorldManager().getLecternDataAt(
session, x + chunkBlockX, y, z + chunkBlockZ, true));
// Fill in tag with a default value
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x + chunkBlockX, y, z + chunkBlockZ, 1);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:written_book").build());
lecternTag.putInt("page", -1);
bedrockBlockEntities.add(lecternTag.build());
lecterns.add(blockEntity);
continue;
}
@ -356,6 +368,10 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
levelChunkPacket.setData(payload);
session.sendUpstreamPacket(levelChunkPacket);
if (!lecterns.isEmpty()) {
session.getGeyser().getWorldManager().sendLecternData(session, packet.getX(), packet.getZ(), lecterns);
}
for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) {
Vector3i position = entry.getKey();
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {

View file

@ -27,9 +27,9 @@ package org.geysermc.geyser.util.collection;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
/**
@ -47,27 +47,24 @@ public class LecternHasBookMap extends FixedInt2BooleanMap {
int offset = blockState - this.start;
if (offset < 0 || offset >= this.value.length) {
// Block state is out of bounds of this map - lectern has been destroyed, if it existed
if (!worldManager.shouldExpectLecternHandled()) {
if (!worldManager.shouldExpectLecternHandled(session)) {
session.getLecternCache().remove(position);
}
return;
}
boolean newLecternHasBook;
if (worldManager.shouldExpectLecternHandled()) {
// As of right now, no tag can be added asynchronously
worldManager.getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
if (worldManager.shouldExpectLecternHandled(session)) {
worldManager.sendLecternData(session, position.getX(), position.getY(), position.getZ());
} else if ((newLecternHasBook = this.value[offset]) != this.get(worldManager.getBlockAt(session, position))) {
// If the lectern block was updated, or it previously had a book
NbtMap newLecternTag;
// newLecternHasBook = the new lectern block state's "has book" toggle.
if (newLecternHasBook) {
newLecternTag = worldManager.getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
worldManager.sendLecternData(session, position.getX(), position.getY(), position.getZ());
} else {
session.getLecternCache().remove(position);
newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
NbtMap newLecternTag = LecternUtils.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
}
BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
}
}
}

View file

@ -1,6 +1,7 @@
[versions]
base-api = "1.0.0-SNAPSHOT"
cumulus = "1.1.1"
erosion = "1.0-20230330.170602-3"
events = "1.0-SNAPSHOT"
jackson = "2.14.0"
fastutil = "8.5.2"
@ -19,9 +20,9 @@ checkerframework = "3.19.0"
log4j = "2.17.1"
jline = "3.21.0"
terminalconsoleappender = "1.2.0"
paper = "1.19-R0.1-SNAPSHOT"
folia = "1.19.4-R0.1-SNAPSHOT"
viaversion = "4.0.0"
adapters = "1.6-SNAPSHOT"
adapters = "1.7-SNAPSHOT"
commodore = "2.2"
bungeecord = "a7c6ede"
velocity = "3.0.0"
@ -35,6 +36,9 @@ base-api = { group = "org.geysermc.api", name = "base-api", version.ref = "base-
cumulus = { group = "org.geysermc.cumulus", name = "cumulus", version.ref = "cumulus" }
events = { group = "org.geysermc.event", name = "events", version.ref = "events" }
erosion-bukkit-common = { group = "org.geysermc.erosion", name = "bukkit-common", version.ref = "erosion" }
erosion-common = { group = "org.geysermc.erosion", name = "common", version.ref = "erosion" }
jackson-annotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jackson" }
jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" }
jackson-dataformat-yaml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml", version.ref = "jackson" }
@ -66,8 +70,8 @@ jline-terminal = { group = "org.jline", name = "jline-terminal", version.ref = "
jline-terminal-jna = { group = "org.jline", name = "jline-terminal-jna", version.ref = "jline" }
jline-reader = { group = "org.jline", name = "jline-reader", version.ref = "jline" }
paper-api = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" }
paper-mojangapi = { group = "io.papermc.paper", name = "paper-mojangapi", version.ref = "paper" }
folia-api = { group = "dev.folia", name = "folia-api", version.ref = "folia" }
paper-mojangapi = { group = "io.papermc.paper", name = "paper-mojangapi", version.ref = "folia" }
# check these on https://modmuss50.me/fabric.html
fabric-minecraft = { group = "com.mojang", name = "minecraft", version.ref = "fabric-minecraft" }