1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2026-01-06 15:41:50 +00:00

Merge branch 'master' of https://github.com/GeyserMC/Geyser into server-inventory

This commit is contained in:
Camotoy
2021-01-11 19:46:39 -05:00
53 changed files with 1336 additions and 518 deletions

View File

@@ -86,6 +86,11 @@ public class GeyserConnector {
public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs
public static final String VERSION = "DEV"; // A fallback for running in IDEs
/**
* Oauth client ID for Microsoft authentication
*/
public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88";
private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b";
private final List<GeyserSession> players = new ArrayList<>();
@@ -101,8 +106,8 @@ public class GeyserConnector {
private final ScheduledExecutorService generalThreadPool;
private BedrockServer bedrockServer;
private PlatformType platformType;
private GeyserBootstrap bootstrap;
private final PlatformType platformType;
private final GeyserBootstrap bootstrap;
private Metrics metrics;

View File

@@ -118,6 +118,8 @@ public interface GeyserConfiguration {
String getAuthType();
boolean isPasswordAuthentication();
boolean isUseProxyProtocol();
}
@@ -125,6 +127,12 @@ public interface GeyserConfiguration {
String getEmail();
String getPassword();
/**
* Will be removed after Microsoft accounts are fully migrated
*/
@Deprecated
boolean isMicrosoftAccount();
}
interface IMetricsInfo {

View File

@@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("auth-type")
private String authType = "online";
@JsonProperty("allow-password-authentication")
private boolean passwordAuthentication = true;
@JsonProperty("use-proxy-protocol")
private boolean useProxyProtocol = false;
}
@Getter
@JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load
public static class UserAuthenticationInfo implements IUserAuthenticationInfo {
@AsteriskSerializer.Asterisk()
private String email;
@AsteriskSerializer.Asterisk()
private String password;
@JsonProperty("microsoft-account")
private boolean microsoftAccount = false;
}
@Getter

View File

@@ -38,11 +38,11 @@ public class ItemedFireballEntity extends ThrowableEntity {
}
@Override
protected void updatePosition(GeyserSession session) {
public void tick(GeyserSession session) {
position = position.add(motion);
// TODO: While this reduces latency in position updating (needed for better fireball reflecting),
// TODO: movement is incredibly stiff. See if the MoveEntityDeltaPacket in 1.16.100 fixes this, and if not,
// TODO: only use this laggy movement for fireballs that be reflected
// TODO: movement is incredibly stiff.
// TODO: Only use this laggy movement for fireballs that be reflected
moveAbsoluteImmediate(session, position, rotation, false, true);
float drag = getDrag(session);
motion = motion.add(acceleration).mul(drag);

View File

@@ -33,50 +33,35 @@ import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Used as a class for any object-like entity that moves as a projectile
*/
public class ThrowableEntity extends Entity {
public class ThrowableEntity extends Entity implements Tickable {
private Vector3f lastPosition;
/**
* Updates the position for the Bedrock client.
*
* Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions
*/
protected ScheduledFuture<?> positionUpdater;
public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
this.lastPosition = position;
}
/**
* Updates the position for the Bedrock client.
*
* Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions
*/
@Override
public void spawnEntity(GeyserSession session) {
super.spawnEntity(session);
positionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> {
if (session.isClosed()) {
positionUpdater.cancel(true);
return;
}
updatePosition(session);
}, 0, 50, TimeUnit.MILLISECONDS);
}
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
}
protected void updatePosition(GeyserSession session) {
public void tick(GeyserSession session) {
super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround);
float drag = getDrag(session);
float gravity = getGravity();
motion = motion.mul(drag).down(gravity);
}
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
}
/**
* Get the gravity of this entity type. Used for applying gravity while the entity is in motion.
*
@@ -140,7 +125,6 @@ public class ThrowableEntity extends Entity {
@Override
public boolean despawnEntity(GeyserSession session) {
positionUpdater.cancel(true);
if (entityType == EntityType.THROWN_ENDERPEARL) {
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_TELEPORT);

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2019-2021 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.connector.entity;
import org.geysermc.connector.network.session.GeyserSession;
/**
* Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds.
*/
public interface Tickable {
void tick(GeyserSession session);
}

View File

@@ -28,19 +28,28 @@ package org.geysermc.connector.entity.living.monster;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.AttributeData;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.AddEntityPacket;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.*;
import lombok.Data;
import org.geysermc.connector.entity.Tickable;
import org.geysermc.connector.entity.attribute.AttributeType;
import org.geysermc.connector.entity.living.InsentientEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.AttributeUtils;
import org.geysermc.connector.utils.DimensionUtils;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
public class EnderDragonEntity extends InsentientEntity {
public class EnderDragonEntity extends InsentientEntity implements Tickable {
/**
* The Ender Dragon has multiple hit boxes, which
* are each its own invisible entity
@@ -61,9 +70,19 @@ public class EnderDragonEntity extends InsentientEntity {
private final Segment[] segmentHistory = new Segment[19];
private int latestSegment = -1;
private boolean hovering;
private int phase;
/**
* The number of ticks since the beginning of the phase
*/
private int phaseTicks;
private ScheduledFuture<?> partPositionUpdater;
private int ticksTillNextGrowl = 100;
/**
* Used to determine when the wing flap sound should be played
*/
private float wingPosition;
private float lastWingPosition;
public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
@@ -73,49 +92,67 @@ public class EnderDragonEntity extends InsentientEntity {
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
// Phase
if (entityMetadata.getId() == 15) {
int value = (int) entityMetadata.getValue();
if (value == 5) {
// Performing breath attack
if (entityMetadata.getId() == 15) { // Phase
phase = (int) entityMetadata.getValue();
phaseTicks = 0;
metadata.getFlags().setFlag(EntityFlag.SITTING, isSitting());
}
super.updateBedrockMetadata(entityMetadata, session);
if (entityMetadata.getId() == 8) { // Health
// Update the health attribute, so that the death animation gets played
// Round health up, so that Bedrock doesn't consider the dragon to be dead when health is between 0 and 1
float health = (float) Math.ceil(metadata.getFloat(EntityData.HEALTH));
if (phase == 9 && health <= 0) { // Dying phase
EntityEventPacket entityEventPacket = new EntityEventPacket();
entityEventPacket.setType(EntityEventType.DRAGON_FLAMING);
entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH);
entityEventPacket.setRuntimeEntityId(geyserId);
entityEventPacket.setData(0);
session.sendUpstreamPacket(entityEventPacket);
}
metadata.getFlags().setFlag(EntityFlag.SITTING, value == 5 || value == 6 || value == 7);
hovering = value == 10;
attributes.put(AttributeType.HEALTH, AttributeType.HEALTH.getAttribute(health, 200));
updateBedrockAttributes(session);
}
super.updateBedrockMetadata(entityMetadata, session);
}
/**
* Send an updated list of attributes to the Bedrock client.
* This is overwritten to allow the health attribute to differ from
* the health specified in the metadata.
*
* @param session GeyserSession
*/
@Override
public void updateBedrockAttributes(GeyserSession session) {
if (!valid) return;
List<AttributeData> attributes = new ArrayList<>();
for (Map.Entry<AttributeType, org.geysermc.connector.entity.attribute.Attribute> entry : this.attributes.entrySet()) {
if (!entry.getValue().getType().isBedrockAttribute())
continue;
attributes.add(AttributeUtils.getBedrockAttribute(entry.getValue()));
}
UpdateAttributesPacket updateAttributesPacket = new UpdateAttributesPacket();
updateAttributesPacket.setRuntimeEntityId(geyserId);
updateAttributesPacket.setAttributes(attributes);
session.sendUpstreamPacket(updateAttributesPacket);
}
@Override
public void spawnEntity(GeyserSession session) {
AddEntityPacket addEntityPacket = new AddEntityPacket();
addEntityPacket.setIdentifier("minecraft:" + entityType.name().toLowerCase());
addEntityPacket.setRuntimeEntityId(geyserId);
addEntityPacket.setUniqueEntityId(geyserId);
addEntityPacket.setPosition(position);
addEntityPacket.setMotion(motion);
addEntityPacket.setRotation(getBedrockRotation());
addEntityPacket.setEntityType(entityType.getType());
addEntityPacket.getMetadata().putAll(metadata);
super.spawnEntity(session);
// Otherwise dragon is always 'dying'
addEntityPacket.getAttributes().add(new AttributeData("minecraft:health", 0.0f, 200f, 200f, 200f));
valid = true;
session.sendUpstreamPacket(addEntityPacket);
head = new EnderDragonPartEntity(entityId + 1, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 1, 1);
neck = new EnderDragonPartEntity(entityId + 2, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 3, 3);
body = new EnderDragonPartEntity(entityId + 3, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 5, 3);
leftWing = new EnderDragonPartEntity(entityId + 4, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2);
rightWing = new EnderDragonPartEntity(entityId + 5, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2);
AtomicLong nextEntityId = session.getEntityCache().getNextEntityId();
head = new EnderDragonPartEntity(entityId + 1, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 1, 1);
neck = new EnderDragonPartEntity(entityId + 2, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 3, 3);
body = new EnderDragonPartEntity(entityId + 3, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 5, 3);
leftWing = new EnderDragonPartEntity(entityId + 4, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2);
rightWing = new EnderDragonPartEntity(entityId + 5, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2);
tail = new EnderDragonPartEntity[3];
for (int i = 0; i < 3; i++) {
tail[i] = new EnderDragonPartEntity(entityId + 6 + i, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 2, 2);
tail[i] = new EnderDragonPartEntity(entityId + 6 + i, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 2, 2);
}
allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]};
@@ -129,25 +166,25 @@ public class EnderDragonEntity extends InsentientEntity {
segmentHistory[i].yaw = rotation.getZ();
segmentHistory[i].y = position.getY();
}
partPositionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> {
pushSegment();
updateBoundingBoxes(session);
}, 0, 50, TimeUnit.MILLISECONDS);
session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")");
}
@Override
public boolean despawnEntity(GeyserSession session) {
partPositionUpdater.cancel(true);
for (EnderDragonPartEntity part : allParts) {
part.despawnEntity(session);
}
return super.despawnEntity(session);
}
@Override
public void tick(GeyserSession session) {
effectTick(session);
if (!metadata.getFlags().getFlag(EntityFlag.NO_AI) && isAlive()) {
pushSegment();
updateBoundingBoxes(session);
}
}
/**
* Updates the positions of the Ender Dragon's multiple bounding boxes
*
@@ -163,7 +200,7 @@ public class EnderDragonEntity extends InsentientEntity {
// Lowers the head when the dragon sits/hovers
float headDuck;
if (hovering || metadata.getFlags().getFlag(EntityFlag.SITTING)) {
if (isHovering() || isSitting()) {
headDuck = -1f;
} else {
headDuck = baseSegment.y - getSegment(0).y;
@@ -193,6 +230,105 @@ public class EnderDragonEntity extends InsentientEntity {
}
}
/**
* Handles the particles and sounds of the Ender Dragon
* @param session GeyserSession.
*/
private void effectTick(GeyserSession session) {
Random random = ThreadLocalRandom.current();
if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) {
if (Math.cos(wingPosition * 2f * Math.PI) <= -0.3f && Math.cos(lastWingPosition * 2f * Math.PI) >= -0.3f) {
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound("mob.enderdragon.flap");
playSoundPacket.setPosition(position);
playSoundPacket.setVolume(5f);
playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f);
session.sendUpstreamPacket(playSoundPacket);
}
if (!isSitting() && !isHovering() && ticksTillNextGrowl-- == 0) {
playGrowlSound(session);
ticksTillNextGrowl = 200 + random.nextInt(200);
}
lastWingPosition = wingPosition;
}
if (isAlive()) {
if (metadata.getFlags().getFlag(EntityFlag.NO_AI)) {
wingPosition = 0.5f;
} else if (isHovering() || isSitting()) {
wingPosition += 0.1f;
} else {
double speed = motion.length();
wingPosition += 0.2f / (speed * 10f + 1) * Math.pow(2, motion.getY());
}
phaseTicks++;
if (phase == 3) { // Landing Phase
float headHeight = head.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
Vector3f headCenter = head.getPosition().up(headHeight * 0.5f);
for (int i = 0; i < 8; i++) {
Vector3f particlePos = headCenter.add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f);
// This is missing velocity information
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_DRAGONS_BREATH);
particlePacket.setPosition(particlePos);
session.sendUpstreamPacket(particlePacket);
}
} else if (phase == 5) { // Sitting Flaming Phase
if (phaseTicks % 2 == 0 && phaseTicks < 10) {
// Performing breath attack
// Entity event DRAGON_FLAMING seems to create particles from the origin of the dragon,
// so we need to manually spawn particles
for (int i = 0; i < 8; i++) {
SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket();
spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension()));
spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f));
spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire");
session.sendUpstreamPacket(spawnParticleEffectPacket);
}
}
} else if (phase == 7) { // Sitting Attacking Phase
playGrowlSound(session);
} else if (phase == 9) { // Dying Phase
// Send explosion particles as the dragon move towards the end portal
if (phaseTicks % 10 == 0) {
float xOffset = 8f * (random.nextFloat() - 0.5f);
float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f;
float zOffset = 8f * (random.nextFloat() - 0.5f);
Vector3f particlePos = position.add(xOffset, yOffset, zOffset);
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_EXPLOSION);
particlePacket.setPosition(particlePos);
session.sendUpstreamPacket(particlePacket);
}
}
}
}
private void playGrowlSound(GeyserSession session) {
Random random = ThreadLocalRandom.current();
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound("mob.enderdragon.growl");
playSoundPacket.setPosition(position);
playSoundPacket.setVolume(2.5f);
playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f);
session.sendUpstreamPacket(playSoundPacket);
}
private boolean isAlive() {
return metadata.getFloat(EntityData.HEALTH) > 0;
}
private boolean isHovering() {
return phase == 10;
}
private boolean isSitting() {
return phase == 5 || phase == 6 || phase == 7;
}
/**
* Store the current yaw and y into the circular buffer
*/

View File

@@ -32,11 +32,12 @@ import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.type.EntityType;
public class EnderDragonPartEntity extends Entity {
public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, float width, float height) {
super(entityId, geyserId, entityType, position, motion, rotation);
public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, float width, float height) {
super(entityId, geyserId, entityType, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO);
metadata.put(EntityData.BOUNDING_BOX_WIDTH, width);
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
}
}

View File

@@ -27,8 +27,10 @@ package org.geysermc.connector.entity.living.monster;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
@@ -45,11 +47,21 @@ public class EndermanEntity extends MonsterEntity {
if (entityMetadata.getId() == 15) {
metadata.put(EntityData.CARRIED_BLOCK, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue()));
}
// 'Angry' - mouth open
// "Is screaming" - controls sound
if (entityMetadata.getId() == 16) {
if ((boolean) entityMetadata.getValue()) {
LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet();
packet.setSound(SoundEvent.STARE);
packet.setPosition(this.position);
packet.setExtraData(-1);
packet.setIdentifier("minecraft:enderman");
session.sendUpstreamPacket(packet);
}
}
// "Is staring/provoked" - controls visuals
if (entityMetadata.getId() == 17) {
metadata.getFlags().setFlag(EntityFlag.ANGRY, (boolean) entityMetadata.getValue());
}
// TODO: ID 17 is stared at but I don't believe it's used - maybe only for the sound effect. Check after particle merge
super.updateBedrockMetadata(entityMetadata, session);
}
}

View File

@@ -30,15 +30,16 @@ import com.nukkitx.protocol.bedrock.BedrockServerEventHandler;
import com.nukkitx.protocol.bedrock.BedrockServerSession;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import org.geysermc.connector.common.ping.GeyserPingInfo;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.ping.GeyserPingInfo;
import org.geysermc.connector.configuration.GeyserConfiguration;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.ping.IGeyserPingPassthrough;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.ping.IGeyserPingPassthrough;
import org.geysermc.connector.utils.LanguageUtils;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class ConnectorServerEventHandler implements BedrockServerEventHandler {
@@ -94,6 +95,20 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
pong.setMaximumPlayerCount(config.getMaxPlayers());
}
// The ping will not appear if the MOTD + sub-MOTD is of a certain length.
// We don't know why, though
byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8);
if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) {
// Remove the sub-MOTD first since that only appears locally
pong.setSubMotd("");
if (motdArray.length > 338) {
// If the top MOTD is still too long, we chop it down
byte[] newMotdArray = new byte[339];
System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length);
pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8));
}
}
//Bedrock will not even attempt a connection if the client thinks the server is full
//so we have to fake it not being full
if (pong.getPlayerCount() >= pong.getMaximumPlayerCount()) {

View File

@@ -161,6 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
if (info != null) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName()));
session.setMicrosoftAccount(info.isMicrosoftAccount());
session.authenticate(info.getEmail(), info.getPassword());
// TODO send a message to bedrock user telling them they are connected (if nothing like a motd

View File

@@ -26,8 +26,12 @@
package org.geysermc.connector.network.session;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.AuthPendingException;
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.data.SubProtocol;
@@ -36,9 +40,9 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.statistic.Statistic;
import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade;
import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionRotationPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket;
import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket;
import com.github.steveice10.packetlib.BuiltinFlags;
import com.github.steveice10.packetlib.Client;
@@ -69,6 +73,7 @@ import lombok.Setter;
import org.geysermc.common.window.CustomFormWindow;
import org.geysermc.common.window.FormWindow;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Tickable;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.Entity;
@@ -113,8 +118,13 @@ public class GeyserSession implements CommandSender {
@Setter
private BedrockClientData clientData;
@Deprecated
@Setter
private boolean microsoftAccount;
private final SessionPlayerEntity playerEntity;
private BookEditCache bookEditCache;
private ChunkCache chunkCache;
private EntityCache entityCache;
private EntityEffectCache effectCache;
@@ -178,7 +188,11 @@ public class GeyserSession implements CommandSender {
@Setter
private GameMode gameMode = GameMode.SURVIVAL;
private final AtomicInteger pendingDimSwitches = new AtomicInteger(0);
/**
* Keeps track of the world name for respawning.
*/
@Setter
private String worldName = null;
private boolean sneaking;
@@ -215,9 +229,6 @@ public class GeyserSession implements CommandSender {
@Setter
private Vector3i lastInteractionPosition = Vector3i.ZERO;
private boolean manyDimPackets = false;
private ServerRespawnPacket lastDimPacket = null;
@Setter
private Entity ridingVehicleEntity;
@@ -280,15 +291,14 @@ public class GeyserSession implements CommandSender {
private ScheduledFuture<?> bucketScheduledFuture;
/**
* Sends a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances.
* Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances.
*/
@Setter
private ScheduledFuture<?> movementSendIfIdle;
private long lastMovementTimestamp = System.currentTimeMillis();
/**
* Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless.
*/
@Setter
private boolean daylightCycle = true;
private boolean reducedDebugInfo = false;
@@ -363,12 +373,18 @@ public class GeyserSession implements CommandSender {
private List<UUID> selectedEmotes = new ArrayList<>();
private final Set<UUID> emotes = new HashSet<>();
/**
* The thread that will run every 50 milliseconds - one Minecraft tick.
*/
private ScheduledFuture<?> tickThread = null;
private MinecraftProtocol protocol;
public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
this.connector = connector;
this.upstream = new UpstreamSession(bedrockServerSession);
this.bookEditCache = new BookEditCache(this);
this.chunkCache = new ChunkCache(this);
this.entityCache = new EntityCache(this);
this.effectCache = new EntityEffectCache();
@@ -473,146 +489,22 @@ public class GeyserSession implements CommandSender {
new Thread(() -> {
try {
if (password != null && !password.isEmpty()) {
protocol = new MinecraftProtocol(username, password);
AuthenticationService authenticationService;
if (microsoftAccount) {
authenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID);
} else {
authenticationService = new MojangAuthenticationService();
}
authenticationService.setUsername(username);
authenticationService.setPassword(password);
authenticationService.login();
protocol = new MinecraftProtocol(authenticationService);
} else {
protocol = new MinecraftProtocol(username);
}
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
final PublicKey publicKey;
if (floodgate) {
PublicKey key = null;
try {
key = EncryptionUtil.getKeyFromFile(
connector.getConfig().getFloodgateKeyPath(),
PublicKey.class
);
} catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e);
}
publicKey = key;
} else publicKey = null;
if (publicKey != null) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
}
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
if (connector.getConfig().getRemote().isUseProxyProtocol()) {
downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
}
// Let Geyser handle sending the keep alive
downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
downstream.getSession().addListener(new SessionAdapter() {
@Override
public void packetSending(PacketSendingEvent event) {
//todo move this somewhere else
if (event.getPacket() instanceof HandshakePacket && floodgate) {
String encrypted = "";
try {
encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData(
clientData.getGameVersion(),
authData.getName(),
authData.getXboxUUID(),
clientData.getDeviceOS().ordinal(),
clientData.getLanguageCode(),
clientData.getCurrentInputMode().ordinal(),
upstream.getSession().getAddress().getAddress().getHostAddress()
));
} catch (Exception e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
}
HandshakePacket handshakePacket = event.getPacket();
event.setPacket(new HandshakePacket(
handshakePacket.getProtocolVersion(),
handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted,
handshakePacket.getPort(),
handshakePacket.getIntent()
));
}
}
@Override
public void connected(ConnectedEvent event) {
loggingIn = false;
loggedIn = true;
if (protocol.getProfile() == null) {
// Java account is offline
disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
return;
}
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress()));
playerEntity.setUuid(protocol.getProfile().getId());
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.toLowerCase().equals("en_us") && !LocaleUtils.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
LocaleUtils.downloadAndLoadLocale(locale);
}
@Override
public void disconnected(DisconnectedEvent event) {
loggingIn = false;
loggedIn = false;
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason()));
if (event.getCause() != null) {
event.getCause().printStackTrace();
}
upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason()));
}
@Override
public void packetReceived(PacketReceivedEvent event) {
if (!closed) {
//handle consecutive respawn packets
if (event.getPacket().getClass().equals(ServerRespawnPacket.class)) {
manyDimPackets = lastDimPacket != null;
lastDimPacket = event.getPacket();
return;
} else if (lastDimPacket != null) {
PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(lastDimPacket.getClass(), lastDimPacket, GeyserSession.this);
lastDimPacket = null;
}
// Required, or else Floodgate players break with Bukkit chunk caching
if (event.getPacket() instanceof LoginSuccessPacket) {
GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile();
playerEntity.setUsername(profile.getName());
playerEntity.setUuid(profile.getId());
// Check if they are not using a linked account
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
SkinManager.handleBedrockSkin(playerEntity, clientData);
}
}
PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this);
}
}
@Override
public void packetError(PacketErrorEvent event) {
connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage()));
if (connector.getConfig().isDebugMode())
event.getCause().printStackTrace();
event.setSuppress(true);
}
});
downstream.getSession().connect();
connector.addPlayer(this);
connectDownstream();
} catch (InvalidCredentialsException | IllegalArgumentException e) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username));
disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode()));
@@ -622,6 +514,199 @@ public class GeyserSession implements CommandSender {
}).start();
}
/**
* Present a form window to the user asking to log in with another web browser
*/
public void authenticateWithMicrosoftCode() {
if (loggedIn) {
connector.getLogger().severe(LanguageUtils.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().getName()));
return;
}
loggingIn = true;
// new thread so clients don't timeout
new Thread(() -> {
try {
MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID);
MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode();
LoginEncryptionUtils.showMicrosoftCodeWindow(this, response);
// This just looks cool
SetTimePacket packet = new SetTimePacket();
packet.setTime(16000);
sendUpstreamPacket(packet);
// Wait for the code to validate
attemptCodeAuthentication(msaAuthenticationService);
} catch (InvalidCredentialsException | IllegalArgumentException e) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", getAuthData().getName()));
disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode()));
} catch (RequestException ex) {
ex.printStackTrace();
}
}).start();
}
/**
* Poll every second to see if the user has successfully signed in
*/
private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) {
if (loggedIn || closed) {
return;
}
try {
msaAuthenticationService.login();
protocol = new MinecraftProtocol(msaAuthenticationService);
connectDownstream();
} catch (RequestException e) {
if (!(e instanceof AuthPendingException)) {
e.printStackTrace();
} else {
// Wait one second before trying again
connector.getGeneralThreadPool().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS);
}
}
}
/**
* After getting whatever credentials needed, we attempt to join the Java server.
*/
private void connectDownstream() {
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
final PublicKey publicKey;
if (floodgate) {
PublicKey key = null;
try {
key = EncryptionUtil.getKeyFromFile(
connector.getConfig().getFloodgateKeyPath(),
PublicKey.class
);
} catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e);
}
publicKey = key;
} else publicKey = null;
if (publicKey != null) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
}
// Start ticking
tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
if (connector.getConfig().getRemote().isUseProxyProtocol()) {
downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
}
// Let Geyser handle sending the keep alive
downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
downstream.getSession().addListener(new SessionAdapter() {
@Override
public void packetSending(PacketSendingEvent event) {
//todo move this somewhere else
if (event.getPacket() instanceof HandshakePacket && floodgate) {
String encrypted = "";
try {
encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData(
clientData.getGameVersion(),
authData.getName(),
authData.getXboxUUID(),
clientData.getDeviceOS().ordinal(),
clientData.getLanguageCode(),
clientData.getCurrentInputMode().ordinal(),
upstream.getSession().getAddress().getAddress().getHostAddress()
));
} catch (Exception e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
}
HandshakePacket handshakePacket = event.getPacket();
event.setPacket(new HandshakePacket(
handshakePacket.getProtocolVersion(),
handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted,
handshakePacket.getPort(),
handshakePacket.getIntent()
));
}
}
@Override
public void connected(ConnectedEvent event) {
loggingIn = false;
loggedIn = true;
if (protocol.getProfile() == null) {
// Java account is offline
disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
return;
}
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress()));
playerEntity.setUuid(protocol.getProfile().getId());
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.toLowerCase().equals("en_us") && !LocaleUtils.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
LocaleUtils.downloadAndLoadLocale(locale);
}
@Override
public void disconnected(DisconnectedEvent event) {
loggingIn = false;
loggedIn = false;
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason()));
if (event.getCause() != null) {
event.getCause().printStackTrace();
}
upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason()));
}
@Override
public void packetReceived(PacketReceivedEvent event) {
if (!closed) {
// Required, or else Floodgate players break with Bukkit chunk caching
if (event.getPacket() instanceof LoginSuccessPacket) {
GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile();
playerEntity.setUsername(profile.getName());
playerEntity.setUuid(profile.getId());
// Check if they are not using a linked account
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
SkinManager.handleBedrockSkin(playerEntity, clientData);
}
}
PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this);
}
}
@Override
public void packetError(PacketErrorEvent event) {
connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage()));
if (connector.getConfig().isDebugMode())
event.getCause().printStackTrace();
event.setSuppress(true);
}
});
if (!daylightCycle) {
setDaylightCycle(true);
}
downstream.getSession().connect();
connector.addPlayer(this);
}
public void disconnect(String reason) {
if (!closed) {
loggedIn = false;
@@ -634,6 +719,11 @@ public class GeyserSession implements CommandSender {
}
}
if (tickThread != null) {
tickThread.cancel(true);
}
this.bookEditCache = null;
this.chunkCache = null;
this.entityCache = null;
this.effectCache = null;
@@ -647,6 +737,28 @@ public class GeyserSession implements CommandSender {
disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.close", getClientData().getLanguageCode()));
}
/**
* Called every 50 milliseconds - one Minecraft tick.
*/
public void 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());
// A null return value cancels the packet
if (position != null) {
ClientPlayerPositionPacket packet = new ClientPlayerPositionPacket(playerEntity.isOnGround(),
position.getX(), position.getY(), position.getZ());
sendDownstreamPacket(packet);
}
lastMovementTimestamp = System.currentTimeMillis();
}
for (Tickable entity : entityCache.getTickableEntities()) {
entity.tick(this);
}
}
public void setAuthenticationData(AuthData authData) {
this.authData = authData;
}
@@ -923,6 +1035,18 @@ public class GeyserSession implements CommandSender {
reducedDebugInfo = value;
}
/**
* 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
*

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2019-2021 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.connector.network.session.cache;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket;
import lombok.Setter;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.item.ItemRegistry;
/**
* Manages updating the current writable book.
*
* Java sends book updates less frequently than Bedrock, and this can cause issues with servers that rate limit
* book packets. Because of this, we need to ensure packets are only send every second or so at maximum.
*/
public class BookEditCache {
private final GeyserSession session;
@Setter
private ClientEditBookPacket packet;
/**
* Stores the last time a book update packet was sent to the server.
*/
private long lastBookUpdate;
public BookEditCache(GeyserSession session) {
this.session = session;
}
/**
* Check to see if there is a book edit update to send, and if so, send it.
*/
public void checkForSend() {
if (packet == null) {
// No new packet has to be sent
return;
}
// Prevent kicks due to rate limiting - specifically on Spigot servers
if ((System.currentTimeMillis() - lastBookUpdate) < 1000) {
return;
}
// Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
if (itemStack == null || itemStack.getJavaId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) {
packet = null;
return;
}
session.getDownstream().getSession().send(packet);
packet = null;
lastBookUpdate = System.currentTimeMillis();
}
}

View File

@@ -28,6 +28,7 @@ package org.geysermc.connector.network.session.cache;
import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.Getter;
import org.geysermc.connector.entity.Tickable;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
@@ -40,17 +41,21 @@ import java.util.concurrent.atomic.AtomicLong;
* for that player (e.g. seeing vanished players from /vanish)
*/
public class EntityCache {
private GeyserSession session;
private final GeyserSession session;
@Getter
private Long2ObjectMap<Entity> entities = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
/**
* A list of all entities that must be ticked.
*/
private final List<Tickable> tickableEntities = Collections.synchronizedList(new ArrayList<>());
private Long2LongMap entityIdTranslations = Long2LongMaps.synchronize(new Long2LongOpenHashMap());
private Map<UUID, PlayerEntity> playerEntities = Collections.synchronizedMap(new HashMap<>());
private Map<UUID, BossBar> bossBars = Collections.synchronizedMap(new HashMap<>());
private Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap());
private final Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap());
@Getter
private AtomicLong nextEntityId = new AtomicLong(2L);
private final AtomicLong nextEntityId = new AtomicLong(2L);
public EntityCache(GeyserSession session) {
this.session = session;
@@ -59,6 +64,11 @@ public class EntityCache {
public void spawnEntity(Entity entity) {
if (cacheEntity(entity)) {
entity.spawnEntity(session);
if (entity instanceof Tickable) {
// Start ticking it
tickableEntities.add((Tickable) entity);
}
}
}
@@ -76,6 +86,10 @@ public class EntityCache {
if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) {
long geyserId = entityIdTranslations.remove(entity.getEntityId());
entities.remove(geyserId);
if (entity instanceof Tickable) {
tickableEntities.remove(entity);
}
return true;
}
return false;
@@ -152,4 +166,8 @@ public class EntityCache {
public void addCachedPlayerEntityLink(long playerId, long linkedEntityId) {
cachedPlayerEntityLinks.put(playerId, linkedEntityId);
}
public List<Tickable> getTickableEntities() {
return tickableEntities;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (c) 2019-2021 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.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.packet.BookEditPacket;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@Translator(packet = BookEditPacket.class)
public class BedrockBookEditTranslator extends PacketTranslator<BookEditPacket> {
@Override
public void translate(BookEditPacket packet, GeyserSession session) {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
if (itemStack != null) {
CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag("");
ItemStack bookItem = new ItemStack(itemStack.getJavaId(), itemStack.getAmount(), tag);
List<Tag> pages = tag.contains("pages") ? new LinkedList<>(((ListTag) tag.get("pages")).getValue()) : new LinkedList<>();
int page = packet.getPageNumber();
// Creative edits the NBT for us
if (session.getGameMode() != GameMode.CREATIVE) {
switch (packet.getAction()) {
case ADD_PAGE: {
// Add empty pages in between
for (int i = pages.size(); i < page; i++) {
pages.add(i, new StringTag("", ""));
}
pages.add(page, new StringTag("", packet.getText()));
break;
}
// Called whenever a page is modified
case REPLACE_PAGE: {
if (page < pages.size()) {
pages.set(page, new StringTag("", packet.getText()));
} else {
// Add empty pages in between
for (int i = pages.size(); i < page; i++) {
pages.add(i, new StringTag("", ""));
}
pages.add(page, new StringTag("", packet.getText()));
}
break;
}
case DELETE_PAGE: {
if (page < pages.size()) {
pages.remove(page);
}
break;
}
case SWAP_PAGES: {
int page2 = packet.getSecondaryPageNumber();
if (page < pages.size() && page2 < pages.size()) {
Collections.swap(pages, page, page2);
}
break;
}
case SIGN_BOOK: {
tag.put(new StringTag("author", packet.getAuthor()));
tag.put(new StringTag("title", packet.getTitle()));
break;
}
default:
return;
}
}
// Remove empty pages at the end
while (pages.size() > 0) {
StringTag currentPage = (StringTag) pages.get(pages.size() - 1);
if (currentPage.getValue() == null || currentPage.getValue().isEmpty()) {
pages.remove(pages.size() - 1);
} else {
break;
}
}
tag.put(new ListTag("pages", pages));
session.getPlayerInventory().setItem(36 + session.getPlayerInventory().getHeldItemSlot(), GeyserItemStack.from(bookItem), session);
session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getPlayerInventory().getHeldItemSlot()));
// There won't be any more book updates after this, so we can try sending the edit packet immediately
if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) {
session.getBookEditCache().checkForSend();
}
}
}
}

View File

@@ -38,14 +38,13 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye
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.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData;
import com.nukkitx.protocol.bedrock.data.inventory.InventorySource;
import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.connector.entity.CommandBlockMinecartEntity;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
@@ -63,11 +62,23 @@ import org.geysermc.connector.utils.BlockUtils;
import java.util.concurrent.TimeUnit;
/**
* BedrockInventoryTransactionTranslator handles most interactions between the client and the world,
* or the client and their inventory.
*/
@Translator(packet = InventoryTransactionPacket.class)
public class BedrockInventoryTransactionTranslator extends PacketTranslator<InventoryTransactionPacket> {
private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f;
private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49;
private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36;
private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f;
@Override
public void translate(InventoryTransactionPacket packet, GeyserSession session) {
// Send book updates before opening inventories
session.getBookEditCache().checkForSend();
switch (packet.getTransactionType()) {
case NORMAL:
System.out.println(packet);
@@ -129,6 +140,46 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
break;
}
Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
/*
Checks to ensure that the range will be accepted by the server.
"Not in range" doesn't refer to how far a vanilla client goes (that's a whole other mess),
but how much a server will accept from the client maximum
*/
// 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();
EntityFlags flags = session.getPlayerEntity().getMetadata().getFlags();
// Adjust position for current eye height
if (flags.getFlag(EntityFlag.SNEAKING)) {
playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 1.27f), 0);
} else if (flags.getFlag(EntityFlag.SWIMMING) || flags.getFlag(EntityFlag.GLIDING) || flags.getFlag(EntityFlag.DAMAGE_NEARBY_MOBS)) {
// Swimming, gliding, or using the trident spin attack
playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.4f), 0);
} else if (flags.getFlag(EntityFlag.SLEEPING)) {
playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.2f), 0);
} // else, we don't have to modify the position
float diffX = playerPosition.getX() - packet.getBlockPosition().getX();
float diffY = playerPosition.getY() - packet.getBlockPosition().getY();
float diffZ = playerPosition.getZ() - packet.getBlockPosition().getZ();
if (((diffX * diffX) + (diffY * diffY) + (diffZ * diffZ)) >
(session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
// Vanilla check
if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0)
.distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) {
// The client thinks that its blocks have been successfully placed. Restore the server's blocks instead.
restoreCorrectBlock(session, blockPos, packet);
return;
}
/*
Block place checks end - client is good to go
*/
ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket(
new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
BlockFace.values()[packet.getBlockFace()],
@@ -176,7 +227,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
ItemEntry handItem = ItemRegistry.getItem(packet.getItemInHand());
if (handItem.isBlock()) {
session.setLastBlockPlacePosition(blockPos);
@@ -200,19 +250,32 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendDownstreamPacket(useItemPacket);
break;
case 2:
int blockState = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ());
double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(blockState);
if (session.getGameMode() == GameMode.CREATIVE || (session.getConnector().getConfig().isCacheChunks() && blockHardness == 0)) {
session.setLastBlockPlacedId(null);
session.setLastBlockPlacePosition(null);
int blockState = session.getGameMode() == GameMode.CREATIVE ?
session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition()) : session.getBreakingBlock();
LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setLastBlockPlacedId(null);
session.setLastBlockPlacePosition(null);
// Same deal with vanilla block placing as above.
// This is working out the distance using 3d Pythagoras and the extra value added to the Y is the sneaking height of a java player.
playerPosition = session.getPlayerEntity().getPosition();
Vector3f floatBlockPosition = packet.getBlockPosition().toFloat();
diffX = playerPosition.getX() - (floatBlockPosition.getX() + 0.5f);
diffY = (playerPosition.getY() - EntityType.PLAYER.getOffset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f;
diffZ = playerPosition.getZ() - (floatBlockPosition.getZ() + 0.5f);
float distanceSquared = diffX * diffX + diffY * diffY + diffZ * diffZ;
if (distanceSquared > MAXIMUM_BLOCK_DESTROYING_DISTANCE) {
restoreCorrectBlock(session, packet.getBlockPosition(), packet);
return;
}
LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID);
long frameEntityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition());
if (frameEntityId != -1 && session.getEntityCache().getEntityByJavaId(frameEntityId) != null) {
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) frameEntityId, InteractAction.ATTACK, session.isSneaking());
@@ -286,4 +349,34 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
break;
}
}
/**
* Restore the correct block state from the server without updating the chunk cache.
*
* @param session the session of the Bedrock client
* @param blockPos the block position to restore
*/
private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) {
int javaBlockState = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPos);
updateBlockPacket.setRuntimeId(BlockTranslator.getBedrockBlockId(javaBlockState));
updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateBlockPacket);
UpdateBlockPacket updateWaterPacket = new UpdateBlockPacket();
updateWaterPacket.setDataLayer(1);
updateWaterPacket.setBlockPosition(blockPos);
updateWaterPacket.setRuntimeId(BlockTranslator.isWaterlogged(javaBlockState) ? BlockTranslator.BEDROCK_WATER_ID : BlockTranslator.BEDROCK_AIR_ID);
updateWaterPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
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);
}
}

View File

@@ -45,6 +45,9 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
return;
}
// Send book update before switching hotbar slot
session.getBookEditCache().checkForSend();
session.getPlayerInventory().setHeldItemSlot(packet.getHotbarSlot());
ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());

View File

@@ -26,12 +26,14 @@
package org.geysermc.connector.network.translators.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState;
import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerAbilitiesPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
@@ -40,10 +42,12 @@ import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.collision.CollisionManager;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.BlockUtils;
@@ -58,6 +62,11 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
if (entity == null)
return;
// Send book update before any player action
if (packet.getAction() != PlayerActionPacket.Action.RESPAWN) {
session.getBookEditCache().checkForSend();
}
Vector3i vector = packet.getBlockPosition();
Position position = new Position(vector.getX(), vector.getY(), vector.getZ());
@@ -126,6 +135,27 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
break;
case START_BREAK:
if (session.getConnector().getConfig().isCacheChunks()) {
// Start the block breaking animation
if (session.getGameMode() != GameMode.CREATIVE) {
int blockState = session.getConnector().getWorldManager().getBlockAt(session, vector);
double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(blockState);
LevelEventPacket startBreak = new LevelEventPacket();
startBreak.setType(LevelEventType.BLOCK_START_BREAK);
startBreak.setPosition(vector.toFloat());
PlayerInventory inventory = session.getPlayerInventory();
GeyserItemStack item = inventory.getItemInHand();
ItemEntry itemEntry = null;
CompoundTag nbtData = new CompoundTag("");
if (item != null) {
itemEntry = item.getItemEntry();
nbtData = item.getNbt();
}
double breakTime = Math.ceil(BlockUtils.getBreakTime(blockHardness, blockState, itemEntry, nbtData, session) * 20);
startBreak.setData((int) (65535 / breakTime));
session.setBreakingBlock(blockState);
session.sendUpstreamPacket(startBreak);
}
// Account for fire - the client likes to hit the block behind.
Vector3i fireBlockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getFace());
int blockUp = session.getConnector().getWorldManager().getBlockAt(session, fireBlockPos);
@@ -134,37 +164,44 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(fireBlockPos.getX(),
fireBlockPos.getY(), fireBlockPos.getZ()), BlockFace.values()[packet.getFace()]);
session.sendDownstreamPacket(startBreakingPacket);
break;
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
}
}
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(),
packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getFace()]);
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, position, BlockFace.values()[packet.getFace()]);
session.sendDownstreamPacket(startBreakingPacket);
break;
case CONTINUE_BREAK:
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
LevelEventPacket continueBreakPacket = new LevelEventPacket();
continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK);
continueBreakPacket.setData(BlockTranslator.getBedrockBlockId(session.getBreakingBlock()));
continueBreakPacket.setPosition(packet.getBlockPosition().toFloat());
continueBreakPacket.setData((BlockTranslator.getBedrockBlockId(session.getBreakingBlock())) | (packet.getFace() << 24));
continueBreakPacket.setPosition(vector.toFloat());
session.sendUpstreamPacket(continueBreakPacket);
break;
case ABORT_BREAK:
ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, new Position(packet.getBlockPosition().getX(),
packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.DOWN);
ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN);
session.sendDownstreamPacket(abortBreakingPacket);
LevelEventPacket stopBreak = new LevelEventPacket();
stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
stopBreak.setPosition(vector.toFloat());
stopBreak.setData(0);
session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID);
session.sendUpstreamPacket(stopBreak);
break;
case STOP_BREAK:
// Handled in BedrockInventoryTransactionTranslator
break;
case DIMENSION_CHANGE_SUCCESS:
if (session.getPendingDimSwitches().decrementAndGet() == 0) {
//sometimes the client doesn't feel like loading
PlayStatusPacket spawnPacket = new PlayStatusPacket();
spawnPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
session.sendUpstreamPacket(spawnPacket);
entity.updateBedrockAttributes(session);
session.getEntityCache().updateBossBars();
}
//sometimes the client doesn't feel like loading
PlayStatusPacket spawnPacket = new PlayStatusPacket();
spawnPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
session.sendUpstreamPacket(spawnPacket);
entity.updateBedrockAttributes(session);
session.getEntityCache().updateBossBars();
break;
case JUMP:
session.setJumping(true);

View File

@@ -33,16 +33,12 @@ import com.nukkitx.math.vector.Vector3d;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.collision.CollisionManager;
import java.util.concurrent.TimeUnit;
@Translator(packet = MovePlayerPacket.class)
public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPacket> {
@@ -50,7 +46,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
@Override
public void translate(MovePlayerPacket packet, GeyserSession session) {
PlayerEntity entity = session.getPlayerEntity();
if (!session.isSpawned() || session.getPendingDimSwitches().get() > 0) return;
if (!session.isSpawned()) return;
if (!session.getUpstream().isInitialized()) {
MoveEntityAbsolutePacket moveEntityBack = new MoveEntityAbsolutePacket();
@@ -63,9 +59,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
return;
}
if (session.getMovementSendIfIdle() != null) {
session.getMovementSendIfIdle().cancel(true);
}
session.setLastMovementTimestamp(System.currentTimeMillis());
// Send book update before the player moves
session.getBookEditCache().checkForSend();
if (session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityType.PLAYER.getOffset(), 0))) {
// head yaw, pitch, head yaw
@@ -86,7 +83,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
session.sendDownstreamPacket(playerRotationPacket);
} else {
Vector3d position = adjustBedrockPosition(session, packet.getPosition(), packet.isOnGround());
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround());
if (position != null) { // A null return value cancels the packet
if (isValidMove(session, packet.getMode(), entity.getPosition(), packet.getPosition())) {
Packet movePacket;
@@ -128,7 +125,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
} else {
// Not a valid move
session.getConnector().getLogger().debug("Recalculating position...");
recalculatePosition(session);
session.getCollisionManager().recalculatePosition();
}
}
}
@@ -141,13 +138,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
if (entity.getRightParrot() != null) {
entity.getRightParrot().moveAbsolute(session, entity.getPosition(), entity.getRotation(), true, false);
}
// Schedule a position send loop if the player is idle
session.setMovementSendIfIdle(session.getConnector().getGeneralThreadPool().schedule(() -> sendPositionIfIdle(session),
3, TimeUnit.SECONDS));
}
public boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) {
private boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) {
if (mode != MovePlayerPacket.Mode.NORMAL)
return true;
@@ -171,81 +164,5 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
return true;
}
/**
* Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between
* the two versions.
*
* @param session the current GeyserSession
* @param bedrockPosition the current Bedrock position of the client
* @param onGround whether the Bedrock player is on the ground
* @return the position to send to the Java server, or null to cancel sending the packet
*/
private Vector3d adjustBedrockPosition(GeyserSession session, Vector3f bedrockPosition, boolean onGround) {
// We need to parse the float as a string since casting a float to a double causes us to
// lose precision and thus, causes players to get stuck when walking near walls
double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset();
Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY,
Double.parseDouble(Float.toString(bedrockPosition.getZ())));
if (session.getConnector().getConfig().isCacheChunks()) {
// With chunk caching, we can do some proper collision checks
CollisionManager collisionManager = session.getCollisionManager();
collisionManager.updatePlayerBoundingBox(position);
// Correct player position
if (!collisionManager.correctPlayerPosition()) {
// Cancel the movement if it needs to be cancelled
recalculatePosition(session);
return null;
}
position = Vector3d.from(collisionManager.getPlayerBoundingBox().getMiddleX(),
collisionManager.getPlayerBoundingBox().getMiddleY() - (collisionManager.getPlayerBoundingBox().getSizeY() / 2),
collisionManager.getPlayerBoundingBox().getMiddleZ());
} else {
// When chunk caching is off, we have to rely on this
// It rounds the Y position up to the nearest 0.5
// This snaps players to snap to the top of stairs and slabs like on Java Edition
// However, it causes issues such as the player floating on carpets
if (onGround) javaY = Math.ceil(javaY * 2) / 2;
position = position.up(javaY - position.getY());
}
return position;
}
// TODO: This makes the player look upwards for some reason, rotation values must be wrong
public void recalculatePosition(GeyserSession session) {
PlayerEntity entity = session.getPlayerEntity();
// Gravity might need to be reset...
SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
entityDataPacket.getMetadata().putAll(entity.getMetadata());
session.sendUpstreamPacket(entityDataPacket);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL);
session.sendUpstreamPacket(movePlayerPacket);
}
private void sendPositionIfIdle(GeyserSession session) {
if (session.isClosed()) return;
PlayerEntity entity = session.getPlayerEntity();
// Recalculate in case something else changed position
Vector3d position = adjustBedrockPosition(session, entity.getPosition(), entity.isOnGround());
// A null return value cancels the packet
if (position != null) {
ClientPlayerPositionPacket packet = new ClientPlayerPositionPacket(session.getPlayerEntity().isOnGround(),
position.getX(), position.getY(), position.getZ());
session.sendDownstreamPacket(packet);
}
session.setMovementSendIfIdle(session.getConnector().getGeneralThreadPool().schedule(() -> sendPositionIfIdle(session),
3, TimeUnit.SECONDS));
}
}

View File

@@ -30,8 +30,12 @@ import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
@@ -105,6 +109,7 @@ public class CollisionManager {
// According to the Minecraft Wiki, when sneaking:
// - In Bedrock Edition, the height becomes 1.65 blocks, allowing movement through spaces as small as 1.75 (2 - 14) blocks high.
// - In Java Edition, the height becomes 1.5 blocks.
// TODO: Have this depend on the player's literal bounding box variable
if (session.isSneaking()) {
playerBoundingBox.setSizeY(1.5);
} else {
@@ -113,6 +118,65 @@ public class CollisionManager {
}
}
/**
* Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between
* the two versions.
*
* @param bedrockPosition the current Bedrock position of the client
* @param onGround whether the Bedrock player is on the ground
* @return the position to send to the Java server, or null to cancel sending the packet
*/
public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) {
// We need to parse the float as a string since casting a float to a double causes us to
// lose precision and thus, causes players to get stuck when walking near walls
double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset();
Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY,
Double.parseDouble(Float.toString(bedrockPosition.getZ())));
if (session.getConnector().getConfig().isCacheChunks()) {
// With chunk caching, we can do some proper collision checks
updatePlayerBoundingBox(position);
// Correct player position
if (!correctPlayerPosition()) {
// Cancel the movement if it needs to be cancelled
recalculatePosition();
return null;
}
position = Vector3d.from(playerBoundingBox.getMiddleX(),
playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2),
playerBoundingBox.getMiddleZ());
} else {
// When chunk caching is off, we have to rely on this
// It rounds the Y position up to the nearest 0.5
// This snaps players to snap to the top of stairs and slabs like on Java Edition
// However, it causes issues such as the player floating on carpets
if (onGround) javaY = Math.ceil(javaY * 2) / 2;
position = position.up(javaY - position.getY());
}
return position;
}
// TODO: This makes the player look upwards for some reason, rotation values must be wrong
public void recalculatePosition() {
PlayerEntity entity = session.getPlayerEntity();
// Gravity might need to be reset...
SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
entityDataPacket.getMetadata().putAll(entity.getMetadata());
session.sendUpstreamPacket(entityDataPacket);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL);
session.sendUpstreamPacket(movePlayerPacket);
}
public List<Vector3i> getPlayerCollidableBlocks() {
List<Vector3i> blocks = new ArrayList<>();

View File

@@ -95,6 +95,10 @@ public class ItemRegistry {
* Wheat item entry, used in AbstractHorseEntity.java
*/
public static ItemEntry WHEAT;
/**
* Writable book item entry, used in BedrockBookEditTranslator.java
*/
public static ItemEntry WRITABLE_BOOK;
public static int BARRIER_INDEX = 0;
@@ -195,6 +199,9 @@ public class ItemRegistry {
case "minecraft:wheat":
WHEAT = ITEM_ENTRIES.get(itemIndex);
break;
case "minecraft:writable_book":
WRITABLE_BOOK = ITEM_ENTRIES.get(itemIndex);
break;
default:
break;
}

View File

@@ -78,9 +78,8 @@ public class BookPagesTranslator extends NbtItemStackTranslator {
CompoundTag pageTag = (CompoundTag) tag;
StringTag textTag = pageTag.get("text");
pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue())));
pages.add(new StringTag("", textTag.getValue()));
}
itemTag.remove("pages");
itemTag.put(new ListTag("pages", pages));
}

View File

@@ -55,12 +55,12 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
// are swapping servers
String newDimension = DimensionUtils.getNewDimension(packet.getDimension());
if (session.isSpawned()) {
String fakeDim = session.getDimension().equals(DimensionUtils.OVERWORLD) ? DimensionUtils.NETHER : DimensionUtils.OVERWORLD;
String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension);
DimensionUtils.switchDimension(session, fakeDim);
DimensionUtils.switchDimension(session, newDimension);
session.getWorldCache().removeScoreboard();
}
session.setWorldName(packet.getWorldName());
AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket();
bedrockPacket.setUniqueEntityId(session.getPlayerEntity().getGeyserId());

View File

@@ -43,8 +43,6 @@ public class JavaRespawnTranslator extends PacketTranslator<ServerRespawnPacket>
@Override
public void translate(ServerRespawnPacket packet, GeyserSession session) {
Entity entity = session.getPlayerEntity();
if (entity == null)
return;
float maxHealth = entity.getAttributes().containsKey(AttributeType.MAX_HEALTH) ? entity.getAttributes().get(AttributeType.MAX_HEALTH).getValue() : 20f;
// Max health must be divisible by two in bedrock
@@ -66,18 +64,24 @@ public class JavaRespawnTranslator extends PacketTranslator<ServerRespawnPacket>
session.setRaining(false);
}
if (session.isThunder()) {
LevelEventPacket stopThunderPacket = new LevelEventPacket();
stopThunderPacket.setType(LevelEventType.STOP_THUNDERSTORM);
stopThunderPacket.setData(0);
stopThunderPacket.setPosition(Vector3f.ZERO);
session.sendUpstreamPacket(stopThunderPacket);
session.setThunder(false);
}
String newDimension = DimensionUtils.getNewDimension(packet.getDimension());
if (!session.getDimension().equals(newDimension)) {
DimensionUtils.switchDimension(session, newDimension);
} else {
if (session.isManyDimPackets()) { //reloading world
String fakeDim = session.getDimension().equals(DimensionUtils.OVERWORLD) ? DimensionUtils.NETHER : DimensionUtils.OVERWORLD;
if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) {
if (!packet.getWorldName().equals(session.getWorldName()) && session.getDimension().equals(newDimension)) {
// Switching to a new world (based off the world name change); send a fake dimension change
String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension);
DimensionUtils.switchDimension(session, fakeDim);
DimensionUtils.switchDimension(session, newDimension);
} else {
// Handled in JavaPlayerPositionRotationTranslator
session.setSpawned(false);
}
session.setWorldName(packet.getWorldName());
DimensionUtils.switchDimension(session, newDimension);
}
}
}

View File

@@ -31,8 +31,8 @@ import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket;
import org.geysermc.connector.entity.Entity;
@@ -183,6 +183,19 @@ public class JavaEntityStatusTranslator extends PacketTranslator<ServerEntitySta
return;
}
break;
case LIVING_EQUIPMENT_BREAK_HEAD:
case LIVING_EQUIPMENT_BREAK_CHEST:
case LIVING_EQUIPMENT_BREAK_LEGS:
case LIVING_EQUIPMENT_BREAK_FEET:
case LIVING_EQUIPMENT_BREAK_MAIN_HAND:
case LIVING_EQUIPMENT_BREAK_OFF_HAND:
LevelSoundEvent2Packet equipmentBreakPacket = new LevelSoundEvent2Packet();
equipmentBreakPacket.setSound(SoundEvent.BREAK);
equipmentBreakPacket.setPosition(entity.getPosition());
equipmentBreakPacket.setExtraData(-1);
equipmentBreakPacket.setIdentifier("");
session.sendUpstreamPacket(equipmentBreakPacket);
return;
}
session.sendUpstreamPacket(entityEventPacket);

View File

@@ -27,8 +27,9 @@ package org.geysermc.connector.network.translators.java.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.ServerPlayerActionAckPacket;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
@@ -36,11 +37,11 @@ import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.utils.BlockUtils;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.utils.ChunkUtils;
@Translator(packet = ServerPlayerActionAckPacket.class)
@@ -48,48 +49,54 @@ public class JavaPlayerActionAckTranslator extends PacketTranslator<ServerPlayer
@Override
public void translate(ServerPlayerActionAckPacket packet, GeyserSession session) {
LevelEventPacket levelEvent = new LevelEventPacket();
switch (packet.getAction()) {
case FINISH_DIGGING:
double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(session.getBreakingBlock());
if (session.getGameMode() != GameMode.CREATIVE && blockHardness != 0) {
levelEvent.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
levelEvent.setPosition(Vector3f.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ()));
levelEvent.setData(BlockTranslator.getBedrockBlockId(session.getBreakingBlock()));
ChunkUtils.updateBlock(session, packet.getNewState(), packet.getPosition());
if (packet.getAction() == PlayerAction.START_DIGGING && !packet.isSuccessful()) {
LevelEventPacket stopBreak = new LevelEventPacket();
stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
stopBreak.setPosition(Vector3f.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ()));
stopBreak.setData(0);
session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID);
session.sendUpstreamPacket(stopBreak);
}
if (!session.getConnector().getConfig().isCacheChunks()) {
LevelEventPacket levelEvent = new LevelEventPacket();
switch (packet.getAction()) {
case START_DIGGING:
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(packet.getNewState());
levelEvent.setType(LevelEventType.BLOCK_START_BREAK);
levelEvent.setPosition(Vector3f.from(
packet.getPosition().getX(),
packet.getPosition().getY(),
packet.getPosition().getZ()
));
PlayerInventory inventory = session.getPlayerInventory();
GeyserItemStack item = inventory.getItemInHand();
ItemEntry itemEntry = null;
CompoundTag nbtData = new CompoundTag("");
if (item != null) {
itemEntry = item.getItemEntry();
nbtData = item.getNbt();
}
double breakTime = Math.ceil(BlockUtils.getBreakTime(blockHardness, packet.getNewState(), itemEntry, nbtData, session) * 20);
levelEvent.setData((int) (65535 / breakTime));
session.setBreakingBlock(packet.getNewState());
session.sendUpstreamPacket(levelEvent);
session.setBreakingBlock(0);
}
ChunkUtils.updateBlock(session, packet.getNewState(), packet.getPosition());
break;
case START_DIGGING:
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(packet.getNewState());
levelEvent.setType(LevelEventType.BLOCK_START_BREAK);
levelEvent.setPosition(Vector3f.from(
packet.getPosition().getX(),
packet.getPosition().getY(),
packet.getPosition().getZ()
));
PlayerInventory inventory = session.getPlayerInventory();
GeyserItemStack item = inventory.getItemInHand();
double breakTime = Math.ceil(BlockUtils.getBreakTime(blockHardness, packet.getNewState(), item.getItemEntry(), item.getNbt(), session) * 20);
levelEvent.setData((int) (65535 / breakTime));
session.setBreakingBlock(packet.getNewState());
session.sendUpstreamPacket(levelEvent);
break;
case CANCEL_DIGGING:
levelEvent.setType(LevelEventType.BLOCK_STOP_BREAK);
levelEvent.setPosition(Vector3f.from(
packet.getPosition().getX(),
packet.getPosition().getY(),
packet.getPosition().getZ()
));
levelEvent.setData(0);
session.setBreakingBlock(0);
session.sendUpstreamPacket(levelEvent);
break;
case CANCEL_DIGGING:
levelEvent.setType(LevelEventType.BLOCK_STOP_BREAK);
levelEvent.setPosition(Vector3f.from(
packet.getPosition().getX(),
packet.getPosition().getY(),
packet.getPosition().getZ()
));
levelEvent.setData(0);
session.setBreakingBlock(0);
session.sendUpstreamPacket(levelEvent);
break;
}
}
}
}

View File

@@ -46,13 +46,11 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator<Serve
@Override
public void translate(ServerPlayerPositionRotationPacket packet, GeyserSession session) {
PlayerEntity entity = session.getPlayerEntity();
if (entity == null)
return;
if (!session.isLoggedIn())
return;
PlayerEntity entity = session.getPlayerEntity();
if (!session.isSpawned()) {
Vector3f pos = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
entity.setPosition(pos);
@@ -83,7 +81,7 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator<Serve
ChunkUtils.updateChunkPosition(session, pos.toInt());
session.getConnector().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
session.getConnector().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
return;
}

View File

@@ -26,6 +26,7 @@
package org.geysermc.connector.network.translators.java.world;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
@@ -37,17 +38,17 @@ import org.geysermc.connector.network.translators.sound.BlockSoundInteractionHan
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.ChunkUtils;
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket;
@Translator(packet = ServerBlockChangePacket.class)
public class JavaBlockChangeTranslator extends PacketTranslator<ServerBlockChangePacket> {
@Override
public void translate(ServerBlockChangePacket packet, GeyserSession session) {
Position pos = packet.getRecord().getPosition();
boolean updatePlacement = !(session.getConnector().getConfig().isCacheChunks() && session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()) == packet.getRecord().getBlock());
ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), packet.getRecord().getPosition());
if (updatePlacement && session.getConnector().getPlatformType() != PlatformType.SPIGOT) {
boolean updatePlacement = session.getConnector().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event
!(session.getConnector().getConfig().isCacheChunks() &&
session.getConnector().getWorldManager().getBlockAt(session, pos) == packet.getRecord().getBlock());
ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), pos);
if (updatePlacement) {
this.checkPlace(session, packet);
}
this.checkInteract(session, packet);

View File

@@ -36,14 +36,14 @@ import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.sound.SoundRegistry;
@Translator(packet = ServerPlaySoundPacket.class)
public class JavaPlayerPlaySoundTranslator extends PacketTranslator<ServerPlaySoundPacket> {
public class JavaPlaySoundTranslator extends PacketTranslator<ServerPlaySoundPacket> {
@Override
public void translate(ServerPlaySoundPacket packet, GeyserSession session) {
String packetSound;
if(packet.getSound() instanceof BuiltinSound) {
if (packet.getSound() instanceof BuiltinSound) {
packetSound = ((BuiltinSound) packet.getSound()).getName();
} else if(packet.getSound() instanceof CustomSound) {
} else if (packet.getSound() instanceof CustomSound) {
packetSound = ((CustomSound) packet.getSound()).getName();
} else {
session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString());

View File

@@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.translators.java.entity.player;
package org.geysermc.connector.network.translators.java.world;
import com.github.steveice10.mc.protocol.data.game.world.sound.BuiltinSound;
import com.github.steveice10.mc.protocol.data.game.world.sound.CustomSound;
@@ -35,26 +35,35 @@ import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.sound.SoundRegistry;
@Translator(packet = ServerStopSoundPacket.class)
public class JavaPlayerStopSoundTranslator extends PacketTranslator<ServerStopSoundPacket> {
public class JavaStopSoundTranslator extends PacketTranslator<ServerStopSoundPacket> {
@Override
public void translate(ServerStopSoundPacket packet, GeyserSession session) {
// Runs if all sounds are stopped
if (packet.getSound() == null) {
StopSoundPacket stopPacket = new StopSoundPacket();
stopPacket.setStoppingAllSound(true);
stopPacket.setSoundName("");
session.sendUpstreamPacket(stopPacket);
return;
}
String packetSound;
if(packet.getSound() instanceof BuiltinSound) {
if (packet.getSound() instanceof BuiltinSound) {
packetSound = ((BuiltinSound) packet.getSound()).getName();
} else if(packet.getSound() instanceof CustomSound) {
} else if (packet.getSound() instanceof CustomSound) {
packetSound = ((CustomSound) packet.getSound()).getName();
} else {
session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString());
return;
}
SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound);
SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound.replace("minecraft:", ""));
session.getConnector().getLogger()
.debug("[StopSound] Sound mapping " + packetSound + " -> "
+ soundMapping + (soundMapping == null ? "[not found]" : "")
+ " - " + packet.toString());
String playsound;
if(soundMapping == null || soundMapping.getPlaysound() == null) {
if (soundMapping == null || soundMapping.getPlaysound() == null) {
// no mapping
session.getConnector().getLogger()
.debug("[StopSound] Defaulting to sound server gave us.");

View File

@@ -46,17 +46,10 @@ public class JavaUpdateTimeTranslator extends PacketTranslator<ServerUpdateTimeP
session.sendUpstreamPacket(setTimePacket);
if (!session.isDaylightCycle() && time >= 0) {
// Client thinks there is no daylight cycle but there is
setDoDaylightCycleGamerule(session, true);
session.setDaylightCycle(true);
} else if (session.isDaylightCycle() && time < 0) {
// Client thinks there is daylight cycle but there isn't
setDoDaylightCycleGamerule(session, false);
session.setDaylightCycle(false);
}
}
private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) {
session.sendGameRule("dodaylightcycle", doCycle);
// Save the value so we don't have to constantly send a daylight cycle gamerule update
session.setDaylightCycle(doCycle);
}
}

View File

@@ -87,7 +87,9 @@ public class BlockTranslator {
*/
public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID;
// For block breaking animation math
/**
* A list of all Java runtime wool IDs, for use with block breaking math and shears
*/
public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet();
public static final int JAVA_RUNTIME_COBWEB_ID;

View File

@@ -47,7 +47,7 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator {
protected NbtMap getItem(CompoundTag tag) {
ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue());
NbtMapBuilder tagBuilder = NbtMap.builder()
.putShort("id", (short) entry.getBedrockId())
.putString("Name", entry.getBedrockIdentifier())
.putByte("Count", (byte) tag.get("Count").getValue())
.putShort("Damage", (short) entry.getBedrockData());
tagBuilder.put("tag", NbtMap.builder().build());

View File

@@ -50,6 +50,7 @@ public class BlockUtils {
if (toolType.equals("shears")) return isWoolBlock ? 5.0 : 15.0;
if (toolType.equals("")) return 1.0;
switch (toolTier) {
// https://minecraft.gamepedia.com/Breaking#Speed
case "wooden":
return 2.0;
case "stone":
@@ -58,6 +59,8 @@ public class BlockUtils {
return 6.0;
case "diamond":
return 8.0;
case "netherite":
return 9.0;
case "golden":
return 12.0;
default:

View File

@@ -57,20 +57,11 @@ public class DimensionUtils {
public static void switchDimension(GeyserSession session, String javaDimension) {
int bedrockDimension = javaToBedrock(javaDimension);
Entity player = session.getPlayerEntity();
if (javaDimension.equals(session.getDimension()))
return;
if (session.getMovementSendIfIdle() != null) {
session.getMovementSendIfIdle().cancel(true);
}
session.getEntityCache().removeAllEntities();
session.getItemFrameCache().clear();
session.getLecternCache().clear();
session.getSkullCache().clear();
if (session.getPendingDimSwitches().getAndIncrement() > 0) {
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
}
Vector3i pos = Vector3i.from(0, Short.MAX_VALUE, 0);
@@ -151,4 +142,20 @@ public class DimensionUtils {
// Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1;
}
/**
* Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional
* dimension switch.
*
* @param currentDimension the current dimension of the player
* @param newDimension the new dimension that the player will be transferred to
* @return the fake dimension to transfer to
*/
public static String getTemporaryDimension(String currentDimension, String newDimension) {
if (BEDROCK_NETHER_ID == 2) {
// Prevents rare instances of Bedrock locking up
return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER;
}
return currentDimension.equals(OVERWORLD) ? NETHER : OVERWORLD;
}
}

View File

@@ -36,6 +36,7 @@ import org.reflections.util.ConfigurationBuilder;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.function.Function;
@@ -62,7 +63,8 @@ public class FileUtils {
}
public static <T> T loadJson(InputStream src, Class<T> valueType) throws IOException {
return GeyserConnector.JSON_MAPPER.readValue(src, valueType);
// Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read
return GeyserConnector.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType);
}
/**

View File

@@ -187,7 +187,11 @@ public class LanguageUtils {
if (FileUtils.class.getResource("/languages/texts/" + locale + ".properties") == null) {
result = false;
if (GeyserConnector.getInstance() != null && GeyserConnector.getInstance().getLogger() != null) { // Could be too early for these to be initialized
GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language
if (locale.equals("en_US")) {
GeyserConnector.getInstance().getLogger().error("English locale not found in Geyser. Did you clone the submodules? (git submodule update --init)");
} else {
GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language
}
}
} else {
if (!LOCALE_MAPPINGS.containsKey(locale)) {

View File

@@ -29,19 +29,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
import com.nimbusds.jose.JWSObject;
import com.nukkitx.network.util.Preconditions;
import com.nukkitx.protocol.bedrock.packet.LoginPacket;
import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket;
import com.nukkitx.protocol.bedrock.util.EncryptionUtils;
import org.geysermc.common.window.CustomFormBuilder;
import org.geysermc.common.window.CustomFormWindow;
import org.geysermc.common.window.FormWindow;
import org.geysermc.common.window.SimpleFormWindow;
import org.geysermc.common.window.*;
import org.geysermc.common.window.button.FormButton;
import org.geysermc.common.window.component.InputComponent;
import org.geysermc.common.window.component.LabelComponent;
import org.geysermc.common.window.response.CustomFormResponse;
import org.geysermc.common.window.response.ModalFormResponse;
import org.geysermc.common.window.response.SimpleFormResponse;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
@@ -156,13 +155,21 @@ public class LoginEncryptionUtils {
session.sendUpstreamPacketImmediately(packet);
}
private static int AUTH_FORM_ID = 1336;
private static int AUTH_DETAILS_FORM_ID = 1337;
private static final int AUTH_MSA_DETAILS_FORM_ID = 1334;
private static final int AUTH_MSA_CODE_FORM_ID = 1335;
private static final int AUTH_FORM_ID = 1336;
private static final int AUTH_DETAILS_FORM_ID = 1337;
public static void showLoginWindow(GeyserSession session) {
// Set DoDaylightCycle to false so the time doesn't accelerate while we're here
session.setDaylightCycle(false);
String userLanguage = session.getLocale();
SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage));
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login", userLanguage)));
if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) {
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage)));
}
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage)));
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage)));
session.sendForm(window, AUTH_FORM_ID);
@@ -179,12 +186,33 @@ public class LoginEncryptionUtils {
session.sendForm(window, AUTH_DETAILS_FORM_ID);
}
/**
* Prompts the user between either OAuth code login or manual password authentication
*/
public static void showMicrosoftAuthenticationWindow(GeyserSession session) {
String userLanguage = session.getLocale();
SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), "");
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage)));
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage)));
session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID);
}
/**
* Shows the code that a user must input into their browser
*/
public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) {
ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" +
response.user_code, "Done", "%menu.disconnect");
session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID);
}
public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) {
WindowCache windowCache = session.getWindowCache();
if (!windowCache.getWindows().containsKey(formId))
return false;
if(formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID) {
if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) {
FormWindow window = windowCache.getWindows().remove(formId);
window.setResponse(formData.trim());
@@ -198,23 +226,57 @@ public class LoginEncryptionUtils {
String password = response.getInputResponses().get(2);
session.authenticate(email, password);
// Clear windows so authentication data isn't accidentally cached
windowCache.getWindows().clear();
} else {
showLoginDetailsWindow(session);
}
// Clear windows so authentication data isn't accidentally cached
windowCache.getWindows().clear();
} else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) {
boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication();
int microsoftButton = isPasswordAuthentication ? 1 : 0;
int disconnectButton = isPasswordAuthentication ? 2 : 1;
SimpleFormResponse response = (SimpleFormResponse) window.getResponse();
if (response != null) {
if (response.getClickedButtonId() == 0) {
if (isPasswordAuthentication && response.getClickedButtonId() == 0) {
session.setMicrosoftAccount(false);
showLoginDetailsWindow(session);
} else if(response.getClickedButtonId() == 1) {
} else if (response.getClickedButtonId() == microsoftButton) {
session.setMicrosoftAccount(true);
if (isPasswordAuthentication) {
showMicrosoftAuthenticationWindow(session);
} else {
// Just show the OAuth code
session.authenticateWithMicrosoftCode();
}
} else if (response.getClickedButtonId() == disconnectButton) {
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
}
} else {
showLoginWindow(session);
}
} else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) {
SimpleFormResponse response = (SimpleFormResponse) window.getResponse();
if (response != null) {
if (response.getClickedButtonId() == 0) {
session.authenticateWithMicrosoftCode();
} else if (response.getClickedButtonId() == 1) {
showLoginDetailsWindow(session);
} else if (response.getClickedButtonId() == 2) {
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
}
} else {
showLoginWindow(session);
}
} else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) {
ModalFormResponse response = (ModalFormResponse) window.getResponse();
if (response != null) {
if (response.getClickedButtonId() == 1) {
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
}
} else {
showMicrosoftAuthenticationWindow(session);
}
}
}
}

View File

@@ -63,7 +63,7 @@ public class ResourcePack {
// As we just created the directory it will be empty
return;
}
for (File file : directory.listFiles()) {
if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) {
ResourcePack pack = new ResourcePack();
@@ -77,12 +77,15 @@ public class ResourcePack {
if (x.getName().contains("manifest.json")) {
try {
ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);
// Sometimes a pack_manifest file is present and not in a valid format,
// but a manifest file is, so we null check through that one
if (manifest.getHeader().getUuid() != null) {
pack.file = file;
pack.manifest = manifest;
pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());
pack.file = file;
pack.manifest = manifest;
pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());
PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
}
} catch (Exception e) {
e.printStackTrace();
}

View File

@@ -32,10 +32,14 @@ remote:
port: 25565
# Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate).
auth-type: online
# Allow for password-based authentication methods through Geyser. Only useful in online mode.
# If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop.
allow-password-authentication: true
# Whether to enable PROXY protocol or not while connecting to the server.
# This is useful only when:
# 1) Your server supports PROXY protocol (it probably doesn't)
# 2) You run Velocity or BungeeCord with respective option enabled.
# 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config.
# IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!
use-proxy-protocol: false
# Floodgate uses encryption to ensure use from authorised sources.
@@ -51,10 +55,12 @@ floodgate-key-file: public-key.pem
# BedrockAccountUsername: # Your Minecraft: Bedrock Edition username
# email: javaccountemail@example.com # Your Minecraft: Java Edition email
# password: javaccountpassword123 # Your Minecraft: Java Edition password
# microsoft-account: true # Whether the account is a Mojang or Microsoft account.
#
# bluerkelp2:
# email: not_really_my_email_address_mr_minecrafter53267@gmail.com
# password: "this isn't really my password"
# microsoft-account: false
# Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands.
# Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.
@@ -132,8 +138,7 @@ above-bedrock-nether-building: false
force-resource-packs: true
# Allows Xbox achievements to be unlocked.
# This disables certain commands so the Bedrock client can't to "cheat" to get them.
# Commands such as /gamemode and /give will not work from Bedrock with this enabled
# THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating.
xbox-achievements-enabled: false
# bStats is a stat tracker that is entirely anonymous and tracks only basic information