mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
1866 lines
74 KiB
Java
1866 lines
74 KiB
Java
/*
|
|
* 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.session;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.github.steveice10.mc.auth.data.GameProfile;
|
|
import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException;
|
|
import com.github.steveice10.mc.auth.exception.request.RequestException;
|
|
import com.github.steveice10.mc.auth.service.AuthenticationService;
|
|
import com.github.steveice10.mc.auth.service.MojangAuthenticationService;
|
|
import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
|
|
import com.github.steveice10.mc.protocol.MinecraftConstants;
|
|
import com.github.steveice10.mc.protocol.MinecraftProtocol;
|
|
import com.github.steveice10.mc.protocol.codec.MinecraftCodecHelper;
|
|
import com.github.steveice10.mc.protocol.data.ProtocolState;
|
|
import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.player.HandPreference;
|
|
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
|
|
import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility;
|
|
import com.github.steveice10.mc.protocol.data.game.setting.SkinPart;
|
|
import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic;
|
|
import com.github.steveice10.mc.protocol.data.game.statistic.Statistic;
|
|
import com.github.steveice10.mc.protocol.packet.handshake.serverbound.ClientIntentionPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatCommandPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientInformationPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
|
|
import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryPacket;
|
|
import com.github.steveice10.packetlib.BuiltinFlags;
|
|
import com.github.steveice10.packetlib.Session;
|
|
import com.github.steveice10.packetlib.event.session.*;
|
|
import com.github.steveice10.packetlib.packet.Packet;
|
|
import com.github.steveice10.packetlib.tcp.TcpClientSession;
|
|
import com.github.steveice10.packetlib.tcp.TcpSession;
|
|
import com.nukkitx.math.GenericMath;
|
|
import com.nukkitx.math.vector.*;
|
|
import com.nukkitx.nbt.NbtMap;
|
|
import com.nukkitx.protocol.bedrock.BedrockPacket;
|
|
import com.nukkitx.protocol.bedrock.BedrockServerSession;
|
|
import com.nukkitx.protocol.bedrock.data.*;
|
|
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
|
import com.nukkitx.protocol.bedrock.packet.*;
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.EventLoop;
|
|
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
|
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
|
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;
|
|
import lombok.Getter;
|
|
import lombok.NonNull;
|
|
import lombok.Setter;
|
|
import lombok.experimental.Accessors;
|
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
import org.checkerframework.common.value.qual.IntRange;
|
|
import org.geysermc.api.util.BedrockPlatform;
|
|
import org.geysermc.api.util.InputMode;
|
|
import org.geysermc.api.util.UiProfile;
|
|
import org.geysermc.common.PlatformType;
|
|
import org.geysermc.cumulus.form.Form;
|
|
import org.geysermc.cumulus.form.util.FormBuilder;
|
|
import org.geysermc.floodgate.crypto.FloodgateCipher;
|
|
import org.geysermc.floodgate.util.BedrockData;
|
|
import org.geysermc.geyser.Constants;
|
|
import org.geysermc.geyser.GeyserImpl;
|
|
import org.geysermc.geyser.api.connection.GeyserConnection;
|
|
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;
|
|
import org.geysermc.geyser.entity.type.Tickable;
|
|
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
|
|
import org.geysermc.geyser.inventory.Inventory;
|
|
import org.geysermc.geyser.inventory.PlayerInventory;
|
|
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
|
|
import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
|
|
import org.geysermc.geyser.level.JavaDimension;
|
|
import org.geysermc.geyser.level.WorldManager;
|
|
import org.geysermc.geyser.level.physics.CollisionManager;
|
|
import org.geysermc.geyser.network.netty.LocalSession;
|
|
import org.geysermc.geyser.registry.Registries;
|
|
import org.geysermc.geyser.registry.type.BlockMappings;
|
|
import org.geysermc.geyser.registry.type.ItemMapping;
|
|
import org.geysermc.geyser.registry.type.ItemMappings;
|
|
import org.geysermc.geyser.session.auth.AuthData;
|
|
import org.geysermc.geyser.session.auth.BedrockClientData;
|
|
import org.geysermc.geyser.session.cache.*;
|
|
import org.geysermc.geyser.skin.FloodgateSkinUploader;
|
|
import org.geysermc.geyser.text.GeyserLocale;
|
|
import org.geysermc.geyser.text.MinecraftLocale;
|
|
import org.geysermc.geyser.text.TextDecoration;
|
|
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
|
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
|
import org.geysermc.geyser.util.ChunkUtils;
|
|
import org.geysermc.geyser.util.DimensionUtils;
|
|
import org.geysermc.geyser.util.LoginEncryptionUtils;
|
|
import org.geysermc.geyser.util.MathUtils;
|
|
|
|
import java.net.ConnectException;
|
|
import java.net.InetSocketAddress;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Instant;
|
|
import java.util.*;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
@Getter
|
|
public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|
|
|
private final @NonNull GeyserImpl geyser;
|
|
private final @NonNull UpstreamSession upstream;
|
|
/**
|
|
* The loop where all packets and ticking is processed to prevent concurrency issues.
|
|
* If this is manually called, ensure that any exceptions are properly handled.
|
|
*/
|
|
private final @NonNull EventLoop eventLoop;
|
|
private TcpSession downstream;
|
|
@Setter
|
|
private AuthData authData;
|
|
@Setter
|
|
private BedrockClientData clientData;
|
|
/**
|
|
* Used for Floodgate skin uploading
|
|
*/
|
|
@Setter
|
|
private JsonNode certChainData;
|
|
|
|
@Accessors(fluent = true)
|
|
@Setter
|
|
private RemoteServer remoteServer;
|
|
|
|
@Deprecated
|
|
@Setter
|
|
private boolean microsoftAccount;
|
|
|
|
private final SessionPlayerEntity playerEntity;
|
|
|
|
private final AdvancementsCache advancementsCache;
|
|
private final BookEditCache bookEditCache;
|
|
private final ChunkCache chunkCache;
|
|
private final EntityCache entityCache;
|
|
private final EntityEffectCache effectCache;
|
|
private final FormCache formCache;
|
|
private final LodestoneCache lodestoneCache;
|
|
private final PistonCache pistonCache;
|
|
private final PreferencesCache preferencesCache;
|
|
private final SkullCache skullCache;
|
|
private final TagCache tagCache;
|
|
private final WorldCache worldCache;
|
|
|
|
@Setter
|
|
private TeleportCache unconfirmedTeleport;
|
|
|
|
private final WorldBorder worldBorder;
|
|
/**
|
|
* Whether simulated fog has been sent to the client or not.
|
|
*/
|
|
private boolean isInWorldBorderWarningArea = false;
|
|
|
|
private final PlayerInventory playerInventory;
|
|
@Setter
|
|
private Inventory openInventory;
|
|
@Setter
|
|
private boolean closingInventory;
|
|
|
|
@Setter
|
|
private InventoryTranslator inventoryTranslator = InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR;
|
|
|
|
/**
|
|
* Use {@link #getNextItemNetId()} instead for consistency
|
|
*/
|
|
@Getter(AccessLevel.NONE)
|
|
private final AtomicInteger itemNetId = new AtomicInteger(2);
|
|
|
|
@Setter
|
|
private ScheduledFuture<?> craftingGridFuture;
|
|
|
|
/**
|
|
* Stores session collision
|
|
*/
|
|
private final CollisionManager collisionManager;
|
|
|
|
/**
|
|
* Stores the block mappings for this specific version.
|
|
*/
|
|
@Setter
|
|
private BlockMappings blockMappings;
|
|
|
|
/**
|
|
* Stores the item translations for this specific version.
|
|
*/
|
|
@Setter
|
|
private ItemMappings itemMappings;
|
|
|
|
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = new Long2ObjectOpenHashMap<>();
|
|
|
|
/**
|
|
* Required to decode biomes correctly.
|
|
*/
|
|
@Setter
|
|
private int biomeGlobalPalette;
|
|
/**
|
|
* Stores the map between Java and Bedrock biome network IDs.
|
|
*/
|
|
private final Int2IntMap biomeTranslations = new Int2IntOpenHashMap();
|
|
|
|
/**
|
|
* A map of Vector3i positions to Java entities.
|
|
* Used for translating Bedrock block actions to Java entity actions.
|
|
*/
|
|
private final Map<Vector3i, ItemFrameEntity> itemFrameCache = new Object2ObjectOpenHashMap<>();
|
|
|
|
/**
|
|
* Stores a list of all lectern locations and their block entity tags.
|
|
* See {@link WorldManager#getLecternDataAt(GeyserSession, int, int, int, boolean)}
|
|
* for more information.
|
|
*/
|
|
private final Set<Vector3i> lecternCache;
|
|
|
|
/**
|
|
* A list of all players that have a player head on with a custom texture.
|
|
* Our workaround for these players is to give them a custom skin and geometry to emulate wearing a custom skull.
|
|
*/
|
|
private final Set<UUID> playerWithCustomHeads = new ObjectOpenHashSet<>();
|
|
|
|
@Setter
|
|
private boolean droppingLecternBook;
|
|
|
|
@Setter
|
|
private Vector2i lastChunkPosition = null;
|
|
@Setter
|
|
private int clientRenderDistance = -1;
|
|
private int serverRenderDistance;
|
|
|
|
// Exposed for GeyserConnect usage
|
|
protected boolean sentSpawnPacket;
|
|
|
|
private boolean loggedIn;
|
|
private boolean loggingIn;
|
|
|
|
@Setter
|
|
private boolean spawned;
|
|
/**
|
|
* Accessed on the initial Java and Bedrock packet processing threads
|
|
*/
|
|
private volatile boolean closed;
|
|
|
|
@Setter
|
|
private GameMode gameMode = GameMode.SURVIVAL;
|
|
|
|
/**
|
|
* Keeps track of the world name for respawning.
|
|
*/
|
|
@Setter
|
|
private String worldName = null;
|
|
|
|
private boolean sneaking;
|
|
|
|
/**
|
|
* Stores the Java pose that the server and/or Geyser believes the player currently has.
|
|
*/
|
|
@Setter
|
|
private Pose pose = Pose.STANDING;
|
|
|
|
@Setter
|
|
private boolean sprinting;
|
|
|
|
/**
|
|
* Whether the player is swimming in water.
|
|
* Used to update speed when crawling.
|
|
*/
|
|
@Setter
|
|
private boolean swimmingInWater;
|
|
|
|
/**
|
|
* Tracks the original speed attribute.
|
|
*
|
|
* We need to do this in order to emulate speeds when sneaking under 1.5-blocks-tall areas if the player isn't sneaking,
|
|
* and when crawling.
|
|
*/
|
|
@Setter
|
|
private float originalSpeedAttribute;
|
|
|
|
/**
|
|
* The dimension of the player.
|
|
* As all entities are in the same world, this can be safely applied to all other entities.
|
|
*/
|
|
@Setter
|
|
private String dimension = DimensionUtils.OVERWORLD;
|
|
@MonotonicNonNull
|
|
@Setter
|
|
private JavaDimension dimensionType = null;
|
|
/**
|
|
* All dimensions that the client could possibly connect to.
|
|
*/
|
|
private final Map<String, JavaDimension> dimensions = new Object2ObjectOpenHashMap<>(3);
|
|
|
|
private final Int2ObjectMap<TextDecoration> chatTypes = new Int2ObjectOpenHashMap<>(7);
|
|
|
|
@Setter
|
|
private int breakingBlock;
|
|
|
|
@Setter
|
|
private Vector3i lastBlockPlacePosition;
|
|
|
|
@Setter
|
|
private String lastBlockPlacedId;
|
|
|
|
@Setter
|
|
private boolean interacting;
|
|
|
|
/**
|
|
* Stores the last position of the block the player interacted with. This can either be a block that the client
|
|
* placed or an existing block the player interacted with (for example, a chest). <br>
|
|
* Initialized as (0, 0, 0) so it is always not-null.
|
|
*/
|
|
@Setter
|
|
private Vector3i lastInteractionBlockPosition = Vector3i.ZERO;
|
|
|
|
/**
|
|
* Stores the position of the player the last time they interacted.
|
|
* Used to verify that the player did not move since their last interaction. <br>
|
|
* Initialized as (0, 0, 0) so it is always not-null.
|
|
*/
|
|
@Setter
|
|
private Vector3f lastInteractionPlayerPosition = Vector3f.ZERO;
|
|
|
|
/**
|
|
* The entity that the client is currently looking at.
|
|
*/
|
|
@Setter
|
|
private Entity mouseoverEntity;
|
|
|
|
@Setter
|
|
private Int2ObjectMap<GeyserRecipe> craftingRecipes;
|
|
private final AtomicInteger lastRecipeNetId;
|
|
|
|
/**
|
|
* Saves a list of all stonecutter recipes, for use in a stonecutter inventory.
|
|
* The key is the Java ID of the item; the values are all the possible outputs' Java IDs sorted by their string identifier
|
|
*/
|
|
@Setter
|
|
private Int2ObjectMap<GeyserStonecutterData> stonecutterRecipes;
|
|
|
|
/**
|
|
* Whether to work around 1.13's different behavior in villager trading menus.
|
|
*/
|
|
@Setter
|
|
private boolean emulatePost1_13Logic = true;
|
|
/**
|
|
* Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container
|
|
* packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot
|
|
* contents before any transaction is done. With the current ViaVersion structure, if we do not send what 1.16 expects
|
|
* and send multiple click container packets, then successive transactions will be rejected.
|
|
*/
|
|
@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.
|
|
* Setting a default fixes cooldowns not showing up on a fresh world.
|
|
*/
|
|
@Setter
|
|
private double attackSpeed = 4.0d;
|
|
/**
|
|
* The time of the last hit. Used to gauge how long the cooldown is taking.
|
|
* This is a session variable in order to prevent more scheduled threads than necessary.
|
|
*/
|
|
@Setter
|
|
private long lastHitTime;
|
|
|
|
/**
|
|
* Saves if the client is steering left on a boat.
|
|
*/
|
|
@Setter
|
|
private boolean steeringLeft;
|
|
/**
|
|
* Saves if the client is steering right on a boat.
|
|
*/
|
|
@Setter
|
|
private boolean steeringRight;
|
|
|
|
/**
|
|
* Store the last time the player interacted. Used to fix a right-click spam bug.
|
|
* See https://github.com/GeyserMC/Geyser/issues/503 for context.
|
|
*/
|
|
@Setter
|
|
private long lastInteractionTime;
|
|
|
|
/**
|
|
* Stores whether the player intended to place a bucket.
|
|
*/
|
|
@Setter
|
|
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.
|
|
*/
|
|
@Setter
|
|
private long lastMovementTimestamp = System.currentTimeMillis();
|
|
|
|
/**
|
|
* Used to send a ServerboundMoveVehiclePacket for every PlayerInputPacket after idling on a boat/horse for more than 100ms
|
|
*/
|
|
@Setter
|
|
private long lastVehicleMoveTimestamp = System.currentTimeMillis();
|
|
|
|
/**
|
|
* Counts how many ticks have occurred since an arm animation started.
|
|
* -1 means there is no active arm swing.
|
|
*/
|
|
@Getter(AccessLevel.NONE)
|
|
private int armAnimationTicks = -1;
|
|
|
|
/**
|
|
* Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless.
|
|
*/
|
|
private boolean daylightCycle = true;
|
|
|
|
private boolean reducedDebugInfo = false;
|
|
|
|
/**
|
|
* The op permission level set by the server
|
|
*/
|
|
@Setter
|
|
private int opPermissionLevel = 0;
|
|
|
|
/**
|
|
* If the current player can fly
|
|
*/
|
|
@Setter
|
|
private boolean canFly = false;
|
|
|
|
/**
|
|
* If the current player is flying
|
|
*/
|
|
private boolean flying = false;
|
|
|
|
@Setter
|
|
private boolean instabuild = false;
|
|
|
|
@Setter
|
|
private float flySpeed;
|
|
@Setter
|
|
private float walkSpeed;
|
|
|
|
/**
|
|
* Caches current rain status.
|
|
*/
|
|
@Setter
|
|
private boolean raining = false;
|
|
|
|
/**
|
|
* Caches current thunder status.
|
|
*/
|
|
@Setter
|
|
private boolean thunder = false;
|
|
|
|
/**
|
|
* 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 Object2IntMap<Statistic> statistics = new Object2IntOpenHashMap<>(0);
|
|
|
|
/**
|
|
* Whether we're expecting statistics to be sent back to us.
|
|
*/
|
|
@Setter
|
|
private boolean waitingForStatistics = false;
|
|
|
|
private final Set<String> fogNameSpaces = new HashSet<>();
|
|
|
|
private final Set<UUID> emotes;
|
|
|
|
/**
|
|
* Whether advanced tooltips will be added to the player's items.
|
|
*/
|
|
@Setter
|
|
private boolean advancedTooltips = false;
|
|
|
|
/**
|
|
* The thread that will run every 50 milliseconds - one Minecraft tick.
|
|
*/
|
|
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) {
|
|
this.geyser = geyser;
|
|
this.upstream = new UpstreamSession(bedrockServerSession);
|
|
this.eventLoop = eventLoop;
|
|
|
|
this.advancementsCache = new AdvancementsCache(this);
|
|
this.bookEditCache = new BookEditCache(this);
|
|
this.chunkCache = new ChunkCache(this);
|
|
this.entityCache = new EntityCache(this);
|
|
this.effectCache = new EntityEffectCache();
|
|
this.formCache = new FormCache(this);
|
|
this.lodestoneCache = new LodestoneCache();
|
|
this.pistonCache = new PistonCache(this);
|
|
this.preferencesCache = new PreferencesCache(this);
|
|
this.skullCache = new SkullCache(this);
|
|
this.tagCache = new TagCache();
|
|
this.worldCache = new WorldCache(this);
|
|
|
|
this.worldBorder = new WorldBorder(this);
|
|
|
|
this.collisionManager = new CollisionManager(this);
|
|
|
|
this.playerEntity = new SessionPlayerEntity(this);
|
|
collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition());
|
|
|
|
this.playerInventory = new PlayerInventory();
|
|
this.openInventory = null;
|
|
this.craftingRecipes = new Int2ObjectOpenHashMap<>();
|
|
this.lastRecipeNetId = new AtomicInteger(1);
|
|
|
|
this.spawned = false;
|
|
this.loggedIn = false;
|
|
|
|
if (geyser.getWorldManager().shouldExpectLecternHandled()) {
|
|
// Unneeded on these platforms
|
|
this.lecternCache = null;
|
|
} else {
|
|
this.lecternCache = new ObjectOpenHashSet<>();
|
|
}
|
|
|
|
if (geyser.getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.NO_EMOTES) {
|
|
this.emotes = new HashSet<>();
|
|
geyser.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes()));
|
|
} else {
|
|
this.emotes = null;
|
|
}
|
|
|
|
bedrockServerSession.addDisconnectHandler(disconnectReason -> {
|
|
String message = switch (disconnectReason) {
|
|
// A generic message that just means the player quit normally.
|
|
case CLOSED_BY_REMOTE_PEER -> GeyserLocale.getLocaleStringLog("geyser.network.disconnect.closed_by_remote_peer");
|
|
case TIMED_OUT -> GeyserLocale.getLocaleStringLog("geyser.network.disconnect.timed_out");
|
|
default -> disconnectReason.name();
|
|
};
|
|
|
|
disconnect(message);
|
|
});
|
|
|
|
this.remoteServer = geyser.defaultRemoteServer();
|
|
}
|
|
|
|
/**
|
|
* Send all necessary packets to load Bedrock into the server
|
|
*/
|
|
public void connect() {
|
|
startGame();
|
|
sentSpawnPacket = true;
|
|
|
|
// Set the hardcoded shield ID to the ID we just defined in StartGamePacket
|
|
upstream.getSession().getHardcodedBlockingId().set(this.itemMappings.getStoredItems().shield().getBedrockId());
|
|
|
|
if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
|
|
ItemComponentPacket componentPacket = new ItemComponentPacket();
|
|
componentPacket.getItems().addAll(itemMappings.getComponentItemData());
|
|
upstream.sendPacket(componentPacket);
|
|
}
|
|
|
|
ChunkUtils.sendEmptyChunks(this, playerEntity.getPosition().toInt(), 0, false);
|
|
|
|
BiomeDefinitionListPacket biomeDefinitionListPacket = new BiomeDefinitionListPacket();
|
|
biomeDefinitionListPacket.setDefinitions(Registries.BIOMES_NBT.get());
|
|
upstream.sendPacket(biomeDefinitionListPacket);
|
|
|
|
AvailableEntityIdentifiersPacket entityPacket = new AvailableEntityIdentifiersPacket();
|
|
entityPacket.setIdentifiers(Registries.BEDROCK_ENTITY_IDENTIFIERS.get());
|
|
upstream.sendPacket(entityPacket);
|
|
|
|
CreativeContentPacket creativePacket = new CreativeContentPacket();
|
|
creativePacket.setContents(this.itemMappings.getCreativeItems());
|
|
upstream.sendPacket(creativePacket);
|
|
|
|
// Potion mixes are registered by default, as they are needed to be able to put ingredients into the brewing stand.
|
|
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
|
|
craftingDataPacket.setCleanRecipes(true);
|
|
craftingDataPacket.getPotionMixData().addAll(Registries.POTION_MIXES.get());
|
|
upstream.sendPacket(craftingDataPacket);
|
|
|
|
PlayStatusPacket playStatusPacket = new PlayStatusPacket();
|
|
playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
|
|
upstream.sendPacket(playStatusPacket);
|
|
|
|
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
|
|
attributesPacket.setRuntimeEntityId(getPlayerEntity().getGeyserId());
|
|
// Default move speed
|
|
// Bedrock clients move very fast by default until they get an attribute packet correcting the speed
|
|
attributesPacket.setAttributes(Collections.singletonList(
|
|
new AttributeData("minecraft:movement", 0.0f, 1024f, 0.1f, 0.1f)));
|
|
upstream.sendPacket(attributesPacket);
|
|
|
|
GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket();
|
|
// Only allow the server to send health information
|
|
// Setting this to false allows natural regeneration to work false but doesn't break it being true
|
|
gamerulePacket.getGameRules().add(new GameRuleData<>("naturalregeneration", false));
|
|
// Don't let the client modify the inventory on death
|
|
// Setting this to true allows keep inventory to work if enabled but doesn't break functionality being false
|
|
gamerulePacket.getGameRules().add(new GameRuleData<>("keepinventory", true));
|
|
// Ensure client doesn't try and do anything funky; the server handles this for us
|
|
gamerulePacket.getGameRules().add(new GameRuleData<>("spawnradius", 0));
|
|
upstream.sendPacket(gamerulePacket);
|
|
}
|
|
|
|
public void authenticate(String username) {
|
|
authenticate(username, "");
|
|
}
|
|
|
|
public void authenticate(String username, String password) {
|
|
if (loggedIn) {
|
|
geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", username));
|
|
return;
|
|
}
|
|
|
|
loggingIn = true;
|
|
|
|
// Use a future to prevent timeouts as all the authentication is handled sync
|
|
CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
if (password != null && !password.isEmpty()) {
|
|
AuthenticationService authenticationService;
|
|
if (microsoftAccount) {
|
|
authenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID);
|
|
} else {
|
|
authenticationService = new MojangAuthenticationService();
|
|
}
|
|
authenticationService.setUsername(username);
|
|
authenticationService.setPassword(password);
|
|
authenticationService.login();
|
|
|
|
GameProfile profile = authenticationService.getSelectedProfile();
|
|
if (profile == null) {
|
|
// Java account is offline
|
|
disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
|
|
return null;
|
|
}
|
|
|
|
protocol = new MinecraftProtocol(profile, authenticationService.getAccessToken());
|
|
} else {
|
|
// always replace spaces when using Floodgate,
|
|
// as usernames with spaces cause issues with Bungeecord's login cycle.
|
|
// However, this doesn't affect the final username as Floodgate is still in charge of that.
|
|
// So if you have (for example) replace spaces enabled on Floodgate the spaces will re-appear.
|
|
String validUsername = username;
|
|
if (this.remoteServer.authType() == AuthType.FLOODGATE) {
|
|
validUsername = username.replace(' ', '_');
|
|
}
|
|
|
|
protocol = new MinecraftProtocol(validUsername);
|
|
}
|
|
} catch (InvalidCredentialsException | IllegalArgumentException e) {
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.login.invalid", username));
|
|
disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode()));
|
|
} catch (RequestException ex) {
|
|
disconnect(ex.getMessage());
|
|
}
|
|
return null;
|
|
}).whenComplete((aVoid, ex) -> {
|
|
if (ex != null) {
|
|
disconnect(ex.toString());
|
|
}
|
|
if (this.closed) {
|
|
if (ex != null) {
|
|
geyser.getLogger().error("", ex);
|
|
}
|
|
// Client disconnected during the authentication attempt
|
|
return;
|
|
}
|
|
|
|
connectDownstream();
|
|
});
|
|
}
|
|
|
|
public void authenticateWithRefreshToken(String refreshToken) {
|
|
if (loggedIn) {
|
|
geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name()));
|
|
return;
|
|
}
|
|
|
|
loggingIn = true;
|
|
|
|
CompletableFuture.supplyAsync(() -> {
|
|
MsaAuthenticationService service = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID);
|
|
service.setRefreshToken(refreshToken);
|
|
try {
|
|
service.login();
|
|
} catch (RequestException e) {
|
|
geyser.getLogger().error("Error while attempting to use refresh token for " + bedrockUsername() + "!", e);
|
|
return Boolean.FALSE;
|
|
}
|
|
|
|
GameProfile profile = service.getSelectedProfile();
|
|
if (profile == null) {
|
|
// Java account is offline
|
|
disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
|
|
return null;
|
|
}
|
|
|
|
protocol = new MinecraftProtocol(profile, service.getAccessToken());
|
|
geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken());
|
|
return Boolean.TRUE;
|
|
}).whenComplete((successful, ex) -> {
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
if (successful == Boolean.FALSE) {
|
|
// The player is waiting for a spawn packet, so let's spawn them in now to show them forms
|
|
connect();
|
|
// Will be cached for after login
|
|
LoginEncryptionUtils.buildAndShowTokenExpiredWindow(this);
|
|
return;
|
|
}
|
|
|
|
connectDownstream();
|
|
});
|
|
}
|
|
|
|
public void authenticateWithMicrosoftCode() {
|
|
authenticateWithMicrosoftCode(false);
|
|
}
|
|
|
|
/**
|
|
* Present a form window to the user asking to log in with another web browser
|
|
*/
|
|
public void authenticateWithMicrosoftCode(boolean offlineAccess) {
|
|
if (loggedIn) {
|
|
geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name()));
|
|
return;
|
|
}
|
|
|
|
loggingIn = true;
|
|
|
|
// This just looks cool
|
|
SetTimePacket packet = new SetTimePacket();
|
|
packet.setTime(16000);
|
|
sendUpstreamPacket(packet);
|
|
|
|
final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask(
|
|
getAuthData().xuid()
|
|
);
|
|
task.setOnline(true);
|
|
task.resetTimer();
|
|
|
|
if (task.getAuthentication().isDone()) {
|
|
onMicrosoftLoginComplete(task);
|
|
} else {
|
|
task.getCode(offlineAccess).whenComplete((response, ex) -> {
|
|
boolean connected = !closed;
|
|
if (ex != null) {
|
|
if (connected) {
|
|
geyser.getLogger().error("Failed to get Microsoft auth code", ex);
|
|
disconnect(ex.toString());
|
|
}
|
|
task.cleanup(); // error getting auth code -> clean up immediately
|
|
} else if (connected) {
|
|
LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response);
|
|
task.getAuthentication().whenComplete((r, $) -> onMicrosoftLoginComplete(task));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If successful, also begins connecting to the Java server.
|
|
*/
|
|
public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.AuthenticationTask task) {
|
|
if (closed) {
|
|
return false;
|
|
}
|
|
task.cleanup(); // player is online -> remove pending authentication immediately
|
|
Throwable ex = task.getLoginException();
|
|
if (ex != null) {
|
|
geyser.getLogger().error("Failed to log in with Microsoft code!", ex);
|
|
disconnect(ex.toString());
|
|
} else {
|
|
MsaAuthenticationService service = task.getMsaAuthenticationService();
|
|
GameProfile selectedProfile = service.getSelectedProfile();
|
|
if (selectedProfile == null) {
|
|
disconnect(GeyserLocale.getPlayerLocaleString(
|
|
"geyser.network.remote.invalid_account",
|
|
clientData.getLanguageCode()
|
|
));
|
|
} else {
|
|
this.protocol = new MinecraftProtocol(
|
|
selectedProfile,
|
|
service.getAccessToken()
|
|
);
|
|
connectDownstream();
|
|
|
|
// Save our refresh token for later use
|
|
geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken());
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* After getting whatever credentials needed, we attempt to join the Java server.
|
|
*/
|
|
private void connectDownstream() {
|
|
boolean floodgate = this.remoteServer.authType() == AuthType.FLOODGATE;
|
|
|
|
// Start ticking
|
|
tickThread = eventLoop.scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
|
|
|
|
if (geyser.getBootstrap().getSocketAddress() != null) {
|
|
// We're going to connect through the JVM and not through TCP
|
|
downstream = new LocalSession(this.remoteServer.address(), this.remoteServer.port(),
|
|
geyser.getBootstrap().getSocketAddress(), upstream.getAddress().getAddress().getHostAddress(),
|
|
this.protocol, this.protocol.createHelper());
|
|
} else {
|
|
downstream = new TcpClientSession(this.remoteServer.address(), this.remoteServer.port(), this.protocol);
|
|
disableSrvResolving();
|
|
}
|
|
|
|
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
|
|
downstream.setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
|
|
downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
|
|
}
|
|
if (geyser.getConfig().isForwardPlayerPing()) {
|
|
// Let Geyser handle sending the keep alive
|
|
downstream.setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
|
|
}
|
|
downstream.addListener(new SessionAdapter() {
|
|
@Override
|
|
public void packetSending(PacketSendingEvent event) {
|
|
//todo move this somewhere else
|
|
if (event.getPacket() instanceof ClientIntentionPacket) {
|
|
String addressSuffix;
|
|
if (floodgate) {
|
|
byte[] encryptedData;
|
|
|
|
try {
|
|
FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
|
|
FloodgateCipher cipher = geyser.getCipher();
|
|
|
|
String bedrockAddress = upstream.getAddress().getAddress().getHostAddress();
|
|
// both BungeeCord and Velocity remove the IPv6 scope (if there is one) for Spigot
|
|
int ipv6ScopeIndex = bedrockAddress.indexOf('%');
|
|
if (ipv6ScopeIndex != -1) {
|
|
bedrockAddress = bedrockAddress.substring(0, ipv6ScopeIndex);
|
|
}
|
|
|
|
encryptedData = cipher.encryptFromString(BedrockData.of(
|
|
clientData.getGameVersion(),
|
|
authData.name(),
|
|
authData.xuid(),
|
|
clientData.getDeviceOs().ordinal(),
|
|
clientData.getLanguageCode(),
|
|
clientData.getUiProfile().ordinal(),
|
|
clientData.getCurrentInputMode().ordinal(),
|
|
bedrockAddress,
|
|
skinUploader.getId(),
|
|
skinUploader.getVerifyCode()
|
|
).toString());
|
|
} catch (Exception e) {
|
|
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
|
|
disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode()));
|
|
return;
|
|
}
|
|
|
|
addressSuffix = '\0' + new String(encryptedData, StandardCharsets.UTF_8);
|
|
} else {
|
|
addressSuffix = "";
|
|
}
|
|
|
|
ClientIntentionPacket intentionPacket = event.getPacket();
|
|
|
|
String address;
|
|
if (geyser.getConfig().getRemote().isForwardHost()) {
|
|
address = clientData.getServerAddress().split(":")[0];
|
|
} else {
|
|
address = intentionPacket.getHostname();
|
|
}
|
|
|
|
event.setPacket(intentionPacket.withHostname(address + addressSuffix));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void connected(ConnectedEvent event) {
|
|
loggingIn = false;
|
|
loggedIn = true;
|
|
|
|
if (downstream instanceof LocalSession) {
|
|
// Connected directly to the server
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect_internal",
|
|
authData.name(), protocol.getProfile().getName()));
|
|
} else {
|
|
// Connected to an IP address
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect",
|
|
authData.name(), protocol.getProfile().getName(), remoteServer.address()));
|
|
}
|
|
|
|
UUID uuid = protocol.getProfile().getId();
|
|
if (uuid == null) {
|
|
// Set what our UUID *probably* is going to be
|
|
if (remoteServer.authType() == AuthType.FLOODGATE) {
|
|
uuid = new UUID(0, Long.parseLong(authData.xuid()));
|
|
} else {
|
|
uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + protocol.getProfile().getName()).getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
}
|
|
playerEntity.setUuid(uuid);
|
|
playerEntity.setUsername(protocol.getProfile().getName());
|
|
|
|
String locale = clientData.getLanguageCode();
|
|
|
|
// Let the user know there locale may take some time to download
|
|
// as it has to be extracted from a JAR
|
|
if (locale.equalsIgnoreCase("en_us") && !MinecraftLocale.LOCALE_MAPPINGS.containsKey("en_us")) {
|
|
// This should probably be left hardcoded as it will only show for en_us clients
|
|
sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
|
|
}
|
|
|
|
// Download and load the language for the player
|
|
MinecraftLocale.downloadAndLoadLocale(locale);
|
|
}
|
|
|
|
@Override
|
|
public void disconnected(DisconnectedEvent event) {
|
|
loggingIn = false;
|
|
loggedIn = false;
|
|
|
|
String disconnectMessage;
|
|
Throwable cause = event.getCause();
|
|
if (cause instanceof UnexpectedEncryptionException) {
|
|
if (remoteServer.authType() != AuthType.FLOODGATE) {
|
|
// Server expects online mode
|
|
disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale());
|
|
// Explain that they may be looking for Floodgate.
|
|
geyser.getLogger().warning(GeyserLocale.getLocaleStringLog(
|
|
geyser.getPlatformType() == PlatformType.STANDALONE ?
|
|
"geyser.network.remote.floodgate_explanation_standalone"
|
|
: "geyser.network.remote.floodgate_explanation_plugin",
|
|
Constants.FLOODGATE_DOWNLOAD_LOCATION
|
|
));
|
|
} else {
|
|
// Likely that Floodgate is not configured correctly.
|
|
disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.floodgate_login_error", locale());
|
|
if (geyser.getPlatformType() == PlatformType.STANDALONE) {
|
|
geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.remote.floodgate_login_error_standalone"));
|
|
}
|
|
}
|
|
} else if (cause instanceof ConnectException) {
|
|
// Server is offline, probably
|
|
disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.server_offline", locale());
|
|
} else {
|
|
disconnectMessage = MessageTranslator.convertMessageLenient(event.getReason());
|
|
}
|
|
|
|
if (downstream instanceof LocalSession) {
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect_internal", authData.name(), disconnectMessage));
|
|
} else {
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", authData.name(), remoteServer.address(), disconnectMessage));
|
|
}
|
|
if (cause != null) {
|
|
cause.printStackTrace();
|
|
}
|
|
|
|
upstream.disconnect(disconnectMessage);
|
|
}
|
|
|
|
@Override
|
|
public void packetReceived(Session session, Packet packet) {
|
|
Registries.JAVA_PACKET_TRANSLATORS.translate(packet.getClass(), packet, GeyserSession.this);
|
|
}
|
|
|
|
@Override
|
|
public void packetError(PacketErrorEvent event) {
|
|
geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage()));
|
|
if (geyser.getConfig().isDebugMode())
|
|
event.getCause().printStackTrace();
|
|
event.setSuppress(true);
|
|
}
|
|
});
|
|
|
|
if (!daylightCycle) {
|
|
setDaylightCycle(true);
|
|
}
|
|
|
|
downstream.connect(false);
|
|
}
|
|
|
|
public void disconnect(String reason) {
|
|
if (!closed) {
|
|
loggedIn = false;
|
|
if (downstream != null) {
|
|
downstream.disconnect(reason);
|
|
} else {
|
|
// Downstream's disconnect will fire an event that prints a log message
|
|
// Otherwise, we print a message here
|
|
String address = geyser.getConfig().isLogPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : "<IP address withheld>";
|
|
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, reason));
|
|
}
|
|
if (!upstream.isClosed()) {
|
|
upstream.disconnect(reason);
|
|
}
|
|
geyser.getSessionManager().removeSession(this);
|
|
if (authData != null) {
|
|
PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid());
|
|
if (task != null) {
|
|
task.setOnline(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tickThread != null) {
|
|
tickThread.cancel(false);
|
|
}
|
|
|
|
closed = true;
|
|
}
|
|
|
|
/**
|
|
* Executes a task and prints a stack trace if an error occurs.
|
|
*/
|
|
public void executeInEventLoop(Runnable runnable) {
|
|
eventLoop.execute(() -> {
|
|
try {
|
|
runnable.run();
|
|
} catch (Throwable e) {
|
|
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Schedules a task and prints a stack trace if an error occurs.
|
|
*/
|
|
public ScheduledFuture<?> scheduleInEventLoop(Runnable runnable, long duration, TimeUnit timeUnit) {
|
|
return eventLoop.schedule(() -> {
|
|
try {
|
|
runnable.run();
|
|
} catch (Throwable e) {
|
|
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
|
|
}
|
|
}, duration, timeUnit);
|
|
}
|
|
|
|
/**
|
|
* Called every 50 milliseconds - one Minecraft tick.
|
|
*/
|
|
protected void tick() {
|
|
try {
|
|
pistonCache.tick();
|
|
// Check to see if the player's position needs updating - a position update should be sent once every 3 seconds
|
|
if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) {
|
|
// Recalculate in case something else changed position
|
|
Vector3d position = collisionManager.adjustBedrockPosition(playerEntity.getPosition(), playerEntity.isOnGround(), false);
|
|
// A null return value cancels the packet
|
|
if (position != null) {
|
|
ServerboundMovePlayerPosPacket packet = new ServerboundMovePlayerPosPacket(playerEntity.isOnGround(),
|
|
position.getX(), position.getY(), position.getZ());
|
|
sendDownstreamPacket(packet);
|
|
}
|
|
lastMovementTimestamp = System.currentTimeMillis();
|
|
}
|
|
|
|
if (worldBorder.isResizing()) {
|
|
worldBorder.resize();
|
|
}
|
|
|
|
boolean shouldShowFog = !worldBorder.isWithinWarningBoundaries();
|
|
if (shouldShowFog || worldBorder.isCloseToBorderBoundaries()) {
|
|
// Show particles representing where the world border is
|
|
worldBorder.drawWall();
|
|
// Set the mood
|
|
if (shouldShowFog && !isInWorldBorderWarningArea) {
|
|
isInWorldBorderWarningArea = true;
|
|
sendFog("minecraft:fog_crimson_forest");
|
|
}
|
|
}
|
|
if (!shouldShowFog && isInWorldBorderWarningArea) {
|
|
// Clear fog as we are outside the world border now
|
|
removeFog("minecraft:fog_crimson_forest");
|
|
isInWorldBorderWarningArea = false;
|
|
}
|
|
|
|
|
|
for (Tickable entity : entityCache.getTickableEntities()) {
|
|
entity.tick();
|
|
}
|
|
|
|
if (armAnimationTicks != -1) {
|
|
// As of 1.18.2 Java Edition, it appears that the swing time is dynamically updated depending on the
|
|
// player's effect status, but the animation can cut short if the duration suddenly decreases
|
|
// (from suddenly no longer having mining fatigue, for example)
|
|
// This math is referenced from Java Edition 1.18.2
|
|
int swingTotalDuration;
|
|
int hasteLevel = Math.max(effectCache.getHaste(), effectCache.getConduitPower());
|
|
if (hasteLevel > 0) {
|
|
swingTotalDuration = 6 - hasteLevel;
|
|
} else {
|
|
int miningFatigueLevel = effectCache.getMiningFatigue();
|
|
if (miningFatigueLevel > 0) {
|
|
swingTotalDuration = 6 + miningFatigueLevel * 2;
|
|
} else {
|
|
swingTotalDuration = 6;
|
|
}
|
|
}
|
|
if (++armAnimationTicks >= swingTotalDuration) {
|
|
if (sneaking) {
|
|
// Attempt to re-activate blocking as our swing animation is up
|
|
if (attemptToBlock()) {
|
|
playerEntity.updateBedrockMetadata();
|
|
}
|
|
}
|
|
armAnimationTicks = -1;
|
|
}
|
|
}
|
|
} catch (Throwable throwable) {
|
|
throwable.printStackTrace();
|
|
}
|
|
}
|
|
|
|
public void setAuthenticationData(AuthData authData) {
|
|
this.authData = authData;
|
|
}
|
|
|
|
public void startSneaking() {
|
|
// Toggle the shield, if there is no ongoing arm animation
|
|
// This matches Bedrock Edition behavior as of 1.18.12
|
|
if (armAnimationTicks == -1) {
|
|
attemptToBlock();
|
|
}
|
|
|
|
setSneaking(true);
|
|
}
|
|
|
|
public void stopSneaking() {
|
|
disableBlocking();
|
|
|
|
setSneaking(false);
|
|
}
|
|
|
|
private void setSneaking(boolean sneaking) {
|
|
this.sneaking = sneaking;
|
|
|
|
// Update pose and bounding box on our end
|
|
AttributeData speedAttribute;
|
|
if (!sneaking && (speedAttribute = adjustSpeed()) != null) {
|
|
// Update attributes since we're still "sneaking" under a 1.5-block-tall area
|
|
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
|
|
attributesPacket.setRuntimeEntityId(playerEntity.getGeyserId());
|
|
attributesPacket.setAttributes(Collections.singletonList(speedAttribute));
|
|
sendUpstreamPacket(attributesPacket);
|
|
// the server *should* update our pose once it has returned to normal
|
|
} else {
|
|
if (!flying) {
|
|
// The pose and bounding box should not be updated if the player is flying
|
|
setSneakingPose(sneaking);
|
|
}
|
|
collisionManager.updateScaffoldingFlags(false);
|
|
}
|
|
|
|
playerEntity.updateBedrockMetadata();
|
|
|
|
if (mouseoverEntity != null) {
|
|
// Horses, etc can change their property depending on if you're sneaking
|
|
mouseoverEntity.updateInteractiveTag();
|
|
}
|
|
}
|
|
|
|
private void setSneakingPose(boolean sneaking) {
|
|
if (this.pose == Pose.SNEAKING && !sneaking) {
|
|
this.pose = Pose.STANDING;
|
|
playerEntity.setBoundingBoxHeight(playerEntity.getDefinition().height());
|
|
} else if (sneaking) {
|
|
this.pose = Pose.SNEAKING;
|
|
playerEntity.setBoundingBoxHeight(1.5f);
|
|
}
|
|
playerEntity.setFlag(EntityFlag.SNEAKING, sneaking);
|
|
}
|
|
|
|
public void setSwimming(boolean swimming) {
|
|
if (swimming) {
|
|
this.pose = Pose.SWIMMING;
|
|
playerEntity.setBoundingBoxHeight(0.6f);
|
|
} else {
|
|
this.pose = Pose.STANDING;
|
|
playerEntity.setBoundingBoxHeight(playerEntity.getDefinition().height());
|
|
}
|
|
playerEntity.setFlag(EntityFlag.SWIMMING, swimming);
|
|
playerEntity.updateBedrockMetadata();
|
|
}
|
|
|
|
public void setFlying(boolean flying) {
|
|
this.flying = flying;
|
|
|
|
if (sneaking) {
|
|
// update bounding box as it is not reduced when flying
|
|
setSneakingPose(!flying);
|
|
playerEntity.updateBedrockMetadata();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adjusts speed if the player is crawling.
|
|
*
|
|
* @return not null if attributes should be updated.
|
|
*/
|
|
public AttributeData adjustSpeed() {
|
|
AttributeData currentPlayerSpeed = playerEntity.getAttributes().get(GeyserAttributeType.MOVEMENT_SPEED);
|
|
if (currentPlayerSpeed != null) {
|
|
if ((pose.equals(Pose.SNEAKING) && !sneaking && collisionManager.mustPlayerSneakHere()) ||
|
|
(!swimmingInWater && playerEntity.getFlag(EntityFlag.SWIMMING) && !collisionManager.isPlayerInWater())) {
|
|
// Either of those conditions means that Bedrock goes zoom when they shouldn't be
|
|
AttributeData speedAttribute = GeyserAttributeType.MOVEMENT_SPEED.getAttribute(originalSpeedAttribute / 3.32f);
|
|
playerEntity.getAttributes().put(GeyserAttributeType.MOVEMENT_SPEED, speedAttribute);
|
|
return speedAttribute;
|
|
} else if (originalSpeedAttribute != currentPlayerSpeed.getValue()) {
|
|
// Speed has reset to normal
|
|
AttributeData speedAttribute = GeyserAttributeType.MOVEMENT_SPEED.getAttribute(originalSpeedAttribute);
|
|
playerEntity.getAttributes().put(GeyserAttributeType.MOVEMENT_SPEED, speedAttribute);
|
|
return speedAttribute;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a shield is in either hand to activate blocking. If so, it sets the Bedrock client to display
|
|
* blocking and sends a packet to the Java server.
|
|
*/
|
|
private boolean attemptToBlock() {
|
|
ItemMapping shield = itemMappings.getStoredItems().shield();
|
|
|
|
ServerboundUseItemPacket useItemPacket;
|
|
if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) {
|
|
useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, worldCache.nextPredictionSequence());
|
|
} else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) {
|
|
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, worldCache.nextPredictionSequence());
|
|
} else {
|
|
// No blocking
|
|
return false;
|
|
}
|
|
|
|
sendDownstreamPacket(useItemPacket);
|
|
playerEntity.setFlag(EntityFlag.BLOCKING, true);
|
|
// Metadata should be updated later
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Starts ticking the amount of time that the Bedrock client has been swinging their arm, and disables blocking if
|
|
* blocking.
|
|
*/
|
|
public void activateArmAnimationTicking() {
|
|
armAnimationTicks = 0;
|
|
if (disableBlocking()) {
|
|
playerEntity.updateBedrockMetadata();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates to the client to stop blocking and tells the Java server the same.
|
|
*/
|
|
private boolean disableBlocking() {
|
|
if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
|
|
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
|
|
Vector3i.ZERO, Direction.DOWN, 0);
|
|
sendDownstreamPacket(releaseItemPacket);
|
|
playerEntity.setFlag(EntityFlag.BLOCKING, false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Will be overwritten for GeyserConnect.
|
|
*/
|
|
protected void disableSrvResolving() {
|
|
this.downstream.setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, false);
|
|
}
|
|
|
|
@Override
|
|
public String name() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void sendMessage(String message) {
|
|
TextPacket textPacket = new TextPacket();
|
|
textPacket.setPlatformChatId("");
|
|
textPacket.setSourceName("");
|
|
textPacket.setXuid("");
|
|
textPacket.setType(TextPacket.Type.CHAT);
|
|
textPacket.setNeedsTranslation(false);
|
|
textPacket.setMessage(message);
|
|
|
|
upstream.sendPacket(textPacket);
|
|
}
|
|
|
|
@Override
|
|
public boolean isConsole() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public String locale() {
|
|
return clientData.getLanguageCode();
|
|
}
|
|
|
|
/**
|
|
* Sends a chat message to the Java server.
|
|
*/
|
|
public void sendChat(String message) {
|
|
sendDownstreamPacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet()));
|
|
}
|
|
|
|
/**
|
|
* Sends a command to the Java server.
|
|
*/
|
|
public void sendCommand(String command) {
|
|
sendDownstreamPacket(new ServerboundChatCommandPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet()));
|
|
}
|
|
|
|
public void setServerRenderDistance(int renderDistance) {
|
|
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
|
|
this.serverRenderDistance = renderDistance;
|
|
|
|
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
|
|
chunkRadiusUpdatedPacket.setRadius(renderDistance);
|
|
upstream.sendPacket(chunkRadiusUpdatedPacket);
|
|
}
|
|
|
|
public InetSocketAddress getSocketAddress() {
|
|
return this.upstream.getAddress();
|
|
}
|
|
|
|
public boolean sendForm(@NonNull Form form) {
|
|
formCache.showForm(form);
|
|
return true;
|
|
}
|
|
|
|
public boolean sendForm(@NonNull FormBuilder<?, ?, ?> formBuilder) {
|
|
formCache.showForm(formBuilder.build());
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Cumulus version 1.1, and will be removed when Cumulus 2.0 releases. Please use the new forms instead.
|
|
*/
|
|
@Deprecated
|
|
public void sendForm(org.geysermc.cumulus.Form<?> form) {
|
|
sendForm(form.newForm());
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Cumulus version 1.1, and will be removed when Cumulus 2.0 releases. Please use the new forms instead.
|
|
*/
|
|
@Deprecated
|
|
public void sendForm(org.geysermc.cumulus.util.FormBuilder<?, ?> formBuilder) {
|
|
sendForm(formBuilder.build());
|
|
}
|
|
|
|
private void startGame() {
|
|
StartGamePacket startGamePacket = new StartGamePacket();
|
|
startGamePacket.setUniqueEntityId(playerEntity.getGeyserId());
|
|
startGamePacket.setRuntimeEntityId(playerEntity.getGeyserId());
|
|
startGamePacket.setPlayerGameType(switch (gameMode) {
|
|
case CREATIVE -> GameType.CREATIVE;
|
|
case ADVENTURE -> GameType.ADVENTURE;
|
|
default -> GameType.SURVIVAL;
|
|
});
|
|
startGamePacket.setPlayerPosition(Vector3f.from(0, 69, 0));
|
|
startGamePacket.setRotation(Vector2f.from(1, 1));
|
|
|
|
startGamePacket.setSeed(-1L);
|
|
startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(chunkCache.getBedrockDimension()));
|
|
startGamePacket.setGeneratorId(1);
|
|
startGamePacket.setLevelGameType(GameType.SURVIVAL);
|
|
startGamePacket.setDifficulty(1);
|
|
startGamePacket.setDefaultSpawn(Vector3i.ZERO);
|
|
startGamePacket.setAchievementsDisabled(!geyser.getConfig().isXboxAchievementsEnabled());
|
|
startGamePacket.setCurrentTick(-1);
|
|
startGamePacket.setEduEditionOffers(0);
|
|
startGamePacket.setEduFeaturesEnabled(false);
|
|
startGamePacket.setRainLevel(0);
|
|
startGamePacket.setLightningLevel(0);
|
|
startGamePacket.setMultiplayerGame(true);
|
|
startGamePacket.setBroadcastingToLan(true);
|
|
startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC);
|
|
startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC);
|
|
startGamePacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled());
|
|
startGamePacket.setTexturePacksRequired(false);
|
|
startGamePacket.setBonusChestEnabled(false);
|
|
startGamePacket.setStartingWithMap(false);
|
|
startGamePacket.setTrustingPlayers(true);
|
|
startGamePacket.setDefaultPlayerPermission(PlayerPermission.MEMBER);
|
|
startGamePacket.setServerChunkTickRange(4);
|
|
startGamePacket.setBehaviorPackLocked(false);
|
|
startGamePacket.setResourcePackLocked(false);
|
|
startGamePacket.setFromLockedWorldTemplate(false);
|
|
startGamePacket.setUsingMsaGamertagsOnly(false);
|
|
startGamePacket.setFromWorldTemplate(false);
|
|
startGamePacket.setWorldTemplateOptionLocked(false);
|
|
|
|
String serverName = geyser.getConfig().getBedrock().serverName();
|
|
startGamePacket.setLevelId(serverName);
|
|
startGamePacket.setLevelName(serverName);
|
|
|
|
startGamePacket.setPremiumWorldTemplateId("00000000-0000-0000-0000-000000000000");
|
|
// startGamePacket.setCurrentTick(0);
|
|
startGamePacket.setEnchantmentSeed(0);
|
|
startGamePacket.setMultiplayerCorrelationId("");
|
|
|
|
startGamePacket.setItemEntries(this.itemMappings.getItemEntries());
|
|
|
|
startGamePacket.setVanillaVersion("*");
|
|
startGamePacket.setInventoriesServerAuthoritative(true);
|
|
startGamePacket.setServerEngine(""); // Do we want to fill this in?
|
|
|
|
startGamePacket.setPlayerPropertyData(NbtMap.EMPTY);
|
|
startGamePacket.setWorldTemplateId(UUID.randomUUID());
|
|
|
|
startGamePacket.setChatRestrictionLevel(ChatRestrictionLevel.NONE);
|
|
|
|
SyncedPlayerMovementSettings settings = new SyncedPlayerMovementSettings();
|
|
settings.setMovementMode(AuthoritativeMovementMode.CLIENT);
|
|
settings.setRewindHistorySize(0);
|
|
settings.setServerAuthoritativeBlockBreaking(false);
|
|
startGamePacket.setPlayerMovementSettings(settings);
|
|
|
|
upstream.sendPacket(startGamePacket);
|
|
}
|
|
|
|
/**
|
|
* @return the next Bedrock item network ID to use for a new item
|
|
*/
|
|
public int getNextItemNetId() {
|
|
return itemNetId.getAndIncrement();
|
|
}
|
|
|
|
public void confirmTeleport(Vector3d position) {
|
|
if (unconfirmedTeleport == null) {
|
|
return;
|
|
}
|
|
|
|
if (unconfirmedTeleport.canConfirm(position)) {
|
|
unconfirmedTeleport = null;
|
|
return;
|
|
}
|
|
|
|
// Resend the teleport every few packets until Bedrock responds
|
|
unconfirmedTeleport.incrementUnconfirmedFor();
|
|
if (unconfirmedTeleport.shouldResend()) {
|
|
unconfirmedTeleport.resetUnconfirmedFor();
|
|
geyser.getLogger().debug("Resending teleport " + unconfirmedTeleport.getTeleportConfirmId());
|
|
getPlayerEntity().moveAbsolute(Vector3f.from(unconfirmedTeleport.getX(), unconfirmedTeleport.getY(), unconfirmedTeleport.getZ()),
|
|
unconfirmedTeleport.getYaw(), unconfirmedTeleport.getPitch(), playerEntity.isOnGround(), true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue a packet to be sent to player.
|
|
*
|
|
* @param packet the bedrock packet from the NukkitX protocol lib
|
|
*/
|
|
public void sendUpstreamPacket(BedrockPacket packet) {
|
|
upstream.sendPacket(packet);
|
|
}
|
|
|
|
/**
|
|
* Send a packet immediately to the player.
|
|
*
|
|
* @param packet the bedrock packet from the NukkitX protocol lib
|
|
*/
|
|
public void sendUpstreamPacketImmediately(BedrockPacket packet) {
|
|
upstream.sendPacketImmediately(packet);
|
|
}
|
|
|
|
/**
|
|
* Send a packet to the remote server.
|
|
*
|
|
* @param packet the java edition packet from MCProtocolLib
|
|
*/
|
|
public void sendDownstreamPacket(Packet packet) {
|
|
if (!closed && this.downstream != null) {
|
|
Channel channel = this.downstream.getChannel();
|
|
if (channel == null) {
|
|
// Channel is only null before the connection has initialized
|
|
geyser.getLogger().warning("Tried to send a packet to the Java server too early!");
|
|
if (geyser.getConfig().isDebugMode()) {
|
|
Thread.dumpStack();
|
|
}
|
|
return;
|
|
}
|
|
|
|
EventLoop eventLoop = channel.eventLoop();
|
|
if (eventLoop.inEventLoop()) {
|
|
sendDownstreamPacket0(packet);
|
|
} else {
|
|
eventLoop.execute(() -> sendDownstreamPacket0(packet));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void sendDownstreamPacket0(Packet packet) {
|
|
if (protocol.getState().equals(ProtocolState.GAME) || packet.getClass() == ServerboundCustomQueryPacket.class) {
|
|
downstream.send(packet);
|
|
} else {
|
|
geyser.getLogger().debug("Tried to send downstream packet " + packet.getClass().getSimpleName() + " before connected to the server");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the cached value for the reduced debug info gamerule.
|
|
* If enabled, also hides the player's coordinates.
|
|
*
|
|
* @param value The new value for reducedDebugInfo
|
|
*/
|
|
public void setReducedDebugInfo(boolean value) {
|
|
reducedDebugInfo = value;
|
|
// Set the showCoordinates data. This is done because updateShowCoordinates() uses this gamerule as a variable.
|
|
preferencesCache.updateShowCoordinates();
|
|
}
|
|
|
|
/**
|
|
* Changes the daylight cycle gamerule on the client
|
|
* This is used in the login screen along-side normal usage
|
|
*
|
|
* @param doCycle If the cycle should continue
|
|
*/
|
|
public void setDaylightCycle(boolean doCycle) {
|
|
sendGameRule("dodaylightcycle", doCycle);
|
|
// Save the value so we don't have to constantly send a daylight cycle gamerule update
|
|
this.daylightCycle = doCycle;
|
|
}
|
|
|
|
/**
|
|
* Send a gamerule value to the client
|
|
*
|
|
* @param gameRule The gamerule to send
|
|
* @param value The value of the gamerule
|
|
*/
|
|
public void sendGameRule(String gameRule, Object value) {
|
|
GameRulesChangedPacket gameRulesChangedPacket = new GameRulesChangedPacket();
|
|
gameRulesChangedPacket.getGameRules().add(new GameRuleData<>(gameRule, value));
|
|
upstream.sendPacket(gameRulesChangedPacket);
|
|
}
|
|
|
|
/**
|
|
* Checks if the given session's player has a permission
|
|
*
|
|
* @param permission The permission node to check
|
|
* @return true if the player has the requested permission, false if not
|
|
*/
|
|
@Override
|
|
public boolean hasPermission(String permission) {
|
|
return geyser.getWorldManager().hasPermission(this, permission);
|
|
}
|
|
|
|
private static final Ability[] USED_ABILITIES = Ability.values();
|
|
|
|
/**
|
|
* Send an AdventureSettingsPacket to the client with the latest flags
|
|
*/
|
|
public void sendAdventureSettings() {
|
|
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
|
|
CommandPermission commandPermission = opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL;
|
|
// Required to make command blocks destroyable
|
|
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;
|
|
|
|
UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket();
|
|
adventureSettingsPacket.setNoMvP(false);
|
|
adventureSettingsPacket.setNoPvM(false);
|
|
adventureSettingsPacket.setImmutableWorld(worldImmutable);
|
|
adventureSettingsPacket.setShowNameTags(false);
|
|
adventureSettingsPacket.setAutoJump(true);
|
|
sendUpstreamPacket(adventureSettingsPacket);
|
|
|
|
UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket();
|
|
updateAbilitiesPacket.setUniqueEntityId(bedrockId);
|
|
updateAbilitiesPacket.setCommandPermission(commandPermission);
|
|
updateAbilitiesPacket.setPlayerPermission(playerPermission);
|
|
|
|
AbilityLayer abilityLayer = new AbilityLayer();
|
|
Set<Ability> abilities = abilityLayer.getAbilityValues();
|
|
if (canFly || spectator) {
|
|
abilities.add(Ability.MAY_FLY);
|
|
}
|
|
|
|
// Default stuff we have to fill in
|
|
abilities.add(Ability.BUILD);
|
|
abilities.add(Ability.MINE);
|
|
// Needed so you can drop items
|
|
abilities.add(Ability.DOORS_AND_SWITCHES);
|
|
if (gameMode == GameMode.CREATIVE) {
|
|
// Needed so the client doesn't attempt to take away items
|
|
abilities.add(Ability.INSTABUILD);
|
|
}
|
|
|
|
if (commandPermission == CommandPermission.OPERATOR) {
|
|
// Fixes a bug? since 1.19.11 where the player can change their gamemode in Bedrock settings and
|
|
// a packet is not sent to the server.
|
|
// https://github.com/GeyserMC/Geyser/issues/3191
|
|
abilities.add(Ability.OPERATOR_COMMANDS);
|
|
}
|
|
|
|
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);
|
|
// https://github.com/GeyserMC/Geyser/issues/3139 as of 1.19.10
|
|
abilityLayer.setWalkSpeed(walkSpeed == 0f ? 0.01f : walkSpeed);
|
|
Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES);
|
|
|
|
updateAbilitiesPacket.getAbilityLayers().add(abilityLayer);
|
|
sendUpstreamPacket(updateAbilitiesPacket);
|
|
}
|
|
|
|
private int getRenderDistance() {
|
|
if (clientRenderDistance != -1) {
|
|
// The client has sent a render distance
|
|
return clientRenderDistance;
|
|
}
|
|
return serverRenderDistance;
|
|
}
|
|
|
|
// We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
|
|
private static final List<SkinPart> SKIN_PARTS = Arrays.asList(SkinPart.values());
|
|
|
|
/**
|
|
* Send a packet to the server to indicate client render distance, locale, skin parts, and hand preference.
|
|
*/
|
|
public void sendJavaClientSettings() {
|
|
ServerboundClientInformationPacket clientSettingsPacket = new ServerboundClientInformationPacket(locale(),
|
|
getRenderDistance(), ChatVisibility.FULL, true, SKIN_PARTS,
|
|
HandPreference.RIGHT_HAND, false, true);
|
|
sendDownstreamPacket(clientSettingsPacket);
|
|
}
|
|
|
|
/**
|
|
* Used for updating statistic values since we only get changes from the server
|
|
*
|
|
* @param statistics Updated statistics values
|
|
*/
|
|
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()) {
|
|
this.statistics.put(customStatistic, 0);
|
|
}
|
|
}
|
|
this.statistics.putAll(statistics);
|
|
}
|
|
|
|
public void refreshEmotes(List<UUID> emotes) {
|
|
this.emotes.addAll(emotes);
|
|
for (GeyserSession player : geyser.getSessionManager().getSessions().values()) {
|
|
List<UUID> pieces = new ArrayList<>();
|
|
for (UUID piece : emotes) {
|
|
if (!player.getEmotes().contains(piece)) {
|
|
pieces.add(piece);
|
|
}
|
|
player.getEmotes().add(piece);
|
|
}
|
|
EmoteListPacket emoteList = new EmoteListPacket();
|
|
emoteList.setRuntimeEntityId(player.getPlayerEntity().getGeyserId());
|
|
emoteList.getPieceIds().addAll(pieces);
|
|
player.sendUpstreamPacket(emoteList);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send the following fog IDs, as well as the cached ones, to the client.
|
|
*
|
|
* Fog IDs can be found here:
|
|
* https://wiki.bedrock.dev/documentation/fog-ids.html
|
|
*
|
|
* @param fogNameSpaces the fog ids to add
|
|
*/
|
|
public void sendFog(String... fogNameSpaces) {
|
|
this.fogNameSpaces.addAll(Arrays.asList(fogNameSpaces));
|
|
|
|
PlayerFogPacket packet = new PlayerFogPacket();
|
|
packet.getFogStack().addAll(this.fogNameSpaces);
|
|
sendUpstreamPacket(packet);
|
|
}
|
|
|
|
/**
|
|
* Removes the following fog IDs from the client and the cache.
|
|
*
|
|
* @param fogNameSpaces the fog ids to remove
|
|
*/
|
|
public void removeFog(String... fogNameSpaces) {
|
|
if (fogNameSpaces.length == 0) {
|
|
this.fogNameSpaces.clear();
|
|
} else {
|
|
this.fogNameSpaces.removeAll(Arrays.asList(fogNameSpaces));
|
|
}
|
|
PlayerFogPacket packet = new PlayerFogPacket();
|
|
packet.getFogStack().addAll(this.fogNameSpaces);
|
|
sendUpstreamPacket(packet);
|
|
}
|
|
|
|
public boolean canUseCommandBlocks() {
|
|
return instabuild && opPermissionLevel >= 2;
|
|
}
|
|
|
|
public void playSoundEvent(SoundEvent sound, Vector3f position) {
|
|
LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet();
|
|
packet.setPosition(position);
|
|
packet.setSound(sound);
|
|
packet.setIdentifier(":");
|
|
packet.setExtraData(-1);
|
|
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();
|
|
}
|
|
|
|
@Override
|
|
public String bedrockUsername() {
|
|
return authData.name();
|
|
}
|
|
|
|
@Override
|
|
public @MonotonicNonNull String javaUsername() {
|
|
return playerEntity.getUsername();
|
|
}
|
|
|
|
@Override
|
|
public UUID javaUuid() {
|
|
return playerEntity.getUuid();
|
|
}
|
|
|
|
@Override
|
|
public String xuid() {
|
|
return authData.xuid();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull String version() {
|
|
return clientData.getGameVersion();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull BedrockPlatform platform() {
|
|
return BedrockPlatform.values()[clientData.getDeviceOs().ordinal()]; //todo
|
|
}
|
|
|
|
@Override
|
|
public @NonNull String languageCode() {
|
|
return locale();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull UiProfile uiProfile() {
|
|
return UiProfile.values()[clientData.getUiProfile().ordinal()]; //todo
|
|
}
|
|
|
|
@Override
|
|
public @NonNull InputMode inputMode() {
|
|
return InputMode.values()[clientData.getCurrentInputMode().ordinal()]; //todo
|
|
}
|
|
|
|
@Override
|
|
public boolean isLinked() {
|
|
return false; //todo
|
|
}
|
|
|
|
@SuppressWarnings("ConstantConditions") // Need to enforce the parameter annotations
|
|
@Override
|
|
public boolean transfer(@NonNull String address, @IntRange(from = 0, to = 65535) int port) {
|
|
if (address == null || address.isBlank()) {
|
|
throw new IllegalArgumentException("Server address cannot be null or blank");
|
|
} else if (port < 0 || port > 65535) {
|
|
throw new IllegalArgumentException("Server port must be between 0 and 65535, was " + port);
|
|
}
|
|
TransferPacket transferPacket = new TransferPacket();
|
|
transferPacket.setAddress(address);
|
|
transferPacket.setPort(port);
|
|
sendUpstreamPacket(transferPacket);
|
|
return true;
|
|
}
|
|
}
|