Merge remote-tracking branch 'origin/master' into feature/floodgate-merge

This commit is contained in:
Tim203 2023-02-08 13:57:30 +01:00
commit ca7d57e541
No known key found for this signature in database
GPG key ID: 736F3CD49EF01DBF
158 changed files with 3719 additions and 14571 deletions

View file

@ -8,7 +8,7 @@ plugins {
dependencies {
api("org.geysermc.floodgate", "core", "2.2.0-SNAPSHOT")
api(projects.geyserApi)
api(libs.geyser.api)
// Jackson JSON and YAML serialization
api(libs.bundles.jackson)

View file

@ -30,7 +30,6 @@ import java.net.URISyntaxException;
public final class Constants {
public static final URI GLOBAL_API_WS_URI;
public static final String NTP_SERVER = "time.cloudflare.com";
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
public static final String NEWS_PROJECT_NAME = "geyser";

View file

@ -76,6 +76,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.session.SessionManager;
import org.geysermc.geyser.skin.BedrockSkinUploader;
import org.geysermc.geyser.skin.ProvidedSkins;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
@ -91,6 +92,7 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -199,7 +201,23 @@ public class GeyserImpl implements GeyserApi {
EntityDefinitions.init();
ItemTranslator.init();
MessageTranslator.init();
MinecraftLocale.init();
// Download the latest asset list and cache it
AssetUtils.generateAssetCache().whenComplete((aVoid, ex) -> {
if (ex != null) {
return;
}
MinecraftLocale.ensureEN_US();
String locale = GeyserLocale.getDefaultLocale();
if (!"en_us".equals(locale)) {
// English will be loaded after assets are downloaded, if necessary
MinecraftLocale.downloadAndLoadLocale(locale);
}
ProvidedSkins.init();
CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks);
});
startInstance();
@ -227,7 +245,10 @@ public class GeyserImpl implements GeyserApi {
logger.info(message);
if (platformType == PlatformType.STANDALONE) {
logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
if (config.getRemote().authType() != AuthType.FLOODGATE) {
// If the auth-type is Floodgate, then this Geyser instance is probably owned by the Java server
logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
}
} else if (config.getRemote().authType() == AuthType.FLOODGATE) {
VersionCheckUtils.checkForOutdatedFloodgate(logger);
}

View file

@ -113,6 +113,8 @@ public interface GeyserConfiguration {
boolean isNotifyOnNewBedrockUpdate();
String getUnusableSpaceBlock();
IMetricsInfo getMetrics();
int getPendingAuthenticationTimeout();
@ -189,6 +191,8 @@ public interface GeyserConfiguration {
boolean isUseDirectConnection();
boolean isDisableCompression();
int getConfigVersion();
static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) {

View file

@ -166,6 +166,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("notify-on-new-bedrock-update")
private boolean notifyOnNewBedrockUpdate = true;
@JsonProperty("unusable-space-block")
private String unusableSpaceBlock = "minecraft:barrier";
private MetricsInfo metrics = new MetricsInfo();
@JsonProperty("pending-authentication-timeout")
@ -344,6 +347,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("use-direct-connection")
private boolean useDirectConnection = true;
@JsonProperty("disable-compression")
private boolean isDisableCompression = true;
@JsonProperty("config-version")
private int configVersion = 0;

View file

@ -60,6 +60,7 @@ public final class EntityDefinitions {
public static final EntityDefinition<BeeEntity> BEE;
public static final EntityDefinition<BlazeEntity> BLAZE;
public static final EntityDefinition<BoatEntity> BOAT;
public static final EntityDefinition<CamelEntity> CAMEL;
public static final EntityDefinition<CatEntity> CAT;
public static final EntityDefinition<SpiderEntity> CAVE_SPIDER;
public static final EntityDefinition<MinecartEntity> CHEST_MINECART;
@ -859,6 +860,13 @@ public final class EntityDefinitions {
.addTranslator(MetadataType.BYTE, AbstractHorseEntity::setHorseFlags)
.addTranslator(null) // UUID of owner
.build();
CAMEL = EntityDefinition.inherited(CamelEntity::new, abstractHorseEntityBase)
.type(EntityType.CAMEL)
.identifier("minecraft:llama") // todo 1.20
.height(2.375f).width(1.7f)
.addTranslator(MetadataType.BOOLEAN, CamelEntity::setDashing)
.addTranslator(null) // Last pose change tick
.build();
HORSE = EntityDefinition.inherited(HorseEntity::new, abstractHorseEntityBase)
.type(EntityType.HORSE)
.height(1.6f).width(1.3965f)

View file

@ -46,6 +46,7 @@ public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity {
@Override
protected void initializeMetadata() {
super.initializeMetadata();
// Required, or else the GUI will not open
dirtyMetadata.put(EntityData.CONTAINER_TYPE, (byte) 16);
dirtyMetadata.put(EntityData.CONTAINER_BASE_SIZE, 1);

View file

@ -355,6 +355,7 @@ public class Entity {
setFlag(EntityFlag.ON_FIRE, ((xd & 0x01) == 0x01) && !getFlag(EntityFlag.FIRE_IMMUNE)); // Otherwise immune entities sometimes flicker onfire
setFlag(EntityFlag.SNEAKING, (xd & 0x02) == 0x02);
setFlag(EntityFlag.SPRINTING, (xd & 0x08) == 0x08);
// Swimming is ignored here and instead we rely on the pose
setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80);

View file

@ -40,6 +40,13 @@ public class AgeableEntity extends CreatureEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
@Override
protected void initializeMetadata() {
super.initializeMetadata();
// Required as of 1.19.3 Java
dirtyMetadata.put(EntityData.SCALE, getAdultSize());
}
public void setBaby(BooleanEntityMetadata entityMetadata) {
boolean isBaby = entityMetadata.getPrimitiveValue();
dirtyMetadata.put(EntityData.SCALE, isBaby ? getBabySize() : getAdultSize());

View file

@ -25,7 +25,6 @@
package org.geysermc.geyser.entity.type.living.animal;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@ -42,11 +41,6 @@ public class RabbitEntity extends AnimalEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
@Override
public void setBaby(BooleanEntityMetadata entityMetadata) {
super.setBaby(entityMetadata);
}
public void setRabbitVariant(IntEntityMetadata entityMetadata) {
int variant = entityMetadata.getPrimitiveValue();

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.entity.type.living.animal.horse;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import java.util.UUID;
public class CamelEntity extends AbstractHorseEntity {
private static final float SITTING_HEIGHT_DIFFERENCE = 1.43F;
public CamelEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
@Override
protected void initializeMetadata() {
super.initializeMetadata();
this.dirtyMetadata.put(EntityData.VARIANT, 2); // Closest llama colour to camel
}
@Override
public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) {
return "cactus".equals(javaIdentifierStripped);
}
@Override
protected void setDimensions(Pose pose) {
if (pose == Pose.SITTING) {
setBoundingBoxWidth(definition.height() - SITTING_HEIGHT_DIFFERENCE);
setBoundingBoxWidth(definition.width());
} else {
super.setDimensions(pose);
}
}
public void setDashing(BooleanEntityMetadata entityMetadata) {
}
}

View file

@ -50,6 +50,13 @@ public class CatEntity extends TameableEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
@Override
protected void initializeMetadata() {
super.initializeMetadata();
// Default value (minecraft:black).
dirtyMetadata.put(EntityData.VARIANT, 1);
}
@Override
public void updateRotation(float yaw, float pitch, boolean isOnGround) {
moveRelative(0, 0, 0, yaw, pitch, yaw, isOnGround);

View file

@ -25,8 +25,9 @@
package org.geysermc.geyser.entity.type.living.monster;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.OptionalIntMetadataType;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@ -35,6 +36,7 @@ import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.session.GeyserSession;
import java.util.OptionalInt;
import java.util.UUID;
public class EndermanEntity extends MonsterEntity {
@ -43,8 +45,15 @@ public class EndermanEntity extends MonsterEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
public void setCarriedBlock(IntEntityMetadata entityMetadata) {
dirtyMetadata.put(EntityData.CARRIED_BLOCK, session.getBlockMappings().getBedrockBlockId(entityMetadata.getPrimitiveValue()));
public void setCarriedBlock(EntityMetadata<OptionalInt, OptionalIntMetadataType> entityMetadata) {
int bedrockBlockId;
if (entityMetadata.getValue().isPresent()) {
bedrockBlockId = session.getBlockMappings().getBedrockBlockId(entityMetadata.getValue().getAsInt());
} else {
bedrockBlockId = session.getBlockMappings().getBedrockAirId();
}
dirtyMetadata.put(EntityData.CARRIED_BLOCK, bedrockBlockId);
}
/**

View file

@ -77,7 +77,6 @@ public class PlayerEntity extends LivingEntity {
}
private String username;
private boolean playerList = true; // Player is in the player list
/**
* The textures property from the GameProfile.
@ -101,6 +100,7 @@ public class PlayerEntity extends LivingEntity {
super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
this.username = username;
this.nametag = username;
this.texturesProperty = texturesProperty;
}
@ -120,7 +120,7 @@ public class PlayerEntity extends LivingEntity {
}
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
updateDisplayName(null, false);
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
addPlayerPacket.setUuid(uuid);
@ -316,19 +316,10 @@ public class PlayerEntity extends LivingEntity {
}
//todo this will become common entity logic once UUID support is implemented for them
/**
* @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team
*/
public void updateDisplayName(@Nullable Team team, boolean useGivenTeam) {
if (team == null && !useGivenTeam) {
// Only search for the team if we are not supposed to use the given team
// If the given team is null, this is intentional that we are being removed from the team
team = session.getWorldCache().getScoreboard().getTeamFor(username);
}
public void updateDisplayName(@Nullable Team team) {
boolean needsUpdate;
String newDisplayName = this.username;
if (team != null) {
String newDisplayName;
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.getColor();
String chatColor = MessageTranslator.toChatColor(color);
@ -340,23 +331,16 @@ public class PlayerEntity extends LivingEntity {
// The name is not visible to the session player; clear name
newDisplayName = "";
}
needsUpdate = useGivenTeam && !newDisplayName.equals(nametag);
nametag = newDisplayName;
dirtyMetadata.put(EntityData.NAMETAG, newDisplayName);
} else if (useGivenTeam) {
// The name has reset, if it was previously something else
needsUpdate = !newDisplayName.equals(nametag);
dirtyMetadata.put(EntityData.NAMETAG, this.username);
needsUpdate = !newDisplayName.equals(this.nametag);
this.nametag = newDisplayName;
} else {
needsUpdate = false;
// The name has reset, if it was previously something else
needsUpdate = !this.nametag.equals(this.username);
this.nametag = this.username;
}
if (needsUpdate) {
// Update the metadata as it won't be updated later
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.getMetadata().put(EntityData.NAMETAG, newDisplayName);
packet.setRuntimeEntityId(geyserId);
session.sendUpstreamPacket(packet);
dirtyMetadata.put(EntityData.NAMETAG, this.nametag);
}
}
@ -417,4 +401,11 @@ public class PlayerEntity extends LivingEntity {
session.sendUpstreamPacket(packet);
}
}
/**
* @return the UUID that should be used when dealing with Bedrock's tab list.
*/
public UUID getTabListUuid() {
return getUuid();
}
}

View file

@ -250,4 +250,9 @@ public class SessionPlayerEntity extends PlayerEntity {
dirtyMetadata.put(EntityData.PLAYER_HAS_DIED, (byte) 0);
}
}
@Override
public UUID getTabListUuid() {
return session.getAuthData().uuid();
}
}

View file

@ -26,17 +26,20 @@
package org.geysermc.geyser.entity.type.player;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.GameType;
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
import lombok.Getter;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkullSkinManager;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -46,9 +49,14 @@ import java.util.concurrent.TimeUnit;
*/
public class SkullPlayerEntity extends PlayerEntity {
@Getter
private UUID skullUUID;
@Getter
private Vector3i skullPosition;
public SkullPlayerEntity(GeyserSession session, long geyserId) {
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
setPlayerList(false);
}
@Override
@ -103,11 +111,14 @@ public class SkullPlayerEntity extends PlayerEntity {
}
public void updateSkull(SkullCache.Skull skull) {
if (!skull.getTexturesProperty().equals(getTexturesProperty())) {
skullPosition = skull.getPosition();
if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) {
// Make skull invisible as we change skins
setFlag(EntityFlag.INVISIBLE, true);
updateBedrockMetadata();
skullUUID = skull.getUuid();
setTexturesProperty(skull.getTexturesProperty());
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
* 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

View file

@ -76,7 +76,7 @@ public class AnvilContainer extends Container {
String originalName = ItemUtils.getCustomName(getInput().getNbt());
String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.locale());
String plainNewName = MessageTranslator.convertToPlainText(rename, session.locale());
String plainNewName = MessageTranslator.convertToPlainText(rename);
if (!plainOriginalName.equals(plainNewName)) {
// Strip out formatting since Java Edition does not allow it
correctRename = plainNewName;

View file

@ -30,10 +30,7 @@ import com.github.steveice10.mc.protocol.data.game.inventory.ContainerActionType
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import com.github.steveice10.mc.protocol.data.game.inventory.MoveToHotbarAction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.*;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.SlotType;
@ -124,12 +121,14 @@ public final class ClickPlan {
}
ItemStack clickedItemStack;
if (!planIter.hasNext() && refresh) {
clickedItemStack = InventoryUtils.REFRESH_ITEM;
if (emulatePost1_16Logic) {
// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
clickedItemStack = simulatedCursor.getItemStack();
} else {
if (emulatePost1_16Logic) {
// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
clickedItemStack = simulatedCursor.getItemStack();
if (!planIter.hasNext() && refresh) {
// Doesn't have the intended effect with state IDs since this won't cause a complete window refresh
// (It will eventually once state IDs desync, but this causes more problems than not)
clickedItemStack = InventoryUtils.REFRESH_ITEM;
} else {
if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) {
clickedItemStack = null;

View file

@ -25,7 +25,6 @@
package org.geysermc.geyser.inventory.holder;
import com.google.common.collect.ImmutableSet;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
@ -39,6 +38,7 @@ import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.Collections;
import java.util.HashSet;
@ -63,14 +63,14 @@ public class BlockInventoryHolder extends InventoryHolder {
Set<String> validBlocksTemp = new HashSet<>(validBlocks.length + 1);
Collections.addAll(validBlocksTemp, validBlocks);
validBlocksTemp.add(BlockUtils.getCleanIdentifier(javaBlockIdentifier));
this.validBlocks = ImmutableSet.copyOf(validBlocksTemp);
this.validBlocks = Set.copyOf(validBlocksTemp);
} else {
this.validBlocks = Collections.singleton(BlockUtils.getCleanIdentifier(javaBlockIdentifier));
}
}
@Override
public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
public boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
// Check to see if there is an existing block we can use that the player just selected.
// First, verify that the player's position has not changed, so we don't try to select a block wildly out of range.
// (This could be a virtual inventory that the player is opening)
@ -83,13 +83,16 @@ public class BlockInventoryHolder extends InventoryHolder {
inventory.setHolderPosition(session.getLastInteractionBlockPosition());
((Container) inventory).setUsingRealBlock(true, javaBlockString[0]);
setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId);
return;
return true;
}
}
// Otherwise, time to conjure up a fake block!
Vector3i position = session.getPlayerEntity().getPosition().toInt();
position = position.add(Vector3i.UP);
Vector3i position = InventoryUtils.findAvailableWorldSpace(session);
if (position == null) {
return false;
}
UpdateBlockPacket blockPacket = new UpdateBlockPacket();
blockPacket.setDataLayer(0);
blockPacket.setBlockPosition(position);
@ -99,6 +102,8 @@ public class BlockInventoryHolder extends InventoryHolder {
inventory.setHolderPosition(position);
setCustomName(session, position, inventory, defaultJavaBlockState);
return true;
}
/**

View file

@ -30,7 +30,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
public abstract class InventoryHolder {
public abstract void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
public abstract boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
public abstract void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
public abstract void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
}

View file

@ -32,43 +32,41 @@ import lombok.Getter;
* It is used to construct the list for the settings menu
*/
public enum GameRule {
ANNOUNCEADVANCEMENTS("announceAdvancements", Boolean.class, true), // JE only
COMMANDBLOCKOUTPUT("commandBlockOutput", Boolean.class, true),
DISABLEELYTRAMOVEMENTCHECK("disableElytraMovementCheck", Boolean.class, false), // JE only
DISABLERAIDS("disableRaids", Boolean.class, false), // JE only
DODAYLIGHTCYCLE("doDaylightCycle", Boolean.class, true),
DOENTITYDROPS("doEntityDrops", Boolean.class, true),
DOFIRETICK("doFireTick", Boolean.class, true),
DOIMMEDIATERESPAWN("doImmediateRespawn", Boolean.class, false),
DOINSOMNIA("doInsomnia", Boolean.class, true),
DOLIMITEDCRAFTING("doLimitedCrafting", Boolean.class, false), // JE only
DOMOBLOOT("doMobLoot", Boolean.class, true),
DOMOBSPAWNING("doMobSpawning", Boolean.class, true),
DOPATROLSPAWNING("doPatrolSpawning", Boolean.class, true), // JE only
DOTILEDROPS("doTileDrops", Boolean.class, true),
DOTRADERSPAWNING("doTraderSpawning", Boolean.class, true), // JE only
DOWEATHERCYCLE("doWeatherCycle", Boolean.class, true),
DROWNINGDAMAGE("drowningDamage", Boolean.class, true),
FALLDAMAGE("fallDamage", Boolean.class, true),
FIREDAMAGE("fireDamage", Boolean.class, true),
FREEZEDAMAGE("freezeDamage", Boolean.class, true),
FORGIVEDEADPLAYERS("forgiveDeadPlayers", Boolean.class, true), // JE only
KEEPINVENTORY("keepInventory", Boolean.class, false),
LOGADMINCOMMANDS("logAdminCommands", Boolean.class, true), // JE only
MAXCOMMANDCHAINLENGTH("maxCommandChainLength", Integer.class, 65536),
MAXENTITYCRAMMING("maxEntityCramming", Integer.class, 24), // JE only
MOBGRIEFING("mobGriefing", Boolean.class, true),
NATURALREGENERATION("naturalRegeneration", Boolean.class, true),
PLAYERSSLEEPINGPERCENTAGE("playersSleepingPercentage", Integer.class, 100), // JE only
RANDOMTICKSPEED("randomTickSpeed", Integer.class, 3),
REDUCEDDEBUGINFO("reducedDebugInfo", Boolean.class, false), // JE only
SENDCOMMANDFEEDBACK("sendCommandFeedback", Boolean.class, true),
SHOWDEATHMESSAGES("showDeathMessages", Boolean.class, true),
SPAWNRADIUS("spawnRadius", Integer.class, 10),
SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", Boolean.class, true), // JE only
UNIVERSALANGER("universalAnger", Boolean.class, false), // JE only
UNKNOWN("unknown", Object.class);
ANNOUNCEADVANCEMENTS("announceAdvancements", true), // JE only
COMMANDBLOCKOUTPUT("commandBlockOutput", true),
DISABLEELYTRAMOVEMENTCHECK("disableElytraMovementCheck", false), // JE only
DISABLERAIDS("disableRaids", false), // JE only
DODAYLIGHTCYCLE("doDaylightCycle", true),
DOENTITYDROPS("doEntityDrops", true),
DOFIRETICK("doFireTick", true),
DOIMMEDIATERESPAWN("doImmediateRespawn", false),
DOINSOMNIA("doInsomnia", true),
DOLIMITEDCRAFTING("doLimitedCrafting", false), // JE only
DOMOBLOOT("doMobLoot", true),
DOMOBSPAWNING("doMobSpawning", true),
DOPATROLSPAWNING("doPatrolSpawning", true), // JE only
DOTILEDROPS("doTileDrops", true),
DOTRADERSPAWNING("doTraderSpawning", true), // JE only
DOWEATHERCYCLE("doWeatherCycle", true),
DROWNINGDAMAGE("drowningDamage", true),
FALLDAMAGE("fallDamage", true),
FIREDAMAGE("fireDamage", true),
FREEZEDAMAGE("freezeDamage", true),
FORGIVEDEADPLAYERS("forgiveDeadPlayers", true), // JE only
KEEPINVENTORY("keepInventory", false),
LOGADMINCOMMANDS("logAdminCommands", true), // JE only
MAXCOMMANDCHAINLENGTH("maxCommandChainLength", 65536),
MAXENTITYCRAMMING("maxEntityCramming", 24), // JE only
MOBGRIEFING("mobGriefing", true),
NATURALREGENERATION("naturalRegeneration", true),
PLAYERSSLEEPINGPERCENTAGE("playersSleepingPercentage", 100), // JE only
RANDOMTICKSPEED("randomTickSpeed", 3),
REDUCEDDEBUGINFO("reducedDebugInfo", false), // JE only
SENDCOMMANDFEEDBACK("sendCommandFeedback", true),
SHOWDEATHMESSAGES("showDeathMessages", true),
SPAWNRADIUS("spawnRadius", 10),
SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", true), // JE only
UNIVERSALANGER("universalAnger", false); // JE only
public static final GameRule[] VALUES = values();
@ -78,48 +76,25 @@ public enum GameRule {
@Getter
private final Class<?> type;
@Getter
private final Object defaultValue;
private final int defaultValue;
GameRule(String javaID, Class<?> type) {
this(javaID, type, null);
GameRule(String javaID, boolean defaultValue) {
this.javaID = javaID;
this.type = Boolean.class;
this.defaultValue = defaultValue ? 1 : 0;
}
GameRule(String javaID, Class<?> type, Object defaultValue) {
GameRule(String javaID, int defaultValue) {
this.javaID = javaID;
this.type = type;
this.type = Integer.class;
this.defaultValue = defaultValue;
}
/**
* Convert a string to an object of the correct type for the current gamerule
*
* @param value The string value to convert
* @return The converted and formatted value
*/
public Object convertValue(String value) {
if (type.equals(Boolean.class)) {
return Boolean.parseBoolean(value);
} else if (type.equals(Integer.class)) {
return Integer.parseInt(value);
}
return null;
public boolean getDefaultBooleanValue() {
return defaultValue != 0;
}
/**
* Fetch a game rule by the given Java ID
*
* @param id The ID of the gamerule
* @return A {@link GameRule} object representing the requested ID or {@link GameRule#UNKNOWN}
*/
public static GameRule fromJavaID(String id) {
for (GameRule gamerule : VALUES) {
if (gamerule.javaID.equals(id)) {
return gamerule;
}
}
return UNKNOWN;
public int getDefaultIntValue() {
return defaultValue;
}
}

View file

@ -25,8 +25,6 @@
package org.geysermc.geyser.level;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
@ -36,11 +34,8 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.ChunkCache;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import java.util.Locale;
public class GeyserWorldManager extends WorldManager {
private static final Object2ObjectMap<String, String> gameruleCache = new Object2ObjectOpenHashMap<>();
private final Object2ObjectMap<String, String> gameruleCache = new Object2ObjectOpenHashMap<>();
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
@ -82,18 +77,18 @@ public class GeyserWorldManager extends WorldManager {
@Override
public void setGameRule(GeyserSession session, String name, Object value) {
session.sendCommand("gamerule " + name + " " + value);
super.setGameRule(session, name, value);
gameruleCache.put(name, String.valueOf(value));
}
@Override
public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
String value = gameruleCache.get(gameRule.getJavaID());
if (value != null) {
return Boolean.parseBoolean(value);
}
return gameRule.getDefaultValue() != null ? (Boolean) gameRule.getDefaultValue() : false;
return gameRule.getDefaultBooleanValue();
}
@Override
@ -103,17 +98,7 @@ public class GeyserWorldManager extends WorldManager {
return Integer.parseInt(value);
}
return gameRule.getDefaultValue() != null ? (int) gameRule.getDefaultValue() : 0;
}
@Override
public void setPlayerGameMode(GeyserSession session, GameMode gameMode) {
session.sendCommand("gamemode " + gameMode.name().toLowerCase(Locale.ROOT));
}
@Override
public void setDifficulty(GeyserSession session, Difficulty difficulty) {
session.sendCommand("difficulty " + difficulty.name().toLowerCase(Locale.ROOT));
return gameRule.getDefaultIntValue();
}
@Override

View file

@ -27,9 +27,15 @@ package org.geysermc.geyser.level;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
/**
* Class that manages or retrieves various information
@ -105,7 +111,9 @@ public abstract class WorldManager {
* @param name The gamerule to change
* @param value The new value for the gamerule
*/
public abstract void setGameRule(GeyserSession session, String name, Object value);
public void setGameRule(GeyserSession session, String name, Object value) {
session.sendCommand("gamerule " + name + " " + value);
}
/**
* Gets a gamerule value as a boolean
@ -114,7 +122,7 @@ public abstract class WorldManager {
* @param gameRule The gamerule to fetch the value of
* @return The boolean representation of the value
*/
public abstract Boolean getGameRuleBool(GeyserSession session, GameRule gameRule);
public abstract boolean getGameRuleBool(GeyserSession session, GameRule gameRule);
/**
* Get a gamerule value as an integer
@ -131,7 +139,9 @@ public abstract class WorldManager {
* @param session The session of the player to change the game mode of
* @param gameMode The game mode to change the player to
*/
public abstract void setPlayerGameMode(GeyserSession session, GameMode gameMode);
public void setPlayerGameMode(GeyserSession session, GameMode gameMode) {
session.sendCommand("gamemode " + gameMode.name().toLowerCase(Locale.ROOT));
}
/**
* Change the difficulty of the Java server
@ -139,7 +149,9 @@ public abstract class WorldManager {
* @param session The session of the user that requested the change
* @param difficulty The difficulty to change to
*/
public abstract void setDifficulty(GeyserSession session, Difficulty difficulty);
public void setDifficulty(GeyserSession session, Difficulty difficulty) {
session.sendCommand("difficulty " + difficulty.name().toLowerCase(Locale.ROOT));
}
/**
* Checks if the given session's player has a permission
@ -149,4 +161,22 @@ public abstract class WorldManager {
* @return True if the player has the requested permission, false if not
*/
public abstract boolean hasPermission(GeyserSession session, String permission);
/**
* Returns a list of biome identifiers available on the server.
*/
@Nullable
public String[] getBiomeIdentifiers(boolean withTags) {
return null;
}
/**
* Used for pick block, so we don't need to cache more data than necessary.
*
* @return expected NBT for this item.
*/
@Nonnull
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
return CompletableFuture.completedFuture(null);
}
}

View file

@ -125,13 +125,9 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
pong.setSubMotd(config.getBedrock().secondaryMotd());
}
if (config.isPassthroughPlayerCounts() && pingInfo != null) {
pong.setPlayerCount(pingInfo.getPlayers().getOnline());
pong.setMaximumPlayerCount(pingInfo.getPlayers().getMax());
} else {
pong.setPlayerCount(geyser.getSessionManager().getSessions().size());
pong.setMaximumPlayerCount(config.getMaxPlayers());
}
// https://github.com/GeyserMC/Geyser/issues/3388
pong.setMotd(pong.getMotd().replace(';', ':'));
pong.setSubMotd(pong.getSubMotd().replace(';', ':'));
// Fallbacks to prevent errors and allow Bedrock to see the server
if (pong.getMotd() == null || pong.getMotd().isBlank()) {
@ -160,6 +156,14 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
}
}
if (config.isPassthroughPlayerCounts() && pingInfo != null) {
pong.setPlayerCount(pingInfo.getPlayers().getOnline());
pong.setMaximumPlayerCount(pingInfo.getPlayers().getMax());
} else {
pong.setPlayerCount(geyser.getSessionManager().getSessions().size());
pong.setMaximumPlayerCount(config.getMaxPlayers());
}
//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

@ -28,12 +28,11 @@ package org.geysermc.geyser.network;
import com.github.steveice10.mc.protocol.codec.MinecraftCodec;
import com.github.steveice10.mc.protocol.codec.PacketCodec;
import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
import com.nukkitx.protocol.bedrock.v534.Bedrock_v534;
import com.nukkitx.protocol.bedrock.v544.Bedrock_v544;
import com.nukkitx.protocol.bedrock.v545.Bedrock_v545;
import com.nukkitx.protocol.bedrock.v554.Bedrock_v554;
import com.nukkitx.protocol.bedrock.v557.Bedrock_v557;
import com.nukkitx.protocol.bedrock.v560.Bedrock_v560;
import org.geysermc.geyser.session.GeyserSession;
import java.util.ArrayList;
@ -48,7 +47,7 @@ public final class GameProtocol {
* Default Bedrock codec that should act as a fallback. Should represent the latest available
* release of the game that Geyser supports.
*/
public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v557.V557_CODEC;
public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v560.V560_CODEC;
/**
* A list of all supported Bedrock versions that can join Geyser
*/
@ -61,12 +60,6 @@ public final class GameProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static {
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v527.V527_CODEC.toBuilder()
.minecraftVersion("1.19.0/1.19.2")
.build());
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v534.V534_CODEC.toBuilder()
.minecraftVersion("1.19.10/1.19.11")
.build());
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v544.V544_CODEC);
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v545.V545_CODEC.toBuilder()
.minecraftVersion("1.19.21/1.19.22")
@ -74,7 +67,12 @@ public final class GameProtocol {
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v554.V554_CODEC.toBuilder()
.minecraftVersion("1.19.30/1.19.31")
.build());
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v557.V557_CODEC.toBuilder()
.minecraftVersion("1.19.40/1.19.41")
.build());
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
.minecraftVersion("1.19.50/1.19.51")
.build());
}
/**
@ -93,14 +91,14 @@ public final class GameProtocol {
/* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
public static boolean supports1_19_10(GeyserSession session) {
return session.getUpstream().getProtocolVersion() >= Bedrock_v534.V534_CODEC.getProtocolVersion();
}
public static boolean supports1_19_30(GeyserSession session) {
return session.getUpstream().getProtocolVersion() >= Bedrock_v554.V554_CODEC.getProtocolVersion();
}
public static boolean supports1_19_50(GeyserSession session) {
return session.getUpstream().getProtocolVersion() >= Bedrock_v560.V560_CODEC.getProtocolVersion();
}
/**
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
*
@ -116,7 +114,7 @@ public final class GameProtocol {
* @return the supported Minecraft: Java Edition version names
*/
public static List<String> getJavaVersions() {
return List.of(DEFAULT_JAVA_CODEC.getMinecraftVersion(), "1.19.2");
return List.of(DEFAULT_JAVA_CODEC.getMinecraftVersion());
}
/**

View file

@ -46,9 +46,13 @@ import org.geysermc.geyser.util.MathUtils;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayDeque;
import java.util.Deque;
public class UpstreamPacketHandler extends LoggingPacketHandler {
private Deque<String> packsToSent = new ArrayDeque<>();
public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) {
super(geyser, session);
}
@ -161,24 +165,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
break;
case SEND_PACKS:
for(String id : packet.getPackIds()) {
ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket();
String[] packID = id.split("_");
ResourcePack pack = ResourcePack.PACKS.get(packID[0]);
ResourcePackManifest.Header header = pack.getManifest().getHeader();
data.setPackId(header.getUuid());
int chunkCount = (int) Math.ceil((int) pack.getFile().length() / (double) ResourcePack.CHUNK_SIZE);
data.setChunkCount(chunkCount);
data.setCompressedPackSize(pack.getFile().length());
data.setMaxChunkSize(ResourcePack.CHUNK_SIZE);
data.setHash(pack.getSha256());
data.setPackVersion(packID[1]);
data.setPremium(false);
data.setType(ResourcePackType.RESOURCE);
session.sendUpstreamPacket(data);
}
packsToSent.addAll(packet.getPackIds());
sendPackDataInfo(packsToSent.pop());
break;
case HAVE_ALL_PACKS:
@ -271,7 +259,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
data.setPackId(packet.getPackId());
int offset = packet.getChunkIndex() * ResourcePack.CHUNK_SIZE;
byte[] packData = new byte[(int) MathUtils.constrain(pack.getFile().length() - offset, 0, ResourcePack.CHUNK_SIZE)];
long remainingSize = pack.getFile().length() - offset;
byte[] packData = new byte[(int) MathUtils.constrain(remainingSize, 0, ResourcePack.CHUNK_SIZE)];
try (InputStream inputStream = new FileInputStream(pack.getFile())) {
inputStream.skip(offset);
@ -283,6 +272,31 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
data.setData(packData);
session.sendUpstreamPacket(data);
// Check if it is the last chunk and send next pack in queue when available.
if (remainingSize <= ResourcePack.CHUNK_SIZE && !packsToSent.isEmpty()) {
sendPackDataInfo(packsToSent.pop());
}
return true;
}
private void sendPackDataInfo(String id) {
ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket();
String[] packID = id.split("_");
ResourcePack pack = ResourcePack.PACKS.get(packID[0]);
ResourcePackManifest.Header header = pack.getManifest().getHeader();
data.setPackId(header.getUuid());
int chunkCount = (int) Math.ceil((int) pack.getFile().length() / (double) ResourcePack.CHUNK_SIZE);
data.setChunkCount(chunkCount);
data.setCompressedPackSize(pack.getFile().length());
data.setMaxChunkSize(ResourcePack.CHUNK_SIZE);
data.setHash(pack.getSha256());
data.setPackVersion(packID[1]);
data.setPremium(false);
data.setType(ResourcePackType.RESOURCE);
session.sendUpstreamPacket(data);
}
}

View file

@ -181,12 +181,15 @@ public final class Registries {
POTION_MIXES = SimpleRegistry.create(PotionMixRegistryLoader::new);
ENCHANTMENTS = SimpleMappedRegistry.create("mappings/enchantments.json", EnchantmentRegistryLoader::new);
// TEMPORARY FIX TO MAKE OLD BIOMES NBT WORK WITH 1.19.30
// Remove unneeded client generation data from NbtMapBuilder
NbtMapBuilder biomesNbt = NbtMap.builder();
for (Map.Entry<String, Object> entry : BIOMES_NBT.get().entrySet()) {
String key = entry.getKey();
NbtMapBuilder value = ((NbtMap) entry.getValue()).toBuilder();
value.put("name_hash", key);
value.remove("minecraft:consolidated_features");
value.remove("minecraft:multinoise_generation_rules");
value.remove("minecraft:surface_material_adjustments");
value.remove( "minecraft:surface_parameters");
biomesNbt.put(key, value.build());
}
BIOMES_NBT.set(biomesNbt.build());

View file

@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableMap;
import com.nukkitx.nbt.*;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
import com.nukkitx.protocol.bedrock.v544.Bedrock_v544;
import com.nukkitx.protocol.bedrock.v560.Bedrock_v560;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
@ -73,13 +74,9 @@ public final class BlockRegistryPopulator {
private static void registerBedrockBlocks() {
BiFunction<String, NbtMapBuilder, String> emptyMapper = (bedrockIdentifier, statesBuilder) -> null;
ImmutableMap<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> blockMappers = ImmutableMap.<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>>builder()
.put(ObjectIntPair.of("1_19_0", Bedrock_v527.V527_CODEC.getProtocolVersion()), (bedrockIdentifier, statesBuilder) -> {
if (bedrockIdentifier.equals("minecraft:muddy_mangrove_roots")) {
statesBuilder.remove("pillar_axis");
}
return null;
})
.put(ObjectIntPair.of("1_19_20", Bedrock_v544.V544_CODEC.getProtocolVersion()), emptyMapper).build();
.put(ObjectIntPair.of("1_19_20", Bedrock_v544.V544_CODEC.getProtocolVersion()), emptyMapper)
.put(ObjectIntPair.of("1_19_50", Bedrock_v560.V560_CODEC.getProtocolVersion()), emptyMapper)
.build();
for (Map.Entry<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> palette : blockMappers.entrySet()) {
NbtList<NbtMap> blocksTag;

View file

@ -124,6 +124,33 @@ public class CustomItemRegistryPopulator {
computeArmorProperties(mapping.getArmorType(), mapping.getProtectionValue(), componentBuilder);
}
if (mapping.getFirstBlockRuntimeId() != null) {
computeBlockItemProperties(mapping.getBedrockIdentifier(), componentBuilder);
}
if (mapping.isEdible()) {
computeConsumableProperties(itemProperties, componentBuilder, 1, false);
}
if (mapping.isEntityPlacer()) {
computeEntityPlacerProperties(componentBuilder);
}
switch (mapping.getBedrockIdentifier()) {
case "minecraft:fire_charge", "minecraft:flint_and_steel" -> {
computeBlockItemProperties("minecraft:fire", componentBuilder);
}
case "minecraft:bow", "minecraft:crossbow", "minecraft:trident" -> {
computeChargeableProperties(itemProperties, componentBuilder);
}
case "minecraft:honey_bottle", "minecraft:milk_bucket", "minecraft:potion" -> {
computeConsumableProperties(itemProperties, componentBuilder, 2, true);
}
case "minecraft:experience_bottle", "minecraft:egg", "minecraft:ender_pearl", "minecraft:ender_eye", "minecraft:lingering_potion", "minecraft:snowball", "minecraft:splash_potion" -> {
computeThrowableProperties(componentBuilder);
}
}
computeRenderOffsets(false, customItemData, componentBuilder);
componentBuilder.putCompound("item_properties", itemProperties.build());
@ -260,6 +287,48 @@ public class CustomItemRegistryPopulator {
}
}
private static void computeBlockItemProperties(String blockItem, NbtMapBuilder componentBuilder) {
// carved pumpkin should be able to be worn and for that we would need to add wearable and armor with protection 0 here
// however this would have the side effect of preventing carved pumpkins from working as an attachable on the RP side outside the head slot
// it also causes the item to glitch when right clicked to "equip" so this should only be added here later if these issues can be overcome
// all block items registered should be given this component to prevent double placement
componentBuilder.putCompound("minecraft:block_placer", NbtMap.builder().putString("block", blockItem).build());
}
private static void computeChargeableProperties(NbtMapBuilder itemProperties, NbtMapBuilder componentBuilder) {
// setting high use_duration prevents the consume animation from playing
itemProperties.putInt("use_duration", Integer.MAX_VALUE);
// display item as tool (mainly for crossbow and bow)
itemProperties.putBoolean("hand_equipped", true);
// ensure client moves at slow speed while charging (note: this was calculated by hand as the movement modifer value does not seem to scale linearly)
componentBuilder.putCompound("minecraft:chargeable", NbtMap.builder().putFloat("movement_modifier", 0.35F).build());
}
private static void computeConsumableProperties(NbtMapBuilder itemProperties, NbtMapBuilder componentBuilder, int useAnimation, boolean canAlwaysEat) {
// this is the duration of the use animation in ticks; note that in behavior packs this is set as a float in seconds, but over the network it is an int in ticks
itemProperties.putInt("use_duration", 32);
// this dictates that the item will use the eat or drink animation (in the first person) and play eat or drink sounds
// note that in behavior packs this is set as the string "eat" or "drink", but over the network it as an int, with these values being 1 and 2 respectively
itemProperties.putInt("use_animation", useAnimation);
// this component is required to allow the eat animation to play
componentBuilder.putCompound("minecraft:food", NbtMap.builder().putBoolean("can_always_eat", canAlwaysEat).build());
}
private static void computeEntityPlacerProperties(NbtMapBuilder componentBuilder) {
// all items registered that place entities should be given this component to prevent double placement
// it is okay that the entity here does not match the actual one since we control what entity actually spawns
componentBuilder.putCompound("minecraft:entity_placer", NbtMap.builder().putString("entity", "minecraft:minecart").build());
}
private static void computeThrowableProperties(NbtMapBuilder componentBuilder) {
// allows item to be thrown when holding down right click (individual presses are required w/o this component)
componentBuilder.putCompound("minecraft:throwable", NbtMap.builder().putBoolean("do_swing_animation", true).build());
// this must be set to something for the swing animation to play
// it is okay that the projectile here does not match the actual one since we control what entity actually spawns
componentBuilder.putCompound("minecraft:projectile", NbtMap.builder().putString("projectile_entity", "minecraft:snowball").build());
}
private static void computeRenderOffsets(boolean isHat, CustomItemData customItemData, NbtMapBuilder componentBuilder) {
if (isHat) {
componentBuilder.remove("minecraft:render_offsets");

View file

@ -37,6 +37,7 @@ import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.StartGamePacket;
import com.nukkitx.protocol.bedrock.v560.Bedrock_v560;
import it.unimi.dsi.fastutil.ints.*;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
import com.nukkitx.protocol.bedrock.v534.Bedrock_v534;
@ -76,10 +77,8 @@ public class ItemRegistryPopulator {
public static void populate() {
Map<String, PaletteVersion> paletteVersions = new Object2ObjectOpenHashMap<>();
paletteVersions.put("1_19_0", new PaletteVersion(Bedrock_v527.V527_CODEC.getProtocolVersion(),
Collections.singletonMap("minecraft:trader_llama_spawn_egg", "minecraft:llama_spawn_egg")));
paletteVersions.put("1_19_10", new PaletteVersion(Bedrock_v534.V534_CODEC.getProtocolVersion(), Collections.emptyMap()));
paletteVersions.put("1_19_20", new PaletteVersion(Bedrock_v544.V544_CODEC.getProtocolVersion(), Collections.emptyMap()));
paletteVersions.put("1_19_50", new PaletteVersion(Bedrock_v560.V560_CODEC.getProtocolVersion(), Collections.emptyMap()));
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();

View file

@ -82,8 +82,6 @@ public class RecipeRegistryPopulator {
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), ++LAST_RECIPE_NET_ID)));
craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPCLONING,
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), ++LAST_RECIPE_NET_ID)));
craftingData.put(RecipeType.CRAFTING_SPECIAL_BANNERADDPATTERN,
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("b5c5d105-75a2-4076-af2b-923ea2bf4bf0"), ++LAST_RECIPE_NET_ID)));
// https://github.com/pmmp/PocketMine-MP/blob/stable/src/pocketmine/inventory/MultiRecipe.php

View file

@ -48,4 +48,6 @@ public class GeyserMappingItem {
@JsonProperty("repair_materials") List<String> repairMaterials;
@JsonProperty("has_suspicious_stew_effect") boolean hasSuspiciousStewEffect = false;
@JsonProperty("dye_color") int dyeColor = -1;
@JsonProperty("is_edible") boolean edible = false;
@JsonProperty("is_entity_placer") boolean entityPlacer = false;
}

View file

@ -30,6 +30,7 @@ import com.nukkitx.protocol.bedrock.data.ScoreInfo;
import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket;
import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket;
import com.nukkitx.protocol.bedrock.packet.SetScorePacket;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
@ -37,6 +38,7 @@ import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nullable;
import java.util.*;
@ -55,6 +57,13 @@ public final class Scoreboard {
@Getter
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class);
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
/**
* Required to preserve vanilla behavior, which also uses a map.
* Otherwise, for example, if TAB has a team for a player and vanilla has a team, "race conditions" that do not
* match vanilla could occur.
*/
@Getter
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
private int lastAddScoreCount = 0;
private int lastRemoveScoreCount = 0;
@ -132,6 +141,10 @@ public final class Scoreboard {
team = new Team(this, teamName);
team.addEntities(players);
teams.put(teamName, team);
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
session.addCommandEnum("Geyser_Teams", team.getId());
return team;
}
@ -328,12 +341,7 @@ public final class Scoreboard {
}
public Team getTeamFor(String entity) {
for (Team team : teams.values()) {
if (team.hasEntity(entity)) {
return team;
}
}
return null;
return playerToTeam.get(entity);
}
public void removeTeam(String teamName) {
@ -343,9 +351,19 @@ public final class Scoreboard {
// We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly
// With the player's lack of a team in visibility checks
updateEntityNames(remove, remove.getEntities(), true);
for (String name : remove.getEntities()) {
playerToTeam.remove(name, remove);
}
session.removeCommandEnum("Geyser_Teams", remove.getId());
}
}
@Contract("-> new")
public String[] getTeamNames() {
return teams.keySet().toArray(new String[0]);
}
/**
* Updates the display names of all entities in a given team.
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
@ -368,7 +386,8 @@ public final class Scoreboard {
for (Entity entity : session.getEntityCache().getEntities().values()) {
// This more complex logic is for the future to iterate over all entities, not just players
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
player.updateDisplayName(team, true);
player.updateDisplayName(team);
player.updateBedrockMetadata();
if (names.isEmpty()) {
break;
}
@ -384,7 +403,8 @@ public final class Scoreboard {
for (Entity entity : session.getEntityCache().getEntities().values()) {
if (entity instanceof PlayerEntity player) {
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
player.updateDisplayName(playerTeam, true);
player.updateDisplayName(playerTeam);
player.updateBedrockMetadata();
}
}
}

View file

@ -65,6 +65,7 @@ public final class Team {
if (entities.add(name)) {
added.add(name);
}
scoreboard.getPlayerToTeam().put(name, this);
}
if (added.isEmpty()) {
@ -93,6 +94,7 @@ public final class Team {
if (entities.remove(name)) {
removed.add(name);
}
scoreboard.getPlayerToTeam().remove(name, this);
}
return removed;
}

View file

@ -62,18 +62,18 @@ import com.github.steveice10.packetlib.event.session.*;
import com.github.steveice10.packetlib.packet.Packet;
import com.github.steveice10.packetlib.tcp.TcpClientSession;
import com.github.steveice10.packetlib.tcp.TcpSession;
import com.nukkitx.math.GenericMath;
import com.nukkitx.math.vector.*;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.BedrockPacket;
import com.nukkitx.protocol.bedrock.BedrockServerSession;
import com.nukkitx.protocol.bedrock.data.*;
import com.nukkitx.protocol.bedrock.data.command.CommandEnumData;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.data.command.SoftEnumUpdateType;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.*;
import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
import it.unimi.dsi.fastutil.bytes.ByteArrays;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@ -120,7 +120,6 @@ import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.LocalSession;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings;
@ -296,6 +295,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
*/
@Setter
private String worldName = null;
/**
* As of Java 1.19.3, the client only uses these for commands.
*/
@Setter
private String[] levels;
private boolean sneaking;
@ -452,9 +456,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Counts how many ticks have occurred since an arm animation started.
* -1 means there is no active arm swing.
* -1 means there is no active arm swing; -2 means an arm swing will start in a tick.
*/
@Getter(AccessLevel.NONE)
private int armAnimationTicks = -1;
/**
@ -534,6 +537,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private ScheduledFuture<?> lookBackScheduledFuture = null;
/**
* Used to return players back to their vehicles if the server doesn't want them unmounting.
*/
@Setter
private ScheduledFuture<?> mountVehicleScheduledFuture = null;
private MinecraftProtocol protocol;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
@ -627,6 +636,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
creativePacket.setContents(this.itemMappings.getCreativeItems());
upstream.sendPacket(creativePacket);
// Potion mixes are registered by default, as they are needed to be able to put ingredients into the brewing stand.
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true);
craftingDataPacket.getPotionMixData().addAll(Registries.POTION_MIXES.get());
upstream.sendPacket(craftingDataPacket);
PlayStatusPacket playStatusPacket = new PlayStatusPacket();
playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
upstream.sendPacket(playStatusPacket);
@ -1063,6 +1078,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
closed = true;
}
/**
* Moves task to the session event loop if already not in it. Otherwise, the task is automatically ran.
*/
public void ensureInEventLoop(Runnable runnable) {
if (eventLoop.inEventLoop()) {
runnable.run();
return;
}
executeInEventLoop(runnable);
}
/**
* Executes a task and prints a stack trace if an error occurs.
*/
@ -1133,7 +1159,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
entity.tick();
}
if (armAnimationTicks != -1) {
if (armAnimationTicks >= 0) {
// As of 1.18.2 Java Edition, it appears that the swing time is dynamically updated depending on the
// player's effect status, but the animation can cut short if the duration suddenly decreases
// (from suddenly no longer having mining fatigue, for example)
@ -1172,7 +1198,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
public void startSneaking() {
// Toggle the shield, if there is no ongoing arm animation
// This matches Bedrock Edition behavior as of 1.18.12
if (armAnimationTicks == -1) {
if (armAnimationTicks < 0) {
attemptToBlock();
}
@ -1304,6 +1330,16 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
}
/**
* For https://github.com/GeyserMC/Geyser/issues/2113 and combating arm ticking activating being delayed in
* BedrockAnimateTranslator.
*/
public void armSwingPending() {
if (armAnimationTicks == -1) {
armAnimationTicks = -2;
}
}
/**
* Indicates to the client to stop blocking and tells the Java server the same.
*/
@ -1357,18 +1393,21 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Sends a chat message to the Java server.
*/
public void sendChat(String message) {
sendDownstreamPacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, ByteArrays.EMPTY_ARRAY, false, Collections.emptyList(), null));
sendDownstreamPacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet()));
}
/**
* Sends a command to the Java server.
*/
public void sendCommand(String command) {
sendDownstreamPacket(new ServerboundChatCommandPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), false, Collections.emptyList(), null));
sendDownstreamPacket(new ServerboundChatCommandPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet()));
}
public void setServerRenderDistance(int renderDistance) {
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
// +1 is for Fabric and Spigot
// Without the client misses loading some chunks per https://github.com/GeyserMC/Geyser/issues/3490
// Fog still appears essentially normally
renderDistance = renderDistance + 1;
this.serverRenderDistance = renderDistance;
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
@ -1420,7 +1459,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setRotation(Vector2f.from(1, 1));
startGamePacket.setSeed(-1L);
startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(dimension));
startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(chunkCache.getBedrockDimension()));
startGamePacket.setGeneratorId(1);
startGamePacket.setLevelGameType(GameType.SURVIVAL);
startGamePacket.setDifficulty(1);
@ -1622,76 +1661,40 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
boolean spectator = gameMode == GameMode.SPECTATOR;
boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator;
if (GameProtocol.supports1_19_10(this)) {
UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket();
adventureSettingsPacket.setNoMvP(false);
adventureSettingsPacket.setNoPvM(false);
adventureSettingsPacket.setImmutableWorld(worldImmutable);
adventureSettingsPacket.setShowNameTags(false);
adventureSettingsPacket.setAutoJump(true);
sendUpstreamPacket(adventureSettingsPacket);
UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket();
adventureSettingsPacket.setNoMvP(false);
adventureSettingsPacket.setNoPvM(false);
adventureSettingsPacket.setImmutableWorld(worldImmutable);
adventureSettingsPacket.setShowNameTags(false);
adventureSettingsPacket.setAutoJump(true);
sendUpstreamPacket(adventureSettingsPacket);
UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket();
updateAbilitiesPacket.setUniqueEntityId(bedrockId);
updateAbilitiesPacket.setCommandPermission(commandPermission);
updateAbilitiesPacket.setPlayerPermission(playerPermission);
UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket();
updateAbilitiesPacket.setUniqueEntityId(bedrockId);
updateAbilitiesPacket.setCommandPermission(commandPermission);
updateAbilitiesPacket.setPlayerPermission(playerPermission);
AbilityLayer abilityLayer = new AbilityLayer();
Set<Ability> abilities = abilityLayer.getAbilityValues();
if (canFly || spectator) {
abilities.add(Ability.MAY_FLY);
}
// Default stuff we have to fill in
abilities.add(Ability.BUILD);
abilities.add(Ability.MINE);
// Needed so you can drop items
abilities.add(Ability.DOORS_AND_SWITCHES);
if (gameMode == GameMode.CREATIVE) {
// Needed so the client doesn't attempt to take away items
abilities.add(Ability.INSTABUILD);
}
if (commandPermission == CommandPermission.OPERATOR) {
// Fixes a bug? since 1.19.11 where the player can change their gamemode in Bedrock settings and
// a packet is not sent to the server.
// https://github.com/GeyserMC/Geyser/issues/3191
abilities.add(Ability.OPERATOR_COMMANDS);
}
if (flying || spectator) {
if (spectator && !flying) {
// We're "flying locked" in this gamemode
flying = true;
ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true);
sendDownstreamPacket(abilitiesPacket);
}
abilities.add(Ability.FLYING);
}
if (spectator) {
abilities.add(Ability.NO_CLIP);
}
abilityLayer.setLayerType(AbilityLayer.Type.BASE);
abilityLayer.setFlySpeed(flySpeed);
// https://github.com/GeyserMC/Geyser/issues/3139 as of 1.19.10
abilityLayer.setWalkSpeed(walkSpeed == 0f ? 0.01f : walkSpeed);
Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES);
updateAbilitiesPacket.getAbilityLayers().add(abilityLayer);
sendUpstreamPacket(updateAbilitiesPacket);
return;
AbilityLayer abilityLayer = new AbilityLayer();
Set<Ability> abilities = abilityLayer.getAbilityValues();
if (canFly || spectator) {
abilities.add(Ability.MAY_FLY);
}
AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket();
adventureSettingsPacket.setUniqueEntityId(bedrockId);
adventureSettingsPacket.setCommandPermission(commandPermission);
adventureSettingsPacket.setPlayerPermission(playerPermission);
// Default stuff we have to fill in
abilities.add(Ability.BUILD);
abilities.add(Ability.MINE);
// Needed so you can drop items
abilities.add(Ability.DOORS_AND_SWITCHES);
if (gameMode == GameMode.CREATIVE) {
// Needed so the client doesn't attempt to take away items
abilities.add(Ability.INSTABUILD);
}
Set<AdventureSetting> flags = adventureSettingsPacket.getSettings();
if (canFly || spectator) {
flags.add(AdventureSetting.MAY_FLY);
if (commandPermission == CommandPermission.OPERATOR) {
// Fixes a bug? since 1.19.11 where the player can change their gamemode in Bedrock settings and
// a packet is not sent to the server.
// https://github.com/GeyserMC/Geyser/issues/3191
abilities.add(Ability.OPERATOR_COMMANDS);
}
if (flying || spectator) {
@ -1701,20 +1704,21 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true);
sendDownstreamPacket(abilitiesPacket);
}
flags.add(AdventureSetting.FLYING);
}
if (worldImmutable) {
flags.add(AdventureSetting.WORLD_IMMUTABLE);
abilities.add(Ability.FLYING);
}
if (spectator) {
flags.add(AdventureSetting.NO_CLIP);
abilities.add(Ability.NO_CLIP);
}
flags.add(AdventureSetting.AUTO_JUMP);
abilityLayer.setLayerType(AbilityLayer.Type.BASE);
abilityLayer.setFlySpeed(flySpeed);
// https://github.com/GeyserMC/Geyser/issues/3139 as of 1.19.10
abilityLayer.setWalkSpeed(walkSpeed == 0f ? 0.01f : walkSpeed);
Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES);
sendUpstreamPacket(adventureSettingsPacket);
updateAbilitiesPacket.getAbilityLayers().add(abilityLayer);
sendUpstreamPacket(updateAbilitiesPacket);
}
private int getRenderDistance() {
@ -1894,4 +1898,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendUpstreamPacket(transferPacket);
return true;
}
public void addCommandEnum(String name, String... enums) {
softEnumPacket(name, SoftEnumUpdateType.ADD, enums);
}
public void removeCommandEnum(String name, String... enums) {
softEnumPacket(name, SoftEnumUpdateType.REMOVE, enums);
}
private void softEnumPacket(String name, SoftEnumUpdateType type, String... enums) {
UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket();
packet.setType(type);
packet.setSoftEnum(new CommandEnumData(name, enums, true));
sendUpstreamPacket(packet);
}
}

View file

@ -123,7 +123,8 @@ public class EntityCache {
}
public void addPlayerEntity(PlayerEntity entity) {
playerEntities.put(entity.getUuid(), entity);
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
playerEntities.putIfAbsent(entity.getUuid(), entity);
}
public PlayerEntity getPlayerEntity(UUID uuid) {

View file

@ -71,8 +71,9 @@ public class SkullCache {
this.skullRenderDistanceSquared = distance * distance;
}
public void putSkull(Vector3i position, String texturesProperty, int blockState) {
public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) {
Skull skull = skulls.computeIfAbsent(position, Skull::new);
skull.uuid = uuid;
skull.texturesProperty = texturesProperty;
skull.blockState = blockState;
@ -201,6 +202,7 @@ public class SkullCache {
@RequiredArgsConstructor
@Data
public static class Skull {
private UUID uuid;
private String texturesProperty;
private int blockState;
private SkullPlayerEntity entity;

View file

@ -33,6 +33,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
import org.geysermc.geyser.session.GeyserSession;
@ -61,6 +63,17 @@ public final class WorldCache {
private int currentSequence;
private final Object2IntMap<Vector3i> unverifiedPredictions = new Object2IntOpenHashMap<>(1);
/**
* <ul>
* <li>NOT_SET = not yet triggered</li>
* <li>FALSE = enforce-secure-profile is true but player hasn't chatted yet</li>
* <li>TRUE = enforce-secure-profile is enabled, and player has chatted and they have seen our message.</li>
* </ul>
*/
@Getter
@Setter
private @NonNull TriState chatWarningSent = TriState.NOT_SET;
public WorldCache(GeyserSession session) {
this.session = session;
this.scoreboard = new Scoreboard(session);

View file

@ -29,10 +29,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@ -45,7 +41,6 @@ import org.geysermc.geyser.text.GeyserLocale;
import javax.annotation.Nonnull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -68,7 +63,7 @@ public class FakeHeadProvider {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry().getGeometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
SkinProvider.SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL;
SkinProvider.Skin headSkin = SkinProvider.getOrDefault(
@ -111,7 +106,7 @@ public class FakeHeadProvider {
try {
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity));
sendSkinPacket(session, entity, mergedSkinData);
SkinManager.sendSkinPacket(session, entity, mergedSkinData);
} catch (ExecutionException e) {
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
}
@ -133,50 +128,10 @@ public class FakeHeadProvider {
return;
}
sendSkinPacket(session, entity, skinData);
SkinManager.sendSkinPacket(session, entity, skinData);
});
}
private static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry();
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
PlayerListPacket.Entry updatedEntry = SkinManager.buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
} else {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
}
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
return SerializedSkin.of(skinId, "", geometry.getGeometryName(),
ImageData.of(skin.getSkinData()), Collections.emptyList(),
ImageData.of(cape.getCapeData()), geometry.getGeometryData(),
"", true, false, false, cape.getCapeId(), skinId);
}
@AllArgsConstructor
@Getter
@Setter

View file

@ -1,63 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.skin;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ProvidedSkin {
@Getter private byte[] skin;
public ProvidedSkin(String internalUrl) {
try {
BufferedImage image;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(internalUrl)) {
image = ImageIO.read(stream);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int rgba = image.getRGB(x, y);
outputStream.write((rgba >> 16) & 0xFF); // Red
outputStream.write((rgba >> 8) & 0xFF); // Green
outputStream.write(rgba & 0xFF); // Blue
outputStream.write((rgba >> 24) & 0xFF); // Alpha
}
}
image.flush();
skin = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.skin;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.util.AssetUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Objects;
import java.util.UUID;
public final class ProvidedSkins {
private static final ProvidedSkin[] PROVIDED_SKINS = {
new ProvidedSkin("textures/entity/player/slim/alex.png", true),
new ProvidedSkin("textures/entity/player/slim/ari.png", true),
new ProvidedSkin("textures/entity/player/slim/efe.png", true),
new ProvidedSkin("textures/entity/player/slim/kai.png", true),
new ProvidedSkin("textures/entity/player/slim/makena.png", true),
new ProvidedSkin("textures/entity/player/slim/noor.png", true),
new ProvidedSkin("textures/entity/player/slim/steve.png", true),
new ProvidedSkin("textures/entity/player/slim/sunny.png", true),
new ProvidedSkin("textures/entity/player/slim/zuri.png", true),
new ProvidedSkin("textures/entity/player/wide/alex.png", false),
new ProvidedSkin("textures/entity/player/wide/ari.png", false),
new ProvidedSkin("textures/entity/player/wide/efe.png", false),
new ProvidedSkin("textures/entity/player/wide/kai.png", false),
new ProvidedSkin("textures/entity/player/wide/makena.png", false),
new ProvidedSkin("textures/entity/player/wide/noor.png", false),
new ProvidedSkin("textures/entity/player/wide/steve.png", false),
new ProvidedSkin("textures/entity/player/wide/sunny.png", false),
new ProvidedSkin("textures/entity/player/wide/zuri.png", false)
};
public static ProvidedSkin getDefaultPlayerSkin(UUID uuid) {
return PROVIDED_SKINS[Math.floorMod(uuid.hashCode(), PROVIDED_SKINS.length)];
}
private ProvidedSkins() {
}
public static final class ProvidedSkin {
private SkinProvider.Skin data;
private final boolean slim;
ProvidedSkin(String asset, boolean slim) {
this.slim = slim;
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
.resolve("cache")
.resolve("default_player_skins")
.resolve(slim ? "slim" : "wide");
String assetName = asset.substring(asset.lastIndexOf('/') + 1);
File location = folder.resolve(assetName).toFile();
AssetUtils.addTask(!location.exists(), new AssetUtils.ClientJarTask("assets/minecraft/" + asset,
(stream) -> AssetUtils.saveFile(location, stream),
() -> {
try {
// TODO lazy initialize?
BufferedImage image;
try (InputStream stream = new FileInputStream(location)) {
image = ImageIO.read(stream);
}
byte[] byteData = SkinProvider.bufferedImageToImageData(image);
image.flush();
String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide");
this.data = new SkinProvider.Skin(-1, identifier, byteData);
} catch (IOException e) {
e.printStackTrace();
}
}));
}
public SkinProvider.Skin getData() {
// Fall back to the default skin if we can't load our skins, or it's not loaded yet.
return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN);
}
public boolean isSlim() {
return slim;
}
}
public static void init() {
// no-op
}
static {
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
.resolve("cache")
.resolve("default_player_skins");
folder.toFile().mkdirs();
// Two directories since there are two skins for each model: one slim, one wide
folder.resolve("slim").toFile().mkdir();
folder.resolve("wide").toFile().mkdir();
}
}

View file

@ -32,9 +32,10 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.auth.BedrockClientData;
import org.geysermc.geyser.text.GeyserLocale;
@ -53,13 +54,30 @@ public class SkinManager {
* Builds a Bedrock player list entry from our existing, cached Bedrock skin information
*/
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
// First: see if we have the cached skin texture ID.
GameProfileData data = GameProfileData.from(playerEntity);
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.capeUrl());
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
SkinProvider.Skin skin = null;
SkinProvider.Cape cape = null;
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.WIDE;
if (data != null) {
// GameProfileData is not null = server provided us with textures data to work with.
skin = SkinProvider.getCachedSkin(data.skinUrl());
cape = SkinProvider.getCachedCape(data.capeUrl());
geometry = data.isAlex() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
}
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.skinUrl());
if (skin == null) {
skin = SkinProvider.EMPTY_SKIN;
if (skin == null || cape == null) {
// The server either didn't have a texture to send, or we didn't have the texture ID cached.
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
// Otherwise, grab the default player skin
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
if (skin == null) {
skin = fallbackSkinData.skin();
geometry = fallbackSkinData.geometry();
}
if (cape == null) {
cape = fallbackSkinData.cape();
}
}
return buildEntryManually(
@ -67,10 +85,8 @@ public class SkinManager {
playerEntity.getUuid(),
playerEntity.getUsername(),
playerEntity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
skin,
cape,
geometry
);
}
@ -79,14 +95,10 @@ public class SkinManager {
* With all the information needed, build a Bedrock player entry with translated skin information.
*/
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
String skinId, byte[] skinData,
String capeId, byte[] capeData,
SkinProvider.Skin skin,
SkinProvider.Cape cape,
SkinProvider.SkinGeometry geometry) {
SerializedSkin serializedSkin = SerializedSkin.of(
skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(capeData), geometry.getGeometryData(), "", true, false,
!capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
);
SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry);
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
String xuid = "";
@ -116,6 +128,45 @@ public class SkinManager {
return entry;
}
public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry();
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
// TODO is this special behavior needed?
PlayerListPacket.Entry updatedEntry = buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin,
cape,
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
} else {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
}
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
return SerializedSkin.of(skinId, "", geometry.geometryName(),
ImageData.of(skin.getSkinData()), Collections.emptyList(),
ImageData.of(cape.capeData()), geometry.geometryData(),
"", true, false, false, cape.capeId(), skinId);
}
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> {
@ -128,34 +179,7 @@ public class SkinManager {
}
if (skinData.geometry() != null) {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry();
PlayerListPacket.Entry updatedEntry = buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
if (!entity.isPlayerList()) {
PlayerListPacket playerRemovePacket = new PlayerListPacket();
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
playerRemovePacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerRemovePacket);
}
sendSkinPacket(session, entity, skinData);
}
if (skinAndCapeConsumer != null) {
@ -186,7 +210,7 @@ public class SkinManager {
}
if (!clientData.getCapeId().equals("")) {
SkinProvider.storeBedrockCape(playerEntity.getUuid(), capeBytes);
SkinProvider.storeBedrockCape(clientData.getCapeId(), capeBytes);
}
} catch (Exception e) {
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
@ -231,30 +255,29 @@ public class SkinManager {
* @param entity entity to build the GameProfileData from
* @return The built GameProfileData
*/
public static GameProfileData from(PlayerEntity entity) {
try {
String texturesProperty = entity.getTexturesProperty();
public static @Nullable GameProfileData from(PlayerEntity entity) {
String texturesProperty = entity.getTexturesProperty();
if (texturesProperty == null) {
// Likely offline mode
return null;
}
if (texturesProperty == null) {
// Likely offline mode
return loadBedrockOrOfflineSkin(entity);
}
GameProfileData data = loadFromJson(texturesProperty);
if (data != null) {
return data;
try {
return loadFromJson(texturesProperty);
} catch (Exception exception) {
if (entity instanceof SkullPlayerEntity skullEntity) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty);
} else {
return loadBedrockOrOfflineSkin(entity);
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty);
}
} catch (IOException exception) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername());
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
exception.printStackTrace();
}
return loadBedrockOrOfflineSkin(entity);
}
return null;
}
private static GameProfileData loadFromJson(String encodedJson) throws IOException {
private static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
JsonNode textures = skinObject.get("textures");
@ -267,38 +290,25 @@ public class SkinManager {
return null;
}
String skinUrl = skinTexture.get("url").asText().replace("http://", "https://");
String skinUrl;
JsonNode skinUrlNode = skinTexture.get("url");
if (skinUrlNode != null && skinUrlNode.isTextual()) {
skinUrl = skinUrlNode.asText().replace("http://", "https://");
} else {
return null;
}
boolean isAlex = skinTexture.has("metadata");
String capeUrl = null;
JsonNode capeTexture = textures.get("CAPE");
if (capeTexture != null) {
capeUrl = capeTexture.get("url").asText().replace("http://", "https://");
}
return new GameProfileData(skinUrl, capeUrl, isAlex);
}
/**
* @return default skin with default cape when texture data is invalid, or the Bedrock player's skin if this
* is a Bedrock player.
*/
private static GameProfileData loadBedrockOrOfflineSkin(PlayerEntity entity) {
// Fallback to the offline mode of working it out
UUID uuid = entity.getUuid();
boolean isAlex = (Math.abs(uuid.hashCode() % 2) == 1);
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
skinUrl = session.getClientData().getSkinId();
capeUrl = session.getClientData().getCapeId();
JsonNode capeUrlNode = capeTexture.get("url");
if (capeUrlNode != null && capeUrlNode.isTextual()) {
capeUrl = capeUrlNode.asText().replace("http://", "https://");
}
}
return new GameProfileData(skinUrl, capeUrl, isAlex);
}
}

View file

@ -26,22 +26,22 @@
package org.geysermc.geyser.skin;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.bytes.ByteArrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
@ -57,28 +57,28 @@ import java.util.concurrent.*;
import java.util.function.Predicate;
public class SkinProvider {
public static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
public static final byte[] STEVE_SKIN = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin();
public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN);
public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
private static final Map<String, Skin> permanentSkins = new HashMap<>() {{
put("steve", EMPTY_SKIN);
put("alex", EMPTY_SKIN_ALEX);
}};
private static final Cache<String, Skin> cachedSkins = CacheBuilder.newBuilder()
static final Skin EMPTY_SKIN;
static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true);
private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Cache<String, Skin> CACHED_JAVA_SKINS = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true);
private static final Cache<String, Cape> cachedCapes = CacheBuilder.newBuilder()
private static final Cache<String, Cape> CACHED_BEDROCK_CAPES = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Cache<String, Skin> CACHED_BEDROCK_SKINS = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
@ -86,18 +86,36 @@ public class SkinProvider {
* Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and
* offline mode players use version 3.
*/
public static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
public static final String EARS_GEOMETRY;
public static final String EARS_GEOMETRY_SLIM;
public static final SkinGeometry SKULL_GEOMETRY;
public static final SkinGeometry WEARING_CUSTOM_SKULL;
public static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
private static final String EARS_GEOMETRY;
private static final String EARS_GEOMETRY_SLIM;
static final SkinGeometry SKULL_GEOMETRY;
static final SkinGeometry WEARING_CUSTOM_SKULL;
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
static {
// Generate the empty texture to use as an emergency fallback
final int pink = -524040;
final int black = -16777216;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 4 + 64 * 4);
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
int rgba;
if (y > 32) {
rgba = x >= 32 ? pink : black;
} else {
rgba = x >= 32 ? black : pink;
}
outputStream.write((rgba >> 16) & 0xFF); // Red
outputStream.write((rgba >> 8) & 0xFF); // Green
outputStream.write(rgba & 0xFF); // Blue
outputStream.write((rgba >> 24) & 0xFF); // Alpha
}
}
EMPTY_SKIN = new Skin(-1, "geysermc:empty", outputStream.toByteArray());
/* Load in the normal ears geometry */
EARS_GEOMETRY = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.ears.json"), StandardCharsets.UTF_8);
@ -141,48 +159,103 @@ public class SkinProvider {
}
}
public static boolean hasCapeCached(String capeUrl) {
return cachedCapes.getIfPresent(capeUrl) != null;
/**
* Search our cached database for an already existing, translated skin of this Java URL.
*/
static Skin getCachedSkin(String skinUrl) {
return CACHED_JAVA_SKINS.getIfPresent(skinUrl);
}
public static Skin getCachedSkin(String skinUrl) {
return permanentSkins.getOrDefault(skinUrl, cachedSkins.getIfPresent(skinUrl));
/**
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
*/
static SkinData determineFallbackSkinData(UUID uuid) {
Skin skin = null;
Cape cape = null;
SkinGeometry geometry = SkinGeometry.WIDE;
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
String skinId = session.getClientData().getSkinId();
skin = CACHED_BEDROCK_SKINS.getIfPresent(skinId);
String capeId = session.getClientData().getCapeId();
cape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
geometry = cachedGeometry.getOrDefault(uuid, geometry);
}
}
if (skin == null) {
// We don't have a skin for the player right now. Fall back to a default.
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
skin = providedSkin.getData();
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
}
if (cape == null) {
cape = EMPTY_CAPE;
}
return new SkinData(skin, cape, geometry);
}
public static Cape getCachedCape(String capeUrl) {
Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE;
return cape != null ? cape : EMPTY_CAPE;
/**
* Used as a fallback if an official Java cape doesn't exist for this user.
*/
@Nonnull
private static Cape getCachedBedrockCape(UUID uuid) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
String capeId = session.getClientData().getCapeId();
Cape bedrockCape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
if (bedrockCape != null) {
return bedrockCape;
}
}
return EMPTY_CAPE;
}
public static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
@Nullable
static Cape getCachedCape(String capeUrl) {
if (capeUrl == null) {
return null;
}
return CACHED_JAVA_CAPES.getIfPresent(capeUrl);
}
static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
if (data == null) {
// This player likely does not have a textures property
return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid()));
}
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
.thenApplyAsync(skinAndCape -> {
try {
Skin skin = skinAndCape.getSkin();
Cape cape = skinAndCape.getCape();
SkinGeometry geometry = SkinGeometry.getLegacy(data.isAlex());
Skin skin = skinAndCape.skin();
Cape cape = skinAndCape.cape();
SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
if (cape.isFailed()) {
cape = getOrDefault(requestBedrockCape(entity.getUuid()),
EMPTY_CAPE, 3);
// Whether we should see if this player has a Bedrock skin we should check for on failure of
// any skin property
boolean checkForBedrock = entity.getUuid().version() != 4;
if (cape.failed() && checkForBedrock) {
cape = getCachedBedrockCape(entity.getUuid());
}
if (cape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) {
cape = getOrDefault(requestUnofficialCape(
cape, entity.getUuid(),
entity.getUsername(), false
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
}
geometry = getOrDefault(requestBedrockGeometry(
geometry, entity.getUuid()
), geometry, 3);
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
// Not a bedrock player check for ears
if (geometry.isFailed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
if (geometry.failed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
boolean isEars;
// Its deadmau5, gotta support his skin :)
@ -213,26 +286,17 @@ public class SkinProvider {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
return new SkinData(skinAndCape.getSkin(), skinAndCape.getCape(), null);
return new SkinData(skinAndCape.skin(), skinAndCape.cape(), null);
});
}
public static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
private static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
return CompletableFuture.supplyAsync(() -> {
long time = System.currentTimeMillis();
String newSkinUrl = skinUrl;
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(playerId);
if (session != null) {
newSkinUrl = session.getClientData().getSkinId();
}
}
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
SkinAndCape skinAndCape = new SkinAndCape(
getOrDefault(requestSkin(playerId, newSkinUrl, false), EMPTY_SKIN, 5),
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
);
@ -241,7 +305,7 @@ public class SkinProvider {
}, EXECUTOR_SERVICE);
}
public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
CompletableFuture<Skin> requestedSkin = requestedSkins.get(textureUrl);
if (requestedSkin != null) {
@ -249,7 +313,7 @@ public class SkinProvider {
return requestedSkin;
}
Skin cachedSkin = getCachedSkin(textureUrl);
Skin cachedSkin = CACHED_JAVA_SKINS.getIfPresent(textureUrl);
if (cachedSkin != null) {
return CompletableFuture.completedFuture(cachedSkin);
}
@ -259,23 +323,26 @@ public class SkinProvider {
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((skin, throwable) -> {
skin.updated = true;
cachedSkins.put(textureUrl, skin);
CACHED_JAVA_SKINS.put(textureUrl, skin);
requestedSkins.remove(textureUrl);
});
requestedSkins.put(textureUrl, future);
} else {
Skin skin = supplySkin(playerId, textureUrl);
future = CompletableFuture.completedFuture(skin);
cachedSkins.put(textureUrl, skin);
CACHED_JAVA_SKINS.put(textureUrl, skin);
}
return future;
}
public static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
private static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
if (requestedCape != null) {
return requestedCape;
}
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
Cape cachedCape = CACHED_JAVA_CAPES.getIfPresent(capeUrl);
if (cachedCape != null) {
return CompletableFuture.completedFuture(cachedCape);
}
@ -284,21 +351,21 @@ public class SkinProvider {
if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
.whenCompleteAsync((cape, throwable) -> {
cachedCapes.put(capeUrl, cape);
CACHED_JAVA_CAPES.put(capeUrl, cape);
requestedCapes.remove(capeUrl);
});
requestedCapes.put(capeUrl, future);
} else {
Cape cape = supplyCape(capeUrl, provider); // blocking
future = CompletableFuture.completedFuture(cape);
cachedCapes.put(capeUrl, cape);
CACHED_JAVA_CAPES.put(capeUrl, cape);
}
return future;
}
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
private static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
String username, boolean newThread) {
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
if (officialCape.failed() && ALLOW_THIRD_PARTY_CAPES) {
for (CapeProvider provider : CapeProvider.VALUES) {
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
continue;
@ -308,7 +375,7 @@ public class SkinProvider {
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
EMPTY_CAPE, 4
);
if (!cape1.isFailed()) {
if (!cape1.failed()) {
return CompletableFuture.completedFuture(cape1);
}
}
@ -316,7 +383,7 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialCape);
}
public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
private static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture<Skin> future;
@ -339,7 +406,7 @@ public class SkinProvider {
* @param newThread Should we start in a new thread
* @return The updated skin with ears
*/
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
private static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) {
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
continue;
@ -357,30 +424,17 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialSkin);
}
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID) {
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
if (bedrockCape == null) {
bedrockCape = EMPTY_CAPE;
}
return CompletableFuture.completedFuture(bedrockCape);
static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) {
Skin skin = new Skin(playerID, skinId, skinData, System.currentTimeMillis(), true, false);
CACHED_BEDROCK_SKINS.put(skin.getTextureUrl(), skin);
}
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) {
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
return CompletableFuture.completedFuture(bedrockGeometry);
static void storeBedrockCape(String capeId, byte[] capeData) {
Cape cape = new Cape(capeId, capeId, capeData, System.currentTimeMillis(), false);
CACHED_BEDROCK_CAPES.put(capeId, cape);
}
public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) {
Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false);
cachedSkins.put(skin.getTextureUrl(), skin);
}
public static void storeBedrockCape(UUID playerID, byte[] capeData) {
Cape cape = new Cape(playerID.toString() + ".Bedrock", playerID.toString(), capeData, System.currentTimeMillis(), false);
cachedCapes.put(playerID.toString() + ".Bedrock", cape);
}
public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData), false);
cachedGeometry.put(playerID, geometry);
}
@ -391,7 +445,7 @@ public class SkinProvider {
* @param skin The skin to cache
*/
public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin);
CACHED_JAVA_SKINS.put(skin.getTextureUrl(), skin);
}
/**
@ -400,7 +454,7 @@ public class SkinProvider {
* @param playerID The UUID to cache it against
* @param isSlim If the player is using an slim base
*/
public static void storeEarGeometry(UUID playerID, boolean isSlim) {
private static void storeEarGeometry(UUID playerID, boolean isSlim) {
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
}
@ -414,7 +468,7 @@ public class SkinProvider {
}
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = EMPTY_CAPE.getCapeData();
byte[] cape = EMPTY_CAPE.capeData();
try {
cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {
@ -539,48 +593,23 @@ public class SkinProvider {
}
/**
* If a skull has a username but no textures, request them.
* Request textures from a player's UUID
*
* @param skullOwner the CompoundTag of the skull with no textures
* @param uuid the player's UUID without any hyphens
* @return a completable GameProfile with textures included
*/
public static CompletableFuture<String> requestTexturesFromUsername(CompoundTag skullOwner) {
public static CompletableFuture<String> requestTexturesFromUUID(String uuid) {
return CompletableFuture.supplyAsync(() -> {
Tag uuidTag = skullOwner.get("Id");
String uuidToString = "";
JsonNode node;
boolean retrieveUuidFromInternet = !(uuidTag instanceof IntArrayTag); // also covers null check
if (!retrieveUuidFromInternet) {
int[] uuidAsArray = ((IntArrayTag) uuidTag).getValue();
// thank u viaversion
UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
retrieveUuidFromInternet = uuid.version() != 4;
uuidToString = uuid.toString().replace("-", "");
}
try {
if (retrieveUuidFromInternet) {
// Offline skin, or no present UUID
node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + skullOwner.get("Name").getValue());
JsonNode id = node.get("id");
if (id == null) {
GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + skullOwner.get("Name").getValue());
return null;
}
uuidToString = id.asText();
}
// Get textures from UUID
node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuidToString);
JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
JsonNode properties = node.get("properties");
if (properties == null) {
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuidToString);
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid);
return null;
}
return node.get("properties").get(0).get("value").asText();
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().debug("Unable to request textures for " + uuid);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
@ -589,6 +618,37 @@ public class SkinProvider {
}, EXECUTOR_SERVICE);
}
/**
* Request textures from a player's username
*
* @param username the player's username
* @return a completable GameProfile with textures included
*/
public static CompletableFuture<String> requestTexturesFromUsername(String username) {
return CompletableFuture.supplyAsync(() -> {
try {
// Offline skin, or no present UUID
JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username);
JsonNode id = node.get("id");
if (id == null) {
GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + username);
return null;
}
return id.asText();
} catch (Exception e) {
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
return null;
}
}, EXECUTOR_SERVICE).thenCompose(uuid -> {
if (uuid == null) {
return CompletableFuture.completedFuture(null);
}
return requestTexturesFromUUID(uuid);
});
}
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
if (provider == CapeProvider.FIVEZIG)
return readFiveZigCape(imageUrl);
@ -604,7 +664,7 @@ public class SkinProvider {
}
private static BufferedImage readFiveZigCape(String url) throws IOException {
JsonNode element = OBJECT_MAPPER.readTree(WebUtils.getBody(url));
JsonNode element = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(url));
if (element != null && element.isObject()) {
JsonNode capeElement = element.get("d");
if (capeElement == null || capeElement.isNull()) return null;
@ -683,13 +743,12 @@ public class SkinProvider {
return defaultValue;
}
@AllArgsConstructor
@Getter
public static class SkinAndCape {
private final Skin skin;
private final Cape cape;
public record SkinAndCape(Skin skin, Cape cape) {
}
/**
* Represents a full package of skin, cape, and geometry.
*/
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
}
@ -703,29 +762,19 @@ public class SkinProvider {
private boolean updated;
private boolean ears;
private Skin(long requestedOn, String textureUrl, byte[] skinData) {
Skin(long requestedOn, String textureUrl, byte[] skinData) {
this.requestedOn = requestedOn;
this.textureUrl = textureUrl;
this.skinData = skinData;
}
}
@AllArgsConstructor
@Getter
public static class Cape {
private final String textureUrl;
private final String capeId;
private final byte[] capeData;
private final long requestedOn;
private final boolean failed;
public record Cape(String textureUrl, String capeId, byte[] capeData, long requestedOn, boolean failed) {
}
@AllArgsConstructor
@Getter
public static class SkinGeometry {
private final String geometryName;
private final String geometryData;
private final boolean failed;
public record SkinGeometry(String geometryName, String geometryData, boolean failed) {
public static SkinGeometry WIDE = getLegacy(false);
public static SkinGeometry SLIM = getLegacy(true);
/**
* Generate generic geometry
@ -733,7 +782,7 @@ public class SkinProvider {
* @param isSlim Should it be the alex model
* @return The generic geometry object
*/
public static SkinGeometry getLegacy(boolean isSlim) {
private static SkinGeometry getLegacy(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
}
@ -743,7 +792,7 @@ public class SkinProvider {
* @param isSlim Should it be the alex model
* @return The generated geometry for the ears model
*/
public static SkinGeometry getEars(boolean isSlim) {
private static SkinGeometry getEars(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
}
}

View file

@ -29,11 +29,12 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import java.util.Collections;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class SkullSkinManager extends SkinManager {
@ -42,34 +43,43 @@ public class SkullSkinManager extends SkinManager {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull";
return SerializedSkin.of(
skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId
);
}
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.Skin> skinConsumer) {
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> {
try {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
if (skinConsumer != null) {
skinConsumer.accept(skin);
}
};
GameProfileData data = GameProfileData.from(entity);
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
.whenCompleteAsync((skin, throwable) -> {
try {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
if (skinConsumer != null) {
skinConsumer.accept(skin);
}
});
if (data == null) {
GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
" with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
// No texture available, fallback using the UUID
SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID());
applySkin.accept(fallback.skin(), null);
} else {
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
.whenCompleteAsync(applySkin);
}
}
}

View file

@ -25,7 +25,7 @@
package org.geysermc.geyser.text;
import com.github.steveice10.mc.protocol.data.game.BuiltinChatType;
import com.github.steveice10.mc.protocol.data.game.chat.BuiltinChatType;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;

View file

@ -25,91 +25,45 @@
package org.geysermc.geyser.text;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
public class MinecraftLocale {
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
private static VersionDownload clientJarInfo;
static {
// Create the locales folder
File localesFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales").toFile();
//noinspection ResultOfMethodCallIgnored
localesFolder.mkdir();
// Download the latest asset list and cache it
generateAssetCache().whenComplete((aVoid, ex) -> downloadAndLoadLocale(GeyserLocale.getDefaultLocale()));
// FIXME TEMPORARY
try {
Files.delete(localesFolder.toPath().resolve("en_us.hash"));
} catch (IOException ignored) {
}
}
/**
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
*/
private static CompletableFuture<Void> generateAssetCache() {
return CompletableFuture.supplyAsync(() -> {
try {
// Get the version manifest from Mojang
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest
String latestInfoURL = "";
for (Version version : versionManifest.getVersions()) {
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
latestInfoURL = version.getUrl();
break;
public static void ensureEN_US() {
File localeFile = getFile("en_us");
AssetUtils.addTask(!localeFile.exists(), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json",
(stream) -> AssetUtils.saveFile(localeFile, stream),
() -> {
if ("en_us".equals(GeyserLocale.getDefaultLocale())) {
loadLocale("en_us");
}
}
// Make sure we definitely got a version
if (latestInfoURL.isEmpty()) {
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
clientJarInfo = versionInfo.getDownloads().get("client");
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(clientJarInfo));
// Get the assets list
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
if (!entry.getKey().startsWith("minecraft/lang/")) {
// No need to cache non-language assets as we don't use them
continue;
}
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
return null;
});
}));
}
/**
@ -125,7 +79,7 @@ public class MinecraftLocale {
}
// Check the locale isn't already loaded
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
if (!AssetUtils.isAssetKnown("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
if (loadLocale(locale)) {
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
} else {
@ -148,33 +102,15 @@ public class MinecraftLocale {
* @param locale Locale to download
*/
private static void downloadLocale(String locale) {
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
if (locale.equals("en_us")) {
return;
}
File localeFile = getFile(locale);
// Check if we have already downloaded the locale file
if (localeFile.exists()) {
String curHash = "";
String targetHash;
if (locale.equals("en_us")) {
try {
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toFile();
if (hashFile.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
curHash = br.readLine().trim();
}
}
} catch (IOException ignored) { }
if (clientJarInfo == null) {
// Likely failed to download
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
return;
}
targetHash = clientJarInfo.getSha1();
} else {
curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
targetHash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
}
String curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
String targetHash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
if (!curHash.equals(targetHash)) {
GeyserImpl.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale);
@ -184,22 +120,19 @@ public class MinecraftLocale {
}
}
// Create the en_us locale
if (locale.equals("en_us")) {
downloadEN_US(localeFile);
return;
}
try {
// Get the hash and download the locale
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
String hash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Unable to download locale file hash", e);
}
}
private static File getFile(String locale) {
return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
}
/**
* Loads a locale already downloaded, if the file doesn't exist it just logs a warning
*
@ -254,51 +187,6 @@ public class MinecraftLocale {
}
}
/**
* Download then en_us locale by downloading the server jar and extracting it from there.
*
* @param localeFile File to save the locale to
*/
private static void downloadEN_US(File localeFile) {
try {
// Let the user know we are downloading the JAR
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
GeyserImpl.getInstance().getLogger().debug("Download URL: " + clientJarInfo.getUrl());
// Download the smallest JAR (client or server)
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString());
// Load in the JAR as a zip and extract the file
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) {
try (FileOutputStream outStream = new FileOutputStream(localeFile)) {
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
}
// Flush all changes to disk and cleanup
outStream.flush();
}
}
}
// Store the latest jar hash
FileUtils.writeFile(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray());
// Delete the nolonger needed client/server jar
Files.delete(tmpFilePath);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
}
}
/**
* Translate the given language string into the given locale, or falls back to the default locale
*
@ -333,111 +221,4 @@ public class MinecraftLocale {
}
return result.toString();
}
public static void init() {
// no-op
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionManifest {
@JsonProperty("latest")
private LatestVersion latestVersion;
@JsonProperty("versions")
private List<Version> versions;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class LatestVersion {
@JsonProperty("release")
private String release;
@JsonProperty("snapshot")
private String snapshot;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Version {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("url")
private String url;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionInfo {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
@JsonProperty("assetIndex")
private AssetIndex assetIndex;
@JsonProperty("downloads")
private Map<String, VersionDownload> downloads;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionDownload {
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class AssetIndex {
@JsonProperty("id")
private String id;
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("totalSize")
private int totalSize;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Asset {
@JsonProperty("hash")
private String hash;
@JsonProperty("size")
private int size;
}
}

View file

@ -65,8 +65,8 @@ public abstract class AbstractBlockInventoryTranslator extends BaseInventoryTran
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
holder.prepareInventory(this, session, inventory);
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
return holder.prepareInventory(this, session, inventory);
}
@Override

View file

@ -59,10 +59,12 @@ public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
CraftRecipeOptionalStackRequestActionData data = (CraftRecipeOptionalStackRequestActionData) request.getActions()[0];
AnvilContainer container = (AnvilContainer) inventory;
// Required as of 1.18.30 - FilterTextPackets no longer appear to be sent
String name = request.getFilterStrings()[data.getFilteredStringIndex()];
if (!Objects.equals(name, container.getNewName())) {
container.checkForRename(session, name);
if (request.getFilterStrings().length != 0) {
// Required as of 1.18.30 - FilterTextPackets no longer appear to be sent
String name = request.getFilterStrings()[data.getFilteredStringIndex()];
if (!Objects.equals(name, container.getNewName())) { // TODO is this still necessary after pre-1.19.50 support is dropped?
container.checkForRename(session, name);
}
}
return super.translateRequest(session, inventory, request);

View file

@ -101,7 +101,7 @@ public abstract class InventoryTranslator {
public final int size;
public abstract void prepareInventory(GeyserSession session, Inventory inventory);
public abstract boolean prepareInventory(GeyserSession session, Inventory inventory);
public abstract void openInventory(GeyserSession session, Inventory inventory);
public abstract void closeInventory(GeyserSession session, Inventory inventory);
public abstract void updateProperty(GeyserSession session, Inventory inventory, int key, int value);

View file

@ -55,7 +55,8 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
return true;
}
@Override

View file

@ -94,7 +94,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
MerchantContainer merchantInventory = (MerchantContainer) inventory;
if (merchantInventory.getVillager() == null) {
long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
@ -117,6 +117,8 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
merchantInventory.setVillager(villager);
}
return true;
}
@Override

View file

@ -514,7 +514,8 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
return true;
}
@Override

View file

@ -40,6 +40,7 @@ import org.geysermc.geyser.level.block.DoubleChestValue;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.level.block.entity.DoubleChestBlockEntityTranslator;
import org.geysermc.geyser.util.InventoryUtils;
public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
private final int defaultJavaBlockState;
@ -50,7 +51,7 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
// See BlockInventoryHolder - same concept there except we're also dealing with a specific block state
if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) {
int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition());
@ -76,11 +77,16 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
dataPacket.setData(tag.build());
dataPacket.setBlockPosition(session.getLastInteractionBlockPosition());
session.sendUpstreamPacket(dataPacket);
return;
return true;
}
}
Vector3i position = session.getPlayerEntity().getPosition().toInt().add(Vector3i.UP);
Vector3i position = InventoryUtils.findAvailableWorldSpace(session);
if (position == null) {
return false;
}
Vector3i pairPosition = position.add(Vector3i.UNIT_X);
int bedrockBlockId = session.getBlockMappings().getBedrockBlockId(defaultJavaBlockState);
@ -125,6 +131,8 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
session.sendUpstreamPacket(dataPacket);
inventory.setHolderPosition(position);
return true;
}
@Override

View file

@ -52,8 +52,8 @@ public class SingleChestInventoryTranslator extends ChestInventoryTranslator {
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
holder.prepareInventory(this, session, inventory);
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
return holder.prepareInventory(this, session, inventory);
}
@Override

View file

@ -40,7 +40,8 @@ public abstract class AbstractHorseInventoryTranslator extends BaseInventoryTran
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
public boolean prepareInventory(GeyserSession session, Inventory inventory) {
return true;
}
@Override

View file

@ -56,17 +56,17 @@ public class BannerTranslator extends NbtItemStackTranslator {
static {
OMINOUS_BANNER_PATTERN = new ListTag("Patterns");
// Construct what an ominous banner is supposed to look like
OMINOUS_BANNER_PATTERN.add(getPatternTag("mr", 9));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bs", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("cs", 7));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("ms", 15));
OMINOUS_BANNER_PATTERN.add(getPatternTag("hh", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("mc", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 15));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mr", 9));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bs", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("cs", 7));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("ms", 15));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("hh", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mc", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 15));
}
private static CompoundTag getPatternTag(String pattern, int color) {
public static CompoundTag getJavaPatternTag(String pattern, int color) {
StringTag patternType = new StringTag("Pattern", pattern);
IntTag colorTag = new IntTag("Color", color);
CompoundTag tag = new CompoundTag("");
@ -117,11 +117,7 @@ public class BannerTranslator extends NbtItemStackTranslator {
* @return The Java edition format pattern nbt
*/
public static CompoundTag getJavaBannerPattern(NbtMap pattern) {
Map<String, Tag> tags = new HashMap<>();
tags.put("Color", new IntTag("Color", 15 - pattern.getInt("Color")));
tags.put("Pattern", new StringTag("Pattern", pattern.getString("Pattern")));
return new CompoundTag("", tags);
return BannerTranslator.getJavaPatternTag(pattern.getString("Pattern"), 15 - pattern.getInt("Color"));
}
/**

View file

@ -52,6 +52,11 @@ public class ShulkerBoxItemTranslator extends NbtItemStackTranslator {
ItemMapping boxMapping = session.getItemMappings().getMapping(Identifier.formalize(((StringTag) itemData.get("id")).getValue()));
if (boxMapping == null) {
// If invalid ID
continue;
}
boxItemTag.put(new StringTag("Name", boxMapping.getBedrockIdentifier()));
boxItemTag.put(new ShortTag("Damage", (short) boxMapping.getBedrockData()));
boxItemTag.put(new ByteTag("Count", MathUtils.getNbtByte(itemData.get("Count").getValue())));

View file

@ -41,6 +41,7 @@ import org.geysermc.geyser.level.physics.Axis;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.PistonCache;
@ -621,8 +622,10 @@ public class PistonBlockEntity {
Vector3i movement = getMovement();
attachedBlocks.forEach((blockPos, javaId) -> {
blockPos = blockPos.add(movement);
// Send a final block entity packet to detach blocks
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos);
if (!GameProtocol.supports1_19_50(session)) {
// Send a final block entity packet to detach blocks for clients older than 1.19.50
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos);
}
// Don't place blocks that collide with the player
if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) {
ChunkUtils.updateBlock(session, javaId, blockPos);
@ -739,8 +742,8 @@ public class PistonBlockEntity {
.putFloat("LastProgress", lastProgress)
.putByte("NewState", getState())
.putByte("State", getState())
.putByte("Sticky", (byte) (sticky ? 1 : 0))
.putByte("isMovable", (byte) 0)
.putBoolean("Sticky", sticky)
.putBoolean("isMovable", false)
.putInt("x", position.getX())
.putInt("y", position.getY())
.putInt("z", position.getZ());
@ -762,8 +765,8 @@ public class PistonBlockEntity {
.putFloat("LastProgress", extended ? 1.0f : 0.0f)
.putByte("NewState", (byte) (extended ? 2 : 0))
.putByte("State", (byte) (extended ? 2 : 0))
.putByte("Sticky", (byte) (sticky ? 1 : 0))
.putByte("isMovable", (byte) 0)
.putBoolean("Sticky", sticky)
.putBoolean("isMovable", false)
.putInt("x", position.getX())
.putInt("y", position.getY())
.putInt("z", position.getZ());
@ -783,8 +786,9 @@ public class PistonBlockEntity {
NbtMap movingBlock = session.getBlockMappings().getBedrockBlockStates().get(session.getBlockMappings().getBedrockBlockId(javaId));
NbtMapBuilder builder = NbtMap.builder()
.putString("id", "MovingBlock")
.putBoolean("expanding", action == PistonValueType.PUSHING)
.putCompound("movingBlock", movingBlock)
.putByte("isMovable", (byte) 1)
.putBoolean("isMovable", true)
.putInt("pistonPosX", pistonPosition.getX())
.putInt("pistonPosY", pistonPosition.getY())
.putInt("pistonPosZ", pistonPosition.getZ())

View file

@ -32,7 +32,7 @@ import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.SignUtils;
@BlockEntity(type = BlockEntityType.SIGN)
@BlockEntity(type = {BlockEntityType.SIGN, BlockEntityType.HANGING_SIGN})
public class SignBlockEntityTranslator extends BlockEntityTranslator {
/**
* Maps a color stored in a sign's Color tag to its ARGB value.
@ -88,6 +88,7 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
signWidth += SignUtils.getCharacterWidth(c);
}
// todo 1.20: update for hanging signs (smaller width). Currently OK because bedrock sees hanging signs as normal signs
if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) {
finalSignLine.append(c);
} else {

View file

@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.level.block.entity;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.math.vector.Vector3i;
@ -35,7 +36,10 @@ import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinProvider;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@BlockEntity(type = BlockEntityType.SKULL)
@ -53,33 +57,54 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
builder.put("SkullType", skullVariant);
}
private static CompletableFuture<String> getTextures(CompoundTag tag) {
CompoundTag owner = tag.get("SkullOwner");
if (owner != null) {
CompoundTag properties = owner.get("Properties");
if (properties == null) {
return SkinProvider.requestTexturesFromUsername(owner);
}
ListTag textures = properties.get("textures");
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
StringTag texture = (StringTag) tag1.get("Value");
return CompletableFuture.completedFuture(texture.getValue());
private static UUID getUUID(CompoundTag owner) {
if (owner.get("Id") instanceof IntArrayTag uuidTag && uuidTag.length() == 4) {
int[] uuidAsArray = uuidTag.getValue();
// thank u viaversion
return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
}
return CompletableFuture.completedFuture(null);
// Convert username to an offline UUID
String username = null;
if (owner.get("Name") instanceof StringTag nameTag) {
username = nameTag.getValue().toLowerCase(Locale.ROOT);
}
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
}
private static CompletableFuture<String> getTextures(CompoundTag owner, UUID uuid) {
CompoundTag properties = owner.get("Properties");
if (properties == null) {
if (uuid != null && uuid.version() == 4) {
String uuidString = uuid.toString().replace("-", "");
return SkinProvider.requestTexturesFromUUID(uuidString);
} else if (owner.get("Name") instanceof StringTag nameTag) {
// Fall back to username if UUID was missing or was an offline mode UUID
return SkinProvider.requestTexturesFromUsername(nameTag.getValue());
}
return CompletableFuture.completedFuture(null);
}
ListTag textures = properties.get("textures");
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
StringTag texture = (StringTag) tag1.get("Value");
return CompletableFuture.completedFuture(texture.getValue());
}
public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
getTextures(tag).whenComplete((texturesProperty, throwable) -> {
if (texturesProperty == null) {
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
return;
}
CompoundTag owner = tag.get("SkullOwner");
if (owner == null) {
session.getSkullCache().removeSkull(blockPosition);
return;
}
UUID uuid = getUUID(owner);
getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> {
if (session.getEventLoop().inEventLoop()) {
session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState);
session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState);
} else {
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState));
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState));
}
});
}

View file

@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.level.block.entity;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.geyser.entity.EntityDefinition;
@ -68,16 +69,18 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator {
CompoundTag spawnData = tag.get("SpawnData");
if (spawnData != null) {
String entityID = (String) ((CompoundTag) spawnData.get("entity"))
.get("id")
.getValue();
builder.put("EntityIdentifier", entityID);
StringTag idTag = ((CompoundTag) spawnData.get("entity")).get("id");
if (idTag != null) {
// As of 1.19.3, spawners can be empty
String entityId = idTag.getValue();
builder.put("EntityIdentifier", entityId);
EntityDefinition<?> definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityID);
if (definition != null) {
builder.put("DisplayEntityWidth", definition.width());
builder.put("DisplayEntityHeight", definition.height());
builder.put("DisplayEntityScale", 1.0f);
EntityDefinition<?> definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityId);
if (definition != null) {
builder.put("DisplayEntityWidth", definition.width());
builder.put("DisplayEntityHeight", definition.height());
builder.put("DisplayEntityScale", 1.0f);
}
}
}

View file

@ -46,15 +46,32 @@ public class BedrockAnimateTranslator extends PacketTranslator<AnimatePacket> {
}
switch (packet.getAction()) {
case SWING_ARM ->
case SWING_ARM -> {
session.armSwingPending();
// Delay so entity damage can be processed first
session.scheduleInEventLoop(() -> {
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
session.activateArmAnimationTicking();
if (session.getArmAnimationTicks() != 0) {
// So, generally, a Java player can only do one *thing* at a time.
// If a player right-clicks, for example, then there's probably only one action associated with
// that right-click that will send a swing.
// The only exception I can think of to this, *maybe*, is a player dropping items
// Bedrock is a little funkier than this - it can send several arm animation packets in the
// same tick, notably with high levels of haste applied.
// Packet limiters do not like this and can crash the player.
// If arm animation ticks is 0, then we just sent an arm swing packet this tick.
// See https://github.com/GeyserMC/Geyser/issues/2875
// This behavior was last touched on with ViaVersion 4.5.1 (with its packet limiter), Java 1.16.5,
// and Bedrock 1.19.51.
// Note for the future: we should probably largely ignore this packet and instead replicate
// all actions on our end, and send swings where needed.
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
session.activateArmAnimationTicking();
}
},
25,
TimeUnit.MILLISECONDS
);
}
// These two might need to be flipped, but my recommendation is getting moving working first
case ROW_LEFT -> {
// Packet value is a float of how long one has been rowing, so we convert that into a boolean

View file

@ -57,6 +57,10 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
// This converts the message into the array'd message Java wants
for (char character : text.toCharArray()) {
widthCount += SignUtils.getCharacterWidth(character);
// todo 1.20: update for hanging signs (smaller width). Currently bedrock thinks hanging signs are normal,
// so it thinks hanging signs have more width than they actually do. Seems like JE just truncates it.
// If we get a return in Bedrock, or go over the character width max, that signals to use the next line.
if (character == '\n' || widthCount > SignUtils.JAVA_CHARACTER_WIDTH_MAX) {
// We need to apply some more logic if we went over the character width max

View file

@ -25,12 +25,18 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
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.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.packet.BlockPickRequestPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -61,6 +67,41 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator<BlockPic
return;
}
InventoryUtils.findOrCreateItem(session, BlockRegistries.JAVA_BLOCKS.get(blockToPick).getPickItem());
BlockMapping blockMapping = BlockRegistries.JAVA_BLOCKS.getOrDefault(blockToPick, BlockMapping.AIR);
boolean addNbtData = packet.isAddUserData() && blockMapping.isBlockEntity(); // Holding down CTRL
if (BlockStateValues.getBannerColor(blockToPick) != -1 || addNbtData) {
session.getGeyser().getWorldManager().getPickItemNbt(session, vector.getX(), vector.getY(), vector.getZ(), addNbtData)
.whenComplete((tag, ex) -> {
if (tag == null) {
pickItem(session, blockMapping);
return;
}
session.ensureInEventLoop(() -> {
if (addNbtData) {
ListTag lore = new ListTag("Lore");
lore.add(new StringTag("", "\"(+NBT)\""));
CompoundTag display = tag.get("display");
if (display == null) {
display = new CompoundTag("display");
tag.put(display);
}
display.put(lore);
}
// I don't really like this... I'd rather get an ID from the block mapping I think
ItemMapping mapping = session.getItemMappings().getMapping(blockMapping.getPickItem());
ItemStack itemStack = new ItemStack(mapping.getJavaId(), 1, tag);
InventoryUtils.findOrCreateItem(session, itemStack);
});
});
return;
}
pickItem(session, blockMapping);
}
private void pickItem(GeyserSession session, BlockMapping blockToPick) {
InventoryUtils.findOrCreateItem(session, blockToPick.getPickItem());
}
}

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.bedrock;
import com.nukkitx.protocol.bedrock.packet.CommandRequestPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@ -38,16 +39,14 @@ public class BedrockCommandRequestTranslator extends PacketTranslator<CommandReq
@Override
public void translate(GeyserSession session, CommandRequestPacket packet) {
String command = packet.getCommand().replace("/", "");
String command = MessageTranslator.convertToPlainText(packet.getCommand());
if (!(session.getGeyser().getPlatformType() == PlatformType.STANDALONE
&& GeyserImpl.getInstance().commandManager().runCommand(session, command))) {
String message = packet.getCommand().trim();
if (MessageTranslator.isTooLong(message, session)) {
&& GeyserImpl.getInstance().commandManager().runCommand(session, command.substring(1)))) {
if (MessageTranslator.isTooLong(command, session)) {
return;
}
session.sendCommand(message.substring(1));
session.sendCommand(command.substring(1));
}
}
}

View file

@ -25,7 +25,6 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.nukkitx.protocol.bedrock.packet.EntityPickRequestPacket;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.Entity;
@ -45,7 +44,10 @@ public class BedrockEntityPickRequestTranslator extends PacketTranslator<EntityP
@Override
public void translate(GeyserSession session, EntityPickRequestPacket packet) {
if (session.getGameMode() != GameMode.CREATIVE) return; // Apparently Java behavior
if (!session.isInstabuild()) {
// As of Java Edition 1.19.3
return;
}
Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
if (entity == null) return;

View file

@ -32,12 +32,21 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
import com.nukkitx.math.vector.Vector3d;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData;
import com.nukkitx.protocol.bedrock.data.inventory.InventorySource;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.data.inventory.LegacySetItemSlotData;
import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
@ -54,7 +63,6 @@ import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
@ -63,7 +71,11 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.*;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -464,10 +476,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
if (GameProtocol.supports1_19_10(session)) {
// Since 1.19.10, LevelSoundEventPackets are no longer sent by the client when attacking entities
CooldownUtils.sendCooldown(session);
}
// Since 1.19.10, LevelSoundEventPackets are no longer sent by the client when attacking entities
CooldownUtils.sendCooldown(session);
}
}
break;

View file

@ -26,8 +26,9 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@ -37,21 +38,7 @@ public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
@Override
public void translate(GeyserSession session, TextPacket packet) {
String message = packet.getMessage();
// The order here is important - strip out illegal characters first, then check if it's blank
// (in case the message is blank after removing)
if (message.indexOf(ChatColor.ESCAPE) != -1) {
// Filter out all escape characters - Java doesn't let you type these
StringBuilder builder = new StringBuilder();
for (int i = 0; i < message.length(); i++) {
char c = message.charAt(i);
if (c != ChatColor.ESCAPE) {
builder.append(c);
}
}
message = builder.toString();
}
String message = MessageTranslator.convertToPlainText(packet.getMessage());
if (message.isBlank()) {
// Java Edition (as of 1.17.1) just doesn't pass on these messages, so... we won't either!
@ -62,6 +49,15 @@ public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
return;
}
if (session.getWorldCache().getChatWarningSent() == TriState.FALSE) {
if (Boolean.parseBoolean(System.getProperty("Geyser.PrintSecureChatInformation", "true"))) {
session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_1", session.locale()));
session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_2", session.locale(), "https://geysermc.link/secure-chat"));
}
// Never send this message again for this session.
session.getWorldCache().setChatWarningSent(TriState.TRUE);
}
session.sendChat(message);
}
}

View file

@ -32,15 +32,19 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InteractPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.concurrent.TimeUnit;
@Translator(packet = InteractPacket.class)
public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
@ -73,6 +77,23 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
case LEAVE_VEHICLE:
ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamPacket(sneakPacket);
Entity currentVehicle = session.getPlayerEntity().getVehicle();
session.setMountVehicleScheduledFuture(session.scheduleInEventLoop(() -> {
if (session.getPlayerEntity().getVehicle() == null) {
return;
}
long vehicleBedrockId = currentVehicle.getGeyserId();
if (session.getPlayerEntity().getVehicle().getGeyserId() == vehicleBedrockId) {
// The Bedrock client, as of 1.19.51, dismounts on its end. The server may not agree with this.
// If the server doesn't agree with our dismount (sends a packet saying we dismounted),
// then remount the player.
SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
linkPacket.setEntityLink(new EntityLinkData(vehicleBedrockId, session.getPlayerEntity().getGeyserId(), EntityLinkData.Type.PASSENGER, true, false));
session.sendUpstreamPacket(linkPacket);
}
}, 1, TimeUnit.SECONDS));
break;
case MOUSEOVER:
// Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc

View file

@ -92,11 +92,29 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
if (isValidMove(session, entity.getPosition(), packet.getPosition())) {
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround(), packet.getMode() == MovePlayerPacket.Mode.TELEPORT);
if (position != null) { // A null return value cancels the packet
boolean onGround = packet.isOnGround();
boolean teleportThroughVoidFloor;
// Compare positions here for void floor fix below before the player's position variable is set to the packet position
if (entity.getPosition().getY() >= packet.getPosition().getY()) {
int floorY = position.getFloorY();
// The void floor is offset about 40 blocks below the bottom of the world
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
int voidFloorLocation = bedrockDimension.minY() - 40;
teleportThroughVoidFloor = floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation;
if (teleportThroughVoidFloor) {
// https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground.
onGround = false;
}
} else {
teleportThroughVoidFloor = false;
}
Packet movePacket;
if (rotationChanged) {
// Send rotation updates as well
movePacket = new ServerboundMovePlayerPosRotPacket(
packet.isOnGround(),
onGround,
position.getX(), position.getY(), position.getZ(),
yaw, pitch
);
@ -105,35 +123,26 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
entity.setHeadYaw(headYaw);
} else {
// Rotation did not change; don't send an update with rotation
movePacket = new ServerboundMovePlayerPosPacket(packet.isOnGround(), position.getX(), position.getY(), position.getZ());
movePacket = new ServerboundMovePlayerPosPacket(onGround, position.getX(), position.getY(), position.getZ());
}
// Compare positions here for void floor fix below before the player's position variable is set to the packet position
boolean notMovingUp = entity.getPosition().getY() >= packet.getPosition().getY();
entity.setPositionManual(packet.getPosition());
entity.setOnGround(packet.isOnGround());
entity.setOnGround(onGround);
// Send final movement changes
session.sendDownstreamPacket(movePacket);
if (notMovingUp) {
int floorY = position.getFloorY();
// The void floor is offset about 40 blocks below the bottom of the world
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
int voidFloorLocation = bedrockDimension.minY() - 40;
if (floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation) {
// Work around there being a floor at the bottom of the world and teleport the player below it
// Moving from below to above the void floor works fine
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
session.sendUpstreamPacket(movePlayerPacket);
}
if (teleportThroughVoidFloor) {
// Work around there being a floor at the bottom of the world and teleport the player below it
// Moving from below to above the void floor works fine
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
session.sendUpstreamPacket(movePlayerPacket);
}
session.getSkullCache().updateVisibleSkulls();

View file

@ -25,7 +25,10 @@
package org.geysermc.geyser.translator.protocol.bedrock.world;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.AnimatePacket;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -46,5 +49,22 @@ public class BedrockLevelSoundEventTranslator extends PacketTranslator<LevelSoun
// Sent here because Java still sends a cooldown if the player doesn't hit anything but Bedrock always sends a sound
CooldownUtils.sendCooldown(session);
}
if (packet.getSound() == SoundEvent.ATTACK_NODAMAGE && session.getArmAnimationTicks() == -1) {
// https://github.com/GeyserMC/Geyser/issues/2113
// Seems like consoles and Android with keyboard send the animation packet on 1.19.51, hence the animation
// tick check - the animate packet is sent first.
// ATTACK_NODAMAGE = player clicked air
// This should only be revisited if Bedrock packets get full Java parity, or Bedrock starts sending arm
// animation packets after ATTACK_NODAMAGE, OR ATTACK_NODAMAGE gets removed/isn't sent in the same spot
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
session.activateArmAnimationTicking();
// Send packet to Bedrock so it knows
AnimatePacket animatePacket = new AnimatePacket();
animatePacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
session.sendUpstreamPacket(animatePacket);
}
}
}

View file

@ -44,8 +44,9 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import lombok.Getter;
import lombok.ToString;
import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent;
import org.geysermc.geyser.api.event.java.ServerDefineCommandsEvent;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.inventory.item.Enchantment;
import org.geysermc.geyser.registry.BlockRegistries;
@ -149,12 +150,20 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
index -> new HashSet<>()).add(node.getName().toLowerCase());
}
ServerDefineCommandsEvent event = new ServerDefineCommandsEvent(session, commands.keySet());
session.getGeyser().eventBus().fire(event);
var eventBus = session.getGeyser().eventBus();
var event = new ServerDefineCommandsEvent(session, commands.keySet());
eventBus.fire(event);
if (event.isCancelled()) {
return;
}
var oldEvent = new org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent(session, commands.keySet());
eventBus.fire(oldEvent);
if (oldEvent.isCancelled()) {
return;
}
// The command flags, not sure what these do apart from break things
List<CommandData.Flag> flags = Collections.emptyList();
@ -198,7 +207,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
if (commandNode.getChildIndices().length >= 1) {
// Create the root param node and build all the children
ParamInfo rootParam = new ParamInfo(commandNode, null);
rootParam.buildChildren(session, allNodes);
rootParam.buildChildren(new CommandBuilderContext(session), allNodes);
List<CommandParamData[]> treeData = rootParam.getTree();
@ -211,11 +220,11 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
/**
* Convert Java edition command types to Bedrock edition
*
* @param session the session
* @param context the session's command context
* @param node Command type to convert
* @return Bedrock parameter data type
*/
private static Object mapCommandType(GeyserSession session, CommandNode node) {
private static Object mapCommandType(CommandBuilderContext context, CommandNode node) {
CommandParser parser = node.getParser();
if (parser == null) {
return CommandParam.STRING;
@ -232,21 +241,25 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
case RESOURCE_LOCATION, FUNCTION -> CommandParam.FILE_PATH;
case BOOL -> ENUM_BOOLEAN;
case OPERATION -> CommandParam.OPERATOR; // ">=", "==", etc
case BLOCK_STATE -> BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.get().keySet().toArray(new String[0]);
case ITEM_STACK -> session.getItemMappings().getItemNames();
case ITEM_ENCHANTMENT -> Enchantment.JavaEnchantment.ALL_JAVA_IDENTIFIERS;
case ENTITY_SUMMON -> Registries.JAVA_ENTITY_IDENTIFIERS.get().keySet().toArray(new String[0]);
case BLOCK_STATE -> context.getBlockStates();
case ITEM_STACK -> context.session.getItemMappings().getItemNames();
case COLOR -> VALID_COLORS;
case SCOREBOARD_SLOT -> VALID_SCOREBOARD_SLOTS;
case MOB_EFFECT -> ALL_EFFECT_IDENTIFIERS;
case RESOURCE, RESOURCE_OR_TAG -> {
String resource = ((ResourceProperties) node.getProperties()).getRegistryKey();
if (resource.equals("minecraft:attribute")) {
yield ATTRIBUTES;
} else {
yield CommandParam.STRING;
}
}
case RESOURCE -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), false);
case RESOURCE_OR_TAG -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), true);
case DIMENSION -> context.session.getLevels();
case TEAM -> context.getTeams(); // Note: as of Java 1.19.3, objectives are currently parsed from the server
default -> CommandParam.STRING;
};
}
private static Object handleResource(CommandBuilderContext context, String resource, boolean tags) {
return switch (resource) {
case "minecraft:attribute" -> ATTRIBUTES;
case "minecraft:enchantment" -> Enchantment.JavaEnchantment.ALL_JAVA_IDENTIFIERS;
case "minecraft:entity_type" -> context.getEntityTypes();
case "minecraft:mob_effect" -> ALL_EFFECT_IDENTIFIERS;
case "minecraft:worldgen/biome" -> tags ? context.getBiomesWithTags() : context.getBiomes();
default -> CommandParam.STRING;
};
}
@ -254,7 +267,67 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
/**
* Stores the command description and parameter data for best optimizing the Bedrock commands packet.
*/
private static record BedrockCommandInfo(String name, String description, CommandParamData[][] paramData) implements ServerDefineCommandsEvent.CommandInfo {
private record BedrockCommandInfo(String name, String description, CommandParamData[][] paramData) implements
org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent.CommandInfo,
ServerDefineCommandsEvent.CommandInfo
{
}
/**
* Stores command completions so we don't have to rebuild the same values multiple times.
*/
@MonotonicNonNull
private static class CommandBuilderContext {
private final GeyserSession session;
private Object biomesWithTags;
private Object biomesNoTags;
private String[] blockStates;
private String[] entityTypes;
private CommandEnumData teams;
CommandBuilderContext(GeyserSession session) {
this.session = session;
}
private Object getBiomes() {
if (biomesNoTags != null) {
return biomesNoTags;
}
String[] identifiers = session.getGeyser().getWorldManager().getBiomeIdentifiers(false);
return (biomesNoTags = identifiers != null ? identifiers : CommandParam.STRING);
}
private Object getBiomesWithTags() {
if (biomesWithTags != null) {
return biomesWithTags;
}
String[] identifiers = session.getGeyser().getWorldManager().getBiomeIdentifiers(true);
return (biomesWithTags = identifiers != null ? identifiers : CommandParam.STRING);
}
private String[] getBlockStates() {
if (blockStates != null) {
return blockStates;
}
return (blockStates = BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.get().keySet().toArray(new String[0]));
}
private String[] getEntityTypes() {
if (entityTypes != null) {
return entityTypes;
}
return (entityTypes = Registries.JAVA_ENTITY_IDENTIFIERS.get().keySet().toArray(new String[0]));
}
private CommandEnumData getTeams() {
if (teams != null) {
return teams;
}
return (teams = new CommandEnumData("Geyser_Teams",
session.getWorldCache().getScoreboard().getTeamNames(), true));
}
}
@Getter
@ -279,10 +352,10 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
/**
* Build the array of all the child parameters (recursive)
*
* @param session the session
* @param context the session's command builder context
* @param allNodes Every command node
*/
public void buildChildren(GeyserSession session, CommandNode[] allNodes) {
public void buildChildren(CommandBuilderContext context, CommandNode[] allNodes) {
for (int paramID : paramNode.getChildIndices()) {
CommandNode paramNode = allNodes[paramID];
@ -320,12 +393,15 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
}
} else {
// Put the non-enum param into the list
Object mappedType = mapCommandType(session, paramNode);
Object mappedType = mapCommandType(context, paramNode);
CommandEnumData enumData = null;
CommandParam type = null;
boolean optional = this.paramNode.isExecutable();
if (mappedType instanceof String[]) {
enumData = new CommandEnumData(paramNode.getParser().name().toLowerCase(Locale.ROOT), (String[]) mappedType, false);
if (mappedType instanceof CommandEnumData) {
// Likely to specify isSoft, to be possibly updated later.
enumData = (CommandEnumData) mappedType;
} else if (mappedType instanceof String[]) {
enumData = new CommandEnumData(getEnumDataName(paramNode).toLowerCase(Locale.ROOT), (String[]) mappedType, false);
} else {
type = (CommandParam) mappedType;
// Bedrock throws a fit if an optional message comes after a string or target
@ -343,10 +419,25 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
// Recursively build all child options
for (ParamInfo child : children) {
child.buildChildren(session, allNodes);
child.buildChildren(context, allNodes);
}
}
/**
* Mitigates https://github.com/GeyserMC/Geyser/issues/3411. Not a perfect solution.
*/
private static String getEnumDataName(CommandNode node) {
if (node.getProperties() instanceof ResourceProperties properties) {
String registryKey = properties.getRegistryKey();
int identifierSplit = registryKey.indexOf(':');
if (identifierSplit != -1) {
return registryKey.substring(identifierSplit);
}
return registryKey;
}
return node.getParser().name();
}
/**
* Comparing CommandNode type a and b, determine if they are in the same overload.
* <p>

View file

@ -23,27 +23,19 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.level;
package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundCustomSoundPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundDisguisedChatPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.SoundUtils;
import org.geysermc.geyser.translator.text.MessageTranslator;
@Translator(packet = ClientboundCustomSoundPacket.class)
public class JavaCustomSoundTranslator extends PacketTranslator<ClientboundCustomSoundPacket> {
@Translator(packet = ClientboundDisguisedChatPacket.class)
public class JavaDisguisedChatTranslator extends PacketTranslator<ClientboundDisguisedChatPacket> {
@Override
public void translate(GeyserSession session, ClientboundCustomSoundPacket packet) {
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound(SoundUtils.translatePlaySound(packet.getSound()));
playSoundPacket.setPosition(Vector3f.from(packet.getX(), packet.getY(), packet.getZ()));
playSoundPacket.setVolume(packet.getVolume());
playSoundPacket.setPitch(packet.getPitch());
session.sendUpstreamPacket(playSoundPacket);
public void translate(GeyserSession session, ClientboundDisguisedChatPacket packet) {
MessageTranslator.handleChatPacket(session, packet.getMessage(), packet.getChatType(), packet.getTargetName(), packet.getName());
}
}

View file

@ -86,6 +86,7 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
session.getWorldCache().removeScoreboard();
}
session.setWorldName(packet.getWorldName());
session.setLevels(packet.getWorldNames());
BiomeTranslator.loadServerBiomes(session, packet.getRegistry());
session.getTagCache().clear();
@ -98,16 +99,11 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
if (needsSpawnPacket) {
// The player has yet to spawn so let's do that using some of the information in this Java packet
session.setDimension(newDimension);
session.setDimensionType(dimensions.get(newDimension));
ChunkUtils.loadDimension(session);
DimensionUtils.setBedrockDimension(session, newDimension);
session.connect();
// It is now safe to send these packets
session.getUpstream().sendPostStartGamePackets();
} else if (!session.isSpawned()) {
// Called for online mode, being presented with a GUI before logging ing
session.setDimensionType(dimensions.get(newDimension));
ChunkUtils.loadDimension(session);
}
AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket();
@ -151,5 +147,7 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
// If the player is spawning into the "fake" nether, send them some fog
session.sendFog("minecraft:fog_hell");
}
ChunkUtils.loadDimension(session);
}
}

View file

@ -26,57 +26,18 @@
package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPlayerChatPacket;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.TextDecoration;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Translator(packet = ClientboundPlayerChatPacket.class)
public class JavaPlayerChatTranslator extends PacketTranslator<ClientboundPlayerChatPacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerChatPacket packet) {
TextPacket textPacket = new TextPacket();
textPacket.setPlatformChatId("");
textPacket.setSourceName("");
textPacket.setXuid(session.getAuthData().xuid());
textPacket.setType(TextPacket.Type.CHAT);
textPacket.setNeedsTranslation(false);
Component message = packet.getUnsignedContent() == null ? packet.getMessageDecorated() : packet.getUnsignedContent();
TextDecoration decoration = session.getChatTypes().get(packet.getChatType());
if (decoration != null) {
// As of 1.19 - do this to apply all the styling for signed messages
// Though, Bedrock cannot care about the signed stuff.
TranslatableComponent.Builder withDecoration = Component.translatable()
.key(decoration.translationKey())
.style(decoration.style());
Set<TextDecoration.Parameter> parameters = decoration.parameters();
List<Component> args = new ArrayList<>(3);
if (parameters.contains(TextDecoration.Parameter.TARGET)) {
args.add(packet.getTargetName());
}
if (parameters.contains(TextDecoration.Parameter.SENDER)) {
args.add(packet.getName());
}
if (parameters.contains(TextDecoration.Parameter.CONTENT)) {
args.add(message);
}
withDecoration.args(args);
textPacket.setMessage(MessageTranslator.convertMessage(withDecoration.build(), session.locale()));
} else {
textPacket.setMessage(MessageTranslator.convertMessage(message, session.locale()));
}
session.sendUpstreamPacket(textPacket);
Component message = packet.getUnsignedContent() == null ? Component.text(packet.getContent()) : packet.getUnsignedContent();
MessageTranslator.handleChatPacket(session, message, packet.getChatType(), packet.getTargetName(), packet.getName());
}
}

View file

@ -36,6 +36,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
@Translator(packet = ClientboundRespawnPacket.class)
@ -82,16 +83,15 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
String newDimension = packet.getDimension();
if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) {
// Switching to a new world (based off the world name change); send a fake dimension change
if (!packet.getWorldName().equals(session.getWorldName()) && (session.getDimension().equals(newDimension)
// Ensure that the player never ever dimension switches to the same dimension - BAD
// Can likely be removed if the Above Bedrock Nether Building option can be removed
|| DimensionUtils.javaToBedrock(session.getDimension()) == DimensionUtils.javaToBedrock(newDimension))) {
// Switching to a new world (based off the world name change or new dimension); send a fake dimension change
if (DimensionUtils.javaToBedrock(session.getDimension()) == DimensionUtils.javaToBedrock(newDimension)) {
String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension);
DimensionUtils.switchDimension(session, fakeDim);
}
session.setWorldName(packet.getWorldName());
DimensionUtils.switchDimension(session, newDimension);
ChunkUtils.loadDimension(session);
}
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundServerDataPacket;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@Translator(packet = ClientboundServerDataPacket.class)
public class JavaServerDataTranslator extends PacketTranslator<ClientboundServerDataPacket> {
@Override
public void translate(GeyserSession session, ClientboundServerDataPacket packet) {
// We only want to warn about chat maybe not working once
if (packet.isEnforcesSecureChat() && session.getWorldCache().getChatWarningSent() == TriState.NOT_SET) {
session.getWorldCache().setChatWarningSent(TriState.FALSE);
}
}
}

View file

@ -40,6 +40,6 @@ public class JavaSoundEntityTranslator extends PacketTranslator<ClientboundSound
if (entity == null) {
return;
}
SoundUtils.playBuiltinSound(session, packet.getSound(), entity.getPosition(), packet.getVolume(), packet.getPitch());
SoundUtils.playSound(session, packet.getSound(), entity.getPosition(), packet.getVolume(), packet.getPitch());
}
}

View file

@ -39,7 +39,7 @@ public class JavaPlayerCombatKillTranslator extends PacketTranslator<Clientbound
@Override
public void translate(GeyserSession session, ClientboundPlayerCombatKillPacket packet) {
if (packet.getPlayerId() == session.getPlayerEntity().getEntityId() && GameProtocol.supports1_19_10(session)) {
if (packet.getPlayerId() == session.getPlayerEntity().getEntityId()) {
Component deathMessage = packet.getMessage();
// TODO - could inject score in, but as of 1.19.10 newlines don't center and start at the left of the first text
DeathInfoPacket deathInfoPacket = new DeathInfoPacket();

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.entity.player;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPlayerInfoRemovePacket;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.UUID;
@Translator(packet = ClientboundPlayerInfoRemovePacket.class)
public class JavaPlayerInfoRemoveTranslator extends PacketTranslator<ClientboundPlayerInfoRemovePacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerInfoRemovePacket packet) {
PlayerListPacket translate = new PlayerListPacket();
translate.setAction(PlayerListPacket.Action.REMOVE);
for (UUID id : packet.getProfileIds()) {
// As the player entity is no longer present, we can remove the entry
PlayerEntity entity = session.getEntityCache().removePlayerEntity(id);
UUID removeId;
if (entity != null) {
// Just remove the entity's player list status
// Don't despawn the entity - the Java server will also take care of that.
removeId = entity.getTabListUuid();
} else {
removeId = id;
}
translate.getEntries().add(new PlayerListPacket.Entry(removeId));
}
session.sendUpstreamPacket(translate);
}
}

View file

@ -1,125 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.entity.player;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPlayerInfoPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@Translator(packet = ClientboundPlayerInfoPacket.class)
public class JavaPlayerInfoTranslator extends PacketTranslator<ClientboundPlayerInfoPacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerInfoPacket packet) {
if (packet.getAction() != PlayerListEntryAction.ADD_PLAYER && packet.getAction() != PlayerListEntryAction.REMOVE_PLAYER)
return;
PlayerListPacket translate = new PlayerListPacket();
translate.setAction(packet.getAction() == PlayerListEntryAction.ADD_PLAYER ? PlayerListPacket.Action.ADD : PlayerListPacket.Action.REMOVE);
for (PlayerListEntry entry : packet.getEntries()) {
switch (packet.getAction()) {
case ADD_PLAYER -> {
GameProfile profile = entry.getProfile();
PlayerEntity playerEntity;
boolean self = profile.getId().equals(session.getPlayerEntity().getUuid());
if (self) {
// Entity is ourself
playerEntity = session.getPlayerEntity();
} else {
playerEntity = session.getEntityCache().getPlayerEntity(profile.getId());
}
GameProfile.Property textures = profile.getProperty("textures");
String texturesProperty = textures == null ? null : textures.getValue();
if (playerEntity == null) {
// It's a new player
playerEntity = new PlayerEntity(
session,
-1,
session.getEntityCache().getNextEntityId().incrementAndGet(),
profile.getId(),
Vector3f.ZERO,
Vector3f.ZERO,
0, 0, 0,
profile.getName(),
texturesProperty
);
session.getEntityCache().addPlayerEntity(playerEntity);
} else {
playerEntity.setUsername(profile.getName());
playerEntity.setTexturesProperty(texturesProperty);
}
playerEntity.setPlayerList(true);
// We'll send our own PlayerListEntry in requestAndHandleSkinAndCape
// But we need to send other player's entries so they show up in the player list
// without processing their skin information - that'll be processed when they spawn in
if (self) {
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername()));
} else {
playerEntity.setValid(true);
PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, playerEntity);
translate.getEntries().add(playerListEntry);
}
}
case REMOVE_PLAYER -> {
// As the player entity is no longer present, we can remove the entry
PlayerEntity entity = session.getEntityCache().removePlayerEntity(entry.getProfile().getId());
if (entity != null) {
// Just remove the entity's player list status
// Don't despawn the entity - the Java server will also take care of that.
entity.setPlayerList(false);
}
if (entity == session.getPlayerEntity()) {
// If removing ourself we use our AuthData UUID
translate.getEntries().add(new PlayerListPacket.Entry(session.getAuthData().uuid()));
} else {
translate.getEntries().add(new PlayerListPacket.Entry(entry.getProfile().getId()));
}
}
}
}
if (!translate.getEntries().isEmpty()) {
session.sendUpstreamPacket(translate);
}
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.entity.player;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPlayerInfoUpdatePacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Translator(packet = ClientboundPlayerInfoUpdatePacket.class)
public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<ClientboundPlayerInfoUpdatePacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerInfoUpdatePacket packet) {
Set<PlayerListEntryAction> actions = packet.getActions();
if (actions.contains(PlayerListEntryAction.ADD_PLAYER)) {
for (PlayerListEntry entry : packet.getEntries()) {
GameProfile profile = entry.getProfile();
PlayerEntity playerEntity;
boolean self = profile.getId().equals(session.getPlayerEntity().getUuid());
GameProfile.Property textures = profile.getProperty("textures");
String texturesProperty = textures == null ? null : textures.getValue();
if (self) {
// Entity is ourself
playerEntity = session.getPlayerEntity();
} else {
// It's a new player
playerEntity = new PlayerEntity(
session,
-1,
session.getEntityCache().getNextEntityId().incrementAndGet(),
profile.getId(),
Vector3f.ZERO,
Vector3f.ZERO,
0, 0, 0,
profile.getName(),
texturesProperty
);
session.getEntityCache().addPlayerEntity(playerEntity);
}
playerEntity.setUsername(profile.getName());
playerEntity.setTexturesProperty(texturesProperty);
if (self) {
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername()));
} else {
playerEntity.setValid(true);
}
}
}
if (actions.contains(PlayerListEntryAction.UPDATE_LISTED)) {
List<PlayerListPacket.Entry> toAdd = new ArrayList<>();
List<PlayerListPacket.Entry> toRemove = new ArrayList<>();
for (PlayerListEntry entry : packet.getEntries()) {
PlayerEntity entity = session.getEntityCache().getPlayerEntity(entry.getProfileId());
if (entity == null) {
session.getGeyser().getLogger().debug("Ignoring player info update for " + entry.getProfileId());
continue;
}
if (entry.isListed()) {
PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, entity);
toAdd.add(playerListEntry);
} else {
toRemove.add(new PlayerListPacket.Entry(entity.getTabListUuid()));
}
}
if (!toAdd.isEmpty()) {
PlayerListPacket tabListPacket = new PlayerListPacket();
tabListPacket.setAction(PlayerListPacket.Action.ADD);
tabListPacket.getEntries().addAll(toAdd);
session.sendUpstreamPacket(tabListPacket);
}
if (!toRemove.isEmpty()) {
PlayerListPacket tabListPacket = new PlayerListPacket();
tabListPacket.setAction(PlayerListPacket.Action.REMOVE);
tabListPacket.getEntries().addAll(toRemove);
session.sendUpstreamPacket(tabListPacket);
}
}
}
}

View file

@ -40,7 +40,6 @@ import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.TeleportCache;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.ChunkUtils;
@ -85,7 +84,7 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId());
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) {
// See DimensionUtils for an explanation
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(session.getServerRenderDistance());
@ -97,7 +96,7 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
ChunkUtils.updateChunkPosition(session, pos.toInt());
if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
session.getGeyser().getLogger().debug("Spawned player at " + packet.getX() + " " + packet.getY() + " " + packet.getZ());
}
return;
}
@ -114,6 +113,13 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
EntityUtils.updateRiderRotationLock(entity, null, false);
EntityUtils.updateMountOffset(entity, null, false, false, entity.getPassengers().size() > 1);
entity.updateBedrockMetadata();
if (session.getMountVehicleScheduledFuture() != null) {
// Cancel this task as it is now unnecessary.
// Note that this isn't present in JavaSetPassengersTranslator as that code is not called for players
// as of Java 1.19.3, but the scheduled future checks for the vehicle being null anyway.
session.getMountVehicleScheduledFuture().cancel(false);
}
}
// If coordinates are relative, then add to the existing coordinate

View file

@ -36,7 +36,6 @@ import org.geysermc.geyser.entity.type.*;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -53,7 +52,7 @@ public class JavaAddEntityTranslator extends PacketTranslator<ClientboundAddEnti
EntityDefinition<?> definition = Registries.ENTITY_DEFINITIONS.get(packet.getType());
if (definition == null) {
session.getGeyser().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.entity.type_null", packet.getType()));
session.getGeyser().getLogger().warning("Could not find an entity definition with type " + packet.getType());
return;
}

View file

@ -31,6 +31,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.InventoryUtils;
@ -46,7 +47,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
int inventorySize = inventory.getSize();
for (int i = 0; i < packet.getItems().length; i++) {
if (i > inventorySize) {
if (i >= inventorySize) {
GeyserImpl geyser = session.getGeyser();
geyser.getLogger().warning("ClientboundContainerSetContentPacket sent to " + session.bedrockUsername()
+ " that exceeds inventory size!");
@ -54,10 +55,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
geyser.getLogger().debug(packet);
geyser.getLogger().debug(inventory);
}
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
translator.updateInventory(session, inventory);
}
updateInventory(session, inventory, packet.getContainerId());
// 1.18.1 behavior: the previous items will be correctly set, but the state ID and carried item will not
// as this produces a stack trace on the client.
// If Java processes this correctly in the future, we can revert this behavior
@ -68,10 +66,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
inventory.setItem(i, newItem, session);
}
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
translator.updateInventory(session, inventory);
}
updateInventory(session, inventory, packet.getContainerId());
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
@ -80,4 +75,14 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
InventoryUtils.updateCursor(session);
}
private void updateInventory(GeyserSession session, Inventory inventory, int containerId) {
InventoryTranslator translator = session.getInventoryTranslator();
if (containerId == 0 && !(translator instanceof PlayerInventoryTranslator)) {
// In rare cases, the window ID can still be 0 but Java treats it as valid
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateInventory(session, inventory);
} else if (translator != null) {
translator.updateInventory(session, inventory);
}
}
}

View file

@ -30,7 +30,6 @@ import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelEventGenericPacket;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
@ -49,9 +48,9 @@ public class JavaExplodeTranslator extends PacketTranslator<ClientboundExplodePa
LevelEventGenericPacket levelEventPacket = new LevelEventGenericPacket();
levelEventPacket.setEventId(2026/*LevelEventType.PARTICLE_BLOCK_EXPLOSION*/);
NbtMapBuilder builder = NbtMap.builder();
builder.putFloat("originX", packet.getX());
builder.putFloat("originY", packet.getY());
builder.putFloat("originZ", packet.getZ());
builder.putFloat("originX", (float) packet.getX());
builder.putFloat("originY", (float) packet.getY());
builder.putFloat("originZ", (float) packet.getZ());
builder.putFloat("radius", packet.getRadius());
builder.putInt("size", packet.getExploded().size());
int i = 0;

View file

@ -51,6 +51,8 @@ public class JavaMapItemDataTranslator extends PacketTranslator<ClientboundMapIt
mapItemDataPacket.setLocked(packet.isLocked());
mapItemDataPacket.setOrigin(Vector3i.ZERO); // Required since 1.19.20
mapItemDataPacket.setScale(packet.getScale());
// Required as of 1.19.50
mapItemDataPacket.getTrackedEntityIds().add(packet.getMapId());
MapData data = packet.getData();
if (data != null) {

View file

@ -38,6 +38,6 @@ public class JavaSoundTranslator extends PacketTranslator<ClientboundSoundPacket
@Override
public void translate(GeyserSession session, ClientboundSoundPacket packet) {
Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
SoundUtils.playBuiltinSound(session, packet.getSound(), position, packet.getVolume(), packet.getPitch());
SoundUtils.playSound(session, packet.getSound(), position, packet.getVolume(), packet.getPitch());
}
}

View file

@ -27,7 +27,9 @@ package org.geysermc.geyser.translator.text;
import com.github.steveice10.mc.protocol.data.DefaultComponentSerializer;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@ -36,8 +38,7 @@ import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.*;
import java.util.EnumMap;
import java.util.Map;
import java.util.*;
public class MessageTranslator {
// These are used for handling the translations of the messages
@ -201,6 +202,28 @@ public class MessageTranslator {
return GSON_SERIALIZER.serialize(component);
}
/**
* Convert legacy format message to plain text
*
* @param message Message to convert
* @return The plain text of the message
*/
public static String convertToPlainText(String message) {
char[] input = message.toCharArray();
char[] output = new char[input.length];
int outputSize = 0;
for (int i = 0, inputLength = input.length; i < inputLength; i++) {
char c = input[i];
if (c == ChatColor.ESCAPE) {
i++;
} else {
output[outputSize++] = c;
}
}
return new String(output, 0, outputSize);
}
/**
* Convert JSON and legacy format message to plain text
*
@ -228,6 +251,46 @@ public class MessageTranslator {
return PlainTextComponentSerializer.plainText().serialize(messageComponent);
}
public static void handleChatPacket(GeyserSession session, Component message, int chatType, Component targetName, Component sender) {
TextPacket textPacket = new TextPacket();
textPacket.setPlatformChatId("");
textPacket.setSourceName("");
textPacket.setXuid(session.getAuthData().xuid());
textPacket.setType(TextPacket.Type.CHAT);
textPacket.setNeedsTranslation(false);
TextDecoration decoration = session.getChatTypes().get(chatType);
if (decoration != null) {
// As of 1.19 - do this to apply all the styling for signed messages
// Though, Bedrock cannot care about the signed stuff.
TranslatableComponent.Builder withDecoration = Component.translatable()
.key(decoration.translationKey())
.style(decoration.style());
Set<TextDecoration.Parameter> parameters = decoration.parameters();
List<Component> args = new ArrayList<>(3);
if (parameters.contains(TextDecoration.Parameter.TARGET)) {
args.add(targetName);
}
if (parameters.contains(TextDecoration.Parameter.SENDER)) {
args.add(sender);
}
if (parameters.contains(TextDecoration.Parameter.CONTENT)) {
args.add(message);
}
withDecoration.args(args);
textPacket.setMessage(MessageTranslator.convertMessage(withDecoration.build(), session.locale()));
} else {
session.getGeyser().getLogger().debug("Likely illegal chat type detection found.");
if (session.getGeyser().getConfig().isDebugMode()) {
Thread.dumpStack();
}
textPacket.setMessage(MessageTranslator.convertMessage(message, session.locale()));
}
session.sendUpstreamPacket(textPacket);
}
/**
* Convert a team color to a chat color
*

View file

@ -0,0 +1,329 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.text.GeyserLocale;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
/**
* Implementation note: try to design processes to fail softly if the client jar can't be downloaded,
* either if Mojang is down or internet access to Mojang is spotty.
*/
public final class AssetUtils {
private static final String CLIENT_JAR_HASH_FILE = "client_jar.hash";
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
private static VersionDownload CLIENT_JAR_INFO;
private static final Queue<ClientJarTask> CLIENT_JAR_TASKS = new ArrayDeque<>();
/**
* Download the client jar even if the hash is correct
*/
private static boolean FORCE_DOWNLOAD_JAR = false;
public static Asset getAsset(String name) {
return ASSET_MAP.get(name);
}
public static boolean isAssetKnown(String name) {
return ASSET_MAP.containsKey(name);
}
/**
* Add task to be ran after the client jar is downloaded or found to be cached.
*
* @param required if set to true, the client jar will always be downloaded, even if a pre-existing hash is matched.
* This means an asset or texture is missing.
*/
public static void addTask(boolean required, ClientJarTask task) {
CLIENT_JAR_TASKS.add(task);
FORCE_DOWNLOAD_JAR |= required;
}
/**
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
*/
public static CompletableFuture<Void> generateAssetCache() {
return CompletableFuture.supplyAsync(() -> {
try {
// Get the version manifest from Mojang
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(
WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest
String latestInfoURL = "";
for (Version version : versionManifest.getVersions()) {
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
latestInfoURL = version.getUrl();
break;
}
}
// Make sure we definitely got a version
if (latestInfoURL.isEmpty()) {
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
CLIENT_JAR_INFO = versionInfo.getDownloads().get("client");
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO));
// Get the assets list
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
if (!entry.getKey().startsWith("minecraft/lang/")) {
// No need to cache non-language assets as we don't use them
continue;
}
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
return null;
});
}
public static void downloadAndRunClientJarTasks() {
if (CLIENT_JAR_INFO == null) {
// Likely failed to download
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
return;
}
if (!FORCE_DOWNLOAD_JAR) { // Don't bother checking the hash if we need to download new files anyway.
String curHash = null;
try {
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve(CLIENT_JAR_HASH_FILE).toFile();
if (hashFile.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
curHash = br.readLine().trim();
}
}
} catch (IOException ignored) { }
String targetHash = CLIENT_JAR_INFO.getSha1();
if (targetHash.equals(curHash)) {
// Just run all tasks - no new download required
ClientJarTask task;
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
task.whenDone.run();
}
return;
}
}
try {
// Let the user know we are downloading the JAR
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
GeyserImpl.getInstance().getLogger().debug("Download URL: " + CLIENT_JAR_INFO.getUrl());
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
WebUtils.downloadFile(CLIENT_JAR_INFO.getUrl(), tmpFilePath.toString());
// Load in the JAR as a zip and extract the files
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
ClientJarTask task;
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry(task.asset))) {
task.ifNewDownload.accept(fileStream);
task.whenDone.run();
}
}
}
// Store the latest jar hash
Path cache = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
Files.createDirectories(cache);
FileUtils.writeFile(cache.resolve(CLIENT_JAR_HASH_FILE).toString(), CLIENT_JAR_INFO.getSha1().toCharArray());
// Delete the nolonger needed client/server jar
Files.delete(tmpFilePath);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
}
}
public static void saveFile(File location, InputStream fileStream) throws IOException {
try (FileOutputStream outStream = new FileOutputStream(location)) {
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
}
// Flush all changes to disk and cleanup
outStream.flush();
}
}
/**
* A process that requires we download the client jar.
* Designed to accommodate Geyser updates that require more assets from the jar.
*/
public record ClientJarTask(String asset, InputStreamConsumer ifNewDownload, Runnable whenDone) {
}
@FunctionalInterface
public interface InputStreamConsumer {
void accept(InputStream stream) throws IOException;
}
/* Classes that map to JSON files served by Mojang */
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionManifest {
@JsonProperty("latest")
private LatestVersion latestVersion;
@JsonProperty("versions")
private List<Version> versions;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class LatestVersion {
@JsonProperty("release")
private String release;
@JsonProperty("snapshot")
private String snapshot;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Version {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("url")
private String url;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionInfo {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
@JsonProperty("assetIndex")
private AssetIndex assetIndex;
@JsonProperty("downloads")
private Map<String, VersionDownload> downloads;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionDownload {
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class AssetIndex {
@JsonProperty("id")
private String id;
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("totalSize")
private int totalSize;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public static class Asset {
@JsonProperty("hash")
private String hash;
@JsonProperty("size")
private int size;
}
private AssetUtils() {
}
}

View file

@ -222,7 +222,8 @@ public class ChunkUtils {
* This must be done after the player has switched dimensions so we know what their dimension is
*/
public static void loadDimension(GeyserSession session) {
JavaDimension dimension = session.getDimensionType();
JavaDimension dimension = session.getDimensions().get(session.getDimension());
session.setDimensionType(dimension);
int minY = dimension.minY();
int maxY = dimension.maxY();
@ -233,13 +234,7 @@ public class ChunkUtils {
throw new RuntimeException("Maximum Y must be a multiple of 16!");
}
BedrockDimension bedrockDimension = switch (session.getDimension()) {
case DimensionUtils.THE_END -> BedrockDimension.THE_END;
case DimensionUtils.NETHER -> DimensionUtils.isCustomBedrockNetherId() ? BedrockDimension.THE_END : BedrockDimension.THE_NETHER;
default -> BedrockDimension.OVERWORLD;
};
session.getChunkCache().setBedrockDimension(bedrockDimension);
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
// Yell in the console if the world height is too height in the current scenario
// The constraints change depending on if the player is in the overworld or not, and if experimental height is enabled
// (Ignore this for the Nether. We can't change that at the moment without the workaround. :/ )

View file

@ -27,11 +27,16 @@ package org.geysermc.geyser.util;
import com.github.steveice10.mc.protocol.data.game.entity.Effect;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.PlayerActionType;
import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket;
import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import com.nukkitx.protocol.bedrock.packet.MobEffectPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket;
import com.nukkitx.protocol.bedrock.packet.StopSoundPacket;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import java.util.Set;
@ -70,18 +75,17 @@ public class DimensionUtils {
session.getPistonCache().clear();
session.getSkullCache().clear();
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) {
// The server-sided view distance wasn't a thing until Minecraft Java 1.14
// So ViaVersion compensates by sending a "view distance" of 64
// That's fine, except when the actual view distance sent from the server is five chunks
// The client locks up when switching dimensions, expecting more chunks than it's getting
// To solve this, we cap at 32 unless we know that the render distance actually exceeds 32
// 47 is the Bedrock equivalent of 32
// Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large
session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of "
+ session.getServerRenderDistance());
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(47);
chunkRadiusUpdatedPacket.setRadius(32);
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
// Will be re-adjusted on spawn
}
@ -93,9 +97,10 @@ public class DimensionUtils {
changeDimensionPacket.setRespawn(true);
changeDimensionPacket.setPosition(pos);
session.sendUpstreamPacket(changeDimensionPacket);
session.setDimension(javaDimension);
session.setDimensionType(session.getDimensions().get(javaDimension));
ChunkUtils.loadDimension(session);
setBedrockDimension(session, javaDimension);
player.setPosition(pos);
session.setSpawned(false);
session.setLastChunkPosition(null);
@ -117,6 +122,19 @@ public class DimensionUtils {
stopSoundPacket.setSoundName("");
session.sendUpstreamPacket(stopSoundPacket);
// Kind of silly but Bedrock 1.19.50 requires an acknowledgement after the
// initial chunks are sent, prior to the client acknowledgement
if (GameProtocol.supports1_19_50(session)) {
// Note: send this before chunks are sent. Fixed https://github.com/GeyserMC/Geyser/issues/3421
PlayerActionPacket ackPacket = new PlayerActionPacket();
ackPacket.setRuntimeEntityId(player.getGeyserId());
ackPacket.setAction(PlayerActionType.DIMENSION_CHANGE_SUCCESS);
ackPacket.setBlockPosition(Vector3i.ZERO);
ackPacket.setResultPosition(Vector3i.ZERO);
ackPacket.setFace(0);
session.sendUpstreamPacket(ackPacket);
}
// TODO - fix this hack of a fix by sending the final dimension switching logic after sections have been sent.
// The client wants sections sent to it before it can successfully respawn.
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
@ -133,6 +151,24 @@ public class DimensionUtils {
}
}
public static void setBedrockDimension(GeyserSession session, String javaDimension) {
session.getChunkCache().setBedrockDimension(switch (javaDimension) {
case DimensionUtils.THE_END -> BedrockDimension.THE_END;
case DimensionUtils.NETHER -> DimensionUtils.isCustomBedrockNetherId() ? BedrockDimension.THE_END : BedrockDimension.THE_NETHER;
default -> BedrockDimension.OVERWORLD;
});
}
public static int javaToBedrock(BedrockDimension dimension) {
if (dimension == BedrockDimension.THE_NETHER) {
return BEDROCK_NETHER_ID;
} else if (dimension == BedrockDimension.THE_END) {
return 2;
} else {
return 0;
}
}
/**
* Map the Java edition dimension IDs to Bedrock edition
*
@ -171,7 +207,9 @@ public class DimensionUtils {
// Prevents rare instances of Bedrock locking up
return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER;
}
return currentDimension.equals(OVERWORLD) ? NETHER : OVERWORLD;
// Check current Bedrock dimension and not just the Java dimension.
// Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161
return javaToBedrock(currentDimension) == 0 ? NETHER : OVERWORLD;
}
public static boolean isCustomBedrockNetherId() {

View file

@ -31,6 +31,7 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundPickItemPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
@ -38,6 +39,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerHotbarPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.Container;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
@ -46,6 +48,7 @@ import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
@ -85,8 +88,7 @@ public class InventoryUtils {
public static void displayInventory(GeyserSession session, Inventory inventory) {
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
translator.prepareInventory(session, inventory);
if (translator != null && translator.prepareInventory(session, inventory)) {
if (translator instanceof DoubleChestInventoryTranslator && !((Container) inventory).isUsingRealBlock()) {
session.scheduleInEventLoop(() -> {
Inventory openInv = session.getOpenInventory();
@ -103,7 +105,6 @@ public class InventoryUtils {
translator.updateInventory(session, inventory);
}
} else {
// Precaution - as of 1.16 every inventory should be translated so this shouldn't happen
session.setOpenInventory(null);
}
}
@ -136,6 +137,28 @@ public class InventoryUtils {
}
}
/**
* Finds a usable block space in the world to place a fake inventory block, and returns the position.
*/
@Nullable
public static Vector3i findAvailableWorldSpace(GeyserSession session) {
// Check if a fake block can be placed, either above the player or beneath.
BedrockDimension dimension = session.getChunkCache().getBedrockDimension();
int minY = dimension.minY(), maxY = minY + dimension.height();
Vector3i flatPlayerPosition = session.getPlayerEntity().getPosition().toInt();
Vector3i position = flatPlayerPosition.add(Vector3i.UP);
if (position.getY() < minY) {
return null;
}
if (position.getY() >= maxY) {
position = flatPlayerPosition.sub(0, 4, 0);
if (position.getY() >= maxY) {
return null;
}
}
return position;
}
public static void updateCursor(GeyserSession session) {
InventorySlotPacket cursorPacket = new InventorySlotPacket();
cursorPacket.setContainerId(ContainerId.UI);
@ -150,18 +173,6 @@ public class InventoryUtils {
return item1.getJavaId() == item2.getJavaId() && Objects.equals(item1.getNbt(), item2.getNbt());
}
public static boolean canStack(ItemStack item1, ItemStack item2) {
if (item1 == null || item2 == null)
return false;
return item1.getId() == item2.getId() && Objects.equals(item1.getNbt(), item2.getNbt());
}
public static boolean canStack(ItemData item1, ItemData item2) {
if (item1 == null || item2 == null)
return false;
return item1.equals(item2, false, true, true);
}
/**
* Checks to see if an item stack represents air or has no count.
*/
@ -186,11 +197,22 @@ public class InventoryUtils {
root.put("display", display.build());
return protocolVersion -> ItemData.builder()
.id(Registries.ITEMS.forVersion(protocolVersion).getStoredItems().barrier().getBedrockId())
.id(getUnusableSpaceBlockID(protocolVersion))
.count(1)
.tag(root.build()).build();
}
private static int getUnusableSpaceBlockID(int protocolVersion) {
String unusableSpaceBlock = GeyserImpl.getInstance().getConfig().getUnusableSpaceBlock();
ItemMapping unusableSpaceBlockID = Registries.ITEMS.forVersion(protocolVersion).getMapping(unusableSpaceBlock);
if (unusableSpaceBlockID != null) {
return unusableSpaceBlockID.getBedrockId();
} else {
GeyserImpl.getInstance().getLogger().error("Invalid value" + unusableSpaceBlock + ". Resorting to barrier block.");
return Registries.ITEMS.forVersion(protocolVersion).getStoredItems().barrier().getBedrockId();
}
}
/**
* See {@link #findOrCreateItem(GeyserSession, String)}. This is for finding a specified {@link ItemStack}.
*

View file

@ -100,11 +100,7 @@ public class SettingsUtils {
.translator(MinecraftLocale::getLocaleString); // we need translate gamerules next
WorldManager worldManager = GeyserImpl.getInstance().getWorldManager();
for (GameRule gamerule : GameRule.values()) {
if (gamerule.equals(GameRule.UNKNOWN)) {
continue;
}
for (GameRule gamerule : GameRule.VALUES) {
// Add the relevant form item based on the gamerule type
if (Boolean.class.equals(gamerule.getType())) {
builder.toggle("gamerule." + gamerule.getJavaID(), worldManager.getGameRuleBool(session, gamerule));
@ -146,10 +142,6 @@ public class SettingsUtils {
if (showGamerules) {
for (GameRule gamerule : GameRule.VALUES) {
if (gamerule.equals(GameRule.UNKNOWN)) {
continue;
}
if (Boolean.class.equals(gamerule.getType())) {
boolean value = response.next();
if (value != session.getGeyser().getWorldManager().getGameRuleBool(session, gamerule)) {

View file

@ -26,7 +26,6 @@
package org.geysermc.geyser.util;
import com.github.steveice10.mc.protocol.data.game.level.sound.BuiltinSound;
import com.github.steveice10.mc.protocol.data.game.level.sound.CustomSound;
import com.github.steveice10.mc.protocol.data.game.level.sound.Sound;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
@ -63,34 +62,38 @@ public final class SoundUtils {
/**
* Translates a Java Custom or Builtin Sound to its Bedrock equivalent
*
* @param sound the sound to translate
* @param javaIdentifier the sound to translate
* @return a Bedrock sound
*/
public static String translatePlaySound(Sound sound) {
String packetSound;
if (sound instanceof BuiltinSound builtinSound) {
packetSound = builtinSound.getName();
} else if (sound instanceof CustomSound customSound) {
packetSound = customSound.getName();
} else {
GeyserImpl.getInstance().getLogger().debug("Unknown sound, we were unable to map this. " + sound);
return "";
}
public static String translatePlaySound(String javaIdentifier) {
javaIdentifier = trim(javaIdentifier);
// Drop the Minecraft namespace if applicable
if (packetSound.startsWith("minecraft:")) {
packetSound = packetSound.substring("minecraft:".length());
}
SoundMapping soundMapping = Registries.SOUNDS.get(packetSound);
SoundMapping soundMapping = Registries.SOUNDS.get(javaIdentifier);
if (soundMapping == null || soundMapping.getPlaysound() == null) {
// no mapping
GeyserImpl.getInstance().getLogger().debug("[PlaySound] Defaulting to sound server gave us for " + sound);
return packetSound;
GeyserImpl.getInstance().getLogger().debug("[PlaySound] Defaulting to sound server gave us for " + javaIdentifier);
return javaIdentifier;
}
return soundMapping.getPlaysound();
}
private static String trim(String identifier) {
// Drop the Minecraft namespace if applicable
if (identifier.startsWith("minecraft:")) {
return identifier.substring("minecraft:".length());
}
return identifier;
}
private static void playSound(GeyserSession session, String bedrockName, Vector3f position, float volume, float pitch) {
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound(bedrockName);
playSoundPacket.setPosition(position);
playSoundPacket.setVolume(volume);
playSoundPacket.setPitch(pitch);
session.sendUpstreamPacket(playSoundPacket);
}
/**
* Translates and plays a Java Builtin Sound for a Bedrock client
*
@ -99,23 +102,25 @@ public final class SoundUtils {
* @param position the position
* @param pitch the pitch
*/
public static void playBuiltinSound(GeyserSession session, BuiltinSound javaSound, Vector3f position, float volume, float pitch) {
String packetSound = javaSound.getName();
public static void playSound(GeyserSession session, Sound javaSound, Vector3f position, float volume, float pitch) {
String packetSound;
if (!(javaSound instanceof BuiltinSound)) {
// Identifier needs trimmed probably.
packetSound = trim(javaSound.getName());
} else {
packetSound = javaSound.getName();
}
SoundMapping soundMapping = Registries.SOUNDS.get(packetSound);
if (soundMapping == null) {
session.getGeyser().getLogger().debug("[Builtin] Sound mapping for " + packetSound + " not found");
session.getGeyser().getLogger().debug("[Builtin] Sound mapping for " + packetSound + " not found; assuming custom.");
playSound(session, packetSound, position, volume, pitch);
return;
}
if (soundMapping.getPlaysound() != null) {
// We always prefer the PlaySound mapping because we can control volume and pitch
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound(soundMapping.getPlaysound());
playSoundPacket.setPosition(position);
playSoundPacket.setVolume(volume);
playSoundPacket.setPitch(pitch);
session.sendUpstreamPacket(playSoundPacket);
playSound(session, soundMapping.getPlaysound(), position, volume, pitch);
return;
}

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more