diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 668c9ca72..ca8b63cc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,25 +23,19 @@ jobs: with: submodules: recursive + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v3 with: java-version: 17 distribution: temurin - - name: Cache Gradle Packages - uses: actions/cache@v3 - with: - path: | - ~/.m2 - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ github.ref_name }}-gradle-${{ hashFiles('*.gradle.kts', 'gradle.properties', 'gradlew', 'gradle/*', 'gradle/**/*', 'build-logic/*', 'build-logic/**/**/**/*', '**/*.gradle.kts', '**/**/*.gradle.kts') }} - restore-keys: ${{ github.ref_name }}-gradle- - - name: Build uses: gradle/gradle-build-action@v2 with: - arguments: build --no-daemon + arguments: build + gradle-home-cache-cleanup: true - name: Archive artifacts (Geyser Fabric) uses: actions/upload-artifact@v3 @@ -87,7 +81,7 @@ jobs: if-no-files-found: error - name: Publish to Maven Repository - if: ${{ job.status == 'success' && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} + if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} uses: gradle/gradle-build-action@v2 env: ORG_GRADLE_PROJECT_geysermcUsername: ${{ vars.DEPLOY_USER }} @@ -96,7 +90,7 @@ jobs: arguments: publish - name: Publish to Downloads API - if: ${{ github.ref_name == 'master' && job.status == 'success' && github.repository == 'GeyserMC/Geyser' }} + if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} shell: bash env: DOWNLOADS_USERNAME: ${{ vars.DOWNLOADS_USERNAME }} @@ -119,6 +113,15 @@ jobs: echo "{\"project\": \"$project\", \"version\": \"$version\", \"id\": $GITHUB_RUN_NUMBER, \"commit\": \"$GITHUB_SHA\"}" > metadata.json rsync -P -e "ssh -o StrictHostKeyChecking=no -i id_ecdsa" metadata.json $DOWNLOADS_USERNAME@$DOWNLOADS_SERVER_IP:~/uploads/$project/$GITHUB_RUN_NUMBER/ + - name: Publish to Modrinth + uses: gradle/gradle-build-action@v2 + if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + with: + arguments: fabric:modrinth + gradle-home-cache-cleanup: true + - name: Notify Discord if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Geyser' }} uses: Tim203/actions-git-discord-webhook@main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 2b002b0de..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Publish -on: - workflow_dispatch: - push: - paths-ignore: - - '.github/ISSUE_TEMPLATE/*.yml' - - '.github/actions/pullrequest.yml' - - '.idea/copyright/*.xml' - - '.gitignore' - - 'CONTRIBUTING.md' - - 'LICENSE' - - 'Jenkinsfile ' - - 'README.md' - - 'licenseheader.txt' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Publish to Modrinth - env: - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - run: ./gradlew fabric:modrinth \ No newline at end of file diff --git a/bootstrap/fabric/build.gradle.kts b/bootstrap/fabric/build.gradle.kts index af9c4547d..35270df80 100644 --- a/bootstrap/fabric/build.gradle.kts +++ b/bootstrap/fabric/build.gradle.kts @@ -94,6 +94,7 @@ tasks { } modrinth { + token.set(System.getenv("MODRINTH_TOKEN")) // Even though this is the default value, apparently this prevents GitHub Actions caching the token? projectId.set("wKkoqHrH") versionNumber.set(project.version as String + "-" + System.getenv("GITHUB_RUN_NUMBER")) versionType.set("beta") diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java index b003a76ba..dc81315d2 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java @@ -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 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 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 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 diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index da4e5af33..aa7958732 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -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) } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java index 36dd81d44..bb0f30e70 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java @@ -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); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java index 634d1f8a8..1e6a0ad6c 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java @@ -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 diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java index 3185f2d30..67e31fea2 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java @@ -60,16 +60,16 @@ public final class ReflectedNames { } static Constructor 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); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java index 5eb99e10c..d7f34c4b3 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java @@ -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 attachedBlocks = new Object2IntOpenHashMap<>(); + Object2IntMap attachedBlocks = new Object2IntArrayMap<>(); boolean blocksFilled = false; for (Map.Entry entry : geyser.getSessionManager().getSessions().entrySet()) { diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java index cca982cbb..481953747 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -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 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 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 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 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. diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml index d8bc264a9..6e81ccdb6 100644 --- a/bootstrap/spigot/src/main/resources/plugin.yml +++ b/bootstrap/spigot/src/main/resources/plugin.yml @@ -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. diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9ba399674..39fa0bb43 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -49,6 +49,10 @@ dependencies { // Adventure text serialization api(libs.bundles.adventure) + api(libs.erosion.common) { + isTransitive = false + } + // Test testImplementation(libs.junit) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index c1b21e943..fbad1ab02 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -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)); diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 41a88f64f..8656be098 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -148,6 +148,7 @@ public final class EntityDefinitions { public static final EntityDefinition STRAY; public static final EntityDefinition STRIDER; public static final EntityDefinition TADPOLE; + public static final EntityDefinition TEXT_DISPLAY; public static final EntityDefinition TNT; public static final EntityDefinition TNT_MINECART; public static final EntityDefinition TRADER_LLAMA; @@ -295,6 +296,28 @@ public final class EntityDefinitions { .addTranslator(MetadataType.INT, TNTEntity::setFuseLength) .build(); + EntityDefinition displayBase = EntityDefinition.inherited(entityBase.factory(), entityBase) + .addTranslator(null) // Interpolation start ticks + .addTranslator(null) // Interpolation duration ID + .addTranslator(null) // Translation + .addTranslator(null) // Scale + .addTranslator(null) // Left rotation + .addTranslator(null) // Right rotation + .addTranslator(null) // Billboard render constraints + .addTranslator(null) // Brightness override + .addTranslator(null) // View range + .addTranslator(null) // Shadow radius + .addTranslator(null) // Shadow strength + .addTranslator(null) // Width + .addTranslator(null) // Height + .addTranslator(null) // Glow color override + .build(); + TEXT_DISPLAY = EntityDefinition.inherited(TextDisplayEntity::new, displayBase) + .type(EntityType.TEXT_DISPLAY) + .identifier("minecraft:armor_stand") + .addTranslator(MetadataType.CHAT, TextDisplayEntity::setText) + .build(); + EntityDefinition fireballBase = EntityDefinition.inherited(FireballEntity::new, entityBase) .addTranslator(null) // Item .build(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java index 65662bbe4..deaca789a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java @@ -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; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java index 89db9b0c8..e804099d8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java @@ -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 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; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java new file mode 100644 index 000000000..fecca8ac0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -0,0 +1,55 @@ +/* + * 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.entity.type; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import net.kyori.adventure.text.Component; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.text.MessageTranslator; + +import java.util.UUID; + +// Note: 1.19.4 requires that the billboard is set to something in order to show, on Java Edition +public class TextDisplayEntity extends Entity { + public TextDisplayEntity(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); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Remove armor stand body + this.dirtyMetadata.put(EntityData.SCALE, 0f); + this.dirtyMetadata.put(EntityData.NAMETAG_ALWAYS_SHOW, (byte) 1); + } + + public void setText(EntityMetadata entityMetadata) { + this.dirtyMetadata.put(EntityData.NAMETAG, MessageTranslator.convertMessage(entityMetadata.getValue())); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java index 6b235a8e5..453d70bf6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java @@ -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 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); } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java index ff1ba9ac3..e484dfc59 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java @@ -43,6 +43,15 @@ public class ShulkerEntity extends GolemEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); // Indicate that invisibility should be fixed through the resource pack setFlag(EntityFlag.BRIBED, true); + + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // As of 1.19.4, it seems Java no longer sends the shulker color if it's the default color on initial spawn + // We still need the special case for 16 color in setShulkerColor though as it will send it for an entity metadata update + dirtyMetadata.put(EntityData.VARIANT, 16); } public void setAttachedFace(EntityMetadata entityMetadata) { diff --git a/core/src/main/java/org/geysermc/geyser/erosion/AbstractGeyserboundPacketHandler.java b/core/src/main/java/org/geysermc/geyser/erosion/AbstractGeyserboundPacketHandler.java new file mode 100644 index 000000000..eabed8f7b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/AbstractGeyserboundPacketHandler.java @@ -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); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserErosionPacketSender.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserErosionPacketSender.java new file mode 100644 index 000000000..610917adc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserErosionPacketSender.java @@ -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 { + + @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) { + } +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java new file mode 100644 index 000000000..196595383 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java @@ -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; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java new file mode 100644 index 000000000..af3098edb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java @@ -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 packetSender; + @Setter + private CompletableFuture pendingLookup = null; + @Getter + private final Int2ObjectMap> asyncPendingLookups = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>(4)); + @Setter + private CompletableFuture pendingBatchLookup = null; + @Setter + private CompletableFuture pickBlockLookup = null; + + private final AtomicInteger nextTransactionId = new AtomicInteger(1); + + public GeyserboundPacketHandlerImpl(GeyserSession session, ErosionPacketSender 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 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 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 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; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/UnixSocketClientListener.java b/core/src/main/java/org/geysermc/geyser/erosion/UnixSocketClientListener.java new file mode 100644 index 000000000..a236099df --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/UnixSocketClientListener.java @@ -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() { + @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(); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java index 5df97e8d7..43ba452a0 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java @@ -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 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 future = new CompletableFuture<>(); // Boxes + erosionHandler.setPendingLookup(future); + erosionHandler.sendPacket(new BackendboundBlockRequestPacket(0, Vector3i.from(x, y, z))); + return future.join(); + } + + @Override + public CompletableFuture 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 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 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 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 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 future = new CompletableFuture<>(); + erosionHandler.setPickBlockLookup(future); + erosionHandler.sendPacket(new BackendboundPickBlockPacket(Vector3i.from(x, y, z))); + return future; + } } diff --git a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java index 1909915db..84a03022f 100644 --- a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java @@ -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 getBlockAtAsync(GeyserSession session, Vector3i vector) { + return this.getBlockAtAsync(session, vector.getX(), vector.getY(), vector.getZ()); + } + + public CompletableFuture 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. *

@@ -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 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 diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockPositionIterator.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockPositionIterator.java deleted file mode 100644 index d22150ccf..000000000 --- a/core/src/main/java/org/geysermc/geyser/level/block/BlockPositionIterator.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java index 2a830cd70..666e1191d 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java @@ -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; diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java index 40b11a883..760bd4d02 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java @@ -108,7 +108,14 @@ public class ResourcePack { try (ZipFile zip = new ZipFile(file); Stream 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, diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index d88d1720c..6f2f9327d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -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 lecternCache; @@ -557,6 +564,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); @@ -585,7 +594,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 { @@ -1094,6 +1103,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { tickThread.cancel(false); } + erosionHandler.close(); + closed = true; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java index 59fe81751..117f4e851 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java @@ -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; - } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java index 3e814a098..68985ae5e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java @@ -48,15 +48,23 @@ public class PotionTranslator extends ItemTranslator { if (itemStack.getNbt() == null) return super.translateToBedrock(itemStack, mapping, mappings); Tag potionTag = itemStack.getNbt().get("Potion"); if (potionTag instanceof StringTag) { - Potion potion = Potion.getByJavaIdentifier(((StringTag) potionTag).getValue()); - if (potion != null) { + int customItemId = CustomItemTranslator.getCustomItem(itemStack.getNbt(), mapping); + if (customItemId == -1) { + Potion potion = Potion.getByJavaIdentifier(((StringTag) potionTag).getValue()); + if (potion != null) { + return ItemData.builder() + .id(mapping.getBedrockId()) + .damage(potion.getBedrockId()) + .count(itemStack.getAmount()) + .tag(translateNbtToBedrock(itemStack.getNbt())); + } + GeyserImpl.getInstance().getLogger().debug("Unknown Java potion: " + potionTag.getValue()); + } else { return ItemData.builder() - .id(mapping.getBedrockId()) - .damage(potion.getBedrockId()) + .id(customItemId) .count(itemStack.getAmount()) .tag(translateNbtToBedrock(itemStack.getNbt())); } - GeyserImpl.getInstance().getLogger().debug("Unknown Java potion: " + potionTag.getValue()); } return super.translateToBedrock(itemStack, mapping, mappings); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java index a69a2cfe9..c0af434c5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java @@ -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")); } /** diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java index 591737b58..69cfbcc19 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java @@ -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); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockPickRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockPickRequestTranslator.java index 90316a8bd..5090de11f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockPickRequestTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockPickRequestTranslator.java @@ -71,30 +71,28 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator { + .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; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index aaedfa443..142ed6e88 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -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 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; + } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 9afa337ef..7839a8102 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -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 dimensions = session.getDimensions(); dimensions.clear(); @@ -129,6 +135,10 @@ public class JavaLoginTranslator extends PacketTranslator { + 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 diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java index cd965e128..aab921aa8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java @@ -43,7 +43,7 @@ public class JavaBlockUpdateTranslator extends PacketTranslator iterator = session.getLecternCache().iterator(); while (iterator.hasNext()) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java index 5bf48549b..5c2b3e928 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java @@ -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; @@ -49,6 +50,7 @@ import it.unimi.dsi.fastutil.ints.IntImmutableList; 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; @@ -95,6 +97,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length); + final List lecterns = new ObjectArrayList<>(); BitSet waterloggedPaletteIds = new BitSet(); BitSet bedrockOnlyBlockEntityIds = new BitSet(); @@ -328,7 +331,9 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator entry : session.getItemFrameCache().entrySet()) { Vector3i position = entry.getKey(); if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) { diff --git a/core/src/main/java/org/geysermc/geyser/util/collection/LecternHasBookMap.java b/core/src/main/java/org/geysermc/geyser/util/collection/LecternHasBookMap.java index 913ea44d5..91cd56f8e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/collection/LecternHasBookMap.java +++ b/core/src/main/java/org/geysermc/geyser/util/collection/LecternHasBookMap.java @@ -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); } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f7433034..dfa459f01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }