mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-12-22 22:45:04 +01:00
Merge remote-tracking branch 'origin/master' into feature/extensions
This commit is contained in:
commit
363d72da91
53 changed files with 1030 additions and 231 deletions
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,10 +1,10 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- 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.
|
||||
- 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.
|
||||
- name: Get help on the GeyserMC Discord server
|
||||
url: https://discord.gg/geysermc
|
||||
|
|
|
@ -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!
|
||||
|
||||
### 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
|
||||
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
/**
|
||||
|
@ -42,6 +43,8 @@ import java.net.InetSocketAddress;
|
|||
* applied.
|
||||
*/
|
||||
public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough {
|
||||
private static final Constructor<PaperServerListPingEvent> OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor();
|
||||
|
||||
private final GeyserSpigotLogger logger;
|
||||
|
||||
public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) {
|
||||
|
@ -54,9 +57,17 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
|
|||
try {
|
||||
// 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.
|
||||
PaperServerListPingEvent event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
|
||||
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers(), Bukkit.getVersion(),
|
||||
GameProtocol.getJavaProtocolVersion(), null);
|
||||
PaperServerListPingEvent event;
|
||||
if (OLD_CONSTRUCTOR != null) {
|
||||
// Approximately pre-1.19
|
||||
event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress),
|
||||
Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
|
||||
Bukkit.getMaxPlayers(), Bukkit.getVersion(), MinecraftProtocol.getJavaProtocolVersion(), null);
|
||||
} else {
|
||||
event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
|
||||
Bukkit.getMotd(), Bukkit.shouldSendChatPreviews(), Bukkit.getOnlinePlayers().size(),
|
||||
Bukkit.getMaxPlayers(), Bukkit.getVersion(), MinecraftProtocol.getJavaProtocolVersion(), null);
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
if (event.isCancelled()) {
|
||||
// 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;
|
||||
} 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);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
|
|||
);
|
||||
Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add);
|
||||
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);
|
||||
return null;
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
|
|||
private static class GeyserPingEvent extends ServerListPingEvent {
|
||||
|
||||
public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) {
|
||||
super(address, motd, numPlayers, maxPlayers);
|
||||
super(address, motd, Bukkit.shouldSendChatPreviews(), numPlayers, maxPlayers);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -168,14 +168,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
|
|||
if (geyserConfig.isLegacyPingPassthrough()) {
|
||||
this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
|
||||
} else {
|
||||
try {
|
||||
Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
|
||||
if (ReflectedNames.checkPaperPingEvent()) {
|
||||
this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger);
|
||||
} catch (ClassNotFoundException e) {
|
||||
} else if (ReflectedNames.newSpigotPingConstructorExists()) {
|
||||
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.init();
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ public class OffhandCommand extends GeyserCommand {
|
|||
}
|
||||
|
||||
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
|
||||
Direction.DOWN, session.getNextSequence());
|
||||
Direction.DOWN, session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(releaseItemPacket);
|
||||
}
|
||||
|
||||
|
|
|
@ -99,19 +99,9 @@ public class FishingHookEntity extends ThrowableEntity {
|
|||
}
|
||||
}
|
||||
|
||||
int waterLevel = BlockStateValues.getWaterLevel(blockID);
|
||||
if (BlockRegistries.WATERLOGGED.get().contains(blockID)) {
|
||||
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;
|
||||
}
|
||||
double waterHeight = BlockStateValues.getWaterHeight(blockID);
|
||||
if (waterHeight != -1 && position.getY() <= (iter.getY() + waterHeight)) {
|
||||
touchingWater = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ public class ArmorStandEntity extends LivingEntity {
|
|||
@Getter
|
||||
private boolean isMarker = false;
|
||||
private boolean isInvisible = false;
|
||||
@Getter
|
||||
private boolean isSmall = false;
|
||||
|
||||
/**
|
||||
|
@ -74,6 +75,7 @@ public class ArmorStandEntity extends LivingEntity {
|
|||
* - No armor, no name: false
|
||||
* - No armor, yes name: true
|
||||
*/
|
||||
@Getter
|
||||
private boolean positionRequiresOffset = false;
|
||||
/**
|
||||
* 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;
|
||||
if (positionRequiresOffset) {
|
||||
this.position = applyOffsetToPosition(position);
|
||||
// Update the passenger offset as armorstand is moving up by roughly 2 blocks
|
||||
updatePassengerOffsets();
|
||||
} else {
|
||||
this.position = removeOffsetFromPosition(position);
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ public class IronGolemEntity extends GolemEntity {
|
|||
setFlag(EntityFlag.BRIBED, true);
|
||||
// Required, or else the overlay is black
|
||||
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
|
||||
|
|
|
@ -41,7 +41,7 @@ import java.util.UUID;
|
|||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class WardenEntity extends MonsterEntity implements Tickable {
|
||||
private int heartBeatDelay;
|
||||
private int heartBeatDelay = 40;
|
||||
private int tickCount;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeMetadata() {
|
||||
super.initializeMetadata();
|
||||
dirtyMetadata.put(EntityData.HEARTBEAT_INTERVAL_TICKS, heartBeatDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPose(Pose pose) {
|
||||
setFlag(EntityFlag.DIGGING, pose == Pose.DIGGING);
|
||||
|
|
|
@ -35,9 +35,7 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
|
|||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.protocol.bedrock.data.AttributeData;
|
||||
import com.nukkitx.protocol.bedrock.data.GameType;
|
||||
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
||||
import com.nukkitx.protocol.bedrock.data.*;
|
||||
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||
|
@ -59,6 +57,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -66,6 +65,16 @@ import java.util.concurrent.TimeUnit;
|
|||
@Getter @Setter
|
||||
public class PlayerEntity extends LivingEntity {
|
||||
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 boolean playerList = true; // Player is in the player list
|
||||
|
@ -127,6 +136,7 @@ public class PlayerEntity extends LivingEntity {
|
|||
addPlayerPacket.setDeviceId("");
|
||||
addPlayerPacket.setPlatformChatId("");
|
||||
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);
|
||||
dirtyMetadata.apply(addPlayerPacket.getMetadata());
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ public class SkullPlayerEntity extends PlayerEntity {
|
|||
addPlayerPacket.setDeviceId("");
|
||||
addPlayerPacket.setPlatformChatId("");
|
||||
addPlayerPacket.setGameType(GameType.SURVIVAL);
|
||||
addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER);
|
||||
addPlayerPacket.getMetadata().putFlags(flags);
|
||||
dirtyMetadata.apply(addPlayerPacket.getMetadata());
|
||||
|
||||
|
|
|
@ -42,16 +42,21 @@ public class StoredItemMappings {
|
|||
private final ItemMapping banner;
|
||||
private final ItemMapping barrier;
|
||||
private final int bowl;
|
||||
private final int bucket;
|
||||
private final int chest;
|
||||
private final ItemMapping compass;
|
||||
private final ItemMapping crossbow;
|
||||
private final ItemMapping enchantedBook;
|
||||
private final ItemMapping fishingRod;
|
||||
private final int flintAndSteel;
|
||||
private final int frogspawn;
|
||||
private final int goatHorn;
|
||||
private final int glassBottle;
|
||||
private final int goldenApple;
|
||||
private final int goldIngot;
|
||||
private final int ironIngot;
|
||||
private final int lead;
|
||||
private final int lilyPad;
|
||||
private final ItemMapping milkBucket;
|
||||
private final int nameTag;
|
||||
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.barrier = load(itemMappings, "barrier");
|
||||
this.bowl = load(itemMappings, "bowl").getJavaId();
|
||||
this.bucket = load(itemMappings, "bucket").getBedrockId();
|
||||
this.chest = load(itemMappings, "chest").getJavaId();
|
||||
this.compass = load(itemMappings, "compass");
|
||||
this.crossbow = load(itemMappings, "crossbow");
|
||||
this.enchantedBook = load(itemMappings, "enchanted_book");
|
||||
this.fishingRod = load(itemMappings, "fishing_rod");
|
||||
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.goldIngot = load(itemMappings, "gold_ingot").getJavaId();
|
||||
this.ironIngot = load(itemMappings, "iron_ingot").getJavaId();
|
||||
this.lead = load(itemMappings, "lead").getJavaId();
|
||||
this.lilyPad = load(itemMappings, "lily_pad").getBedrockId();
|
||||
this.milkBucket = load(itemMappings, "milk_bucket");
|
||||
this.nameTag = load(itemMappings, "name_tag").getJavaId();
|
||||
this.powderSnowBucket = load(itemMappings, "powder_snow_bucket");
|
||||
|
|
|
@ -44,6 +44,7 @@ import java.util.Locale;
|
|||
* Used for block entities if the Java block state contains Bedrock block information.
|
||||
*/
|
||||
public final class BlockStateValues {
|
||||
private static final IntSet ALL_CAULDRONS = new IntOpenHashSet();
|
||||
private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap();
|
||||
private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap();
|
||||
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_WATER_ID;
|
||||
|
||||
public static final int NUM_WATER_LEVELS = 9;
|
||||
|
||||
/**
|
||||
* Determines if the block state contains Bedrock block information
|
||||
*
|
||||
|
@ -193,6 +196,9 @@ public final class BlockStateValues {
|
|||
return;
|
||||
}
|
||||
|
||||
if (javaId.contains("cauldron")) {
|
||||
ALL_CAULDRONS.add(javaBlockState);
|
||||
}
|
||||
if (javaId.contains("_cauldron") && !javaId.contains("water_")) {
|
||||
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
|
||||
*/
|
||||
public static boolean isCauldron(int state) {
|
||||
public static boolean isNonWaterCauldron(int 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
|
||||
* in Bedrock need the conditional information.
|
||||
|
@ -436,7 +451,6 @@ public final class BlockStateValues {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This is used in ItemEntity to calculate the friction on an item as it slides across the ground
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
package org.geysermc.geyser.level.physics;
|
||||
|
||||
import com.nukkitx.math.GenericMath;
|
||||
import com.nukkitx.math.vector.Vector3d;
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
* Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side
|
||||
|
|
|
@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.codec.MinecraftCodec;
|
|||
import com.github.steveice10.mc.protocol.codec.PacketCodec;
|
||||
import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
|
||||
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.Collections;
|
||||
|
@ -43,7 +45,7 @@ public final class GameProtocol {
|
|||
* Default Bedrock codec that should act as a fallback. Should represent the latest available
|
||||
* 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
|
||||
*/
|
||||
|
@ -56,9 +58,10 @@ public final class GameProtocol {
|
|||
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
|
||||
|
||||
static {
|
||||
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
|
||||
.minecraftVersion("1.19.0")
|
||||
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v527.V527_CODEC.toBuilder()
|
||||
.minecraftVersion("1.19.0/1.19.2")
|
||||
.build());
|
||||
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,6 +78,12 @@ public final class GameProtocol {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -91,8 +91,10 @@ public class QueryPacketHandler {
|
|||
switch (type) {
|
||||
case HANDSHAKE:
|
||||
sendToken();
|
||||
break;
|
||||
case STATISTICS:
|
||||
sendQueryData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,11 @@ package org.geysermc.geyser.ping;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.github.steveice10.mc.protocol.MinecraftProtocol;
|
||||
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.network.GameProtocol;
|
||||
|
||||
|
@ -35,13 +39,12 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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;
|
||||
|
||||
public GeyserLegacyPingPassthrough(GeyserImpl geyser) {
|
||||
|
@ -74,54 +77,68 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Socket socket = new Socket();
|
||||
try (Socket socket = new Socket()) {
|
||||
String address = geyser.getConfig().getRemote().getAddress();
|
||||
int port = geyser.getConfig().getRemote().getPort();
|
||||
socket.connect(new InetSocketAddress(address, port), 5000);
|
||||
|
||||
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
|
||||
DataOutputStream handshake = new DataOutputStream(byteArrayStream);
|
||||
handshake.write(0x0);
|
||||
VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
|
||||
VarInts.writeUnsignedInt(handshake, address.length());
|
||||
handshake.writeBytes(address);
|
||||
handshake.writeShort(port);
|
||||
VarInts.writeUnsignedInt(handshake, 1);
|
||||
try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) {
|
||||
handshake.write(0x0);
|
||||
VarInts.writeUnsignedInt(handshake, MinecraftProtocol.getJavaProtocolVersion());
|
||||
VarInts.writeUnsignedInt(handshake, address.length());
|
||||
handshake.writeBytes(address);
|
||||
handshake.writeShort(port);
|
||||
VarInts.writeUnsignedInt(handshake, 1);
|
||||
}
|
||||
|
||||
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
|
||||
VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
|
||||
dataOutputStream.write(byteArrayStream.toByteArray());
|
||||
dataOutputStream.writeByte(0x01);
|
||||
dataOutputStream.writeByte(0x00);
|
||||
byte[] buffer;
|
||||
|
||||
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
int length = VarInts.readUnsignedInt(dataInputStream);
|
||||
byte[] buffer = new byte[length];
|
||||
dataInputStream.readFully(buffer);
|
||||
dataOutputStream.writeByte(0x09);
|
||||
dataOutputStream.writeByte(0x01);
|
||||
dataOutputStream.writeLong(System.currentTimeMillis());
|
||||
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);
|
||||
}
|
||||
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
String json = new String(buffer);
|
||||
VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
|
||||
dataOutputStream.write(byteArrayStream.toByteArray());
|
||||
dataOutputStream.writeByte(0x01);
|
||||
dataOutputStream.writeByte(0x00);
|
||||
|
||||
this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(json, GeyserPingInfo.class);
|
||||
try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
int length = VarInts.readUnsignedInt(dataInputStream);
|
||||
buffer = new byte[length];
|
||||
dataInputStream.readFully(buffer);
|
||||
dataOutputStream.writeByte(0x09);
|
||||
dataOutputStream.writeByte(0x01);
|
||||
dataOutputStream.writeLong(System.currentTimeMillis());
|
||||
|
||||
byteArrayStream.close();
|
||||
handshake.close();
|
||||
dataOutputStream.close();
|
||||
dataInputStream.close();
|
||||
socket.close();
|
||||
VarInts.readUnsignedInt(dataInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class);
|
||||
} catch (SocketTimeoutException | ConnectException ex) {
|
||||
this.pingInfo = null;
|
||||
this.geyser.getLogger().debug("Connection timeout for ping passthrough.");
|
||||
} catch (JsonParseException | JsonMappingException ex) {
|
||||
this.geyser.getLogger().error("Failed to parse json when pinging server!", ex);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,16 @@ public class BlockRegistries {
|
|||
*/
|
||||
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 {
|
||||
BlockRegistryPopulator.populate();
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
package org.geysermc.geyser.registry.populator;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.nukkitx.nbt.*;
|
||||
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]));
|
||||
|
||||
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) {
|
||||
|
|
|
@ -235,6 +235,9 @@ public class ItemRegistryPopulator {
|
|||
} else if (identifier.equals("minecraft:empty_map") && damage == 2) {
|
||||
// Bedrock-only as its own item
|
||||
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);
|
||||
int id = -1;
|
||||
|
|
|
@ -136,9 +136,9 @@ public class ItemMappings {
|
|||
}
|
||||
} else {
|
||||
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().equals("minecraft:firework_star")))) {
|
||||
|| mapping.getJavaIdentifier().equals("minecraft:firework_star") || mapping.getJavaIdentifier().equals("minecraft:goat_horn")))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
|||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
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.ObjectOpenHashSet;
|
||||
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.command.GeyserCommandSource;
|
||||
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
|
||||
import org.geysermc.geyser.entity.EntityDefinitions;
|
||||
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||
import org.geysermc.geyser.entity.type.Entity;
|
||||
import org.geysermc.geyser.entity.type.ItemFrameEntity;
|
||||
|
@ -395,6 +398,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
*/
|
||||
@Setter
|
||||
private boolean emulatePost1_16Logic = true;
|
||||
@Setter
|
||||
private boolean emulatePost1_18Logic = true;
|
||||
|
||||
/**
|
||||
* The current attack speed of the player. Used for sending proper cooldown timings.
|
||||
|
@ -428,11 +433,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
private long lastInteractionTime;
|
||||
|
||||
/**
|
||||
* Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to
|
||||
* interact with a block.
|
||||
* Stores whether the player intended to place a bucket.
|
||||
*/
|
||||
@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.
|
||||
|
@ -480,6 +484,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
@Setter
|
||||
private boolean instabuild = false;
|
||||
|
||||
@Setter
|
||||
private float flySpeed;
|
||||
@Setter
|
||||
private float walkSpeed;
|
||||
|
||||
/**
|
||||
* Caches current rain status.
|
||||
*/
|
||||
|
@ -496,7 +505,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
* 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.
|
||||
*/
|
||||
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.
|
||||
|
@ -519,6 +528,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
*/
|
||||
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;
|
||||
|
||||
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
|
||||
|
@ -1265,9 +1280,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
|
||||
ServerboundUseItemPacket useItemPacket;
|
||||
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()) {
|
||||
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, getNextSequence());
|
||||
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, worldCache.nextPredictionSequence());
|
||||
} else {
|
||||
// No blocking
|
||||
return false;
|
||||
|
@ -1296,7 +1311,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
private boolean disableBlocking() {
|
||||
if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
|
||||
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
|
||||
Vector3i.ZERO, Direction.DOWN, getNextSequence());
|
||||
Vector3i.ZERO, Direction.DOWN, worldCache.nextPredictionSequence());
|
||||
sendDownstreamPacket(releaseItemPacket);
|
||||
playerEntity.setFlag(EntityFlag.BLOCKING, false);
|
||||
return true;
|
||||
|
@ -1610,23 +1625,81 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
return geyser.getWorldManager().hasPermission(this, permission);
|
||||
}
|
||||
|
||||
private static final Ability[] USED_ABILITIES = Ability.values();
|
||||
|
||||
/**
|
||||
* Send an AdventureSettingsPacket to the client with the latest flags
|
||||
*/
|
||||
public void sendAdventureSettings() {
|
||||
AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket();
|
||||
adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId());
|
||||
long bedrockId = playerEntity.getGeyserId();
|
||||
// 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
|
||||
// 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
|
||||
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
|
||||
boolean spectator = gameMode == GameMode.SPECTATOR;
|
||||
boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator;
|
||||
|
||||
if (org.geysermc.geyser.network.MinecraftProtocol.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);
|
||||
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();
|
||||
if (canFly || spectator) {
|
||||
flags.add(AdventureSetting.MAY_FLY);
|
||||
|
@ -1676,16 +1749,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
sendDownstreamPacket(clientSettingsPacket);
|
||||
}
|
||||
|
||||
public int getNextSequence() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for updating statistic values since we only get changes from the server
|
||||
*
|
||||
* @param statistics Updated statistics values
|
||||
*/
|
||||
public void updateStatistics(@NonNull Map<Statistic, Integer> statistics) {
|
||||
public void updateStatistics(@Nonnull Object2IntMap<Statistic> statistics) {
|
||||
if (this.statistics.isEmpty()) {
|
||||
// Initialize custom statistics to 0, so that they appear in the form
|
||||
for (CustomStatistic customStatistic : CustomStatistic.values()) {
|
||||
|
@ -1757,6 +1826,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||
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() {
|
||||
return (MinecraftCodecHelper) this.downstream.getCodecHelper();
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache;
|
|||
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import it.unimi.dsi.fastutil.ints.IntLists;
|
||||
import org.geysermc.geyser.GeyserLogger;
|
||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||
import org.geysermc.geyser.registry.type.BlockMapping;
|
||||
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.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");
|
||||
this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items"));
|
||||
this.fishes = IntList.of(itemTags.get("minecraft:fishes"));
|
||||
|
@ -93,8 +103,8 @@ public class TagCache {
|
|||
// Hack btw
|
||||
boolean emulatePost1_14Logic = itemTags.get("minecraft:signs").length > 1;
|
||||
session.setEmulatePost1_14Logic(emulatePost1_14Logic);
|
||||
if (session.getGeyser().getLogger().isDebug()) {
|
||||
session.getGeyser().getLogger().debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,12 +26,18 @@
|
|||
package org.geysermc.geyser.session.cache;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.protocol.bedrock.packet.SetTitlePacket;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.util.ChunkUtils;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public final class WorldCache {
|
||||
private final GeyserSession session;
|
||||
|
@ -51,6 +57,9 @@ public final class WorldCache {
|
|||
private int trueTitleStayTime;
|
||||
private int trueTitleFadeOutTime;
|
||||
|
||||
private int currentSequence;
|
||||
private final Map<Vector3i, ServerVerifiedState> unverifiedPredictions = new Object2ObjectOpenHashMap<>(1);
|
||||
|
||||
public WorldCache(GeyserSession session) {
|
||||
this.session = session;
|
||||
this.scoreboard = new Scoreboard(session);
|
||||
|
@ -121,4 +130,75 @@ public final class WorldCache {
|
|||
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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
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 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.
|
||||
// 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.
|
||||
chatTypes.put(MessageType.CHAT.ordinal(), CHAT);
|
||||
chatTypes.put(MessageType.SYSTEM.ordinal(), SYSTEM);
|
||||
chatTypes.put(MessageType.GAME_INFO.ordinal(), TIP);
|
||||
chatTypes.put(MessageType.SAY_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(MessageType.MSG_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(MessageType.TEAM_MSG_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(MessageType.EMOTE_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(MessageType.TELLRAW_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(BuiltinChatType.CHAT.ordinal(), CHAT);
|
||||
chatTypes.put(BuiltinChatType.SYSTEM.ordinal(), SYSTEM);
|
||||
chatTypes.put(BuiltinChatType.GAME_INFO.ordinal(), TIP);
|
||||
chatTypes.put(BuiltinChatType.SAY_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(BuiltinChatType.MSG_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(BuiltinChatType.TEAM_MSG_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(BuiltinChatType.EMOTE_COMMAND.ordinal(), RAW);
|
||||
chatTypes.put(BuiltinChatType.TELLRAW_COMMAND.ordinal(), RAW);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,14 +126,20 @@ public class MinecraftLocale {
|
|||
|
||||
// Check the locale isn't already loaded
|
||||
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
|
||||
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale));
|
||||
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));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GeyserImpl.getInstance().getLogger().debug("Downloading and loading locale: " + 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
|
||||
*/
|
||||
private static void loadLocale(String locale) {
|
||||
private static boolean loadLocale(String locale) {
|
||||
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
|
||||
|
||||
// Load the locale
|
||||
|
@ -242,8 +248,9 @@ public class MinecraftLocale {
|
|||
} catch (IOException e) {
|
||||
throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage()));
|
||||
}
|
||||
return true;
|
||||
} 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
|
||||
*/
|
||||
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) {
|
||||
localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale());
|
||||
localeStrings = LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale());
|
||||
if (localeStrings == null) {
|
||||
// Don't cause a NPE if the locale is STILL missing
|
||||
GeyserImpl.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + GeyserLocale.getDefaultLocale());
|
||||
|
|
|
@ -119,7 +119,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
|
|||
}
|
||||
|
||||
private OptionalInt toJava(int effectChoice) {
|
||||
return effectChoice == -1 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
|
||||
return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -60,7 +60,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
|
|||
|
||||
static {
|
||||
// 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("br", index++);
|
||||
PATTERN_TO_INDEX.put("tl", index++);
|
||||
|
@ -119,15 +119,16 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
|
|||
@Override
|
||||
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
|
||||
// Remove the CRAFT_NON_IMPLEMENTED_DEPRECATED when 1.17.30 is dropped
|
||||
return (action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_LOOM)
|
||||
&& inventory.getItem(2).isEmpty();
|
||||
return action.getType() == StackRequestActionType.CRAFT_LOOM && inventory.getItem(2).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
|
||||
StackRequestActionData headerData = request.getActions()[0];
|
||||
StackRequestActionData data = request.getActions()[1];
|
||||
if (!(headerData instanceof CraftLoomStackRequestActionData)) {
|
||||
return rejectRequest(request);
|
||||
}
|
||||
if (!(data instanceof CraftResultsDeprecatedStackRequestActionData craftData)) {
|
||||
return rejectRequest(request);
|
||||
}
|
||||
|
@ -136,15 +137,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
|
|||
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
|
||||
NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1);
|
||||
String bedrockPattern;
|
||||
|
||||
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");
|
||||
}
|
||||
String bedrockPattern = ((CraftLoomStackRequestActionData) headerData).getPatternId();
|
||||
|
||||
// Get the Java index of this pattern
|
||||
int index = PATTERN_TO_INDEX.getOrDefault(bedrockPattern, -1);
|
||||
|
|
|
@ -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.MinecraftProtocol;
|
||||
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(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion())
|
||||
.getMapping("minecraft:goat_horn")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState {
|
|||
return FlowerPotBlockEntityTranslator.getTag(session, blockState, position);
|
||||
} else if (PistonBlockEntityTranslator.isBlock(blockState)) {
|
||||
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)
|
||||
return NbtMap.builder()
|
||||
.putString("id", "Cauldron")
|
||||
|
|
|
@ -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.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
|
||||
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.Vector3i;
|
||||
import com.nukkitx.protocol.bedrock.data.LevelEventType;
|
||||
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.Int2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import org.geysermc.geyser.entity.EntityDefinitions;
|
||||
import org.geysermc.geyser.entity.type.Entity;
|
||||
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.Inventory;
|
||||
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.ItemMappings;
|
||||
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.Translator;
|
||||
import org.geysermc.geyser.util.BlockUtils;
|
||||
|
@ -122,7 +129,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
|
||||
Vector3i.ZERO,
|
||||
Direction.DOWN,
|
||||
session.getNextSequence()
|
||||
session.getWorldCache().nextPredictionSequence()
|
||||
);
|
||||
session.sendDownstreamPacket(dropPacket);
|
||||
|
||||
|
@ -170,6 +177,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
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
|
||||
if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) {
|
||||
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
|
||||
Vector3f playerPosition = session.getPlayerEntity().getPosition();
|
||||
|
||||
// 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
|
||||
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
|
||||
|
||||
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());
|
||||
if (blockState == BlockStateValues.JAVA_WATER_ID) {
|
||||
// Otherwise causes multiple mobs to spawn - just send a use item packet
|
||||
// TODO when we fix mobile bucket rotation, use it for this, too
|
||||
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
|
||||
session.sendDownstreamPacket(itemPacket);
|
||||
useItem(session, packet, blockState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -268,33 +267,33 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
Hand.MAIN_HAND,
|
||||
packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
|
||||
false,
|
||||
session.getNextSequence());
|
||||
session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(blockPacket);
|
||||
|
||||
if (packet.getItemInHand() != null) {
|
||||
// Otherwise boats will not be able to be placed in survival and buckets won't work on mobile
|
||||
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);
|
||||
int itemId = packet.getItemInHand().getId();
|
||||
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
|
||||
// Otherwise boats will not be able to be placed in survival and buckets, lily pads, frogspawn, and glass bottles won't work on mobile
|
||||
if (session.getItemMappings().getBoatIds().contains(itemId) ||
|
||||
itemId == session.getItemMappings().getStoredItems().lilyPad() ||
|
||||
itemId == session.getItemMappings().getStoredItems().frogspawn()) {
|
||||
useItem(session, packet, blockState);
|
||||
} else if (itemId == session.getItemMappings().getStoredItems().glassBottle()) {
|
||||
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 (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());
|
||||
if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) {
|
||||
// Delay the interaction in case the client doesn't intend to actually use the bucket
|
||||
// See BedrockActionTranslator.java
|
||||
session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> {
|
||||
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
|
||||
session.sendDownstreamPacket(itemPacket);
|
||||
}, 5, TimeUnit.MILLISECONDS));
|
||||
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);
|
||||
}
|
||||
case 1 -> {
|
||||
if (isIncorrectHeldItem(session, packet)) {
|
||||
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
|
||||
break;
|
||||
}
|
||||
|
||||
// Handled when sneaking
|
||||
if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
|
||||
break;
|
||||
|
@ -334,10 +338,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
} else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
|
||||
// Handled in case 0
|
||||
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);
|
||||
|
||||
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
|
||||
|
@ -402,12 +409,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
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();
|
||||
blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
|
||||
blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
|
||||
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
|
||||
session.sendUpstreamPacket(blockBreakPacket);
|
||||
session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID);
|
||||
session.setBreakingBlock(-1);
|
||||
|
||||
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -427,7 +444,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
if (packet.getActionType() == 0) {
|
||||
// Followed to the Minecraft Protocol specification outlined at wiki.vg
|
||||
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
|
||||
Direction.DOWN, session.getNextSequence());
|
||||
Direction.DOWN, session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(releaseItemPacket);
|
||||
}
|
||||
break;
|
||||
|
@ -520,10 +537,117 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||
session.sendUpstreamPacket(updateWaterPacket);
|
||||
|
||||
// Reset the item in hand to prevent "missing" blocks
|
||||
InventorySlotPacket slotPacket = new InventorySlotPacket();
|
||||
slotPacket.setContainerId(ContainerId.INVENTORY);
|
||||
slotPacket.setSlot(packet.getHotbarSlot());
|
||||
slotPacket.setItem(packet.getItemInHand());
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
|
||||
}
|
||||
|
||||
private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
|
|||
Hand.MAIN_HAND,
|
||||
0, 0, 0, // Java doesn't care about these when dealing with a lectern
|
||||
false,
|
||||
session.getNextSequence());
|
||||
session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(blockPacket);
|
||||
} else {
|
||||
// Bedrock wants to either move a page or exit
|
||||
|
|
|
@ -65,7 +65,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
|
|||
// Activate shield since we are already sneaking
|
||||
// (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
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ import com.nukkitx.protocol.bedrock.packet.*;
|
|||
import org.geysermc.geyser.entity.type.Entity;
|
||||
import org.geysermc.geyser.entity.type.ItemFrameEntity;
|
||||
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.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
|
@ -129,21 +128,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
|
|||
break;
|
||||
case 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);
|
||||
break;
|
||||
case STOP_SLEEP:
|
||||
ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
|
||||
session.sendDownstreamPacket(stopSleepingPacket);
|
||||
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:
|
||||
// Start the block breaking animation
|
||||
if (session.getGameMode() != GameMode.CREATIVE) {
|
||||
|
@ -163,7 +154,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
|
|||
String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp);
|
||||
if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
|
||||
ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos,
|
||||
Direction.VALUES[packet.getFace()], session.getNextSequence());
|
||||
Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(startBreakingPacket);
|
||||
if (session.getGameMode() == GameMode.CREATIVE) {
|
||||
break;
|
||||
|
@ -171,17 +162,22 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
|
|||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
case CONTINUE_BREAK:
|
||||
if (session.getGameMode() == GameMode.CREATIVE) {
|
||||
break;
|
||||
}
|
||||
int breakingBlock = session.getBreakingBlock();
|
||||
if (breakingBlock == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
Vector3f vectorFloat = vector.toFloat();
|
||||
LevelEventPacket continueBreakPacket = new LevelEventPacket();
|
||||
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);
|
||||
session.sendUpstreamPacket(continueBreakPacket);
|
||||
|
||||
|
@ -189,7 +185,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
|
|||
LevelEventPacket updateBreak = new LevelEventPacket();
|
||||
updateBreak.setType(LevelEventType.BLOCK_UPDATE_BREAK);
|
||||
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));
|
||||
session.sendUpstreamPacket(updateBreak);
|
||||
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);
|
||||
LevelEventPacket stopBreak = new LevelEventPacket();
|
||||
stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
|
||||
stopBreak.setPosition(vector.toFloat());
|
||||
stopBreak.setData(0);
|
||||
session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID);
|
||||
session.setBreakingBlock(-1);
|
||||
session.sendUpstreamPacket(stopBreak);
|
||||
break;
|
||||
case STOP_BREAK:
|
||||
|
|
|
@ -44,7 +44,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
|
|||
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) {
|
||||
// Activate the workaround - we should trigger the offhand now
|
||||
ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
|
||||
Direction.DOWN, session.getNextSequence());
|
||||
Direction.DOWN, session.getWorldCache().nextPredictionSequence());
|
||||
session.sendDownstreamPacket(swapHandsPacket);
|
||||
|
||||
if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {
|
||||
|
|
|
@ -77,6 +77,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
|||
boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
|
||||
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
|
||||
// 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
|
||||
|
|
|
@ -50,6 +50,8 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<ClientboundL
|
|||
if (disconnectReason instanceof TranslatableComponent component) {
|
||||
String key = component.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)
|
||||
"multiplayer.disconnect.outdated_server".equals(key)
|
||||
// Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
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.serverbound.ServerboundCustomPayloadPacket;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
|
@ -82,14 +82,15 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
|
|||
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?
|
||||
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 SYSTEM -> TextPacket.Type.SYSTEM;
|
||||
case GAME_INFO -> TextPacket.Type.TIP;
|
||||
default -> TextPacket.Type.RAW;
|
||||
};
|
||||
} : TextPacket.Type.RAW;
|
||||
chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration));
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.java;
|
|||
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.TextPacket;
|
||||
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.Translator;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
|
@ -37,11 +38,15 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
|
|||
|
||||
@Override
|
||||
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.setPlatformChatId("");
|
||||
textPacket.setSourceName("");
|
||||
textPacket.setXuid(session.getAuthData().xuid());
|
||||
textPacket.setType(session.getChatTypes().get(packet.getTypeId()).bedrockChatType());
|
||||
textPacket.setType(chatType);
|
||||
|
||||
textPacket.setNeedsTranslation(false);
|
||||
textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale()));
|
||||
|
|
|
@ -147,6 +147,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
|
|||
soundPacket.setRelativeVolumeDisabled(false);
|
||||
session.sendUpstreamPacket(soundPacket);
|
||||
return;
|
||||
case VILLAGER_MATE:
|
||||
case ANIMAL_EMIT_HEARTS:
|
||||
entityEventPacket.setType(EntityEventType.LOVE_PARTICLES);
|
||||
break;
|
||||
|
@ -176,6 +177,18 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
|
|||
case IRON_GOLEM_HOLD_POPPY:
|
||||
entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_OFFER);
|
||||
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:
|
||||
entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW);
|
||||
break;
|
||||
|
|
|
@ -35,6 +35,6 @@ public class JavaBlockChangedAckTranslator extends PacketTranslator<ClientboundB
|
|||
|
||||
@Override
|
||||
public void translate(GeyserSession session, ClientboundBlockChangedAckPacket packet) {
|
||||
// TODO
|
||||
session.getWorldCache().endPredictionsUpTo(packet.getSequence());
|
||||
}
|
||||
}
|
|
@ -38,6 +38,8 @@ public class JavaPlayerAbilitiesTranslator extends PacketTranslator<ClientboundP
|
|||
session.setCanFly(packet.isCanFly());
|
||||
session.setFlying(packet.isFlying());
|
||||
session.setInstabuild(packet.isCreative());
|
||||
session.setFlySpeed(packet.getFlySpeed());
|
||||
session.setWalkSpeed(packet.getWalkSpeed());
|
||||
session.sendAdventureSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.MinecraftProtocol;
|
||||
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() && MinecraftProtocol.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.getLocale()));
|
||||
session.sendUpstreamPacket(deathInfoPacket);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.packet.CraftingDataPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||
import org.geysermc.geyser.inventory.Inventory;
|
||||
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
|
||||
|
@ -66,27 +67,41 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
|
|||
if (inventory == null)
|
||||
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();
|
||||
if (translator != null) {
|
||||
if (session.getCraftingGridFuture() != null) {
|
||||
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());
|
||||
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
|
||||
// In rare cases, the window ID can still be 0 but Java treats it as valid
|
||||
session.getPlayerInventory().setItem(packet.getSlot(), newItem, session);
|
||||
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), packet.getSlot());
|
||||
session.getPlayerInventory().setItem(slot, newItem, session);
|
||||
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot);
|
||||
} else {
|
||||
inventory.setItem(packet.getSlot(), newItem, session);
|
||||
translator.updateSlot(session, inventory, packet.getSlot());
|
||||
inventory.setItem(slot, newItem, session);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
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,
|
||||
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
|
|||
|
||||
@Override
|
||||
public void translate(GeyserSession session, Vector3f position, String identifier) {
|
||||
if (session.getBucketScheduledFuture() == null) {
|
||||
if (!session.isPlacedBucket()) {
|
||||
return; // No bucket was really interacted with
|
||||
}
|
||||
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
|
||||
|
@ -71,6 +71,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
|
|||
case "minecraft:salmon_bucket":
|
||||
case "minecraft:pufferfish_bucket":
|
||||
case "minecraft:tropical_fish_bucket":
|
||||
case "minecraft:tadpole_bucket":
|
||||
soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
|
||||
break;
|
||||
case "minecraft:water_bucket":
|
||||
|
@ -83,7 +84,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
|
|||
if (soundEvent != null) {
|
||||
soundEventPacket.setSound(soundEvent);
|
||||
session.sendUpstreamPacket(soundEventPacket);
|
||||
session.setBucketScheduledFuture(null);
|
||||
session.setPlacedBucket(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ import org.geysermc.geyser.registry.type.ItemMapping;
|
|||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.collision.BlockCollision;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class BlockUtils {
|
||||
|
||||
private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) {
|
||||
|
@ -101,7 +103,7 @@ public final class BlockUtils {
|
|||
// https://minecraft.gamepedia.com/Breaking
|
||||
private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock,
|
||||
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 speed = 1.0 / baseTime;
|
||||
|
||||
|
@ -129,12 +131,11 @@ public final class BlockUtils {
|
|||
}
|
||||
|
||||
if (insideOfWaterWithoutAquaAffinity) speed *= 0.2;
|
||||
if (outOfWaterButNotOnGround) speed *= 0.2;
|
||||
if (insideWaterAndNotOnGround) speed *= 0.2;
|
||||
if (!onGround) speed *= 0.2;
|
||||
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 canHarvestWithHand = blockMapping.isCanBreakWithHand();
|
||||
String toolType = "";
|
||||
|
@ -154,36 +155,28 @@ public final class BlockUtils {
|
|||
if (!isSessionPlayer) {
|
||||
// Another entity is currently mining; we have all the information we know
|
||||
return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
|
||||
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false,
|
||||
false, false);
|
||||
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, true);
|
||||
}
|
||||
|
||||
hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower());
|
||||
miningFatigueLevel = session.getEffectCache().getMiningFatigue();
|
||||
|
||||
boolean isInWater = session.getCollisionManager().isPlayerInWater();
|
||||
|
||||
boolean insideOfWaterWithoutAquaAffinity = isInWater &&
|
||||
boolean waterInEyes = session.getCollisionManager().isWaterInEyes();
|
||||
boolean insideOfWaterWithoutAquaAffinity = waterInEyes &&
|
||||
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,
|
||||
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity,
|
||||
outOfWaterButNotOnGround, insideWaterNotOnGround);
|
||||
toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, session.getPlayerEntity().isOnGround());
|
||||
}
|
||||
|
||||
public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) {
|
||||
PlayerInventory inventory = session.getPlayerInventory();
|
||||
GeyserItemStack item = inventory.getItemInHand();
|
||||
ItemMapping mapping;
|
||||
CompoundTag nbtData;
|
||||
ItemMapping mapping = ItemMapping.AIR;
|
||||
CompoundTag nbtData = null;
|
||||
if (item != null) {
|
||||
mapping = item.getMapping(session);
|
||||
nbtData = item.getNbt();
|
||||
} else {
|
||||
mapping = ItemMapping.AIR;
|
||||
nbtData = new CompoundTag("");
|
||||
}
|
||||
return getBreakTime(session, blockMapping, mapping, nbtData, true);
|
||||
}
|
||||
|
|
|
@ -123,13 +123,21 @@ public class ChunkUtils {
|
|||
* @param position the position of the block
|
||||
*/
|
||||
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
|
||||
ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position);
|
||||
if (itemFrameEntity != null) {
|
||||
if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it
|
||||
itemFrameEntity.updateBlock(true);
|
||||
// Still update the chunk cache with the new block
|
||||
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
|
||||
// Still update the chunk cache with the new block if updateBlock is called
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
|
||||
}
|
||||
|
||||
public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) {
|
||||
|
|
|
@ -187,6 +187,15 @@ public final class EntityUtils {
|
|||
case MINECART, HOPPER_MINECART, TNT_MINECART, CHEST_MINECART, FURNACE_MINECART, SPAWNER_MINECART,
|
||||
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);
|
||||
passenger.setRiderSeatPosition(offset);
|
||||
}
|
||||
|
|
|
@ -314,7 +314,14 @@ public class LoginEncryptionUtils {
|
|||
.label("geyser.auth.login.form.details.desc")
|
||||
.input("geyser.auth.login.form.details.email", "account@geysermc.org", "")
|
||||
.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())));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue