Merge pull request #3131 from Kas-tle/feature/extensions

Feature/extensions 1.19.10
This commit is contained in:
Camotoy 2022-07-13 22:45:59 -04:00 committed by GitHub
commit 240af3cf2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1091 additions and 246 deletions

View file

@ -1,10 +1,10 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Common Issues - name: Common Issues
url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues url: https://wiki.geysermc.org/geyser/common-issues
about: Check the common issues to see if you are not alone with that issue and see how you can fix them. about: Check the common issues to see if you are not alone with that issue and see how you can fix them.
- name: Frequently Asked Questions - name: Frequently Asked Questions
url: https://github.com/GeyserMC/Geyser/wiki/FAQ url: https://wiki.geysermc.org/geyser/faq
about: Look at the FAQ page for answers to frequently asked questions. about: Look at the FAQ page for answers to frequently asked questions.
- name: Get help on the GeyserMC Discord server - name: Get help on the GeyserMC Discord server
url: https://discord.gg/geysermc url: https://discord.gg/geysermc

View file

@ -9,11 +9,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 16 - name: Set up JDK 17
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 16 java-version: 17
cache: 'gradle' cache: 'gradle'
- name: submodules-init - name: submodules-init
uses: snickerbockers/submodules-init@v4 uses: snickerbockers/submodules-init@v4

2
Jenkinsfile vendored
View file

@ -2,7 +2,7 @@ pipeline {
agent any agent any
tools { tools {
gradle 'Gradle 7' gradle 'Gradle 7'
jdk 'Java 16' jdk 'Java 17'
} }
options { options {
buildDiscarder(logRotator(artifactNumToKeepStr: '20')) buildDiscarder(logRotator(artifactNumToKeepStr: '20'))

View file

@ -17,7 +17,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! 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.19 and Minecraft Java 1.19.0. ### Currently supporting Minecraft Bedrock 1.19.0 - 1.19.10 and Minecraft Java 1.19.0.
## Setting Up ## Setting Up
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.

View file

@ -1,6 +1,6 @@
val paperVersion = "1.17.1-R0.1-SNAPSHOT" // Needed because we do not support Java 17 yet val paperVersion = "1.19-R0.1-SNAPSHOT"
val viaVersion = "4.0.0" val viaVersion = "4.0.0"
val adaptersVersion = "1.4-SNAPSHOT" val adaptersVersion = "1.5-SNAPSHOT"
val commodoreVersion = "1.13" val commodoreVersion = "1.13"
dependencies { dependencies {
@ -9,6 +9,18 @@ dependencies {
implementation("org.geysermc.geyser.adapters", "spigot-all", adaptersVersion) implementation("org.geysermc.geyser.adapters", "spigot-all", adaptersVersion)
implementation("me.lucko", "commodore", commodoreVersion) implementation("me.lucko", "commodore", commodoreVersion)
// Both paper-api and paper-mojangapi only provide Java 17 versions for 1.19
compileOnly("io.papermc.paper", "paper-api", paperVersion) {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
}
compileOnly("io.papermc.paper", "paper-mojangapi", paperVersion) {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
}
} }
platformRelocate("it.unimi.dsi.fastutil") platformRelocate("it.unimi.dsi.fastutil")
@ -19,8 +31,6 @@ platformRelocate("me.lucko.commodore")
platformRelocate("io.netty.channel.kqueue") platformRelocate("io.netty.channel.kqueue")
// These dependencies are already present on the platform // These dependencies are already present on the platform
provided("io.papermc.paper", "paper-api", paperVersion)
provided("io.papermc.paper", "paper-mojangapi", paperVersion)
provided("com.viaversion", "viaversion", viaVersion) provided("com.viaversion", "viaversion", viaVersion)
application { application {

View file

@ -35,6 +35,7 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
/** /**
@ -42,6 +43,8 @@ import java.net.InetSocketAddress;
* applied. * applied.
*/ */
public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough { public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough {
private static final Constructor<PaperServerListPingEvent> OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor();
private final GeyserSpigotLogger logger; private final GeyserSpigotLogger logger;
public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) { public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) {
@ -54,9 +57,17 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
try { try {
// We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at // We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at
// runtime because we still have to shade in our own Adventure class. For now. // runtime because we still have to shade in our own Adventure class. For now.
PaperServerListPingEvent event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress), PaperServerListPingEvent event;
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers(), Bukkit.getVersion(), if (OLD_CONSTRUCTOR != null) {
GameProtocol.getJavaProtocolVersion(), null); // Approximately pre-1.19
event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress),
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
} else {
event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
Bukkit.getMotd(), Bukkit.shouldSendChatPreviews(), Bukkit.getOnlinePlayers().size(),
Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
}
Bukkit.getPluginManager().callEvent(event); Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) { if (event.isCancelled()) {
// We have to send a ping, so not really sure what else to do here. // We have to send a ping, so not really sure what else to do here.
@ -80,7 +91,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
} }
return geyserPingInfo; return geyserPingInfo;
} catch (Exception e) { } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Paper ping passthrough: " + e); logger.debug("Error while getting Paper ping passthrough: " + e);
return null; return null;
} }

View file

@ -56,7 +56,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
); );
Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add); Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add);
return geyserPingInfo; return geyserPingInfo;
} catch (Exception e) { } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Bukkit ping passthrough: " + e); logger.debug("Error while getting Bukkit ping passthrough: " + e);
return null; return null;
} }
@ -66,7 +66,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
private static class GeyserPingEvent extends ServerListPingEvent { private static class GeyserPingEvent extends ServerListPingEvent {
public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) { public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) {
super(address, motd, numPlayers, maxPlayers); super(address, motd, Bukkit.shouldSendChatPreviews(), numPlayers, maxPlayers);
} }
@Override @Override

View file

@ -168,14 +168,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
if (geyserConfig.isLegacyPingPassthrough()) { if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
} else { } else {
try { if (ReflectedNames.checkPaperPingEvent()) {
Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger); this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger);
} catch (ClassNotFoundException e) { } else if (ReflectedNames.newSpigotPingConstructorExists()) {
this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger); this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger);
} else {
// Can't enable one of the other options
this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
} }
} }
geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); geyserLogger.info("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass()));
this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
this.geyserCommandManager.init(); this.geyserCommandManager.init();

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.platform.spigot;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.network.StatusClient;
import org.bukkit.event.server.ServerListPingEvent;
import org.bukkit.util.CachedServerIcon;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.net.InetAddress;
/**
* A utility class for checking on the existence of classes, constructors, fields, methods
*/
public final class ReflectedNames {
static boolean checkPaperPingEvent() {
return classExists("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
}
/**
* @return if this class name exists
*/
private static boolean classExists(String clazz) {
try {
Class.forName(clazz);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
static boolean newSpigotPingConstructorExists() {
return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null;
}
static Constructor<PaperServerListPingEvent> getOldPaperPingConstructor() {
if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class,
int.class, String.class, int.class, CachedServerIcon.class) != null) {
// @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers,
// @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
// New constructor is present
return null;
}
// @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
// @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, int.class,
String.class, int.class, CachedServerIcon.class);
}
/**
* @return if this class has a constructor with the specified arguments
*/
@Nullable
private static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... args) {
try {
return clazz.getConstructor(args);
} catch (NoSuchMethodException e) {
return null;
}
}
private ReflectedNames() {
}
}

View file

@ -30,10 +30,12 @@ object Versions {
const val guavaVersion = "29.0-jre" const val guavaVersion = "29.0-jre"
const val nbtVersion = "2.1.0" const val nbtVersion = "2.1.0"
const val websocketVersion = "1.5.1" const val websocketVersion = "1.5.1"
const val protocolVersion = "977a9a1" const val protocolVersion = "a78a64b"
// Not pinned to specific version due to possible gradle bug
// See comment in settings.gradle.kts
const val raknetVersion = "1.6.28-SNAPSHOT" const val raknetVersion = "1.6.28-SNAPSHOT"
const val mcauthlibVersion = "d9d773e" const val mcauthlibVersion = "d9d773e"
const val mcprotocollibversion = "bb2b414" const val mcprotocollibversion = "54fc9f0"
const val packetlibVersion = "3.0" const val packetlibVersion = "3.0"
const val adventureVersion = "4.9.3" const val adventureVersion = "4.9.3"
const val eventVersion = "3.0.0" const val eventVersion = "3.0.0"

View file

@ -31,7 +31,7 @@ dependencies {
// Network libraries // Network libraries
implementation("org.java-websocket", "Java-WebSocket", Versions.websocketVersion) implementation("org.java-websocket", "Java-WebSocket", Versions.websocketVersion)
api("com.github.CloudburstMC.Protocol", "bedrock-v527", Versions.protocolVersion) { api("com.github.CloudburstMC.Protocol", "bedrock-v534", Versions.protocolVersion) {
exclude("com.nukkitx.network", "raknet") exclude("com.nukkitx.network", "raknet")
exclude("com.nukkitx", "nbt") exclude("com.nukkitx", "nbt")
} }

View file

@ -47,7 +47,7 @@ public class OffhandCommand extends GeyserCommand {
} }
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO, ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
Direction.DOWN, session.getNextSequence()); Direction.DOWN, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(releaseItemPacket); session.sendDownstreamPacket(releaseItemPacket);
} }

View file

@ -99,21 +99,11 @@ public class FishingHookEntity extends ThrowableEntity {
} }
} }
int waterLevel = BlockStateValues.getWaterLevel(blockID); double waterHeight = BlockStateValues.getWaterHeight(blockID);
if (BlockRegistries.WATERLOGGED.get().contains(blockID)) { if (waterHeight != -1 && position.getY() <= (iter.getY() + waterHeight)) {
waterLevel = 0;
}
if (waterLevel >= 0) {
double waterMaxY = iter.getY() + 1 - (waterLevel + 1) / 9.0;
// Falling water is a full block
if (waterLevel >= 8) {
waterMaxY = iter.getY() + 1;
}
if (position.getY() <= waterMaxY) {
touchingWater = true; touchingWater = true;
} }
} }
}
if (!inWater && touchingWater) { if (!inWater && touchingWater) {
sendSplashSound(session); sendSplashSound(session);

View file

@ -51,6 +51,7 @@ public class ArmorStandEntity extends LivingEntity {
@Getter @Getter
private boolean isMarker = false; private boolean isMarker = false;
private boolean isInvisible = false; private boolean isInvisible = false;
@Getter
private boolean isSmall = false; private boolean isSmall = false;
/** /**
@ -74,6 +75,7 @@ public class ArmorStandEntity extends LivingEntity {
* - No armor, no name: false * - No armor, no name: false
* - No armor, yes name: true * - No armor, yes name: true
*/ */
@Getter
private boolean positionRequiresOffset = false; private boolean positionRequiresOffset = false;
/** /**
* Whether we should update the position of this armor stand after metadata updates. * Whether we should update the position of this armor stand after metadata updates.
@ -411,6 +413,8 @@ public class ArmorStandEntity extends LivingEntity {
this.positionRequiresOffset = newValue; this.positionRequiresOffset = newValue;
if (positionRequiresOffset) { if (positionRequiresOffset) {
this.position = applyOffsetToPosition(position); this.position = applyOffsetToPosition(position);
// Update the passenger offset as armorstand is moving up by roughly 2 blocks
updatePassengerOffsets();
} else { } else {
this.position = removeOffsetFromPosition(position); this.position = removeOffsetFromPosition(position);
} }

View file

@ -45,6 +45,9 @@ public class IronGolemEntity extends GolemEntity {
setFlag(EntityFlag.BRIBED, true); setFlag(EntityFlag.BRIBED, true);
// Required, or else the overlay is black // Required, or else the overlay is black
dirtyMetadata.put(EntityData.COLOR_2, (byte) 0); dirtyMetadata.put(EntityData.COLOR_2, (byte) 0);
// Default max health. Ensures correct cracked texture is used
// Bug reproducible in 1.19.0 JE vanilla/fabric when spawning a new iron golem
maxHealth = 100f;
} }
@Nonnull @Nonnull

View file

@ -41,7 +41,7 @@ import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
public class WardenEntity extends MonsterEntity implements Tickable { public class WardenEntity extends MonsterEntity implements Tickable {
private int heartBeatDelay; private int heartBeatDelay = 40;
private int tickCount; private int tickCount;
private int sonicBoomTickDuration; private int sonicBoomTickDuration;
@ -50,6 +50,12 @@ public class WardenEntity extends MonsterEntity implements Tickable {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
} }
@Override
protected void initializeMetadata() {
super.initializeMetadata();
dirtyMetadata.put(EntityData.HEARTBEAT_INTERVAL_TICKS, heartBeatDelay);
}
@Override @Override
public void setPose(Pose pose) { public void setPose(Pose pose) {
setFlag(EntityFlag.DIGGING, pose == Pose.DIGGING); setFlag(EntityFlag.DIGGING, pose == Pose.DIGGING);

View file

@ -35,9 +35,7 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.AttributeData; import com.nukkitx.protocol.bedrock.data.*;
import com.nukkitx.protocol.bedrock.data.GameType;
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
@ -59,6 +57,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -66,6 +65,16 @@ import java.util.concurrent.TimeUnit;
@Getter @Setter @Getter @Setter
public class PlayerEntity extends LivingEntity { public class PlayerEntity extends LivingEntity {
public static final float SNEAKING_POSE_HEIGHT = 1.5f; public static final float SNEAKING_POSE_HEIGHT = 1.5f;
protected static final List<AbilityLayer> BASE_ABILITY_LAYER;
static {
AbilityLayer abilityLayer = new AbilityLayer();
abilityLayer.setLayerType(AbilityLayer.Type.BASE);
Ability[] abilities = Ability.values();
Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with
Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with
BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer);
}
private String username; private String username;
private boolean playerList = true; // Player is in the player list private boolean playerList = true; // Player is in the player list
@ -127,6 +136,7 @@ public class PlayerEntity extends LivingEntity {
addPlayerPacket.setDeviceId(""); addPlayerPacket.setDeviceId("");
addPlayerPacket.setPlatformChatId(""); addPlayerPacket.setPlatformChatId("");
addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO
addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing
addPlayerPacket.getMetadata().putFlags(flags); addPlayerPacket.getMetadata().putFlags(flags);
dirtyMetadata.apply(addPlayerPacket.getMetadata()); dirtyMetadata.apply(addPlayerPacket.getMetadata());

View file

@ -81,6 +81,7 @@ public class SkullPlayerEntity extends PlayerEntity {
addPlayerPacket.setDeviceId(""); addPlayerPacket.setDeviceId("");
addPlayerPacket.setPlatformChatId(""); addPlayerPacket.setPlatformChatId("");
addPlayerPacket.setGameType(GameType.SURVIVAL); addPlayerPacket.setGameType(GameType.SURVIVAL);
addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER);
addPlayerPacket.getMetadata().putFlags(flags); addPlayerPacket.getMetadata().putFlags(flags);
dirtyMetadata.apply(addPlayerPacket.getMetadata()); dirtyMetadata.apply(addPlayerPacket.getMetadata());

View file

@ -42,16 +42,21 @@ public class StoredItemMappings {
private final ItemMapping banner; private final ItemMapping banner;
private final ItemMapping barrier; private final ItemMapping barrier;
private final int bowl; private final int bowl;
private final int bucket;
private final int chest; private final int chest;
private final ItemMapping compass; private final ItemMapping compass;
private final ItemMapping crossbow; private final ItemMapping crossbow;
private final ItemMapping enchantedBook; private final ItemMapping enchantedBook;
private final ItemMapping fishingRod; private final ItemMapping fishingRod;
private final int flintAndSteel; private final int flintAndSteel;
private final int frogspawn;
private final int goatHorn;
private final int glassBottle;
private final int goldenApple; private final int goldenApple;
private final int goldIngot; private final int goldIngot;
private final int ironIngot; private final int ironIngot;
private final int lead; private final int lead;
private final int lilyPad;
private final ItemMapping milkBucket; private final ItemMapping milkBucket;
private final int nameTag; private final int nameTag;
private final ItemMapping powderSnowBucket; private final ItemMapping powderSnowBucket;
@ -70,16 +75,21 @@ public class StoredItemMappings {
this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID
this.barrier = load(itemMappings, "barrier"); this.barrier = load(itemMappings, "barrier");
this.bowl = load(itemMappings, "bowl").getJavaId(); this.bowl = load(itemMappings, "bowl").getJavaId();
this.bucket = load(itemMappings, "bucket").getBedrockId();
this.chest = load(itemMappings, "chest").getJavaId(); this.chest = load(itemMappings, "chest").getJavaId();
this.compass = load(itemMappings, "compass"); this.compass = load(itemMappings, "compass");
this.crossbow = load(itemMappings, "crossbow"); this.crossbow = load(itemMappings, "crossbow");
this.enchantedBook = load(itemMappings, "enchanted_book"); this.enchantedBook = load(itemMappings, "enchanted_book");
this.fishingRod = load(itemMappings, "fishing_rod"); this.fishingRod = load(itemMappings, "fishing_rod");
this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId(); this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId();
this.frogspawn = load(itemMappings, "frogspawn").getBedrockId();
this.goatHorn = load(itemMappings, "goat_horn").getJavaId();
this.glassBottle = load(itemMappings, "glass_bottle").getBedrockId();
this.goldenApple = load(itemMappings, "golden_apple").getJavaId(); this.goldenApple = load(itemMappings, "golden_apple").getJavaId();
this.goldIngot = load(itemMappings, "gold_ingot").getJavaId(); this.goldIngot = load(itemMappings, "gold_ingot").getJavaId();
this.ironIngot = load(itemMappings, "iron_ingot").getJavaId(); this.ironIngot = load(itemMappings, "iron_ingot").getJavaId();
this.lead = load(itemMappings, "lead").getJavaId(); this.lead = load(itemMappings, "lead").getJavaId();
this.lilyPad = load(itemMappings, "lily_pad").getBedrockId();
this.milkBucket = load(itemMappings, "milk_bucket"); this.milkBucket = load(itemMappings, "milk_bucket");
this.nameTag = load(itemMappings, "name_tag").getJavaId(); this.nameTag = load(itemMappings, "name_tag").getJavaId();
this.powderSnowBucket = load(itemMappings, "powder_snow_bucket"); this.powderSnowBucket = load(itemMappings, "powder_snow_bucket");

View file

@ -44,6 +44,7 @@ import java.util.Locale;
* Used for block entities if the Java block state contains Bedrock block information. * Used for block entities if the Java block state contains Bedrock block information.
*/ */
public final class BlockStateValues { public final class BlockStateValues {
private static final IntSet ALL_CAULDRONS = new IntOpenHashSet();
private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap(); private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap();
private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap(); private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap();
private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap();
@ -76,6 +77,8 @@ public final class BlockStateValues {
public static int JAVA_SPAWNER_ID; public static int JAVA_SPAWNER_ID;
public static int JAVA_WATER_ID; public static int JAVA_WATER_ID;
public static final int NUM_WATER_LEVELS = 9;
/** /**
* Determines if the block state contains Bedrock block information * Determines if the block state contains Bedrock block information
* *
@ -193,6 +196,9 @@ public final class BlockStateValues {
return; return;
} }
if (javaId.contains("cauldron")) {
ALL_CAULDRONS.add(javaBlockState);
}
if (javaId.contains("_cauldron") && !javaId.contains("water_")) { if (javaId.contains("_cauldron") && !javaId.contains("water_")) {
NON_WATER_CAULDRONS.add(javaBlockState); NON_WATER_CAULDRONS.add(javaBlockState);
} }
@ -225,10 +231,19 @@ public final class BlockStateValues {
* *
* @return if this Java block state is a non-empty non-water cauldron * @return if this Java block state is a non-empty non-water cauldron
*/ */
public static boolean isCauldron(int state) { public static boolean isNonWaterCauldron(int state) {
return NON_WATER_CAULDRONS.contains(state); return NON_WATER_CAULDRONS.contains(state);
} }
/**
* When using a bucket on a cauldron sending a ServerboundUseItemPacket can result in the liquid being placed.
*
* @return if this Java block state is a cauldron
*/
public static boolean isCauldron(int state) {
return ALL_CAULDRONS.contains(state);
}
/** /**
* The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags
* in Bedrock need the conditional information. * in Bedrock need the conditional information.
@ -436,7 +451,6 @@ public final class BlockStateValues {
/** /**
* Get the level of water from the block state. * Get the level of water from the block state.
* This is used in FishingHookEntity to create splash sounds when the hook hits the water.
* *
* @param state BlockState of the block * @param state BlockState of the block
* @return The water level or -1 if the block isn't water * @return The water level or -1 if the block isn't water
@ -445,6 +459,30 @@ public final class BlockStateValues {
return WATER_LEVEL.getOrDefault(state, -1); return WATER_LEVEL.getOrDefault(state, -1);
} }
/**
* Get the height of water from the block state
* This is used in FishingHookEntity to create splash sounds when the hook hits the water. In addition,
* CollisionManager uses this to determine if the player's eyes are in water.
*
* @param state BlockState of the block
* @return The water height or -1 if the block does not contain water
*/
public static double getWaterHeight(int state) {
int waterLevel = BlockStateValues.getWaterLevel(state);
if (BlockRegistries.WATERLOGGED.get().contains(state)) {
waterLevel = 0;
}
if (waterLevel >= 0) {
double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_WATER_LEVELS);
// Falling water is a full block
if (waterLevel >= 8) {
waterHeight = 1;
}
return waterHeight;
}
return -1;
}
/** /**
* Get the slipperiness of a block. * Get the slipperiness of a block.
* This is used in ItemEntity to calculate the friction on an item as it slides across the ground * This is used in ItemEntity to calculate the friction on an item as it slides across the ground

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.level.physics; package org.geysermc.geyser.level.physics;
import com.nukkitx.math.GenericMath;
import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3d;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
@ -405,6 +406,18 @@ public class CollisionManager {
return session.getGeyser().getWorldManager().getBlockAt(session, session.getPlayerEntity().getPosition().toInt()) == BlockStateValues.JAVA_WATER_ID; return session.getGeyser().getWorldManager().getBlockAt(session, session.getPlayerEntity().getPosition().toInt()) == BlockStateValues.JAVA_WATER_ID;
} }
public boolean isWaterInEyes() {
double eyeX = playerBoundingBox.getMiddleX();
double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight();
double eyeZ = playerBoundingBox.getMiddleZ();
eyeY -= 1 / ((double) BlockStateValues.NUM_WATER_LEVELS); // Subtract the height of one water layer
int blockID = session.getGeyser().getWorldManager().getBlockAt(session, GenericMath.floor(eyeX), GenericMath.floor(eyeY), GenericMath.floor(eyeZ));
double waterHeight = BlockStateValues.getWaterHeight(blockID);
return waterHeight != -1 && eyeY < (Math.floor(eyeY) + waterHeight);
}
/** /**
* Updates scaffolding entity flags * Updates scaffolding entity flags
* Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side * Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side

View file

@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.codec.MinecraftCodec;
import com.github.steveice10.mc.protocol.codec.PacketCodec; import com.github.steveice10.mc.protocol.codec.PacketCodec;
import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
import com.nukkitx.protocol.bedrock.v534.Bedrock_v534;
import org.geysermc.geyser.session.GeyserSession;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -43,7 +45,7 @@ public final class GameProtocol {
* Default Bedrock codec that should act as a fallback. Should represent the latest available * Default Bedrock codec that should act as a fallback. Should represent the latest available
* release of the game that Geyser supports. * release of the game that Geyser supports.
*/ */
public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v527.V527_CODEC; public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v534.V534_CODEC;
/** /**
* A list of all supported Bedrock versions that can join Geyser * A list of all supported Bedrock versions that can join Geyser
*/ */
@ -56,9 +58,10 @@ public final class GameProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC; private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static { static {
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() SUPPORTED_BEDROCK_CODECS.add(Bedrock_v527.V527_CODEC.toBuilder()
.minecraftVersion("1.19.0") .minecraftVersion("1.19.0/1.19.2")
.build()); .build());
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
} }
/** /**
@ -75,6 +78,12 @@ public final class GameProtocol {
return null; return null;
} }
/* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
public static boolean supports1_19_10(GeyserSession session) {
return session.getUpstream().getProtocolVersion() >= Bedrock_v534.V534_CODEC.getProtocolVersion();
}
/** /**
* Gets the {@link PacketCodec} for Minecraft: Java Edition. * Gets the {@link PacketCodec} for Minecraft: Java Edition.
* *

View file

@ -91,8 +91,10 @@ public class QueryPacketHandler {
switch (type) { switch (type) {
case HANDSHAKE: case HANDSHAKE:
sendToken(); sendToken();
break;
case STATISTICS: case STATISTICS:
sendQueryData(); sendQueryData();
break;
} }
} }

View file

@ -28,6 +28,9 @@ package org.geysermc.geyser.ping;
import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException;
import com.nukkitx.nbt.util.VarInts; import com.nukkitx.nbt.util.VarInts;
import io.netty.handler.codec.haproxy.HAProxyCommand;
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.util.NetUtil;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.GameProtocol;
@ -35,13 +38,12 @@ import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException; import java.net.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable { public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable {
private static final byte[] HAPROXY_BINARY_PREFIX = new byte[]{13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10};
private final GeyserImpl geyser; private final GeyserImpl geyser;
public GeyserLegacyPingPassthrough(GeyserImpl geyser) { public GeyserLegacyPingPassthrough(GeyserImpl geyser) {
@ -74,54 +76,68 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
@Override @Override
public void run() { public void run() {
try { try (Socket socket = new Socket()) {
Socket socket = new Socket();
String address = geyser.getConfig().getRemote().getAddress(); String address = geyser.getConfig().getRemote().getAddress();
int port = geyser.getConfig().getRemote().getPort(); int port = geyser.getConfig().getRemote().getPort();
socket.connect(new InetSocketAddress(address, port), 5000); socket.connect(new InetSocketAddress(address, port), 5000);
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
DataOutputStream handshake = new DataOutputStream(byteArrayStream); try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) {
handshake.write(0x0); handshake.write(0x0);
VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion()); VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
VarInts.writeUnsignedInt(handshake, address.length()); VarInts.writeUnsignedInt(handshake, address.length());
handshake.writeBytes(address); handshake.writeBytes(address);
handshake.writeShort(port); handshake.writeShort(port);
VarInts.writeUnsignedInt(handshake, 1); VarInts.writeUnsignedInt(handshake, 1);
}
byte[] buffer;
try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) {
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
// HAProxy support
// Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78
dataOutputStream.write(HAPROXY_BINARY_PREFIX);
dataOutputStream.writeByte((0x02 << 4) | HAProxyCommand.PROXY.byteValue());
dataOutputStream.writeByte(socket.getLocalAddress() instanceof Inet4Address ?
HAProxyProxiedProtocol.TCP4.byteValue() : HAProxyProxiedProtocol.TCP6.byteValue());
byte[] srcAddrBytes = NetUtil.createByteArrayFromIpAddressString(
((InetSocketAddress) socket.getLocalSocketAddress()).getAddress().getHostAddress());
byte[] dstAddrBytes = NetUtil.createByteArrayFromIpAddressString(address);
dataOutputStream.writeShort(srcAddrBytes.length + dstAddrBytes.length + 4);
dataOutputStream.write(srcAddrBytes);
dataOutputStream.write(dstAddrBytes);
dataOutputStream.writeShort(((InetSocketAddress) socket.getLocalSocketAddress()).getPort());
dataOutputStream.writeShort(port);
}
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size()); VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
dataOutputStream.write(byteArrayStream.toByteArray()); dataOutputStream.write(byteArrayStream.toByteArray());
dataOutputStream.writeByte(0x01); dataOutputStream.writeByte(0x01);
dataOutputStream.writeByte(0x00); dataOutputStream.writeByte(0x00);
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
VarInts.readUnsignedInt(dataInputStream); VarInts.readUnsignedInt(dataInputStream);
VarInts.readUnsignedInt(dataInputStream); VarInts.readUnsignedInt(dataInputStream);
int length = VarInts.readUnsignedInt(dataInputStream); int length = VarInts.readUnsignedInt(dataInputStream);
byte[] buffer = new byte[length]; buffer = new byte[length];
dataInputStream.readFully(buffer); dataInputStream.readFully(buffer);
dataOutputStream.writeByte(0x09); dataOutputStream.writeByte(0x09);
dataOutputStream.writeByte(0x01); dataOutputStream.writeByte(0x01);
dataOutputStream.writeLong(System.currentTimeMillis()); dataOutputStream.writeLong(System.currentTimeMillis());
VarInts.readUnsignedInt(dataInputStream); VarInts.readUnsignedInt(dataInputStream);
String json = new String(buffer); }
}
this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(json, GeyserPingInfo.class); this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class);
byteArrayStream.close();
handshake.close();
dataOutputStream.close();
dataInputStream.close();
socket.close();
} catch (SocketTimeoutException | ConnectException ex) { } catch (SocketTimeoutException | ConnectException ex) {
this.pingInfo = null; this.pingInfo = null;
this.geyser.getLogger().debug("Connection timeout for ping passthrough."); this.geyser.getLogger().debug("Connection timeout for ping passthrough.");
} catch (JsonParseException | JsonMappingException ex) { } catch (JsonParseException | JsonMappingException ex) {
this.geyser.getLogger().error("Failed to parse json when pinging server!", ex); this.geyser.getLogger().error("Failed to parse json when pinging server!", ex);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e);
} }
} }
} }

View file

@ -72,6 +72,16 @@ public class BlockRegistries {
*/ */
public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
/**
* A registry containing all blockstates which are always interactive.
*/
public static final SimpleRegistry<IntSet> INTERACTIVE = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
/**
* A registry containing all blockstates which are interactive if the player has the may build permission.
*/
public static final SimpleRegistry<IntSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
static { static {
BlockRegistryPopulator.populate(); BlockRegistryPopulator.populate();
} }

View file

@ -26,6 +26,7 @@
package org.geysermc.geyser.registry.populator; package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.nukkitx.nbt.*; import com.nukkitx.nbt.*;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
@ -355,6 +356,24 @@ public class BlockRegistryPopulator {
BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0])); BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0]));
BLOCKS_JSON = blocksJson; BLOCKS_JSON = blocksJson;
JsonNode blockInteractionsJson;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("mappings/interactions.json")) {
blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream);
} catch (Exception e) {
throw new AssertionError("Unable to load Java block interaction mappings", e);
}
BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes")));
BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build")));
}
private static IntSet toBlockStateSet(ArrayNode node) {
IntSet blockStateSet = new IntOpenHashSet(node.size());
for (JsonNode javaIdentifier : node) {
blockStateSet.add(BlockRegistries.JAVA_IDENTIFIERS.get().getInt(javaIdentifier.textValue()));
}
return blockStateSet;
} }
private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction<String, NbtMapBuilder, String> statesMapper) { private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction<String, NbtMapBuilder, String> statesMapper) {

View file

@ -235,6 +235,9 @@ public class ItemRegistryPopulator {
} else if (identifier.equals("minecraft:empty_map") && damage == 2) { } else if (identifier.equals("minecraft:empty_map") && damage == 2) {
// Bedrock-only as its own item // Bedrock-only as its own item
continue; continue;
} else if (identifier.equals("minecraft:bordure_indented_banner_pattern") || identifier.equals("minecraft:field_masoned_banner_pattern")) {
// Bedrock-only banner patterns
continue;
} }
StartGamePacket.ItemEntry entry = entries.get(identifier); StartGamePacket.ItemEntry entry = entries.get(identifier);
int id = -1; int id = -1;

View file

@ -136,9 +136,9 @@ public class ItemMappings {
} }
} else { } else {
if (!(mapping.getBedrockData() == data.getDamage() || if (!(mapping.getBedrockData() == data.getDamage() ||
// Make exceptions for potions, tipped arrows, and firework stars, whose damage values can vary // Make exceptions for potions, tipped arrows, firework stars, and goat horns, whose damage values can vary
(mapping.getJavaIdentifier().endsWith("potion") || mapping.getJavaIdentifier().equals("minecraft:arrow") (mapping.getJavaIdentifier().endsWith("potion") || mapping.getJavaIdentifier().equals("minecraft:arrow")
|| mapping.getJavaIdentifier().equals("minecraft:firework_star")))) { || mapping.getJavaIdentifier().equals("minecraft:firework_star") || mapping.getJavaIdentifier().equals("minecraft:goat_horn")))) {
continue; continue;
} }
} }

View file

@ -80,6 +80,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.AccessLevel; import lombok.AccessLevel;
@ -101,6 +103,7 @@ import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.ItemFrameEntity;
@ -113,6 +116,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.LocalSession; import org.geysermc.geyser.network.netty.LocalSession;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.BlockMappings;
@ -386,7 +390,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Whether to work around 1.13's different behavior in villager trading menus. * Whether to work around 1.13's different behavior in villager trading menus.
*/ */
@Setter @Setter
private boolean emulatePost1_14Logic = true; private boolean emulatePost1_13Logic = true;
/** /**
* Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container * Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container
* packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot * packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot
@ -395,6 +399,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
*/ */
@Setter @Setter
private boolean emulatePost1_16Logic = true; private boolean emulatePost1_16Logic = true;
@Setter
private boolean emulatePost1_18Logic = true;
/** /**
* The current attack speed of the player. Used for sending proper cooldown timings. * The current attack speed of the player. Used for sending proper cooldown timings.
@ -428,11 +434,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private long lastInteractionTime; private long lastInteractionTime;
/** /**
* Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to * Stores whether the player intended to place a bucket.
* interact with a block.
*/ */
@Setter @Setter
private ScheduledFuture<?> bucketScheduledFuture; private boolean placedBucket;
/** /**
* Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances.
@ -480,6 +485,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter @Setter
private boolean instabuild = false; private boolean instabuild = false;
@Setter
private float flySpeed;
@Setter
private float walkSpeed;
/** /**
* Caches current rain status. * Caches current rain status.
*/ */
@ -496,7 +506,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Stores a map of all statistics sent from the server. * Stores a map of all statistics sent from the server.
* The server only sends new statistics back to us, so in order to show all statistics we need to cache existing ones. * The server only sends new statistics back to us, so in order to show all statistics we need to cache existing ones.
*/ */
private final Map<Statistic, Integer> statistics = new HashMap<>(); private final Object2IntMap<Statistic> statistics = new Object2IntOpenHashMap<>(0);
/** /**
* Whether we're expecting statistics to be sent back to us. * Whether we're expecting statistics to be sent back to us.
@ -519,6 +529,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
*/ */
private ScheduledFuture<?> tickThread = null; private ScheduledFuture<?> tickThread = null;
/**
* Used to return the player to their original rotation after using an item in BedrockInventoryTransactionTranslator
*/
@Setter
private ScheduledFuture<?> lookBackScheduledFuture = null;
private MinecraftProtocol protocol; private MinecraftProtocol protocol;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) { public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
@ -1265,9 +1281,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
ServerboundUseItemPacket useItemPacket; ServerboundUseItemPacket useItemPacket;
if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) { if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) {
useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, getNextSequence()); useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, worldCache.nextPredictionSequence());
} else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) { } else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) {
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, getNextSequence()); useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, worldCache.nextPredictionSequence());
} else { } else {
// No blocking // No blocking
return false; return false;
@ -1296,7 +1312,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private boolean disableBlocking() { private boolean disableBlocking() {
if (playerEntity.getFlag(EntityFlag.BLOCKING)) { if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
Vector3i.ZERO, Direction.DOWN, getNextSequence()); Vector3i.ZERO, Direction.DOWN, worldCache.nextPredictionSequence());
sendDownstreamPacket(releaseItemPacket); sendDownstreamPacket(releaseItemPacket);
playerEntity.setFlag(EntityFlag.BLOCKING, false); playerEntity.setFlag(EntityFlag.BLOCKING, false);
return true; return true;
@ -1610,23 +1626,83 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
return geyser.getWorldManager().hasPermission(this, permission); return geyser.getWorldManager().hasPermission(this, permission);
} }
private static final Ability[] USED_ABILITIES = Ability.values();
/** /**
* Send an AdventureSettingsPacket to the client with the latest flags * Send an AdventureSettingsPacket to the client with the latest flags
*/ */
public void sendAdventureSettings() { public void sendAdventureSettings() {
AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); long bedrockId = playerEntity.getGeyserId();
adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId());
// Set command permission if OP permission level is high enough // Set command permission if OP permission level is high enough
// This allows mobile players access to a GUI for doing commands. The commands there do not change above OPERATOR // This allows mobile players access to a GUI for doing commands. The commands there do not change above OPERATOR
// and all commands there are accessible with OP permission level 2 // and all commands there are accessible with OP permission level 2
adventureSettingsPacket.setCommandPermission(opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL); CommandPermission commandPermission = opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL;
// Required to make command blocks destroyable // Required to make command blocks destroyable
adventureSettingsPacket.setPlayerPermission(opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER); PlayerPermission playerPermission = opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER;
// Update the noClip and worldImmutable values based on the current gamemode // Update the noClip and worldImmutable values based on the current gamemode
boolean spectator = gameMode == GameMode.SPECTATOR; boolean spectator = gameMode == GameMode.SPECTATOR;
boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator; boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator;
if (GameProtocol.supports1_19_10(this)) {
UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket();
adventureSettingsPacket.setNoMvP(false);
adventureSettingsPacket.setNoPvM(false);
adventureSettingsPacket.setImmutableWorld(worldImmutable);
adventureSettingsPacket.setShowNameTags(false);
adventureSettingsPacket.setAutoJump(true);
sendUpstreamPacket(adventureSettingsPacket);
UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket();
updateAbilitiesPacket.setUniqueEntityId(bedrockId);
updateAbilitiesPacket.setCommandPermission(commandPermission);
updateAbilitiesPacket.setPlayerPermission(playerPermission);
AbilityLayer abilityLayer = new AbilityLayer();
Set<Ability> abilities = abilityLayer.getAbilityValues();
if (canFly || spectator) {
abilities.add(Ability.MAY_FLY);
}
// Default stuff we have to fill in
abilities.add(Ability.BUILD);
abilities.add(Ability.MINE);
// Needed so you can drop items
abilities.add(Ability.DOORS_AND_SWITCHES);
if (gameMode == GameMode.CREATIVE) {
// Needed so the client doesn't attempt to take away items
abilities.add(Ability.INSTABUILD);
}
if (flying || spectator) {
if (spectator && !flying) {
// We're "flying locked" in this gamemode
flying = true;
ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true);
sendDownstreamPacket(abilitiesPacket);
}
abilities.add(Ability.FLYING);
}
if (spectator) {
abilities.add(Ability.NO_CLIP);
}
abilityLayer.setLayerType(AbilityLayer.Type.BASE);
abilityLayer.setFlySpeed(flySpeed);
abilityLayer.setWalkSpeed(walkSpeed);
Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES);
updateAbilitiesPacket.getAbilityLayers().add(abilityLayer);
sendUpstreamPacket(updateAbilitiesPacket);
return;
}
AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket();
adventureSettingsPacket.setUniqueEntityId(bedrockId);
adventureSettingsPacket.setCommandPermission(commandPermission);
adventureSettingsPacket.setPlayerPermission(playerPermission);
Set<AdventureSetting> flags = adventureSettingsPacket.getSettings(); Set<AdventureSetting> flags = adventureSettingsPacket.getSettings();
if (canFly || spectator) { if (canFly || spectator) {
flags.add(AdventureSetting.MAY_FLY); flags.add(AdventureSetting.MAY_FLY);
@ -1676,16 +1752,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendDownstreamPacket(clientSettingsPacket); sendDownstreamPacket(clientSettingsPacket);
} }
public int getNextSequence() {
return 0;
}
/** /**
* Used for updating statistic values since we only get changes from the server * Used for updating statistic values since we only get changes from the server
* *
* @param statistics Updated statistics values * @param statistics Updated statistics values
*/ */
public void updateStatistics(@NonNull Map<Statistic, Integer> statistics) { public void updateStatistics(@Nonnull Object2IntMap<Statistic> statistics) {
if (this.statistics.isEmpty()) { if (this.statistics.isEmpty()) {
// Initialize custom statistics to 0, so that they appear in the form // Initialize custom statistics to 0, so that they appear in the form
for (CustomStatistic customStatistic : CustomStatistic.values()) { for (CustomStatistic customStatistic : CustomStatistic.values()) {
@ -1757,6 +1829,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendUpstreamPacket(packet); sendUpstreamPacket(packet);
} }
public float getEyeHeight() {
return switch (pose) {
case SNEAKING -> 1.27f;
case SWIMMING,
FALL_FLYING, // Elytra
SPIN_ATTACK -> 0.4f; // Trident spin attack
case SLEEPING -> 0.2f;
default -> EntityDefinitions.PLAYER.offset();
};
}
public MinecraftCodecHelper getCodecHelper() { public MinecraftCodecHelper getCodecHelper() {
return (MinecraftCodecHelper) this.downstream.getCodecHelper(); return (MinecraftCodecHelper) this.downstream.getCodecHelper();
} }

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket;
import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists; import it.unimi.dsi.fastutil.ints.IntLists;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMapping;
@ -82,6 +83,15 @@ public class TagCache {
this.requiresIronTool = IntList.of(blockTags.get("minecraft:needs_iron_tool")); this.requiresIronTool = IntList.of(blockTags.get("minecraft:needs_iron_tool"));
this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool")); this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool"));
// Hack btw
GeyserLogger logger = session.getGeyser().getLogger();
int[] convertableToMud = blockTags.get("minecraft:convertable_to_mud");
boolean emulatePost1_18Logic = convertableToMud != null && convertableToMud.length != 0;
session.setEmulatePost1_18Logic(emulatePost1_18Logic);
if (logger.isDebug()) {
logger.debug("Emulating post 1.18 block predication logic for " + session.name() + "? " + emulatePost1_18Logic);
}
Map<String, int[]> itemTags = packet.getTags().get("minecraft:item"); Map<String, int[]> itemTags = packet.getTags().get("minecraft:item");
this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items")); this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items"));
this.fishes = IntList.of(itemTags.get("minecraft:fishes")); this.fishes = IntList.of(itemTags.get("minecraft:fishes"));
@ -91,10 +101,10 @@ public class TagCache {
this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers")); this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers"));
// Hack btw // Hack btw
boolean emulatePost1_14Logic = itemTags.get("minecraft:signs").length > 1; boolean emulatePost1_13Logic = itemTags.get("minecraft:signs").length > 1;
session.setEmulatePost1_14Logic(emulatePost1_14Logic); session.setEmulatePost1_13Logic(emulatePost1_13Logic);
if (session.getGeyser().getLogger().isDebug()) { if (logger.isDebug()) {
session.getGeyser().getLogger().debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic); logger.debug("Emulating post 1.13 villager logic for " + session.name() + "? " + emulatePost1_13Logic);
} }
} }

View file

@ -26,12 +26,18 @@
package org.geysermc.geyser.session.cache; package org.geysermc.geyser.session.cache;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; import com.nukkitx.protocol.bedrock.packet.SetTitlePacket;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession; import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.ChunkUtils;
import java.util.Iterator;
import java.util.Map;
public final class WorldCache { public final class WorldCache {
private final GeyserSession session; private final GeyserSession session;
@ -51,6 +57,9 @@ public final class WorldCache {
private int trueTitleStayTime; private int trueTitleStayTime;
private int trueTitleFadeOutTime; private int trueTitleFadeOutTime;
private int currentSequence;
private final Map<Vector3i, ServerVerifiedState> unverifiedPredictions = new Object2ObjectOpenHashMap<>(1);
public WorldCache(GeyserSession session) { public WorldCache(GeyserSession session) {
this.session = session; this.session = session;
this.scoreboard = new Scoreboard(session); this.scoreboard = new Scoreboard(session);
@ -121,4 +130,75 @@ public final class WorldCache {
forceSyncCorrectTitleTimes(); forceSyncCorrectTitleTimes();
} }
} }
/* Code to support the prediction structure introduced in Java Edition 1.19.0
Blocks can be rolled back if invalid, but this requires some client-side information storage. */
public int nextPredictionSequence() {
return ++currentSequence;
}
/**
* Stores a record of a block at a certain position to rollback in the event it is incorrect.
*/
public void addServerCorrectBlockState(Vector3i position, int blockState) {
if (session.isEmulatePost1_18Logic()) {
// Cheap hack
// On non-Bukkit platforms, ViaVersion will always confirm the sequence before the block is updated,
// meaning we'd send two block updates after (ChunkUtils.updateBlockClientSide in endPredictionsUpTo
// and the packet updating from the client)
this.unverifiedPredictions.compute(position, ($, serverVerifiedState) -> serverVerifiedState == null
? new ServerVerifiedState(currentSequence, blockState) : serverVerifiedState.setData(currentSequence, blockState));
}
}
public void updateServerCorrectBlockState(Vector3i position) {
if (this.unverifiedPredictions.isEmpty()) {
return;
}
this.unverifiedPredictions.remove(position);
}
public void endPredictionsUpTo(int sequence) {
if (this.unverifiedPredictions.isEmpty()) {
return;
}
Iterator<Map.Entry<Vector3i, ServerVerifiedState>> it = this.unverifiedPredictions.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Vector3i, ServerVerifiedState> entry = it.next();
ServerVerifiedState serverVerifiedState = entry.getValue();
if (serverVerifiedState.sequence <= sequence) {
// This block may be out of sync with the server
// In 1.19.0 Java, you can verify this by trying to mine in spawn protection
ChunkUtils.updateBlockClientSide(session, serverVerifiedState.blockState, entry.getKey());
it.remove();
}
}
}
private static class ServerVerifiedState {
private int sequence;
private int blockState;
ServerVerifiedState(int sequence, int blockState) {
this.sequence = sequence;
this.blockState = blockState;
}
ServerVerifiedState setData(int sequence, int blockState) {
this.sequence = sequence;
this.blockState = blockState;
return this;
}
@Override
public String toString() {
return "ServerVerifiedState{" +
"sequence=" + sequence +
", blockState=" + blockState +
'}';
}
}
} }

View file

@ -25,7 +25,7 @@
package org.geysermc.geyser.text; package org.geysermc.geyser.text;
import com.github.steveice10.mc.protocol.data.game.MessageType; import com.github.steveice10.mc.protocol.data.game.BuiltinChatType;
import com.nukkitx.protocol.bedrock.packet.TextPacket; import com.nukkitx.protocol.bedrock.packet.TextPacket;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@ -45,13 +45,13 @@ public record ChatTypeEntry(@Nonnull TextPacket.Type bedrockChatType, @Nullable
// So the proper way to do this, probably, would be to dump the NBT data from vanilla and load it. // So the proper way to do this, probably, would be to dump the NBT data from vanilla and load it.
// But, the only way this happens is if a chat message is sent to us before the login packet, which is rare. // But, the only way this happens is if a chat message is sent to us before the login packet, which is rare.
// So we'll just make sure chat ends up in the right place. // So we'll just make sure chat ends up in the right place.
chatTypes.put(MessageType.CHAT.ordinal(), CHAT); chatTypes.put(BuiltinChatType.CHAT.ordinal(), CHAT);
chatTypes.put(MessageType.SYSTEM.ordinal(), SYSTEM); chatTypes.put(BuiltinChatType.SYSTEM.ordinal(), SYSTEM);
chatTypes.put(MessageType.GAME_INFO.ordinal(), TIP); chatTypes.put(BuiltinChatType.GAME_INFO.ordinal(), TIP);
chatTypes.put(MessageType.SAY_COMMAND.ordinal(), RAW); chatTypes.put(BuiltinChatType.SAY_COMMAND.ordinal(), RAW);
chatTypes.put(MessageType.MSG_COMMAND.ordinal(), RAW); chatTypes.put(BuiltinChatType.MSG_COMMAND.ordinal(), RAW);
chatTypes.put(MessageType.TEAM_MSG_COMMAND.ordinal(), RAW); chatTypes.put(BuiltinChatType.TEAM_MSG_COMMAND.ordinal(), RAW);
chatTypes.put(MessageType.EMOTE_COMMAND.ordinal(), RAW); chatTypes.put(BuiltinChatType.EMOTE_COMMAND.ordinal(), RAW);
chatTypes.put(MessageType.TELLRAW_COMMAND.ordinal(), RAW); chatTypes.put(BuiltinChatType.TELLRAW_COMMAND.ordinal(), RAW);
} }
} }

View file

@ -126,14 +126,20 @@ public class MinecraftLocale {
// Check the locale isn't already loaded // Check the locale isn't already loaded
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) { if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
if (loadLocale(locale)) {
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
} else {
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale)); GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale));
}
return; return;
} }
GeyserImpl.getInstance().getLogger().debug("Downloading and loading locale: " + locale); GeyserImpl.getInstance().getLogger().debug("Downloading and loading locale: " + locale);
downloadLocale(locale); downloadLocale(locale);
loadLocale(locale); if (!loadLocale(locale)) {
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale));
}
} }
/** /**
@ -199,7 +205,7 @@ public class MinecraftLocale {
* *
* @param locale Locale to load * @param locale Locale to load
*/ */
private static void loadLocale(String locale) { private static boolean loadLocale(String locale) {
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile(); File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
// Load the locale // Load the locale
@ -242,8 +248,9 @@ public class MinecraftLocale {
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage())); throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage()));
} }
return true;
} else { } else {
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale)); return false;
} }
} }
@ -300,9 +307,9 @@ public class MinecraftLocale {
* @return Translated string or the original message if it was not found in the given locale * @return Translated string or the original message if it was not found in the given locale
*/ */
public static String getLocaleString(String messageText, String locale) { public static String getLocaleString(String messageText, String locale) {
Map<String, String> localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(locale.toLowerCase()); Map<String, String> localeStrings = LOCALE_MAPPINGS.get(locale.toLowerCase(Locale.ROOT));
if (localeStrings == null) { if (localeStrings == null) {
localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale()); localeStrings = LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale());
if (localeStrings == null) { if (localeStrings == null) {
// Don't cause a NPE if the locale is STILL missing // Don't cause a NPE if the locale is STILL missing
GeyserImpl.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + GeyserLocale.getDefaultLocale()); GeyserImpl.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + GeyserLocale.getDefaultLocale());

View file

@ -119,7 +119,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
} }
private OptionalInt toJava(int effectChoice) { private OptionalInt toJava(int effectChoice) {
return effectChoice == -1 ? OptionalInt.empty() : OptionalInt.of(effectChoice); return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
} }
@Override @Override

View file

@ -60,7 +60,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
static { static {
// Added from left-to-right then up-to-down in the order Java presents it // Added from left-to-right then up-to-down in the order Java presents it
int index = 1; int index = 0;
PATTERN_TO_INDEX.put("bl", index++); PATTERN_TO_INDEX.put("bl", index++);
PATTERN_TO_INDEX.put("br", index++); PATTERN_TO_INDEX.put("br", index++);
PATTERN_TO_INDEX.put("tl", index++); PATTERN_TO_INDEX.put("tl", index++);
@ -119,15 +119,16 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
@Override @Override
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
// If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item // If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item
// Remove the CRAFT_NON_IMPLEMENTED_DEPRECATED when 1.17.30 is dropped return action.getType() == StackRequestActionType.CRAFT_LOOM && inventory.getItem(2).isEmpty();
return (action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_LOOM)
&& inventory.getItem(2).isEmpty();
} }
@Override @Override
public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
StackRequestActionData headerData = request.getActions()[0]; StackRequestActionData headerData = request.getActions()[0];
StackRequestActionData data = request.getActions()[1]; StackRequestActionData data = request.getActions()[1];
if (!(headerData instanceof CraftLoomStackRequestActionData)) {
return rejectRequest(request);
}
if (!(data instanceof CraftResultsDeprecatedStackRequestActionData craftData)) { if (!(data instanceof CraftResultsDeprecatedStackRequestActionData craftData)) {
return rejectRequest(request); return rejectRequest(request);
} }
@ -136,15 +137,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
List<NbtMap> newBlockEntityTag = craftData.getResultItems()[0].getTag().getList("Patterns", NbtType.COMPOUND); List<NbtMap> newBlockEntityTag = craftData.getResultItems()[0].getTag().getList("Patterns", NbtType.COMPOUND);
// Get the pattern that the Bedrock client requests - the last pattern in the Patterns list // Get the pattern that the Bedrock client requests - the last pattern in the Patterns list
NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1); NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1);
String bedrockPattern; String bedrockPattern = ((CraftLoomStackRequestActionData) headerData).getPatternId();
if (headerData instanceof CraftLoomStackRequestActionData loomData) {
// Prioritize this if on 1.17.40
// Remove the below if statement when 1.17.30 is dropped
bedrockPattern = loomData.getPatternId();
} else {
bedrockPattern = pattern.getString("Pattern");
}
// Get the Java index of this pattern // Get the Java index of this pattern
int index = PATTERN_TO_INDEX.getOrDefault(bedrockPattern, -1); int index = PATTERN_TO_INDEX.getOrDefault(bedrockPattern, -1);

View file

@ -155,7 +155,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice); ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice);
session.sendDownstreamPacket(packet); session.sendDownstreamPacket(packet);
if (session.isEmulatePost1_14Logic()) { if (session.isEmulatePost1_13Logic()) {
// 1.18 Java cooperates nicer than older versions // 1.18 Java cooperates nicer than older versions
if (inventory instanceof MerchantContainer merchantInventory) { if (inventory instanceof MerchantContainer merchantInventory) {
merchantInventory.onTradeSelected(session, tradeChoice); merchantInventory.onTradeSelected(session, tradeChoice);

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.inventory.item;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import java.util.Collections;
import java.util.List;
@ItemRemapper
public class GoatHornTranslator extends ItemTranslator {
private static final List<String> INSTRUMENTS = List.of(
"ponder_goat_horn",
"sing_goat_horn",
"seek_goat_horn",
"feel_goat_horn",
"admire_goat_horn",
"call_goat_horn",
"yearn_goat_horn",
"dream_goat_horn" // Called "Resist" on Bedrock 1.19.0 due to https://bugs.mojang.com/browse/MCPE-155059
);
@Override
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
ItemData.Builder builder = super.translateToBedrock(itemStack, mapping, mappings);
if (itemStack.getNbt() != null && itemStack.getNbt().get("instrument") instanceof StringTag instrumentTag) {
String instrument = instrumentTag.getValue();
// Drop the Minecraft namespace if applicable
if (instrument.startsWith("minecraft:")) {
instrument = instrument.substring("minecraft:".length());
}
int damage = INSTRUMENTS.indexOf(instrument);
if (damage == -1) {
damage = 0;
GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument: " + instrumentTag.getValue());
}
builder.damage(damage);
}
return builder;
}
@Override
public ItemStack translateToJava(ItemData itemData, ItemMapping mapping, ItemMappings mappings) {
ItemStack itemStack = super.translateToJava(itemData, mapping, mappings);
int damage = itemData.getDamage();
if (damage < 0 || damage >= INSTRUMENTS.size()) {
GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument for damage: " + damage);
damage = 0;
}
String instrument = INSTRUMENTS.get(damage);
StringTag instrumentTag = new StringTag("instrument", "minecraft:" + instrument);
itemStack.getNbt().put(instrumentTag);
return itemStack;
}
@Override
public List<ItemMapping> getAppliedItems() {
return Collections.singletonList(
Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion())
.getMapping("minecraft:goat_horn")
);
}
}

View file

@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState {
return FlowerPotBlockEntityTranslator.getTag(session, blockState, position); return FlowerPotBlockEntityTranslator.getTag(session, blockState, position);
} else if (PistonBlockEntityTranslator.isBlock(blockState)) { } else if (PistonBlockEntityTranslator.isBlock(blockState)) {
return PistonBlockEntityTranslator.getTag(blockState, position); return PistonBlockEntityTranslator.getTag(blockState, position);
} else if (BlockStateValues.isCauldron(blockState)) { } else if (BlockStateValues.isNonWaterCauldron(blockState)) {
// As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim) // As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim)
return NbtMap.builder() return NbtMap.builder()
.putString("id", "Cauldron") .putString("id", "Cauldron")

View file

@ -33,17 +33,22 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
import com.nukkitx.math.vector.Vector3d;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.inventory.*; import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.PlayerInventory;
@ -53,6 +58,8 @@ import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.BlockUtils;
@ -122,7 +129,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
Vector3i.ZERO, Vector3i.ZERO,
Direction.DOWN, Direction.DOWN,
session.getNextSequence() session.getWorldCache().nextPredictionSequence()
); );
session.sendDownstreamPacket(dropPacket); session.sendDownstreamPacket(dropPacket);
@ -170,6 +177,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.setLastInteractionTime(System.currentTimeMillis()); session.setLastInteractionTime(System.currentTimeMillis());
} }
if (isIncorrectHeldItem(session, packet)) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
// Bedrock sends block interact code for a Java entity so we send entity code back to Java // Bedrock sends block interact code for a Java entity so we send entity code back to Java
if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) { if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) {
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition()); Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
@ -192,18 +204,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
// CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
Vector3f playerPosition = session.getPlayerEntity().getPosition(); Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
// Adjust position for current eye height
switch (session.getPose()) {
case SNEAKING ->
playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 1.27f), 0);
case SWIMMING,
FALL_FLYING, // Elytra
SPIN_ATTACK -> // Trident spin attack
playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.4f), 0);
case SLEEPING ->
playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.2f), 0);
} // else, we don't have to modify the position
boolean creative = session.getGameMode() == GameMode.CREATIVE; boolean creative = session.getGameMode() == GameMode.CREATIVE;
@ -255,9 +256,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
if (blockState == BlockStateValues.JAVA_WATER_ID) { if (blockState == BlockStateValues.JAVA_WATER_ID) {
// Otherwise causes multiple mobs to spawn - just send a use item packet // Otherwise causes multiple mobs to spawn - just send a use item packet
// TODO when we fix mobile bucket rotation, use it for this, too useItem(session, packet, blockState);
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
session.sendDownstreamPacket(itemPacket);
break; break;
} }
} }
@ -268,33 +267,33 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
Hand.MAIN_HAND, Hand.MAIN_HAND,
packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(), packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
false, false,
session.getNextSequence()); session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(blockPacket); session.sendDownstreamPacket(blockPacket);
if (packet.getItemInHand() != null) { if (packet.getItemInHand() != null) {
// Otherwise boats will not be able to be placed in survival and buckets won't work on mobile int itemId = packet.getItemInHand().getId();
if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) {
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
session.sendDownstreamPacket(itemPacket);
} else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) {
// Let the server decide if the bucket item should change, not the client, and revert the changes the client made
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.INVENTORY);
slotPacket.setSlot(packet.getHotbarSlot());
slotPacket.setItem(packet.getItemInHand());
session.sendUpstreamPacket(slotPacket);
// Don't send ServerboundUseItemPacket for powder snow buckets
if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
// Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) { // Otherwise boats will not be able to be placed in survival and buckets, lily pads, frogspawn, and glass bottles won't work on mobile
// Delay the interaction in case the client doesn't intend to actually use the bucket if (session.getItemMappings().getBoatIds().contains(itemId) ||
// See BedrockActionTranslator.java itemId == session.getItemMappings().getStoredItems().lilyPad() ||
session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> { itemId == session.getItemMappings().getStoredItems().frogspawn()) {
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); useItem(session, packet, blockState);
session.sendDownstreamPacket(itemPacket); } else if (itemId == session.getItemMappings().getStoredItems().glassBottle()) {
}, 5, TimeUnit.MILLISECONDS)); if (!session.isSneaking() && BlockStateValues.isCauldron(blockState) && !BlockStateValues.isNonWaterCauldron(blockState)) {
// ServerboundUseItemPacket is not sent for water cauldrons and glass bottles
return;
} }
useItem(session, packet, blockState);
} else if (session.getItemMappings().getBucketIds().contains(itemId)) {
// Don't send ServerboundUseItemPacket for powder snow buckets
if (itemId != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
if (!session.isSneaking() && BlockStateValues.isCauldron(blockState)) {
// ServerboundUseItemPacket is not sent for cauldrons and buckets
return;
}
session.setPlacedBucket(useItem(session, packet, blockState));
} else {
session.setPlacedBucket(true);
} }
} }
} }
@ -320,6 +319,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.setInteracting(true); session.setInteracting(true);
} }
case 1 -> { case 1 -> {
if (isIncorrectHeldItem(session, packet)) {
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
break;
}
// Handled when sneaking // Handled when sneaking
if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) { if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
break; break;
@ -334,10 +338,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
} else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) { } else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
// Handled in case 0 // Handled in case 0
break; break;
} else if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) {
// Handled in case 0
break;
} }
} }
ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(useItemPacket); session.sendDownstreamPacket(useItemPacket);
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots(); List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
@ -402,12 +409,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
return; return;
} }
int sequence = session.getWorldCache().nextPredictionSequence();
if (blockState != -1) {
session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(), blockState);
} else {
blockState = BlockStateValues.JAVA_AIR_ID;
// Client will desync here anyway
session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(),
session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()));
}
LevelEventPacket blockBreakPacket = new LevelEventPacket(); LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState)); blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket); session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID); session.setBreakingBlock(-1);
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition()); Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
if (itemFrameEntity != null) { if (itemFrameEntity != null) {
@ -418,7 +435,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
} }
PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING; PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], session.getNextSequence()); ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], sequence);
session.sendDownstreamPacket(breakPacket); session.sendDownstreamPacket(breakPacket);
} }
} }
@ -427,7 +444,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
if (packet.getActionType() == 0) { if (packet.getActionType() == 0) {
// Followed to the Minecraft Protocol specification outlined at wiki.vg // Followed to the Minecraft Protocol specification outlined at wiki.vg
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO, ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
Direction.DOWN, session.getNextSequence()); Direction.DOWN, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(releaseItemPacket); session.sendDownstreamPacket(releaseItemPacket);
} }
break; break;
@ -520,10 +537,117 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendUpstreamPacket(updateWaterPacket); session.sendUpstreamPacket(updateWaterPacket);
// Reset the item in hand to prevent "missing" blocks // Reset the item in hand to prevent "missing" blocks
InventorySlotPacket slotPacket = new InventorySlotPacket(); InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
slotPacket.setContainerId(ContainerId.INVENTORY); }
slotPacket.setSlot(packet.getHotbarSlot());
slotPacket.setItem(packet.getItemInHand()); private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
session.sendUpstreamPacket(slotPacket); int javaSlot = session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot());
int expectedItemId = ItemTranslator.getBedrockItemMapping(session, session.getPlayerInventory().getItem(javaSlot)).getBedrockId();
int heldItemId = packet.getItemInHand() == null ? ItemData.AIR.getId() : packet.getItemInHand().getId();
if (expectedItemId != heldItemId) {
session.getGeyser().getLogger().debug(session.name() + "'s held item has desynced! Expected: " + expectedItemId + " Received: " + heldItemId);
session.getGeyser().getLogger().debug("Packet: " + packet);
return true;
}
return false;
}
private boolean useItem(GeyserSession session, InventoryTransactionPacket packet, int blockState) {
// Update the player's inventory to remove any items added by the client itself
Inventory playerInventory = session.getPlayerInventory();
int heldItemSlot = playerInventory.getOffsetForHotbar(packet.getHotbarSlot());
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, heldItemSlot);
if (playerInventory.getItem(heldItemSlot).getAmount() > 1) {
if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().bucket() ||
packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) {
// Using a stack of buckets or glass bottles will result in an item being added to the first empty slot.
// We need to revert the item in case the interaction fails. The order goes from left to right in the
// hotbar. Then left to right and top to bottom in the inventory.
for (int i = 0; i < 36; i++) {
int slot = i;
if (i < 9) {
slot = playerInventory.getOffsetForHotbar(slot);
}
if (playerInventory.getItem(slot).isEmpty()) {
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, slot);
break;
}
}
}
}
// Check if the player is interacting with a block
if (!session.isSneaking()) {
if (BlockRegistries.INTERACTIVE.get().contains(blockState)) {
return false;
}
boolean mayBuild = session.getGameMode() == GameMode.SURVIVAL || session.getGameMode() == GameMode.CREATIVE;
if (mayBuild && BlockRegistries.INTERACTIVE_MAY_BUILD.get().contains(blockState)) {
return false;
}
}
Vector3f target = packet.getBlockPosition().toFloat().add(packet.getClickPosition());
lookAt(session, target);
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(itemPacket);
return true;
}
/**
* Determine the rotation necessary to activate this transaction.
*
* The position between the intended click position and the player can be determined with two triangles.
* First, we compute the difference of the X and Z coordinates:
*
* Player position (0, 0)
* |
* |
* |
* |_____________ Intended target (-3, 2)
*
* We then use the Pythagorean Theorem to find the direct line (hypotenuse) on the XZ plane. Finding the angle of the
* triangle from there, closest to the player, gives us our yaw rotation value
* Then doing the same using the new XZ distance and Y difference, we can find the direct line of sight from the
* player to the intended target, and the pitch rotation value. We can then send the necessary packets to update
* the player's rotation.
*
* @param session the Geyser Session
* @param target the position to look at
*/
private void lookAt(GeyserSession session, Vector3f target) {
// Use the bounding box's position since we need the player's position seen by the Java server
Vector3d playerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
float xDiff = (float) (target.getX() - playerPosition.getX());
float yDiff = (float) (target.getY() - (playerPosition.getY() + session.getEyeHeight()));
float zDiff = (float) (target.getZ() - playerPosition.getZ());
// First triangle on the XZ plane
float yaw = (float) -Math.toDegrees(Math.atan2(xDiff, zDiff));
// Second triangle on the Y axis using the hypotenuse of the first triangle as a side
double xzHypot = Math.sqrt(xDiff * xDiff + zDiff * zDiff);
float pitch = (float) -Math.toDegrees(Math.atan2(yDiff, xzHypot));
SessionPlayerEntity entity = session.getPlayerEntity();
ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch());
// This matches Java edition behavior
ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch);
session.sendDownstreamPacket(movementPacket);
if (session.getLookBackScheduledFuture() != null) {
session.getLookBackScheduledFuture().cancel(false);
}
if (Math.abs(entity.getYaw() - yaw) > 1f || Math.abs(entity.getPitch() - pitch) > 1f) {
session.setLookBackScheduledFuture(session.scheduleInEventLoop(() -> {
Vector3d newPlayerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
if (!newPlayerPosition.equals(playerPosition) || entity.getYaw() != returnPacket.getYaw() || entity.getPitch() != returnPacket.getPitch()) {
// The player moved/rotated so there is no need to change their rotation back
return;
}
session.sendDownstreamPacket(returnPacket);
}, 150, TimeUnit.MILLISECONDS));
}
} }
} }

View file

@ -57,7 +57,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
Hand.MAIN_HAND, Hand.MAIN_HAND,
0, 0, 0, // Java doesn't care about these when dealing with a lectern 0, 0, 0, // Java doesn't care about these when dealing with a lectern
false, false,
session.getNextSequence()); session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(blockPacket); session.sendDownstreamPacket(blockPacket);
} else { } else {
// Bedrock wants to either move a page or exit // Bedrock wants to either move a page or exit

View file

@ -65,7 +65,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
// Activate shield since we are already sneaking // Activate shield since we are already sneaking
// (No need to send a release item packet - Java doesn't do this when swapping items) // (No need to send a release item packet - Java doesn't do this when swapping items)
// Required to do it a tick later or else it doesn't register // Required to do it a tick later or else it doesn't register
session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence())), session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence())),
50, TimeUnit.MILLISECONDS); 50, TimeUnit.MILLISECONDS);
} }

View file

@ -41,7 +41,6 @@ import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -129,21 +128,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
break; break;
case DROP_ITEM: case DROP_ITEM:
ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM,
vector, Direction.VALUES[packet.getFace()], session.getNextSequence()); vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(dropItemPacket); session.sendDownstreamPacket(dropItemPacket);
break; break;
case STOP_SLEEP: case STOP_SLEEP:
ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED); ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
session.sendDownstreamPacket(stopSleepingPacket); session.sendDownstreamPacket(stopSleepingPacket);
break; break;
case BLOCK_INTERACT:
// Client means to interact with a block; cancel bucket interaction, if any
if (session.getBucketScheduledFuture() != null) {
session.getBucketScheduledFuture().cancel(true);
session.setBucketScheduledFuture(null);
}
// Otherwise handled in BedrockInventoryTransactionTranslator
break;
case START_BREAK: case START_BREAK:
// Start the block breaking animation // Start the block breaking animation
if (session.getGameMode() != GameMode.CREATIVE) { if (session.getGameMode() != GameMode.CREATIVE) {
@ -163,7 +154,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp); String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp);
if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) { if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos, ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos,
Direction.VALUES[packet.getFace()], session.getNextSequence()); Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(startBreakingPacket); session.sendDownstreamPacket(startBreakingPacket);
if (session.getGameMode() == GameMode.CREATIVE) { if (session.getGameMode() == GameMode.CREATIVE) {
break; break;
@ -171,17 +162,22 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
} }
ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING,
vector, Direction.VALUES[packet.getFace()], session.getNextSequence()); vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(startBreakingPacket); session.sendDownstreamPacket(startBreakingPacket);
break; break;
case CONTINUE_BREAK: case CONTINUE_BREAK:
if (session.getGameMode() == GameMode.CREATIVE) { if (session.getGameMode() == GameMode.CREATIVE) {
break; break;
} }
int breakingBlock = session.getBreakingBlock();
if (breakingBlock == -1) {
break;
}
Vector3f vectorFloat = vector.toFloat(); Vector3f vectorFloat = vector.toFloat();
LevelEventPacket continueBreakPacket = new LevelEventPacket(); LevelEventPacket continueBreakPacket = new LevelEventPacket();
continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK); continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK);
continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(session.getBreakingBlock())) | (packet.getFace() << 24)); continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(breakingBlock)) | (packet.getFace() << 24));
continueBreakPacket.setPosition(vectorFloat); continueBreakPacket.setPosition(vectorFloat);
session.sendUpstreamPacket(continueBreakPacket); session.sendUpstreamPacket(continueBreakPacket);
@ -189,7 +185,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
LevelEventPacket updateBreak = new LevelEventPacket(); LevelEventPacket updateBreak = new LevelEventPacket();
updateBreak.setType(LevelEventType.BLOCK_UPDATE_BREAK); updateBreak.setType(LevelEventType.BLOCK_UPDATE_BREAK);
updateBreak.setPosition(vectorFloat); updateBreak.setPosition(vectorFloat);
double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(session.getBreakingBlock())) * 20; double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(breakingBlock)) * 20;
updateBreak.setData((int) (65535 / breakTime)); updateBreak.setData((int) (65535 / breakTime));
session.sendUpstreamPacket(updateBreak); session.sendUpstreamPacket(updateBreak);
break; break;
@ -206,13 +202,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
} }
} }
ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getNextSequence()); ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(abortBreakingPacket); session.sendDownstreamPacket(abortBreakingPacket);
LevelEventPacket stopBreak = new LevelEventPacket(); LevelEventPacket stopBreak = new LevelEventPacket();
stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK); stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
stopBreak.setPosition(vector.toFloat()); stopBreak.setPosition(vector.toFloat());
stopBreak.setData(0); stopBreak.setData(0);
session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID); session.setBreakingBlock(-1);
session.sendUpstreamPacket(stopBreak); session.sendUpstreamPacket(stopBreak);
break; break;
case STOP_BREAK: case STOP_BREAK:

View file

@ -44,7 +44,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) { if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) {
// Activate the workaround - we should trigger the offhand now // Activate the workaround - we should trigger the offhand now
ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO, ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
Direction.DOWN, session.getNextSequence()); Direction.DOWN, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(swapHandsPacket); session.sendDownstreamPacket(swapHandsPacket);
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) { if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {

View file

@ -77,6 +77,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
boolean positionChanged = !entity.getPosition().equals(packet.getPosition()); boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw; boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw;
if (session.getLookBackScheduledFuture() != null) {
// Resend the rotation if it was changed by Geyser
rotationChanged |= !session.getLookBackScheduledFuture().isDone();
session.getLookBackScheduledFuture().cancel(false);
session.setLookBackScheduledFuture(null);
}
// If only the pitch and yaw changed // If only the pitch and yaw changed
// This isn't needed, but it makes the packets closer to vanilla // This isn't needed, but it makes the packets closer to vanilla
// It also means you can't "lag back" while only looking, in theory // It also means you can't "lag back" while only looking, in theory

View file

@ -50,6 +50,8 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<ClientboundL
if (disconnectReason instanceof TranslatableComponent component) { if (disconnectReason instanceof TranslatableComponent component) {
String key = component.key(); String key = component.key();
isOutdatedMessage = "multiplayer.disconnect.incompatible".equals(key) || isOutdatedMessage = "multiplayer.disconnect.incompatible".equals(key) ||
// Seen with Velocity 1.18 rejecting a 1.19 client
"multiplayer.disconnect.outdated_client".equals(key) ||
// Legacy string (starting from at least 1.15.2) // Legacy string (starting from at least 1.15.2)
"multiplayer.disconnect.outdated_server".equals(key) "multiplayer.disconnect.outdated_server".equals(key)
// Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client // Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client

View file

@ -25,7 +25,7 @@
package org.geysermc.geyser.translator.protocol.java; package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.data.game.MessageType; import com.github.steveice10.mc.protocol.data.game.BuiltinChatType;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@ -82,14 +82,15 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
textDecoration = new TextDecoration(decorationTag); textDecoration = new TextDecoration(decorationTag);
} }
} }
MessageType type = MessageType.from(((StringTag) tag.get("name")).getValue()); BuiltinChatType type = BuiltinChatType.from(((StringTag) tag.get("name")).getValue());
// TODO new types? // TODO new types?
TextPacket.Type bedrockType = switch (type) { // The built-in type can be null if custom plugins/mods add in new types
TextPacket.Type bedrockType = type != null ? switch (type) {
case CHAT -> TextPacket.Type.CHAT; case CHAT -> TextPacket.Type.CHAT;
case SYSTEM -> TextPacket.Type.SYSTEM; case SYSTEM -> TextPacket.Type.SYSTEM;
case GAME_INFO -> TextPacket.Type.TIP; case GAME_INFO -> TextPacket.Type.TIP;
default -> TextPacket.Type.RAW; default -> TextPacket.Type.RAW;
}; } : TextPacket.Type.RAW;
chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration)); chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration));
} }

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket;
import com.nukkitx.protocol.bedrock.packet.TextPacket; import com.nukkitx.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatTypeEntry;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.translator.text.MessageTranslator;
@ -37,11 +38,15 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
@Override @Override
public void translate(GeyserSession session, ClientboundSystemChatPacket packet) { public void translate(GeyserSession session, ClientboundSystemChatPacket packet) {
ChatTypeEntry chatTypeEntry = session.getChatTypes().get(packet.getTypeId());
// This probably isn't proper but system chat won't care about the registry in 1.19.1 anyway
TextPacket.Type chatType = chatTypeEntry == null ? TextPacket.Type.RAW : chatTypeEntry.bedrockChatType();
TextPacket textPacket = new TextPacket(); TextPacket textPacket = new TextPacket();
textPacket.setPlatformChatId(""); textPacket.setPlatformChatId("");
textPacket.setSourceName(""); textPacket.setSourceName("");
textPacket.setXuid(session.getAuthData().xuid()); textPacket.setXuid(session.getAuthData().xuid());
textPacket.setType(session.getChatTypes().get(packet.getTypeId()).bedrockChatType()); textPacket.setType(chatType);
textPacket.setNeedsTranslation(false); textPacket.setNeedsTranslation(false);
textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale())); textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale()));

View file

@ -147,6 +147,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
soundPacket.setRelativeVolumeDisabled(false); soundPacket.setRelativeVolumeDisabled(false);
session.sendUpstreamPacket(soundPacket); session.sendUpstreamPacket(soundPacket);
return; return;
case VILLAGER_MATE:
case ANIMAL_EMIT_HEARTS: case ANIMAL_EMIT_HEARTS:
entityEventPacket.setType(EntityEventType.LOVE_PARTICLES); entityEventPacket.setType(EntityEventType.LOVE_PARTICLES);
break; break;
@ -176,6 +177,18 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
case IRON_GOLEM_HOLD_POPPY: case IRON_GOLEM_HOLD_POPPY:
entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_OFFER); entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_OFFER);
break; break;
case VILLAGER_ANGRY:
entityEventPacket.setType(EntityEventType.VILLAGER_ANGRY);
break;
case VILLAGER_HAPPY:
entityEventPacket.setType(EntityEventType.VILLAGER_HAPPY);
break;
case VILLAGER_SWEAT:
LevelEventPacket levelEventPacket = new LevelEventPacket();
levelEventPacket.setType(LevelEventType.PARTICLE_SPLASH);
levelEventPacket.setPosition(entity.getPosition().up(entity.getDefinition().height()));
session.sendUpstreamPacket(levelEventPacket);
return;
case IRON_GOLEM_EMPTY_HAND: case IRON_GOLEM_EMPTY_HAND:
entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW); entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW);
break; break;

View file

@ -35,6 +35,6 @@ public class JavaBlockChangedAckTranslator extends PacketTranslator<ClientboundB
@Override @Override
public void translate(GeyserSession session, ClientboundBlockChangedAckPacket packet) { public void translate(GeyserSession session, ClientboundBlockChangedAckPacket packet) {
// TODO session.getWorldCache().endPredictionsUpTo(packet.getSequence());
} }
} }

View file

@ -38,6 +38,8 @@ public class JavaPlayerAbilitiesTranslator extends PacketTranslator<ClientboundP
session.setCanFly(packet.isCanFly()); session.setCanFly(packet.isCanFly());
session.setFlying(packet.isFlying()); session.setFlying(packet.isFlying());
session.setInstabuild(packet.isCreative()); session.setInstabuild(packet.isCreative());
session.setFlySpeed(packet.getFlySpeed());
session.setWalkSpeed(packet.getWalkSpeed());
session.sendAdventureSettings(); session.sendAdventureSettings();
} }
} }

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.entity.player;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerCombatKillPacket;
import com.nukkitx.protocol.bedrock.packet.DeathInfoPacket;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@Translator(packet = ClientboundPlayerCombatKillPacket.class)
public class JavaPlayerCombatKillTranslator extends PacketTranslator<ClientboundPlayerCombatKillPacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerCombatKillPacket packet) {
if (packet.getPlayerId() == session.getPlayerEntity().getEntityId() && GameProtocol.supports1_19_10(session)) {
Component deathMessage = packet.getMessage();
// TODO - could inject score in, but as of 1.19.10 newlines don't center and start at the left of the first text
DeathInfoPacket deathInfoPacket = new DeathInfoPacket();
deathInfoPacket.setCauseAttackName(MessageTranslator.convertMessage(deathMessage, session.locale()));
session.sendUpstreamPacket(deathInfoPacket);
}
}
}

View file

@ -31,6 +31,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.Serverb
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import com.nukkitx.protocol.bedrock.packet.RespawnPacket; import com.nukkitx.protocol.bedrock.packet.RespawnPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
@ -84,6 +85,15 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId()); acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId());
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
// See DimensionUtils for an explanation
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(session.getServerRenderDistance());
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
session.setLastChunkPosition(null);
}
ChunkUtils.updateChunkPosition(session, pos.toInt()); ChunkUtils.updateChunkPosition(session, pos.toInt());
if (session.getGeyser().getConfig().isDebugMode()) { if (session.getGeyser().getConfig().isDebugMode()) {

View file

@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
@ -66,27 +67,41 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
if (inventory == null) if (inventory == null)
return; return;
// Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
InventoryTranslator translator = session.getInventoryTranslator(); InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) { if (translator != null) {
if (session.getCraftingGridFuture() != null) { if (session.getCraftingGridFuture() != null) {
session.getCraftingGridFuture().cancel(false); session.getCraftingGridFuture().cancel(false);
} }
updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator);
int slot = packet.getSlot();
if (slot >= inventory.getSize()) {
GeyserImpl geyser = session.getGeyser();
geyser.getLogger().warning("ClientboundContainerSetSlotPacket sent to " + session.name()
+ " that exceeds inventory size!");
if (geyser.getConfig().isDebugMode()) {
geyser.getLogger().debug(packet);
geyser.getLogger().debug(inventory);
}
// 1.19.0 behavior: the state ID will not be set due to exception
return;
}
updateCraftingGrid(session, slot, packet.getItem(), inventory, translator);
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
// In rare cases, the window ID can still be 0 but Java treats it as valid // In rare cases, the window ID can still be 0 but Java treats it as valid
session.getPlayerInventory().setItem(packet.getSlot(), newItem, session); session.getPlayerInventory().setItem(slot, newItem, session);
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), packet.getSlot()); InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot);
} else { } else {
inventory.setItem(packet.getSlot(), newItem, session); inventory.setItem(slot, newItem, session);
translator.updateSlot(session, inventory, packet.getSlot()); translator.updateSlot(session, inventory, slot);
} }
// Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
} }
} }

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.level;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundCooldownPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerStartItemCooldownPacket;
import org.geysermc.geyser.inventory.item.StoredItemMappings;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@Translator(packet = ClientboundCooldownPacket.class)
public class JavaCooldownTranslator extends PacketTranslator<ClientboundCooldownPacket> {
@Override
public void translate(GeyserSession session, ClientboundCooldownPacket packet) {
StoredItemMappings itemMappings = session.getItemMappings().getStoredItems();
int itemId = packet.getItemId();
// Not every item, as of 1.19, appears to be server-driven. Just these two.
// Use a map here if it gets too big.
String cooldownCategory;
if (itemId == itemMappings.goatHorn()) {
cooldownCategory = "goat_horn";
} else if (itemId == itemMappings.shield().getJavaId()) {
cooldownCategory = "shield";
} else {
cooldownCategory = null;
}
if (cooldownCategory != null) {
PlayerStartItemCooldownPacket bedrockPacket = new PlayerStartItemCooldownPacket();
bedrockPacket.setItemCategory(cooldownCategory);
bedrockPacket.setCooldownDuration(packet.getCooldownTicks());
session.sendUpstreamPacket(bedrockPacket);
}
}
}

View file

@ -142,7 +142,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
} }
// Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)), Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
javaId javaId
@ -183,7 +183,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
} }
// Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock // Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockOnlyBlockEntityIds.set(i); bedrockOnlyBlockEntityIds.set(i);
} }
} }

View file

@ -38,7 +38,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
@Override @Override
public void translate(GeyserSession session, Vector3f position, String identifier) { public void translate(GeyserSession session, Vector3f position, String identifier) {
if (session.getBucketScheduledFuture() == null) { if (!session.isPlacedBucket()) {
return; // No bucket was really interacted with return; // No bucket was really interacted with
} }
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
@ -71,6 +71,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
case "minecraft:salmon_bucket": case "minecraft:salmon_bucket":
case "minecraft:pufferfish_bucket": case "minecraft:pufferfish_bucket":
case "minecraft:tropical_fish_bucket": case "minecraft:tropical_fish_bucket":
case "minecraft:tadpole_bucket":
soundEvent = SoundEvent.BUCKET_EMPTY_FISH; soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
break; break;
case "minecraft:water_bucket": case "minecraft:water_bucket":
@ -83,7 +84,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
if (soundEvent != null) { if (soundEvent != null) {
soundEventPacket.setSound(soundEvent); soundEventPacket.setSound(soundEvent);
session.sendUpstreamPacket(soundEventPacket); session.sendUpstreamPacket(soundEventPacket);
session.setBucketScheduledFuture(null); session.setPlacedBucket(false);
} }
} }
} }

View file

@ -36,6 +36,8 @@ import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.collision.BlockCollision; import org.geysermc.geyser.translator.collision.BlockCollision;
import javax.annotation.Nullable;
public final class BlockUtils { public final class BlockUtils {
private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) { private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) {
@ -101,7 +103,7 @@ public final class BlockUtils {
// https://minecraft.gamepedia.com/Breaking // https://minecraft.gamepedia.com/Breaking
private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock, private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock,
String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel, String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel,
boolean insideOfWaterWithoutAquaAffinity, boolean outOfWaterButNotOnGround, boolean insideWaterAndNotOnGround) { boolean insideOfWaterWithoutAquaAffinity, boolean onGround) {
double baseTime = (((correctTool && canTierMineBlock) || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness; double baseTime = (((correctTool && canTierMineBlock) || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness;
double speed = 1.0 / baseTime; double speed = 1.0 / baseTime;
@ -129,12 +131,11 @@ public final class BlockUtils {
} }
if (insideOfWaterWithoutAquaAffinity) speed *= 0.2; if (insideOfWaterWithoutAquaAffinity) speed *= 0.2;
if (outOfWaterButNotOnGround) speed *= 0.2; if (!onGround) speed *= 0.2;
if (insideWaterAndNotOnGround) speed *= 0.2;
return 1.0 / speed; return 1.0 / speed;
} }
public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, CompoundTag nbtData, boolean isSessionPlayer) { public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, @Nullable CompoundTag nbtData, boolean isSessionPlayer) {
boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice
boolean canHarvestWithHand = blockMapping.isCanBreakWithHand(); boolean canHarvestWithHand = blockMapping.isCanBreakWithHand();
String toolType = ""; String toolType = "";
@ -154,36 +155,28 @@ public final class BlockUtils {
if (!isSessionPlayer) { if (!isSessionPlayer) {
// Another entity is currently mining; we have all the information we know // Another entity is currently mining; we have all the information we know
return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective, return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, true);
false, false);
} }
hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower()); hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower());
miningFatigueLevel = session.getEffectCache().getMiningFatigue(); miningFatigueLevel = session.getEffectCache().getMiningFatigue();
boolean isInWater = session.getCollisionManager().isPlayerInWater(); boolean waterInEyes = session.getCollisionManager().isWaterInEyes();
boolean insideOfWaterWithoutAquaAffinity = waterInEyes &&
boolean insideOfWaterWithoutAquaAffinity = isInWater &&
ItemUtils.getEnchantmentLevel(session.getPlayerInventory().getItem(5).getNbt(), "minecraft:aqua_affinity") < 1; ItemUtils.getEnchantmentLevel(session.getPlayerInventory().getItem(5).getNbt(), "minecraft:aqua_affinity") < 1;
boolean outOfWaterButNotOnGround = (!isInWater) && (!session.getPlayerEntity().isOnGround());
boolean insideWaterNotOnGround = isInWater && !session.getPlayerEntity().isOnGround();
return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective, return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, session.getPlayerEntity().isOnGround());
outOfWaterButNotOnGround, insideWaterNotOnGround);
} }
public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) { public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) {
PlayerInventory inventory = session.getPlayerInventory(); PlayerInventory inventory = session.getPlayerInventory();
GeyserItemStack item = inventory.getItemInHand(); GeyserItemStack item = inventory.getItemInHand();
ItemMapping mapping; ItemMapping mapping = ItemMapping.AIR;
CompoundTag nbtData; CompoundTag nbtData = null;
if (item != null) { if (item != null) {
mapping = item.getMapping(session); mapping = item.getMapping(session);
nbtData = item.getNbt(); nbtData = item.getNbt();
} else {
mapping = ItemMapping.AIR;
nbtData = new CompoundTag("");
} }
return getBreakTime(session, blockMapping, mapping, nbtData, true); return getBreakTime(session, blockMapping, mapping, nbtData, true);
} }

View file

@ -123,13 +123,21 @@ public class ChunkUtils {
* @param position the position of the block * @param position the position of the block
*/ */
public static void updateBlock(GeyserSession session, int blockState, Vector3i position) { public static void updateBlock(GeyserSession session, int blockState, Vector3i position) {
updateBlockClientSide(session, blockState, position);
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
session.getWorldCache().updateServerCorrectBlockState(position);
}
/**
* Updates a block, but client-side only.
*/
public static void updateBlockClientSide(GeyserSession session, int blockState, Vector3i position) {
// Checks for item frames so they aren't tripped up and removed // Checks for item frames so they aren't tripped up and removed
ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position); ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position);
if (itemFrameEntity != null) { if (itemFrameEntity != null) {
if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it
itemFrameEntity.updateBlock(true); itemFrameEntity.updateBlock(true);
// Still update the chunk cache with the new block // Still update the chunk cache with the new block if updateBlock is called
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
return; return;
} }
// Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now // Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now
@ -175,7 +183,6 @@ public class ChunkUtils {
break; //No block will be a part of two classes break; //No block will be a part of two classes
} }
} }
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
} }
public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) { public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) {

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.util;
import com.github.steveice10.mc.protocol.data.game.entity.Effect; import com.github.steveice10.mc.protocol.data.game.entity.Effect;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket; import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket;
import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import com.nukkitx.protocol.bedrock.packet.MobEffectPacket; import com.nukkitx.protocol.bedrock.packet.MobEffectPacket;
import com.nukkitx.protocol.bedrock.packet.StopSoundPacket; import com.nukkitx.protocol.bedrock.packet.StopSoundPacket;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
@ -69,6 +70,22 @@ public class DimensionUtils {
session.getPistonCache().clear(); session.getPistonCache().clear();
session.getSkullCache().clear(); session.getSkullCache().clear();
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
// The server-sided view distance wasn't a thing until Minecraft Java 1.14
// So ViaVersion compensates by sending a "view distance" of 64
// That's fine, except when the actual view distance sent from the server is five chunks
// The client locks up when switching dimensions, expecting more chunks than it's getting
// To solve this, we cap at 32 unless we know that the render distance actually exceeds 32
// 47 is the Bedrock equivalent of 32
// Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large
session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of "
+ session.getServerRenderDistance());
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(47);
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
// Will be re-adjusted on spawn
}
Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0); Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0);
ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket(); ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket();

View file

@ -187,6 +187,15 @@ public final class EntityUtils {
case MINECART, HOPPER_MINECART, TNT_MINECART, CHEST_MINECART, FURNACE_MINECART, SPAWNER_MINECART, case MINECART, HOPPER_MINECART, TNT_MINECART, CHEST_MINECART, FURNACE_MINECART, SPAWNER_MINECART,
COMMAND_BLOCK_MINECART, BOAT, CHEST_BOAT -> yOffset -= mount.getDefinition().height() * 0.5f; COMMAND_BLOCK_MINECART, BOAT, CHEST_BOAT -> yOffset -= mount.getDefinition().height() * 0.5f;
} }
if (passenger.getDefinition().entityType() == EntityType.FALLING_BLOCK) {
yOffset += 0.5f;
}
if (mount.getDefinition().entityType() == EntityType.ARMOR_STAND) {
ArmorStandEntity armorStand = (ArmorStandEntity) mount;
if (armorStand.isPositionRequiresOffset()) {
yOffset -= EntityDefinitions.ARMOR_STAND.height() * (armorStand.isSmall() ? 0.55d : 1d);
}
}
Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset); Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset);
passenger.setRiderSeatPosition(offset); passenger.setRiderSeatPosition(offset);
} }

View file

@ -314,7 +314,14 @@ public class LoginEncryptionUtils {
.label("geyser.auth.login.form.details.desc") .label("geyser.auth.login.form.details.desc")
.input("geyser.auth.login.form.details.email", "account@geysermc.org", "") .input("geyser.auth.login.form.details.email", "account@geysermc.org", "")
.input("geyser.auth.login.form.details.pass", "123456", "") .input("geyser.auth.login.form.details.pass", "123456", "")
.closedOrInvalidResultHandler(() -> buildAndShowLoginDetailsWindow(session)) .invalidResultHandler(() -> buildAndShowLoginDetailsWindow(session))
.closedResultHandler(() -> {
if (session.isMicrosoftAccount()) {
buildAndShowMicrosoftAuthenticationWindow(session);
} else {
buildAndShowLoginWindow(session);
}
})
.validResultHandler((response) -> session.authenticate(response.next(), response.next()))); .validResultHandler((response) -> session.authenticate(response.next(), response.next())));
} }

View file

@ -8,7 +8,12 @@ dependencyResolutionManagement {
mavenContent { releasesOnly() } mavenContent { releasesOnly() }
} }
maven("https://repo.opencollab.dev/maven-snapshots") { maven("https://repo.opencollab.dev/maven-snapshots") {
mavenContent { snapshotsOnly() } mavenContent {
// This has the unintended side effect of not allowing snapshot version pinning.
// Likely a bug in Gradle's implementation of snapshot pinning
// See https://github.com/gradle/gradle/pull/406
snapshotsOnly()
}
} }
// Paper, Velocity // Paper, Velocity