mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Merge remote-tracking branch 'origin/master' into feature/floodgate-merge
This commit is contained in:
commit
ca7d57e541
158 changed files with 3719 additions and 14571 deletions
|
@ -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)
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normal file
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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())));
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
329
core/src/main/java/org/geysermc/geyser/util/AssetUtils.java
Normal file
329
core/src/main/java/org/geysermc/geyser/util/AssetUtils.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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. :/ )
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}.
|
||||
*
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
BIN
core/src/main/resources/bedrock/block_palette.1_19_50.nbt
Normal file
BIN
core/src/main/resources/bedrock/block_palette.1_19_50.nbt
Normal file
Binary file not shown.
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
Loading…
Add table
Add a link
Reference in a new issue