diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c32bad0c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +tab_width = 4 +max_line_length = off + +[*.java] +ij_java_class_count_to_use_import_on_demand = 9999 +ij_java_doc_align_exception_comments = false +ij_java_doc_align_param_comments = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bcf25e8e..59aa89086 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ on: - 'LICENSE' - 'Jenkinsfile ' - 'README.md' - - 'licenseheader.txt' jobs: build: diff --git a/LICENSE b/LICENSE index bde252698..ba3a723ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2023 GeyserMC. http://geysermc.org +Copyright (c) 2019-2024 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 diff --git a/README.md b/README.md index e3b5a496a..14bdb17a9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here! -### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.1 and Minecraft Java 1.21 +### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.2 and Minecraft Java 1.21 ## Setting Up Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e7b63ff56..1d1794cf7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -75,7 +75,7 @@ tasks.processResources { expand( "branch" to info.branch, "buildNumber" to info.buildNumber, - "projectVersion" to project.version, + "projectVersion" to info.version, "commit" to info.commit, "commitAbbrev" to info.commitAbbrev, "commitMessage" to info.commitMessage, @@ -89,20 +89,25 @@ sourceSets { blossom { val info = GitInfo() javaSources { - property("version", "${project.version} (${info.gitVersion})") + property("version", info.version) property("gitVersion", info.gitVersion) property("buildNumber", info.buildNumber.toString()) property("branch", info.branch) property("commit", info.commit) property("repository", info.repository) + property("devVersion", info.isDev.toString()) } } } } -fun Project.buildNumber(): Int = +fun buildNumber(): Int = (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 +fun isDevBuild(branch: String, repository: String): Boolean { + return branch != "master" || repository.equals("https://github.com/GeyserMC/Geyser", ignoreCase = true).not() +} + inner class GitInfo { val branch: String val commit: String @@ -115,22 +120,25 @@ inner class GitInfo { val commitMessage: String val repository: String + val isDev: Boolean + init { - // On Jenkins, a detached head is checked out, so indra cannot determine the branch. - // Fortunately, this environment variable is available. - branch = indraGit.branchName() ?: System.getenv("BRANCH_NAME") ?: "DEV" + branch = indraGit.branchName() ?: "DEV" val commit = indraGit.commit() this.commit = commit?.name ?: "0".repeat(40) commitAbbrev = commit?.name?.substring(0, 7) ?: "0".repeat(7) gitVersion = "git-${branch}-${commitAbbrev}" - version = "${project.version} ($gitVersion)" - buildNumber = buildNumber() val git = indraGit.git() commitMessage = git?.commit()?.message ?: "" repository = git?.repository?.config?.getString("remote", "origin", "url") ?: "" + + buildNumber = buildNumber() + isDev = isDevBuild(branch, repository) + val projectVersion = if (isDev) project.version else project.version.toString().replace("SNAPSHOT", "b${buildNumber}") + version = "$projectVersion ($gitVersion)" } } @@ -148,4 +156,4 @@ tasks.register("downloadBedrockData") { suffixedFiles = listOf("block_palette.nbt", "creative_items.json", "runtime_item_states.json") destinationDir = "$projectDir/src/main/resources/bedrock" -} \ No newline at end of file +} diff --git a/core/src/main/java-templates/org/geysermc/geyser/BuildData.java b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java index d489d8d17..0e4d08bfe 100644 --- a/core/src/main/java-templates/org/geysermc/geyser/BuildData.java +++ b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java @@ -34,4 +34,9 @@ public class BuildData { public static final String BRANCH = "{{ branch }}"; public static final String COMMIT = "{{ commit }}"; public static final String REPOSITORY = "{{ repository }}"; + private static final String DEV = "{{ devVersion }}"; + + public static boolean isDevBuild() { + return Boolean.parseBoolean(DEV); + } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 0975ea5a9..88cc74691 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -122,6 +122,7 @@ public class GeyserImpl implements GeyserApi { public static final String BRANCH = BuildData.BRANCH; public static final String COMMIT = BuildData.COMMIT; public static final String REPOSITORY = BuildData.REPOSITORY; + public static final boolean IS_DEV = BuildData.isDevBuild(); /** * Oauth client ID for Microsoft authentication @@ -207,6 +208,12 @@ public class GeyserImpl implements GeyserApi { logger.info(""); logger.info(GeyserLocale.getLocaleStringLog("geyser.core.load", NAME, VERSION)); logger.info(""); + if (IS_DEV) { + // TODO cloud use language string + //logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc")); + logger.info("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s".formatted("https://discord.gg/geysermc")); + logger.info(""); + } logger.info("******************************************"); /* Initialize registries */ @@ -684,9 +691,10 @@ public class GeyserImpl implements GeyserApi { * * @return true if the version number is not 'DEV'. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isProductionEnvironment() { // First is if Blossom runs, second is if Blossom doesn't run - //noinspection ConstantConditions,MismatchedStringCase - changes in production + //noinspection ConstantConditions - changes in production return !("git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION) || "${gitVersion}".equals(GeyserImpl.GIT_VERSION)); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java index eb2e8ff47..c6852d577 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.command.defaults; +import com.fasterxml.jackson.databind.JsonNode; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.GeyserCommand; @@ -37,8 +37,7 @@ import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.WebUtils; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import java.io.IOException; import java.util.List; public class VersionCommand extends GeyserCommand { @@ -72,27 +71,36 @@ public class VersionCommand extends GeyserCommand { GeyserImpl.NAME, GeyserImpl.VERSION, javaVersions, bedrockVersions)); // Disable update checking in dev mode and for players in Geyser Standalone - if (GeyserImpl.getInstance().isProductionEnvironment() && !(!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", sender.locale())); - try { - String buildXML = WebUtils.getBody("https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + - URLEncoder.encode(GeyserImpl.BRANCH, StandardCharsets.UTF_8) + "/lastSuccessfulBuild/api/xml?xpath=//buildNumber"); - if (buildXML.startsWith("")) { - int latestBuildNum = Integer.parseInt(buildXML.replaceAll("<(\\\\)?(/)?buildNumber>", "").trim()); - int buildNum = this.geyser.buildNumber(); - if (latestBuildNum == buildNum) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", sender.locale())); - } else { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.outdated", - sender.locale(), (latestBuildNum - buildNum), Constants.GEYSER_DOWNLOAD_LOCATION)); - } - } else { - throw new AssertionError("buildNumber missing"); - } - } catch (Exception e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e); - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", sender.locale())); + if (!GeyserImpl.getInstance().isProductionEnvironment() || (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { + return; + } + + if (GeyserImpl.IS_DEV) { + // TODO cloud use language string + sender.sendMessage("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s" + .formatted("https://discord.gg/geysermc")); + //sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", sender.locale(), "https://discord.gg/geysermc")); + return; + } + + sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", sender.locale())); + try { + int buildNumber = this.geyser.buildNumber(); + JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest"); + int latestBuildNumber = response.get("build").asInt(); + + if (latestBuildNumber == buildNumber) { + sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", sender.locale())); + return; } + + sender.sendMessage(GeyserLocale.getPlayerLocaleString( + "geyser.commands.version.outdated", + sender.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download" + )); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e); + sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", sender.locale())); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java index 31aa7cc73..f9b65a545 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java @@ -47,6 +47,7 @@ import java.util.function.BiConsumer; * metadata translators needed to translate the properties sent from the server. The translators are structured in such * a way that inserting a new one (for example in version updates) is convenient. * + * @param identifier the Bedrock identifier of this entity * @param the entity type this definition represents */ public record EntityDefinition(EntityFactory factory, EntityType entityType, String identifier, 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 1496f8a82..11b4a32d1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -307,11 +307,11 @@ public final class EntityDefinitions { .addTranslator(MetadataType.INT, TNTEntity::setFuseLength) .build(); - EntityDefinition displayBase = EntityDefinition.inherited(entityBase.factory(), entityBase) + EntityDefinition displayBase = EntityDefinition.inherited(DisplayBaseEntity::new, entityBase) .addTranslator(null) // Interpolation delay .addTranslator(null) // Transformation interpolation duration .addTranslator(null) // Position/Rotation interpolation duration - .addTranslator(null) // Translation + .addTranslator(MetadataType.VECTOR3, DisplayBaseEntity::setTranslation) // Translation .addTranslator(null) // Scale .addTranslator(null) // Left rotation .addTranslator(null) // Right rotation diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java new file mode 100644 index 000000000..16587d125 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 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 net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; + +import java.util.Optional; +import java.util.UUID; + +public class DisplayBaseEntity extends Entity { + + private @Nullable Vector3f baseTranslation; + + public DisplayBaseEntity(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 + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Don't allow the display name to be hidden - messes with our armor stand. + // On JE: Hiding the display name still shows the display entity text. + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. + // On JE: custom name does not override text display. + } + + public void setTranslation(EntityMetadata translationMeta){ + this.baseTranslation = translationMeta.getValue(); + if (this.baseTranslation == null) { + return; + } + if (this.vehicle == null) { + this.setRiderSeatPosition(this.baseTranslation); + this.moveRelative(this.baseTranslation.getX(), this.baseTranslation.getY(), this.baseTranslation.getZ(), yaw, pitch, headYaw, false); + } else { + EntityUtils.updateMountOffset(this, this.vehicle, true, true, false); + this.updateBedrockMetadata(); + } + } + + public Vector3f getTranslation() { + return baseTranslation; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java index 6d0294783..09c055c84 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java @@ -86,7 +86,11 @@ public class PaintingEntity extends Entity { private Vector3f fixOffset(PaintingType paintingName) { Vector3f position = super.position; - position = position.add(0.5, 0.5, 0.5); + // ViaVersion already adds the offset for us on older versions, + // so no need to do it then otherwise it will be spaced + if (session.isEmulatePost1_18Logic()) { + position = position.add(0.5, 0.5, 0.5); + } double widthOffset = paintingName.getWidth() > 1 && paintingName.getWidth() != 3 ? 0.5 : 0; double heightOffset = paintingName.getHeight() > 1 && paintingName.getHeight() != 3 ? 0.5 : 0; 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 index 28f38f919..ff5604c19 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * Copyright (c) 2024 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 @@ -32,13 +32,11 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import java.util.Optional; 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 class TextDisplayEntity extends DisplayBaseEntity { 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); } @@ -51,18 +49,6 @@ public class TextDisplayEntity extends Entity { this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); } - @Override - public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { - // Don't allow the display name to be hidden - messes with our armor stand. - // On JE: Hiding the display name still shows the display entity text. - } - - @Override - public void setDisplayName(EntityMetadata, ?> entityMetadata) { - // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. - // On JE: custom name does not override text display. - } - public void setText(EntityMetadata entityMetadata) { this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue())); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index 31eb02984..45fea4d48 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -27,15 +27,18 @@ package org.geysermc.geyser.entity.type.player; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.DimensionUtils; @@ -69,6 +72,15 @@ public class SessionPlayerEntity extends PlayerEntity { private int lastAirSupply = getMaxAir(); + /** + * Determines if our position is currently out-of-sync with the Java server + * due to our workaround for the void floor + *

+ * Must be reset when dying, switching worlds, or being teleported out of the void + */ + @Getter @Setter + private boolean voidPositionDesynched; + public SessionPlayerEntity(GeyserSession session) { super(session, -1, 1, null, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, null, null); @@ -87,10 +99,25 @@ public class SessionPlayerEntity extends PlayerEntity { @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + if (voidPositionDesynched) { + if (!isBelowVoidFloor()) { + voidPositionDesynched = false; // No need to fix our offset; we've been moved + } + } super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); session.getCollisionManager().updatePlayerBoundingBox(this.position.down(definition.offset())); } + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + if (voidPositionDesynched) { + if (!isBelowVoidFloor()) { + voidPositionDesynched = false; // No need to fix our offset; we've been moved + } + } + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); + } + @Override public void setPosition(Vector3f position) { if (valid) { // Don't update during session init @@ -225,6 +252,9 @@ public class SessionPlayerEntity extends PlayerEntity { } else { dirtyMetadata.put(EntityDataTypes.PLAYER_HAS_DIED, false); } + + // We're either respawning or switching worlds, either way, we are no longer desynched + this.setVoidPositionDesynched(false); } @Override @@ -276,4 +306,48 @@ public class SessionPlayerEntity extends PlayerEntity { public void resetAir() { this.setAirSupply(getMaxAir()); } + + private boolean isBelowVoidFloor() { + return position.getY() < voidFloorPosition(); + } + + public int voidFloorPosition() { + // The void floor is offset about 40 blocks below the bottom of the world + BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + return bedrockDimension.minY() - 40; + } + + /** + * This method handles teleporting the player below or above the Bedrock void floor. + * The Java server should never see this desync as we adjust the position that we send to it + * + * @param up in which direction to teleport - true to resync our position, or false to be + * teleported below the void floor. + */ + public void teleportVoidFloorFix(boolean up) { + // Safety to avoid double teleports + if ((voidPositionDesynched && !up) || (!voidPositionDesynched && up)) { + return; + } + + // Work around there being a floor at the bottom of the world and teleport the player below it + // Moving from below to above the void floor works fine + Vector3f newPosition = this.getPosition(); + if (up) { + newPosition = newPosition.up(4f); + voidPositionDesynched = false; + } else { + newPosition = newPosition.down(4f); + voidPositionDesynched = true; + } + + this.setPositionManual(newPosition); + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(newPosition); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + session.sendUpstreamPacketImmediately(movePlayerPacket); + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java index 8289813a4..b5e76a296 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java @@ -32,9 +32,8 @@ import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor; import org.geysermc.geyser.registry.type.ItemMapping; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; /** * Stores information on trim materials and patterns, including smithing armor hacks for pre-1.20. @@ -46,18 +45,18 @@ public final class TrimRecipe { public static final ItemDescriptorWithCount ADDITION = tagDescriptor("minecraft:trim_materials"); public static final ItemDescriptorWithCount TEMPLATE = tagDescriptor("minecraft:trim_templates"); - public static TrimMaterial readTrimMaterial(GeyserSession session, RegistryEntry entry) { - String key = entry.getId().asMinimalString(); + public static TrimMaterial readTrimMaterial(RegistryEntryContext context) { + String key = context.id().asMinimalString(); // Color is used when hovering over the item // Find the nearest legacy color from the RGB Java gives us to work with // Also yes this is a COMPLETE hack but it works ok!!!!! - String colorTag = entry.getData().getCompound("description").getString("color"); + String colorTag = context.data().getCompound("description").getString("color"); TextColor color = TextColor.fromHexString(colorTag); String legacy = MessageTranslator.convertMessage(Component.space().color(color)); - String itemIdentifier = entry.getData().getString("ingredient"); - ItemMapping itemMapping = session.getItemMappings().getMapping(itemIdentifier); + String itemIdentifier = context.data().getString("ingredient"); + ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); if (itemMapping == null) { // This should never happen so not sure what to do here. itemMapping = ItemMapping.AIR; @@ -66,11 +65,11 @@ public final class TrimRecipe { return new TrimMaterial(key, legacy.substring(2).trim(), itemMapping.getBedrockIdentifier()); } - public static TrimPattern readTrimPattern(GeyserSession session, RegistryEntry entry) { - String key = entry.getId().asMinimalString(); + public static TrimPattern readTrimPattern(RegistryEntryContext context) { + String key = context.id().asMinimalString(); - String itemIdentifier = entry.getData().getString("template_item"); - ItemMapping itemMapping = session.getItemMappings().getMapping(itemIdentifier); + String itemIdentifier = context.data().getString("template_item"); + ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); if (itemMapping == null) { // This should never happen so not sure what to do here. itemMapping = ItemMapping.AIR; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java index c3ac73372..7afd31cc9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.inventory.updater; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.stream.IntStream; import net.kyori.adventure.text.Component; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -41,11 +42,14 @@ import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.EnchantmentTag; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ItemUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; @@ -310,17 +314,18 @@ public class AnvilInventoryUpdater extends InventoryUpdater { for (Object2IntMap.Entry entry : getEnchantments(session, material).object2IntEntrySet()) { Enchantment enchantment = entry.getKey(); - boolean canApply = isEnchantedBook(input) || session.getTagCache().is(enchantment.supportedItems(), input); - var exclusiveSet = enchantment.exclusiveSet(); - if (exclusiveSet != null) { - int[] incompatibleEnchantments = session.getTagCache().get(exclusiveSet); - for (int i : incompatibleEnchantments) { - Enchantment incompatible = session.getRegistryCache().enchantments().byId(i); - if (combinedEnchantments.containsKey(incompatible)) { - canApply = false; - if (!bedrock) { - cost++; - } + HolderSet supportedItems = enchantment.supportedItems(); + int[] supportedItemIds = supportedItems.resolve(tagId -> session.getTagCache().get(ItemTag.ALL_ITEM_TAGS.get(tagId))); + boolean canApply = isEnchantedBook(input) || IntStream.of(supportedItemIds).anyMatch(id -> id == input.getJavaId()); + + HolderSet exclusiveSet = enchantment.exclusiveSet(); + int[] incompatibleEnchantments = exclusiveSet.resolve(tagId -> session.getTagCache().get(EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(tagId))); + for (int i : incompatibleEnchantments) { + Enchantment incompatible = session.getRegistryCache().enchantments().byId(i); + if (combinedEnchantments.containsKey(incompatible)) { + canApply = false; + if (!bedrock) { + cost++; } } } diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java index 1ddd14982..462e98d19 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.components.ToolTier; import org.geysermc.geyser.item.type.*; import org.geysermc.geyser.level.block.Blocks; @@ -122,7 +123,7 @@ public final class Items { public static final Item RAW_IRON_BLOCK = register(new BlockItem(builder(), Blocks.RAW_IRON_BLOCK)); public static final Item RAW_COPPER_BLOCK = register(new BlockItem(builder(), Blocks.RAW_COPPER_BLOCK)); public static final Item RAW_GOLD_BLOCK = register(new BlockItem(builder(), Blocks.RAW_GOLD_BLOCK)); - public static final Item HEAVY_CORE = register(new BlockItem(builder(), Blocks.HEAVY_CORE)); + public static final Item HEAVY_CORE = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.HEAVY_CORE)); public static final Item AMETHYST_BLOCK = register(new BlockItem(builder(), Blocks.AMETHYST_BLOCK)); public static final Item BUDDING_AMETHYST = register(new BlockItem(builder(), Blocks.BUDDING_AMETHYST)); public static final Item IRON_BLOCK = register(new BlockItem(builder(), Blocks.IRON_BLOCK)); @@ -416,7 +417,7 @@ public final class Items { public static final Item END_PORTAL_FRAME = register(new BlockItem(builder(), Blocks.END_PORTAL_FRAME)); public static final Item END_STONE = register(new BlockItem(builder(), Blocks.END_STONE)); public static final Item END_STONE_BRICKS = register(new BlockItem(builder(), Blocks.END_STONE_BRICKS)); - public static final Item DRAGON_EGG = register(new BlockItem(builder(), Blocks.DRAGON_EGG)); + public static final Item DRAGON_EGG = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.DRAGON_EGG)); public static final Item SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SANDSTONE_STAIRS)); public static final Item ENDER_CHEST = register(new BlockItem(builder(), Blocks.ENDER_CHEST)); public static final Item EMERALD_BLOCK = register(new BlockItem(builder(), Blocks.EMERALD_BLOCK)); @@ -432,8 +433,8 @@ public final class Items { public static final Item BAMBOO_MOSAIC_STAIRS = register(new BlockItem(builder(), Blocks.BAMBOO_MOSAIC_STAIRS)); public static final Item CRIMSON_STAIRS = register(new BlockItem(builder(), Blocks.CRIMSON_STAIRS)); public static final Item WARPED_STAIRS = register(new BlockItem(builder(), Blocks.WARPED_STAIRS)); - public static final Item COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.COMMAND_BLOCK)); - public static final Item BEACON = register(new BlockItem(builder(), Blocks.BEACON)); + public static final Item COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.COMMAND_BLOCK)); + public static final Item BEACON = register(new BlockItem(builder().rarity(Rarity.RARE), Blocks.BEACON)); public static final Item COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.COBBLESTONE_WALL)); public static final Item MOSSY_COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE_WALL)); public static final Item BRICK_WALL = register(new BlockItem(builder(), Blocks.BRICK_WALL)); @@ -480,8 +481,8 @@ public final class Items { public static final Item GREEN_TERRACOTTA = register(new BlockItem(builder(), Blocks.GREEN_TERRACOTTA)); public static final Item RED_TERRACOTTA = register(new BlockItem(builder(), Blocks.RED_TERRACOTTA)); public static final Item BLACK_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLACK_TERRACOTTA)); - public static final Item BARRIER = register(new BlockItem(builder(), Blocks.BARRIER)); - public static final Item LIGHT = register(new BlockItem(builder(), Blocks.LIGHT)); + public static final Item BARRIER = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.BARRIER)); + public static final Item LIGHT = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.LIGHT)); public static final Item HAY_BLOCK = register(new BlockItem(builder(), Blocks.HAY_BLOCK)); public static final Item WHITE_CARPET = register(new BlockItem(builder(), Blocks.WHITE_CARPET)); public static final Item ORANGE_CARPET = register(new BlockItem(builder(), Blocks.ORANGE_CARPET)); @@ -551,14 +552,14 @@ public final class Items { public static final Item CHISELED_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CHISELED_RED_SANDSTONE)); public static final Item CUT_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CUT_RED_SANDSTONE)); public static final Item RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.RED_SANDSTONE_STAIRS)); - public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.REPEATING_COMMAND_BLOCK)); - public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.CHAIN_COMMAND_BLOCK)); + public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.REPEATING_COMMAND_BLOCK)); + public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.CHAIN_COMMAND_BLOCK)); public static final Item MAGMA_BLOCK = register(new BlockItem(builder(), Blocks.MAGMA_BLOCK)); public static final Item NETHER_WART_BLOCK = register(new BlockItem(builder(), Blocks.NETHER_WART_BLOCK)); public static final Item WARPED_WART_BLOCK = register(new BlockItem(builder(), Blocks.WARPED_WART_BLOCK)); public static final Item RED_NETHER_BRICKS = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICKS)); public static final Item BONE_BLOCK = register(new BlockItem(builder(), Blocks.BONE_BLOCK)); - public static final Item STRUCTURE_VOID = register(new BlockItem(builder(), Blocks.STRUCTURE_VOID)); + public static final Item STRUCTURE_VOID = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.STRUCTURE_VOID)); public static final Item SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.SHULKER_BOX)); public static final Item WHITE_SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.WHITE_SHULKER_BOX)); public static final Item ORANGE_SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.ORANGE_SHULKER_BOX)); @@ -657,7 +658,7 @@ public final class Items { public static final Item DEAD_FIRE_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_FIRE_CORAL_FAN, Blocks.DEAD_FIRE_CORAL_WALL_FAN)); public static final Item DEAD_HORN_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_HORN_CORAL_FAN, Blocks.DEAD_HORN_CORAL_WALL_FAN)); public static final Item BLUE_ICE = register(new BlockItem(builder(), Blocks.BLUE_ICE)); - public static final Item CONDUIT = register(new BlockItem(builder(), Blocks.CONDUIT)); + public static final Item CONDUIT = register(new BlockItem(builder().rarity(Rarity.RARE), Blocks.CONDUIT)); public static final Item POLISHED_GRANITE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_GRANITE_STAIRS)); public static final Item SMOOTH_RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SMOOTH_RED_SANDSTONE_STAIRS)); public static final Item MOSSY_STONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICK_STAIRS)); @@ -810,7 +811,7 @@ public final class Items { public static final Item HOPPER_MINECART = register(new Item("hopper_minecart", builder().stackSize(1))); public static final Item CARROT_ON_A_STICK = register(new Item("carrot_on_a_stick", builder().stackSize(1).maxDamage(25))); public static final Item WARPED_FUNGUS_ON_A_STICK = register(new Item("warped_fungus_on_a_stick", builder().stackSize(1).maxDamage(100))); - public static final Item ELYTRA = register(new ElytraItem("elytra", builder().stackSize(1).maxDamage(432))); + public static final Item ELYTRA = register(new ElytraItem("elytra", builder().stackSize(1).maxDamage(432).rarity(Rarity.UNCOMMON))); public static final Item OAK_BOAT = register(new BoatItem("oak_boat", builder().stackSize(1))); public static final Item OAK_CHEST_BOAT = register(new BoatItem("oak_chest_boat", builder().stackSize(1))); public static final Item SPRUCE_BOAT = register(new BoatItem("spruce_boat", builder().stackSize(1))); @@ -829,8 +830,8 @@ public final class Items { public static final Item MANGROVE_CHEST_BOAT = register(new BoatItem("mangrove_chest_boat", builder().stackSize(1))); public static final Item BAMBOO_RAFT = register(new BoatItem("bamboo_raft", builder().stackSize(1))); public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder().stackSize(1))); - public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder(), Blocks.STRUCTURE_BLOCK)); - public static final Item JIGSAW = register(new BlockItem(builder(), Blocks.JIGSAW)); + public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.STRUCTURE_BLOCK)); + public static final Item JIGSAW = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.JIGSAW)); public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", ArmorMaterial.TURTLE, builder().stackSize(1).maxDamage(275))); public static final Item TURTLE_SCUTE = register(new Item("turtle_scute", builder())); public static final Item ARMADILLO_SCUTE = register(new Item("armadillo_scute", builder())); @@ -921,8 +922,8 @@ public final class Items { public static final Item PORKCHOP = register(new Item("porkchop", builder())); public static final Item COOKED_PORKCHOP = register(new Item("cooked_porkchop", builder())); public static final Item PAINTING = register(new Item("painting", builder())); - public static final Item GOLDEN_APPLE = register(new Item("golden_apple", builder())); - public static final Item ENCHANTED_GOLDEN_APPLE = register(new Item("enchanted_golden_apple", builder())); + public static final Item GOLDEN_APPLE = register(new Item("golden_apple", builder().rarity(Rarity.RARE))); + public static final Item ENCHANTED_GOLDEN_APPLE = register(new Item("enchanted_golden_apple", builder().rarity(Rarity.EPIC))); public static final Item OAK_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.OAK_SIGN, Blocks.OAK_WALL_SIGN)); public static final Item SPRUCE_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.SPRUCE_SIGN, Blocks.SPRUCE_WALL_SIGN)); public static final Item BIRCH_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.BIRCH_SIGN, Blocks.BIRCH_WALL_SIGN)); @@ -1042,7 +1043,7 @@ public final class Items { public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder())); public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder())); public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND)); - public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON)); public static final Item ENDER_EYE = register(new Item("ender_eye", builder())); public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder())); public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder())); @@ -1125,12 +1126,12 @@ public final class Items { public static final Item ZOMBIE_HORSE_SPAWN_EGG = register(new SpawnEggItem("zombie_horse_spawn_egg", builder())); public static final Item ZOMBIE_VILLAGER_SPAWN_EGG = register(new SpawnEggItem("zombie_villager_spawn_egg", builder())); public static final Item ZOMBIFIED_PIGLIN_SPAWN_EGG = register(new SpawnEggItem("zombified_piglin_spawn_egg", builder())); - public static final Item EXPERIENCE_BOTTLE = register(new Item("experience_bottle", builder())); + public static final Item EXPERIENCE_BOTTLE = register(new Item("experience_bottle", builder().rarity(Rarity.UNCOMMON))); public static final Item FIRE_CHARGE = register(new Item("fire_charge", builder())); public static final Item WIND_CHARGE = register(new Item("wind_charge", builder())); public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder().stackSize(1))); public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder().stackSize(16))); - public static final Item MACE = register(new MaceItem("mace", builder().stackSize(1).maxDamage(500))); + public static final Item MACE = register(new MaceItem("mace", builder().stackSize(1).maxDamage(500).rarity(Rarity.EPIC))); public static final Item ITEM_FRAME = register(new Item("item_frame", builder())); public static final Item GLOW_ITEM_FRAME = register(new Item("glow_item_frame", builder())); public static final Item FLOWER_POT = register(new BlockItem(builder(), Blocks.FLOWER_POT)); @@ -1140,18 +1141,18 @@ public final class Items { public static final Item POISONOUS_POTATO = register(new Item("poisonous_potato", builder())); public static final Item MAP = register(new MapItem("map", builder())); public static final Item GOLDEN_CARROT = register(new Item("golden_carrot", builder())); - public static final Item SKELETON_SKULL = register(new BlockItem(builder(), Blocks.SKELETON_SKULL, Blocks.SKELETON_WALL_SKULL)); - public static final Item WITHER_SKELETON_SKULL = register(new BlockItem(builder(), Blocks.WITHER_SKELETON_SKULL, Blocks.WITHER_SKELETON_WALL_SKULL)); - public static final Item PLAYER_HEAD = register(new PlayerHeadItem(builder(), Blocks.PLAYER_HEAD, Blocks.PLAYER_WALL_HEAD)); - public static final Item ZOMBIE_HEAD = register(new BlockItem(builder(), Blocks.ZOMBIE_HEAD, Blocks.ZOMBIE_WALL_HEAD)); - public static final Item CREEPER_HEAD = register(new BlockItem(builder(), Blocks.CREEPER_HEAD, Blocks.CREEPER_WALL_HEAD)); - public static final Item DRAGON_HEAD = register(new BlockItem(builder(), Blocks.DRAGON_HEAD, Blocks.DRAGON_WALL_HEAD)); - public static final Item PIGLIN_HEAD = register(new BlockItem(builder(), Blocks.PIGLIN_HEAD, Blocks.PIGLIN_WALL_HEAD)); - public static final Item NETHER_STAR = register(new Item("nether_star", builder())); + public static final Item SKELETON_SKULL = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.SKELETON_SKULL, Blocks.SKELETON_WALL_SKULL)); + public static final Item WITHER_SKELETON_SKULL = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.WITHER_SKELETON_SKULL, Blocks.WITHER_SKELETON_WALL_SKULL)); + public static final Item PLAYER_HEAD = register(new PlayerHeadItem(builder().rarity(Rarity.UNCOMMON), Blocks.PLAYER_HEAD, Blocks.PLAYER_WALL_HEAD)); + public static final Item ZOMBIE_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.ZOMBIE_HEAD, Blocks.ZOMBIE_WALL_HEAD)); + public static final Item CREEPER_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.CREEPER_HEAD, Blocks.CREEPER_WALL_HEAD)); + public static final Item DRAGON_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.DRAGON_HEAD, Blocks.DRAGON_WALL_HEAD)); + public static final Item PIGLIN_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.PIGLIN_HEAD, Blocks.PIGLIN_WALL_HEAD)); + public static final Item NETHER_STAR = register(new Item("nether_star", builder().rarity(Rarity.UNCOMMON))); public static final Item PUMPKIN_PIE = register(new Item("pumpkin_pie", builder())); public static final Item FIREWORK_ROCKET = register(new FireworkRocketItem("firework_rocket", builder())); public static final Item FIREWORK_STAR = register(new FireworkStarItem("firework_star", builder())); - public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder().stackSize(1))); + public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder().stackSize(1).rarity(Rarity.UNCOMMON))); public static final Item NETHER_BRICK = register(new Item("nether_brick", builder())); public static final Item PRISMARINE_SHARD = register(new Item("prismarine_shard", builder())); public static final Item PRISMARINE_CRYSTALS = register(new Item("prismarine_crystals", builder())); @@ -1167,7 +1168,7 @@ public final class Items { public static final Item LEATHER_HORSE_ARMOR = register(new DyeableArmorItem("leather_horse_armor", ArmorMaterial.LEATHER, builder().stackSize(1))); public static final Item LEAD = register(new Item("lead", builder())); public static final Item NAME_TAG = register(new Item("name_tag", builder())); - public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder().stackSize(1))); + public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder().stackSize(1).rarity(Rarity.EPIC))); public static final Item MUTTON = register(new Item("mutton", builder())); public static final Item COOKED_MUTTON = register(new Item("cooked_mutton", builder())); public static final Item WHITE_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.WHITE_BANNER, Blocks.WHITE_WALL_BANNER)); @@ -1186,7 +1187,7 @@ public final class Items { public static final Item GREEN_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.GREEN_BANNER, Blocks.GREEN_WALL_BANNER)); public static final Item RED_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.RED_BANNER, Blocks.RED_WALL_BANNER)); public static final Item BLACK_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.BLACK_BANNER, Blocks.BLACK_WALL_BANNER)); - public static final Item END_CRYSTAL = register(new Item("end_crystal", builder())); + public static final Item END_CRYSTAL = register(new Item("end_crystal", builder().rarity(Rarity.RARE))); public static final Item CHORUS_FRUIT = register(new Item("chorus_fruit", builder())); public static final Item POPPED_CHORUS_FRUIT = register(new Item("popped_chorus_fruit", builder())); public static final Item TORCHFLOWER_SEEDS = register(new BlockItem("torchflower_seeds", builder(), Blocks.TORCHFLOWER_CROP)); @@ -1194,52 +1195,52 @@ public final class Items { public static final Item BEETROOT = register(new Item("beetroot", builder())); public static final Item BEETROOT_SEEDS = register(new BlockItem("beetroot_seeds", builder(), Blocks.BEETROOTS)); public static final Item BEETROOT_SOUP = register(new Item("beetroot_soup", builder().stackSize(1))); - public static final Item DRAGON_BREATH = register(new Item("dragon_breath", builder())); + public static final Item DRAGON_BREATH = register(new Item("dragon_breath", builder().rarity(Rarity.UNCOMMON))); public static final Item SPLASH_POTION = register(new PotionItem("splash_potion", builder().stackSize(1))); public static final Item SPECTRAL_ARROW = register(new Item("spectral_arrow", builder())); public static final Item TIPPED_ARROW = register(new TippedArrowItem("tipped_arrow", builder())); public static final Item LINGERING_POTION = register(new PotionItem("lingering_potion", builder().stackSize(1))); public static final Item SHIELD = register(new ShieldItem("shield", builder().stackSize(1).maxDamage(336))); - public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder().stackSize(1))); + public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder().stackSize(1).rarity(Rarity.UNCOMMON))); public static final Item SHULKER_SHELL = register(new Item("shulker_shell", builder())); public static final Item IRON_NUGGET = register(new Item("iron_nugget", builder())); - public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder().stackSize(1))); - public static final Item DEBUG_STICK = register(new Item("debug_stick", builder().stackSize(1))); - public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder().stackSize(1))); - public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder().stackSize(1))); - public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder().stackSize(1))); - public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder().stackSize(1))); - public static final Item MUSIC_DISC_CREATOR = register(new Item("music_disc_creator", builder().stackSize(1))); - public static final Item MUSIC_DISC_CREATOR_MUSIC_BOX = register(new Item("music_disc_creator_music_box", builder().stackSize(1))); - public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder().stackSize(1))); - public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder().stackSize(1))); - public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder().stackSize(1))); - public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder().stackSize(1))); - public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder().stackSize(1))); - public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder().stackSize(1))); - public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder().stackSize(1))); - public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder().stackSize(1))); - public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder().stackSize(1))); - public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder().stackSize(1))); - public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder().stackSize(1))); - public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder().stackSize(1))); - public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder().stackSize(1))); + public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder().stackSize(1).rarity(Rarity.EPIC))); + public static final Item DEBUG_STICK = register(new Item("debug_stick", builder().stackSize(1).rarity(Rarity.EPIC).glint(true))); + public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CREATOR = register(new Item("music_disc_creator", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CREATOR_MUSIC_BOX = register(new Item("music_disc_creator_music_box", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder().stackSize(1).rarity(Rarity.RARE))); public static final Item DISC_FRAGMENT_5 = register(new Item("disc_fragment_5", builder())); - public static final Item TRIDENT = register(new Item("trident", builder().stackSize(1).maxDamage(250).attackDamage(9.0))); + public static final Item TRIDENT = register(new Item("trident", builder().stackSize(1).maxDamage(250).attackDamage(9.0).rarity(Rarity.EPIC))); public static final Item PHANTOM_MEMBRANE = register(new Item("phantom_membrane", builder())); public static final Item NAUTILUS_SHELL = register(new Item("nautilus_shell", builder())); - public static final Item HEART_OF_THE_SEA = register(new Item("heart_of_the_sea", builder())); + public static final Item HEART_OF_THE_SEA = register(new Item("heart_of_the_sea", builder().rarity(Rarity.UNCOMMON))); public static final Item CROSSBOW = register(new CrossbowItem("crossbow", builder().stackSize(1).maxDamage(465))); public static final Item SUSPICIOUS_STEW = register(new Item("suspicious_stew", builder().stackSize(1))); public static final Item LOOM = register(new BlockItem(builder(), Blocks.LOOM)); public static final Item FLOWER_BANNER_PATTERN = register(new Item("flower_banner_pattern", builder().stackSize(1))); - public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder().stackSize(1))); - public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder().stackSize(1))); - public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder().stackSize(1))); + public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder().stackSize(1).rarity(Rarity.EPIC))); public static final Item GLOBE_BANNER_PATTERN = register(new Item("globe_banner_pattern", builder().stackSize(1))); - public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder().stackSize(1))); - public static final Item FLOW_BANNER_PATTERN = register(new Item("flow_banner_pattern", builder().stackSize(1))); - public static final Item GUSTER_BANNER_PATTERN = register(new Item("guster_banner_pattern", builder().stackSize(1))); + public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item FLOW_BANNER_PATTERN = register(new Item("flow_banner_pattern", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item GUSTER_BANNER_PATTERN = register(new Item("guster_banner_pattern", builder().stackSize(1).rarity(Rarity.RARE))); public static final Item GOAT_HORN = register(new GoatHornItem("goat_horn", builder().stackSize(1))); public static final Item COMPOSTER = register(new BlockItem(builder(), Blocks.COMPOSTER)); public static final Item BARREL = register(new BlockItem(builder(), Blocks.BARREL)); diff --git a/licenseheader.txt b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java similarity index 65% rename from licenseheader.txt rename to core/src/main/java/org/geysermc/geyser/item/components/Rarity.java index 9bfe117f9..6fa74ea35 100644 --- a/licenseheader.txt +++ b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 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 @@ -21,4 +21,31 @@ * * @author GeyserMC * @link https://github.com/GeyserMC/Geyser - */ \ No newline at end of file + */ + +package org.geysermc.geyser.item.components; + +import lombok.Getter; + +@Getter +public enum Rarity { + COMMON("common", 'f'), + UNCOMMON("uncommon", 'e'), + RARE("rare", 'b'), + EPIC("epic", 'd'); + + private final String name; + private final char color; + + Rarity(final String name, char chatColor) { + this.name = name; + this.color = chatColor; + } + + private static final Rarity[] VALUES = values(); + + public static Rarity fromId(int id) { + return VALUES.length > id ? VALUES[id] : VALUES[0]; + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java index c5c0d2611..3c0caa60c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java @@ -25,18 +25,21 @@ package org.geysermc.geyser.item.enchantment; +import java.util.List; +import java.util.function.Function; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.inventory.item.BedrockEnchantment; -import org.geysermc.geyser.session.cache.tags.EnchantmentTag; -import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; /** * @param description only populated if {@link #bedrockEnchantment()} is not null. @@ -44,28 +47,32 @@ import java.util.Set; */ public record Enchantment(String identifier, Set effects, - ItemTag supportedItems, + HolderSet supportedItems, int maxLevel, String description, int anvilCost, - @Nullable EnchantmentTag exclusiveSet, + HolderSet exclusiveSet, @Nullable BedrockEnchantment bedrockEnchantment) { - // Implementation note: I have a feeling the tags can be a list of items, because in vanilla they're HolderSet classes. - // I'm not sure how that's wired over the network, so we'll put it off. - public static Enchantment read(RegistryEntry entry) { - NbtMap data = entry.getData(); + public static Enchantment read(RegistryEntryContext context) { + NbtMap data = context.data(); Set effects = readEnchantmentComponents(data.getCompound("effects")); - String supportedItems = data.getString("supported_items").substring(1); // Remove '#' at beginning that indicates tag + + HolderSet supportedItems = readHolderSet(data.get("supported_items"), itemId -> Registries.JAVA_ITEM_IDENTIFIERS.getOrDefault(itemId.asString(), Items.AIR).javaId()); + int maxLevel = data.getInt("max_level"); int anvilCost = data.getInt("anvil_cost"); - String exclusiveSet = data.getString("exclusive_set", null); - EnchantmentTag exclusiveSetTag = exclusiveSet == null ? null : EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(MinecraftKey.key(exclusiveSet.substring(1))); - BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(entry.getId().asString()); + + HolderSet exclusiveSet = readHolderSet(data.getOrDefault("exclusive_set", null), context::getNetworkId); + + BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString()); + + // TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java, + // but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name. String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null; - return new Enchantment(entry.getId().asString(), effects, ItemTag.ALL_ITEM_TAGS.get(MinecraftKey.key(supportedItems)), maxLevel, - description, anvilCost, exclusiveSetTag, bedrockEnchantment); + return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel, + description, anvilCost, exclusiveSet, bedrockEnchantment); } private static Set readEnchantmentComponents(NbtMap effects) { @@ -77,4 +84,24 @@ public record Enchantment(String identifier, } return Set.copyOf(components); // Also ensures any empty sets are consolidated } + + // TODO holder set util? + private static HolderSet readHolderSet(@Nullable Object holderSet, Function keyIdMapping) { + if (holderSet == null) { + return new HolderSet(new int[]{}); + } + + if (holderSet instanceof String stringTag) { + // Tag + if (stringTag.startsWith("#")) { + return new HolderSet(Key.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag + } else { + return new HolderSet(new int[]{keyIdMapping.apply(Key.key(stringTag))}); + } + } else if (holderSet instanceof List list) { + // Assume the list is a list of strings + return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).map(keyIdMapping).mapToInt(Integer::intValue).toArray()); + } + throw new IllegalArgumentException("Holder set must either be a tag, a string ID or a list of string IDs"); + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FilledMapItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FilledMapItem.java index 70a04b863..e571a796a 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FilledMapItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FilledMapItem.java @@ -51,6 +51,7 @@ public class FilledMapItem extends MapItem { switch (mapColor) { case 3830373 -> builder.damage(3); // Ocean Monument case 5393476 -> builder.damage(4); // Woodland explorer + case 12741452 -> builder.damage(14); // Trial Chamber } } } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 362b760c7..57538565a 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -35,6 +35,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; @@ -63,12 +64,16 @@ public class Item { private final int stackSize; private final int attackDamage; private final int maxDamage; + private final Rarity rarity; + private final boolean glint; public Item(String javaIdentifier, Builder builder) { this.javaIdentifier = MinecraftKey.key(javaIdentifier).asString().intern(); this.stackSize = builder.stackSize; this.maxDamage = builder.maxDamage; this.attackDamage = builder.attackDamage; + this.rarity = builder.rarity; + this.glint = builder.glint; } public String javaIdentifier() { @@ -91,6 +96,14 @@ public class Item { return stackSize; } + public Rarity rarity() { + return rarity; + } + + public boolean glint() { + return glint; + } + public boolean isValidRepairItem(Item other) { return false; } @@ -275,6 +288,8 @@ public class Item { private int stackSize = 64; private int maxDamage; private int attackDamage; + private Rarity rarity = Rarity.COMMON; + private boolean glint = false; public Builder stackSize(int stackSize) { this.stackSize = stackSize; @@ -292,6 +307,16 @@ public class Item { return this; } + public Builder rarity(Rarity rarity) { + this.rarity = rarity; + return this; + } + + public Builder glint(boolean glintOverride) { + this.glint = glintOverride; + return this; + } + private Builder() { } } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java index a539fa739..c3b739adc 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java @@ -29,15 +29,19 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; +import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.geyser.translator.item.CustomItemTranslator; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; import java.util.ArrayList; import java.util.List; @@ -64,12 +68,35 @@ public class ShulkerBoxItem extends BlockItem { } ItemMapping boxMapping = session.getItemMappings().getMapping(item.getId()); - NbtMapBuilder boxItemNbt = BedrockItemBuilder.createItemNbt(boxMapping, item.getAmount(), boxMapping.getBedrockData()); // Final item tag to add to the list + int bedrockData = boxMapping.getBedrockData(); + String bedrockIdentifier = boxMapping.getBedrockIdentifier(); + DataComponents boxComponents = item.getDataComponents(); + + if (boxComponents != null) { + // Check for custom items + ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(boxComponents, boxMapping); + if (customItemDefinition != null) { + bedrockIdentifier = customItemDefinition.getIdentifier(); + bedrockData = 0; + } else { + // Manual checks for potions/tipped arrows + if (boxMapping.getJavaItem() instanceof PotionItem || boxMapping.getJavaItem() instanceof ArrowItem) { + PotionContents potionContents = boxComponents.get(DataComponentType.POTION_CONTENTS); + if (potionContents != null) { + Potion potion = Potion.getByJavaId(potionContents.getPotionId()); + if (potion != null) { + bedrockData = potion.getBedrockId(); + } + } + } + } + } + + NbtMapBuilder boxItemNbt = BedrockItemBuilder.createItemNbt(bedrockIdentifier, item.getAmount(), bedrockData); // Final item tag to add to the list boxItemNbt.putByte("Slot", (byte) slot); boxItemNbt.putByte("WasPickedUp", (byte) 0); // ??? TODO might not be needed // Only the display name is what we have interest in, so just translate that if relevant - DataComponents boxComponents = item.getDataComponents(); if (boxComponents != null) { String customName = ItemTranslator.getCustomName(session, boxComponents, boxMapping, '7'); if (customName != null) { diff --git a/core/src/main/java/org/geysermc/geyser/level/BedrockMapIcon.java b/core/src/main/java/org/geysermc/geyser/level/BedrockMapIcon.java index c55a74cd2..eacf6bd1b 100644 --- a/core/src/main/java/org/geysermc/geyser/level/BedrockMapIcon.java +++ b/core/src/main/java/org/geysermc/geyser/level/BedrockMapIcon.java @@ -63,7 +63,8 @@ public enum BedrockMapIcon { ICON_SNOWY_VILLAGE(MapIconType.SNOWY_VILLAGE, 20), ICON_TAIGA_VILLAGE(MapIconType.TAIGA_VILLAGE, 21), ICON_JUNGLE_TEMPLE(MapIconType.JUNGLE_TEMPLE, 22), - ICON_SWAMP_HUT(MapIconType.SWAMP_HUT, 23); + ICON_SWAMP_HUT(MapIconType.SWAMP_HUT, 23), + ICON_TRIAL_CHAMBERS(MapIconType.TRIAL_CHAMBERS, 24); private static final BedrockMapIcon[] VALUES = values(); diff --git a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java index 6112dc6cf..dd0f4215e 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java +++ b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java @@ -26,7 +26,7 @@ package org.geysermc.geyser.level; import org.cloudburstmc.nbt.NbtMap; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; /** * Represents the information we store from the current Java dimension @@ -35,8 +35,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; */ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale) { - public static JavaDimension read(RegistryEntry entry) { - NbtMap dimension = entry.getData(); + public static JavaDimension read(RegistryEntryContext entry) { + NbtMap dimension = entry.data(); int minY = dimension.getInt("min_y"); int maxY = dimension.getInt("height"); // Logical height can be ignored probably - seems to be for artificial limits like the Nether. diff --git a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java index 156a62cd1..b00dc9f98 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java +++ b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java @@ -27,13 +27,13 @@ package org.geysermc.geyser.level; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; public record JukeboxSong(String soundEvent, String description) { - public static JukeboxSong read(RegistryEntry entry) { - NbtMap data = entry.getData(); + public static JukeboxSong read(RegistryEntryContext context) { + NbtMap data = context.data(); Object soundEventObject = data.get("sound_event"); String soundEvent; if (soundEventObject instanceof NbtMap map) { @@ -42,7 +42,7 @@ public record JukeboxSong(String soundEvent, String description) { soundEvent = string; } else { soundEvent = ""; - GeyserImpl.getInstance().getLogger().debug("Sound event for " + entry.getId() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject); + GeyserImpl.getInstance().getLogger().debug("Sound event for " + context.id() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject); } String description = MessageTranslator.deserializeDescription(data); return new JukeboxSong(soundEvent, description); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java index bfde51a79..956d4b771 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java @@ -37,9 +37,22 @@ public class DoorBlock extends Block { @Override public void updateBlock(GeyserSession session, BlockState state, Vector3i position) { + // Needed to check whether we must force the client to update the door state. + String doubleBlockHalf = state.getValue(Properties.DOUBLE_BLOCK_HALF); + + if (!session.getGeyser().getWorldManager().hasOwnChunkCache() && doubleBlockHalf.equals("lower")) { + BlockState oldBlockState = session.getGeyser().getWorldManager().blockAt(session, position); + // If these are the same, it means that we already updated the lower door block (manually in the workaround below), + // and we do not need to update the block in the cache/on the client side using the super.updateBlock() method again. + // Otherwise, we send the door updates twice which will cause visual glitches on the client side + if (oldBlockState == state) { + return; + } + } + super.updateBlock(session, state, position); - if (state.getValue(Properties.DOUBLE_BLOCK_HALF).equals("upper")) { + if (doubleBlockHalf.equals("upper")) { // Update the lower door block as Bedrock client doesn't like door to be closed from the top // See https://github.com/GeyserMC/Geyser/issues/4358 Vector3i belowDoorPosition = position.sub(0, 1, 0); diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index de995301a..c79ef365d 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -43,13 +43,17 @@ import java.util.StringJoiner; */ public final class GameProtocol { + // Surprise protocol bump WOW + private static final BedrockCodec BEDROCK_V686 = Bedrock_v685.CODEC.toBuilder() + .protocolVersion(686) + .minecraftVersion("1.21.2") + .build(); + /** * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() - .minecraftVersion("1.21.1") - .build()); + public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(BEDROCK_V686); /** * A list of all supported Bedrock versions that can join Geyser @@ -66,9 +70,10 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v671.CODEC.toBuilder() .minecraftVersion("1.20.80/1.20.81") .build())); - SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(DEFAULT_BEDROCK_CODEC.toBuilder() - .minecraftVersion("1.21.0/1.20.1") + SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() + .minecraftVersion("1.21.0/1.21.1") .build())); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java index c536d739c..2c033edc7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java @@ -49,8 +49,6 @@ import java.util.function.Consumer; public class CreativeItemRegistryPopulator { private static final List> JAVA_ONLY_ITEM_FILTER = List.of( - // Just shows an empty texture; either way it doesn't exist in the creative menu on Java - (identifier, data) -> identifier.equals("minecraft:debug_stick"), // Bedrock-only as its own item (identifier, data) -> identifier.equals("minecraft:empty_map") && data == 2, // Bedrock-only banner patterns @@ -103,16 +101,8 @@ public class CreativeItemRegistryPopulator { } GeyserBedrockBlock blockDefinition = null; - JsonNode blockRuntimeIdNode = itemNode.get("blockRuntimeId"); JsonNode blockStateNode; - if (blockRuntimeIdNode != null) { - bedrockBlockRuntimeId = blockRuntimeIdNode.asInt(); - if (bedrockBlockRuntimeId == 0 && !identifier.equals("minecraft:blue_candle")) { // FIXME - bedrockBlockRuntimeId = -1; - } - - blockDefinition = bedrockBlockRuntimeId == -1 ? null : blockMappings.getDefinition(bedrockBlockRuntimeId); - } else if ((blockStateNode = itemNode.get("block_state_b64")) != null) { + if ((blockStateNode = itemNode.get("block_state_b64")) != null) { byte[] bytes = Base64.getDecoder().decode(blockStateNode.asText()); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); try { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index e19066462..aad5e494d 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -58,6 +58,8 @@ import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; +import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -252,10 +254,7 @@ public class ItemRegistryPopulator { } else { // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1); - if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) { - // Fallback - bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId); - } else { + if (aValidBedrockBlockId != -1 || customBlockItemOverride != null) { // As of 1.16.220, every item requires a block runtime ID attached to it. // This is mostly for identifying different blocks with the same item ID - wool, slabs, some walls. // However, in order for some visuals and crafting to work, we need to send the first matching block state @@ -266,7 +265,7 @@ public class ItemRegistryPopulator { boolean firstPass = true; // Block states are all grouped together. In the mappings, we store the first block runtime ID in order, // and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents - Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); + int lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) { GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i); NbtMap blockTag = bedrockBlockRuntimeId.getState(); @@ -402,9 +401,10 @@ public class ItemRegistryPopulator { } } - if (javaOnlyItems.contains(javaItem)) { + if (javaOnlyItems.contains(javaItem) || javaItem.rarity() != Rarity.COMMON) { // These items don't exist on Bedrock, so set up a variable that indicates they should have custom names - mappingBuilder = mappingBuilder.translationString((bedrockBlock != null ? "block." : "item.") + entry.getKey().replace(":", ".")); + // Or, ensure that we are translating these at all times to account for rarity colouring + mappingBuilder = mappingBuilder.translationString((javaItem instanceof BlockItem ? "block." : "item.") + entry.getKey().replace(":", ".")); GeyserImpl.getInstance().getLogger().debug("Adding " + entry.getKey() + " as an item that needs to be translated."); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java index 5c4e835e4..be96fec0e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java @@ -44,7 +44,7 @@ import java.util.Set; @Builder @Value -public class BlockMappings implements DefinitionRegistry { +public class BlockMappings implements DefinitionRegistry { GeyserBedrockBlock bedrockAir; BlockDefinition bedrockWater; BlockDefinition bedrockMovingBlock; @@ -134,7 +134,7 @@ public class BlockMappings implements DefinitionRegistry { } @Override - public boolean isRegistered(GeyserBedrockBlock bedrockBlock) { + public boolean isRegistered(BlockDefinition bedrockBlock) { return getDefinition(bedrockBlock.getRuntimeId()) == bedrockBlock; } } \ No newline at end of file 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 8e808f022..60ff45821 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -57,7 +57,6 @@ import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.*; -import org.cloudburstmc.protocol.common.DefinitionRegistry; import org.cloudburstmc.protocol.common.util.OptionalBoolean; import org.geysermc.api.util.BedrockPlatform; import org.geysermc.api.util.InputMode; @@ -1490,7 +1489,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private void startGame() { this.upstream.getCodecHelper().setItemDefinitions(this.itemMappings); - this.upstream.getCodecHelper().setBlockDefinitions((DefinitionRegistry) this.blockMappings); //FIXME + this.upstream.getCodecHelper().setBlockDefinitions(this.blockMappings); this.upstream.getCodecHelper().setCameraPresetDefinitions(CameraDefinitions.CAMERA_DEFINITIONS); StartGamePacket startGamePacket = new StartGamePacket(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java index 3121af369..a393d461d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java @@ -27,6 +27,8 @@ package org.geysermc.geyser.session.cache; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -45,6 +47,7 @@ import org.geysermc.geyser.level.JukeboxSong; import org.geysermc.geyser.level.PaintingType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry; import org.geysermc.geyser.text.TextDecoration; import org.geysermc.geyser.translator.level.BiomeTranslator; @@ -59,7 +62,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.ToIntFunction; @@ -76,16 +78,16 @@ public final class RegistryCache { private static final Map>> REGISTRIES = new HashMap<>(); static { - register("chat_type", cache -> cache.chatTypes, ($, entry) -> TextDecoration.readChatType(entry)); - register("dimension_type", cache -> cache.dimensions, ($, entry) -> JavaDimension.read(entry)); - register("enchantment", cache -> cache.enchantments, ($, entry) -> Enchantment.read(entry)); - register("jukebox_song", cache -> cache.jukeboxSongs, ($, entry) -> JukeboxSong.read(entry)); - register("painting_variant", cache -> cache.paintings, ($, entry) -> PaintingType.getByName(entry.getId())); + register("chat_type", cache -> cache.chatTypes, TextDecoration::readChatType); + register("dimension_type", cache -> cache.dimensions, JavaDimension::read); + register("enchantment", cache -> cache.enchantments, Enchantment::read); + register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read); + register("painting_variant", cache -> cache.paintings, context -> PaintingType.getByName(context.id())); register("trim_material", cache -> cache.trimMaterials, TrimRecipe::readTrimMaterial); register("trim_pattern", cache -> cache.trimPatterns, TrimRecipe::readTrimPattern); register("worldgen/biome", (cache, array) -> cache.biomeTranslations = array, BiomeTranslator::loadServerBiome); - register("banner_pattern", cache -> cache.bannerPatterns, ($, entry) -> BannerPattern.getByJavaIdentifier(entry.getId())); - register("wolf_variant", cache -> cache.wolfVariants, ($, entry) -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(entry.getId().asString())); + register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); + register("wolf_variant", cache -> cache.wolfVariants, context -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(context.id().asString())); // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); @@ -149,25 +151,35 @@ public final class RegistryCache { * @param reader converts the RegistryEntry NBT into a class file * @param the class that represents these entries. */ - private static void register(String registry, Function> localCacheFunction, BiFunction reader) { - Key key = MinecraftKey.key(registry); - REGISTRIES.put(key, (registryCache, entries) -> { + private static void register(String registry, Function> localCacheFunction, Function reader) { + Key registryKey = MinecraftKey.key(registry); + REGISTRIES.put(registryKey, (registryCache, entries) -> { Map localRegistry = null; JavaRegistry localCache = localCacheFunction.apply(registryCache); // Clear each local cache every time a new registry entry is given to us // (e.g. proxy server switches) + + // Store each of the entries resource location IDs and their respective network ID, + // used for the key mapper that's currently only used by the Enchantment class + Object2IntMap entryIdMap = new Object2IntOpenHashMap<>(); + for (int i = 0; i < entries.size(); i++) { + entryIdMap.put(entries.get(i).getId(), i); + } + List builder = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { RegistryEntry entry = entries.get(i); // If the data is null, that's the server telling us we need to use our default values. if (entry.getData() == null) { if (localRegistry == null) { // Lazy initialize - localRegistry = DEFAULTS.get(key); + localRegistry = DEFAULTS.get(registryKey); } entry = new RegistryEntry(entry.getId(), localRegistry.get(entry.getId())); } + + RegistryEntryContext context = new RegistryEntryContext(entry, entryIdMap, registryCache.session); // This is what Geyser wants to keep as a value for this registry. - T cacheEntry = reader.apply(registryCache.session, entry); + T cacheEntry = reader.apply(context); builder.add(i, cacheEntry); } localCache.reset(builder); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index c8bfc7eed..f4d69dcdb 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -130,8 +130,12 @@ public final class TagCache { return contains(values, item.javaId()); } - public int[] get(EnchantmentTag tag) { - return this.enchantments[tag.ordinal()]; + public int[] get(ItemTag itemTag) { + return this.items[itemTag.ordinal()]; + } + + public int[] get(EnchantmentTag enchantmentTag) { + return this.enchantments[enchantmentTag.ordinal()]; } private static boolean contains(int[] array, int i) { diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java new file mode 100644 index 000000000..415890d95 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 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.session.cache.registry; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import java.util.Map; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; + +/** + * Used to store context around a single registry entry when reading said entry's NBT. + * + * @param entry the registry entry being read. + * @param keyIdMap a map for each of the resource location's in the registry and their respective network IDs. + * @param session the Geyser session. + */ +public record RegistryEntryContext(RegistryEntry entry, Object2IntMap keyIdMap, GeyserSession session) { + + public int getNetworkId(Key registryKey) { + return keyIdMap.getOrDefault(registryKey, 0); + } + + public Key id() { + return entry.getId(); + } + + // Not annotated as nullable because data should never be null here + public NbtMap data() { + return entry.getData(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java b/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java index ab9e2b5ed..94aec22ef 100644 --- a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java +++ b/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java @@ -29,7 +29,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; @@ -43,11 +43,11 @@ public record TextDecoration(String translationKey, List parameters, throw new UnsupportedOperationException(); } - public static ChatType readChatType(RegistryEntry entry) { + public static ChatType readChatType(RegistryEntryContext context) { // Note: The ID is NOT ALWAYS THE SAME! ViaVersion as of 1.19 adds two registry entries that do NOT match vanilla. // (This note has been passed around through several classes and iterations. It stays as a warning // to anyone that dares to try and hardcode registry IDs.) - NbtMap tag = entry.getData(); + NbtMap tag = context.data(); NbtMap chat = tag.getCompound("chat", null); if (chat != null) { String translationKey = chat.getString("translation_key"); diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java index 52d5b7e31..e989288c2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java @@ -145,8 +145,15 @@ public final class BedrockItemBuilder { * Creates item NBT to nest within NBT with name, count, and damage set. */ public static NbtMapBuilder createItemNbt(ItemMapping mapping, int count, int damage) { + return createItemNbt(mapping.getBedrockIdentifier(), count, damage); + } + + /** + * Creates item NBT to nest within NBT with name, count, and damage set. + */ + public static NbtMapBuilder createItemNbt(String bedrockIdentifier, int count, int damage) { NbtMapBuilder builder = NbtMap.builder(); - builder.putString("Name", mapping.getBedrockIdentifier()); + builder.putString("Name", bedrockIdentifier); builder.putByte("Count", (byte) count); builder.putShort("Damage", (short) damage); return builder; diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index a8d29c465..6a781dcb8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -33,7 +33,9 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -41,6 +43,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; @@ -52,6 +55,7 @@ import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.*; @@ -144,7 +148,20 @@ public final class ItemTranslator { if (components.get(DataComponentType.HIDE_TOOLTIP) != null) hideTooltips = true; } - String customName = getCustomName(session, components, bedrockItem); + Rarity rarity = javaItem.rarity(); + boolean enchantmentGlint = javaItem.glint(); + if (components != null) { + Integer rarityIndex = components.get(DataComponentType.RARITY); + if (rarityIndex != null) { + rarity = Rarity.fromId(rarityIndex); + } + Boolean enchantmentGlintOverride = components.get(DataComponentType.ENCHANTMENT_GLINT_OVERRIDE); + if (enchantmentGlintOverride != null) { + enchantmentGlint = enchantmentGlintOverride; + } + } + + String customName = getCustomName(session, components, bedrockItem, rarity.getColor()); if (customName != null) { nbtBuilder.setCustomName(customName); } @@ -161,6 +178,12 @@ public final class ItemTranslator { addAdvancedTooltips(components, nbtBuilder, javaItem, session.locale()); } + // Add enchantment override. We can't remove it - enchantments would stop showing - but we can add it. + if (enchantmentGlint) { + NbtMapBuilder nbtMapBuilder = nbtBuilder.getOrCreateNbt(); + nbtMapBuilder.putIfAbsent("ench", NbtList.EMPTY); + } + ItemData.Builder builder = javaItem.translateToBedrock(count, components, bedrockItem, session.getItemMappings()); // Finalize the Bedrock NBT builder.tag(nbtBuilder.build()); @@ -209,7 +232,7 @@ public final class ItemTranslator { Map> slotsToModifiers = new HashMap<>(); for (ItemAttributeModifiers.Entry entry : modifiers.getModifiers()) { // convert the modifier tag to a lore entry - String loreEntry = attributeToLore(entry.getModifier(), language); + String loreEntry = attributeToLore(entry.getAttribute(), entry.getModifier(), language); if (loreEntry == null) { continue; // invalid or failed } @@ -254,13 +277,13 @@ public final class ItemTranslator { } @Nullable - private static String attributeToLore(ItemAttributeModifiers.AttributeModifier modifier, String language) { + private static String attributeToLore(int attribute, ItemAttributeModifiers.AttributeModifier modifier, String language) { double amount = modifier.getAmount(); if (amount == 0) { return null; } - String name = modifier.getId().asMinimalString(); + String name = AttributeType.Builtin.from(attribute).getIdentifier().asMinimalString(); // the namespace does not need to be present, but if it is, the java client ignores it as of pre-1.20.5 ModifierOperation operation = modifier.getOperation(); @@ -400,16 +423,6 @@ public final class ItemTranslator { } } - /** - * Translates the display name of the item - * @param session the Bedrock client's session - * @param components the components to translate - * @param mapping the item entry, in case it requires translation - */ - public static String getCustomName(GeyserSession session, DataComponents components, ItemMapping mapping) { - return getCustomName(session, components, mapping, 'f'); - } - /** * @param translationColor if this item is not available on Java, the color that the new name should be. * Normally, this should just be white, but for shulker boxes this should be gray. @@ -418,13 +431,15 @@ public final class ItemTranslator { if (components != null) { // ItemStack#getHoverName as of 1.20.5 Component customName = components.get(DataComponentType.CUSTOM_NAME); - if (customName == null) { - customName = components.get(DataComponentType.ITEM_NAME); - } if (customName != null) { - // Get the translated name and prefix it with a reset char return MessageTranslator.convertMessage(customName, session.locale()); } + customName = components.get(DataComponentType.ITEM_NAME); + if (customName != null) { + // Get the translated name and prefix it with a reset char to prevent italics - matches Java Edition + // behavior as of 1.21 + return ChatColor.RESET + ChatColor.ESCAPE + translationColor + MessageTranslator.convertMessage(customName, session.locale()); + } } if (mapping.hasTranslation()) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java index 6b26f2b2c..14f706f27 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java @@ -107,7 +107,7 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator { bedrockNbt.put("isMovable", (byte) 1); } - static void translateSpawnData(@NonNull NbtMapBuilder builder, @Nullable NbtMap spawnData) { + private static void translateSpawnData(@NonNull NbtMapBuilder builder, @Nullable NbtMap spawnData) { if (spawnData == null) { return; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/TrialSpawnerBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/TrialSpawnerBlockEntityTranslator.java index dd58bfa6f..25925a89b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/TrialSpawnerBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/TrialSpawnerBlockEntityTranslator.java @@ -27,22 +27,31 @@ package org.geysermc.geyser.translator.level.block.entity; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; +import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType; @BlockEntity(type = BlockEntityType.TRIAL_SPAWNER) public class TrialSpawnerBlockEntityTranslator extends BlockEntityTranslator { + // Note that it would appear block entity updates don't include the NBT, but we do need it on chunk load. @Override public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { if (javaNbt == null) { return; } - // trial spawners have "spawn_data" instead of "SpawnData" - SpawnerBlockEntityTranslator.translateSpawnData(bedrockNbt, javaNbt.getCompound("spawn_data", null)); - - // Because trial spawners don't exist on bedrock yet - bedrockNbt.put("id", "MobSpawner"); + NbtMap entityData = javaNbt.getCompound("spawn_data").getCompound("entity"); + if (entityData.isEmpty()) { + return; + } + NbtMapBuilder spawnData = NbtMap.builder(); + EntityDefinition definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityData.getString("id")); + if (definition != null) { + spawnData.putString("TypeId", definition.identifier()); + } + spawnData.putInt("Weight", entityData.getInt("Size", 1)); // ??? presumably since these are the only other two extra attributes + bedrockNbt.putCompound("spawn_data", spawnData.build()); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java index cae12170d..3d612c481 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -25,20 +25,19 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerRotPacket; -import org.geysermc.mcprotocollib.network.packet.Packet; import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; -import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.network.packet.Packet; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerRotPacket; @Translator(packet = MovePlayerPacket.class) public class BedrockMovePlayerTranslator extends PacketTranslator { @@ -93,29 +92,42 @@ public class BedrockMovePlayerTranslator extends PacketTranslator= packet.getPosition().getY()) { + if (entity.getPosition().getY() >= packet.getPosition().getY() && !isBelowVoid) { int floorY = position.getFloorY(); - // The void floor is offset about 40 blocks below the bottom of the world - BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); - int voidFloorLocation = bedrockDimension.minY() - 40; - teleportThroughVoidFloor = floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation; - if (teleportThroughVoidFloor) { - // https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground. - onGround = false; - } + int voidFloorLocation = entity.voidFloorPosition(); + teleportThroughVoidFloor = floorY <= (voidFloorLocation + 1) && floorY >= voidFloorLocation; } else { teleportThroughVoidFloor = false; } + if (teleportThroughVoidFloor || isBelowVoid) { + // https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground. + onGround = false; + } + + if (isBelowVoid) { + int floorY = position.getFloorY(); + int voidFloorLocation = entity.voidFloorPosition(); + mustResyncPosition = floorY < voidFloorLocation && floorY >= voidFloorLocation - 1; + } else { + mustResyncPosition = false; + } + + double yPosition = position.getY(); + if (entity.isVoidPositionDesynched()) { // not using the cached variable on purpose + yPosition += 4; // We are de-synched since we had to teleport below the void floor. + } + Packet movePacket; if (rotationChanged) { // Send rotation updates as well movePacket = new ServerboundMovePlayerPosRotPacket( onGround, - position.getX(), position.getY(), position.getZ(), + position.getX(), yPosition, position.getZ(), yaw, pitch ); entity.setYaw(yaw); @@ -123,7 +135,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator { @@ -291,6 +312,20 @@ public class JavaLevelEventTranslator extends PacketTranslator effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_SPAWN_COBWEB); + case ANIMATION_VAULT_ACTIVATE -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_ACTIVATE); + case ANIMATION_VAULT_DEACTIVATE -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_DEACTIVATE); + case ANIMATION_VAULT_EJECT_ITEM -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_EJECT_ITEM); + case ANIMATION_TRIAL_SPAWNER_EJECT_ITEM -> { + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("trial_spawner.eject_item"); + playSoundPacket.setPosition(pos); + playSoundPacket.setVolume(1.0f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + return; + } case DRIPSTONE_DRIP -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_DRIPSTONE_DRIP); case PARTICLES_ELECTRIC_SPARK -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_ELECTRIC_SPARK); // Matches with a Bedrock server but doesn't seem to match up with Java case PARTICLES_AND_SOUND_WAX_ON -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_WAX_ON); @@ -303,22 +338,22 @@ public class JavaLevelEventTranslator extends PacketTranslator 0) { levelEventPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.SCULK_CHARGE); levelEventPacket.setTag( - NbtMap.builder() - .putInt("x", packet.getPosition().getX()) - .putInt("y", packet.getPosition().getY()) - .putInt("z", packet.getPosition().getZ()) - .putShort("charge", (short) eventData.getCharge()) - .putShort("facing", encodeFacing(eventData.getBlockFaces())) // TODO check if this is actually correct - .build() + NbtMap.builder() + .putInt("x", packet.getPosition().getX()) + .putInt("y", packet.getPosition().getY()) + .putInt("z", packet.getPosition().getZ()) + .putShort("charge", (short) eventData.getCharge()) + .putShort("facing", encodeFacing(eventData.getBlockFaces())) // TODO check if this is actually correct + .build() ); } else { levelEventPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.SCULK_CHARGE_POP); levelEventPacket.setTag( - NbtMap.builder() - .putInt("x", packet.getPosition().getX()) - .putInt("y", packet.getPosition().getY()) - .putInt("z", packet.getPosition().getZ()) - .build() + NbtMap.builder() + .putInt("x", packet.getPosition().getX()) + .putInt("y", packet.getPosition().getY()) + .putInt("z", packet.getPosition().getZ()) + .build() ); } session.sendUpstreamPacket(levelEventPacket); @@ -328,11 +363,11 @@ public class JavaLevelEventTranslator extends PacketTranslator { + // Particles spawn here + TrialSpawnerDetectEventData eventData = (TrialSpawnerDetectEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_DETECTION); + // 0.75 is used here for Y instead of 0.5 to match Java Positioning. + // 0.5 is what the BDS uses for positioning. + effectPacket.setPosition(pos.sub(0.5f, 0.75f, 0.5f)); + effectPacket.setData(eventData.getDetectedPlayers()); + } + case PARTICLES_TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS -> { + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_DETECTION_CHARGED); + effectPacket.setPosition(pos.sub(0.5f, 0.75f, 0.5f)); + /* + Particles don't spawn here for some reason, only sound plays + This seems to be a bug in v1.21.0 and v1.21.1: see https://bugs.mojang.com/browse/MCPE-181465 + If this gets fixed, the spawnOminousTrialSpawnerParticles function can be removed. + The positioning should be the same as normal activation. + */ + spawnOminousTrialSpawnerParticles(session, pos); + } + case PARTICLES_TRIAL_SPAWNER_BECOME_OMINOUS -> { + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_BECOME_CHARGED); + effectPacket.setPosition(pos.sub(0.5f, 0.5f, 0.5f)); + // Same issue as above here + spawnOminousTrialSpawnerParticles(session, pos); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN_MOB_AT -> { + // This should be its own class in MCProtocolLib. + // if 0, use Orange Flames, + // if 1, use Blue Flames for ominous spawners + UnknownLevelEventData eventData = (UnknownLevelEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_SPAWNING); + effectPacket.setData(eventData.getData()); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN -> { + UnknownLevelEventData eventData = (UnknownLevelEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_SPAWNING); + effectPacket.setData(eventData.getData()); + + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("trial_spawner.spawn_mob"); + playSoundPacket.setPosition(pos); + playSoundPacket.setVolume(1.0f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN_ITEM -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_EJECTING); case SOUND_STOP_JUKEBOX_SONG -> { String bedrockSound = session.getWorldCache().removeActiveRecord(origin); if (bedrockSound == null) { @@ -396,4 +479,15 @@ public class JavaLevelEventTranslator extends PacketTranslator { + private static final int MAX_PARTICLES = 100; @Override public void translate(GeyserSession session, ClientboundLevelParticlesPacket packet) { @@ -71,7 +72,8 @@ public class JavaLevelParticlesTranslator extends PacketTranslator { - +public interface BlockSoundInteractionTranslator extends SoundInteractionTranslator { /** * Handles the block interaction when a player * right-clicks a block. * * @param session the session interacting with the block * @param position the position of the block - * @param identifier the identifier of the block + * @param state the state of the block */ - static void handleBlockInteraction(GeyserSession session, Vector3f position, String identifier) { + static void handleBlockInteraction(GeyserSession session, Vector3f position, BlockState state) { // If we need to get the hand identifier, only get it once and save it to a variable String handIdentifier = null; @@ -58,7 +58,7 @@ public interface BlockSoundInteractionTranslator extends SoundInteractionTransla if (interactionEntry.getKey().blocks().length != 0) { boolean contains = false; for (String blockIdentifier : interactionEntry.getKey().blocks()) { - if (identifier.contains(blockIdentifier)) { + if (state.toString().contains(blockIdentifier)) { contains = true; break; } @@ -87,7 +87,7 @@ public interface BlockSoundInteractionTranslator extends SoundInteractionTransla continue; } } - ((BlockSoundInteractionTranslator) interactionEntry.getValue()).translate(session, position, identifier); + ((BlockSoundInteractionTranslator) interactionEntry.getValue()).translate(session, position, state); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java index 7a5e86af7..45800e1ab 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java @@ -29,6 +29,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class BucketSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); if (!session.isPlacedBucket()) { return; // No bucket was really interacted with } diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java index e77539e6e..9d5d3cd59 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class ComparatorSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); boolean powered = identifier.contains("mode=compare"); LevelEventPacket levelEventPacket = new LevelEventPacket(); levelEventPacket.setPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java index a822f3520..e8225a336 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,7 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class FlintAndSteelInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java index 0a91f8896..a25beaa50 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class GrassPathInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java index 410422b4a..1a583ab8a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class HoeInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java index 17b8768bc..fd045739c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class LeverSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); boolean powered = identifier.contains("powered=true"); LevelEventPacket levelEventPacket = new LevelEventPacket(); levelEventPacket.setPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java similarity index 55% rename from core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java rename to core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java index 8f8ab8bf6..93d55ca33 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java @@ -27,21 +27,42 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @SoundTranslator(blocks = {"door", "fence_gate"}) -public class DoorSoundInteractionTranslator implements BlockSoundInteractionTranslator { +public class OpenableSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); if (identifier.contains("iron")) return; - LevelEventPacket levelEventPacket = new LevelEventPacket(); - levelEventPacket.setType(LevelEvent.SOUND_DOOR_OPEN); - levelEventPacket.setPosition(position); - levelEventPacket.setData(0); - session.sendUpstreamPacket(levelEventPacket); + SoundEvent event = getSound(identifier.contains("open=true"), identifier); + LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); + levelSoundEventPacket.setPosition(position.add(0.5, 0.5, 0.5)); + levelSoundEventPacket.setBabySound(false); + levelSoundEventPacket.setRelativeVolumeDisabled(false); + levelSoundEventPacket.setIdentifier(":"); + levelSoundEventPacket.setSound(event); + levelSoundEventPacket.setExtraData(session.getBlockMappings().getBedrockBlock(state).getRuntimeId()); + session.sendUpstreamPacket(levelSoundEventPacket); + } + + private SoundEvent getSound(boolean open, String identifier) { + if (identifier.contains("_door")) { + return open ? SoundEvent.DOOR_OPEN : SoundEvent.DOOR_CLOSE; + } + + if (identifier.contains("_trapdoor")) { + return open ? SoundEvent.TRAPDOOR_OPEN : SoundEvent.TRAPDOOR_CLOSE; + } + + // Fence Gate + return open ? SoundEvent.FENCE_GATE_OPEN : SoundEvent.FENCE_GATE_CLOSE; } } diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index bfb70a4ed..53aefde1e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 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 @@ -32,6 +32,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.TextDisplayEntity; import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; @@ -172,11 +173,7 @@ public final class EntityUtils { case BOAT -> { // Without the X offset, more than one entity on a boat is stacked on top of each other if (moreThanOneEntity) { - if (rider) { - xOffset = 0.2f; - } else { - xOffset = -0.6f; - } + xOffset = rider ? 0.2f : -0.6f; if (passenger instanceof AnimalEntity) { xOffset += 0.2f; } @@ -203,6 +200,18 @@ public final class EntityUtils { case CHEST_BOAT -> xOffset = 0.15F; case CHICKEN -> zOffset = -0.1f; case TRADER_LLAMA, LLAMA -> zOffset = -0.3f; + case TEXT_DISPLAY -> { + if (passenger instanceof TextDisplayEntity textDisplay) { + Vector3f displayTranslation = textDisplay.getTranslation(); + if (displayTranslation == null) { + return; + } + + xOffset = displayTranslation.getX(); + yOffset = displayTranslation.getY() + 0.2f; + zOffset = displayTranslation.getZ(); + } + } } /* * Bedrock Differences @@ -228,8 +237,7 @@ public final class EntityUtils { if (mount instanceof ArmorStandEntity armorStand) { yOffset -= armorStand.getYOffset(); } - Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset); - passenger.setRiderSeatPosition(offset); + passenger.setRiderSeatPosition(Vector3f.from(xOffset, yOffset, zOffset)); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java index 524d241db..693ce136a 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java @@ -133,7 +133,7 @@ public final class SoundUtils { } if (sound == null) { session.getGeyser().getLogger().debug("[Builtin] Sound for original '" + soundIdentifier + "' to mappings '" + soundMapping.getBedrock() - + "' was not a playable level sound, or has yet to be mapped to an enum in SoundEvent."); + + "' was not a playable level sound, or has yet to be mapped to an enum in SoundEvent."); return; } @@ -144,7 +144,7 @@ public final class SoundUtils { // Minecraft Wiki: 2^(x/12) = Java pitch where x is -12 to 12 // Java sends the note value as above starting with -12 and ending at 12 // Bedrock has a number for each type of note, then proceeds up the scale by adding to that number - soundPacket.setExtraData(soundMapping.getExtraData() + (int)(Math.round((Math.log10(pitch) / Math.log10(2)) * 12)) + 12); + soundPacket.setExtraData(soundMapping.getExtraData() + (int) (Math.round((Math.log10(pitch) / Math.log10(2)) * 12)) + 12); } else if (sound == SoundEvent.PLACE && soundMapping.getExtraData() == -1) { if (!soundMapping.getIdentifier().equals(":")) { int javaId = BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getOrDefault(soundMapping.getIdentifier(), Block.JAVA_AIR_ID); diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 23cb22f9c..aaf53d695 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 23cb22f9ceeb7f24b896a69a711944d7f3e756ed +Subproject commit aaf53d6953c927e5ac1b87fd6627ffbfd4aa7cf5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a9b7888e..49c02d190 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ netty-io-uring = "0.0.25.Final-SNAPSHOT" guava = "29.0-jre" gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" -protocol = "3.0.0.Beta2-20240616.144648-10" +protocol = "3.0.0.Beta2-20240704.153116-14" raknet = "1.0.0.CR3-20240416.144209-1" mcauthlib = "e5b0bcc" mcprotocollib = "1.21-20240616.154144-5" @@ -39,7 +39,7 @@ minecraft = "1.21" indra = "3.1.3" shadow = "8.1.1" architectury-plugin = "3.4-SNAPSHOT" -architectury-loom = "1.6-SNAPSHOT" +architectury-loom = "1.7-SNAPSHOT" minotaur = "2.8.7" lombok = "8.4" blossom = "2.1.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917..e6441136f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..a4413138c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME