Merge branch 'master' into tick-rate-handling

This commit is contained in:
LetsGoAway 2024-07-28 12:17:43 +08:00 committed by GitHub
commit 8d4f259ba6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 215 additions and 131 deletions

View file

@ -30,7 +30,11 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.ParticleType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket;
import org.cloudburstmc.protocol.bedrock.packet.SpawnParticleEffectPacket;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.Tickable;
import org.geysermc.geyser.entity.type.living.MobEntity; import org.geysermc.geyser.entity.type.living.MobEntity;
@ -260,7 +264,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
// so we need to manually spawn particles // so we need to manually spawn particles
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket(); SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket();
spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session));
spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f));
spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire");
spawnParticleEffectPacket.setMolangVariablesJson(Optional.empty()); spawnParticleEffectPacket.setMolangVariablesJson(Optional.empty());

View file

@ -28,6 +28,9 @@ package org.geysermc.geyser.item.type;
import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.block.type.Block;
public class BlockItem extends Item { public class BlockItem extends Item {
// If item is instanceof ItemNameBlockItem
private final boolean treatLikeBlock;
public BlockItem(Builder builder, Block block, Block... otherBlocks) { public BlockItem(Builder builder, Block block, Block... otherBlocks) {
super(block.javaIdentifier().value(), builder); super(block.javaIdentifier().value(), builder);
@ -36,6 +39,7 @@ public class BlockItem extends Item {
for (Block otherBlock : otherBlocks) { for (Block otherBlock : otherBlocks) {
registerBlock(otherBlock, this); registerBlock(otherBlock, this);
} }
treatLikeBlock = true;
} }
// Use this constructor if the item name is not the same as its primary block // Use this constructor if the item name is not the same as its primary block
@ -46,5 +50,14 @@ public class BlockItem extends Item {
for (Block otherBlock : otherBlocks) { for (Block otherBlock : otherBlocks) {
registerBlock(otherBlock, this); registerBlock(otherBlock, this);
} }
treatLikeBlock = false;
}
@Override
public String translationKey() {
if (!treatLikeBlock) {
return super.translationKey();
}
return "block." + this.javaIdentifier.namespace() + "." + this.javaIdentifier.value();
} }
} }

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.item.type; package org.geysermc.geyser.item.type;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@ -59,7 +60,7 @@ import java.util.Map;
public class Item { public class Item {
private static final Map<Block, Item> BLOCK_TO_ITEM = new HashMap<>(); private static final Map<Block, Item> BLOCK_TO_ITEM = new HashMap<>();
private final String javaIdentifier; protected final Key javaIdentifier;
private int javaId = -1; private int javaId = -1;
private final int stackSize; private final int stackSize;
private final int attackDamage; private final int attackDamage;
@ -68,7 +69,7 @@ public class Item {
private final boolean glint; private final boolean glint;
public Item(String javaIdentifier, Builder builder) { public Item(String javaIdentifier, Builder builder) {
this.javaIdentifier = MinecraftKey.key(javaIdentifier).asString().intern(); this.javaIdentifier = MinecraftKey.key(javaIdentifier);
this.stackSize = builder.stackSize; this.stackSize = builder.stackSize;
this.maxDamage = builder.maxDamage; this.maxDamage = builder.maxDamage;
this.attackDamage = builder.attackDamage; this.attackDamage = builder.attackDamage;
@ -77,7 +78,7 @@ public class Item {
} }
public String javaIdentifier() { public String javaIdentifier() {
return javaIdentifier; return javaIdentifier.asString();
} }
public int javaId() { public int javaId() {
@ -108,6 +109,10 @@ public class Item {
return false; return false;
} }
public String translationKey() {
return "item." + javaIdentifier.namespace() + "." + javaIdentifier.value();
}
/* Translation methods to Bedrock and back */ /* Translation methods to Bedrock and back */
public ItemData.Builder translateToBedrock(int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) { public ItemData.Builder translateToBedrock(int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) {

View file

@ -25,15 +25,17 @@
package org.geysermc.geyser.level; package org.geysermc.geyser.level;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.util.DimensionUtils;
/** /**
* Represents the information we store from the current Java dimension * Represents the information we store from the current Java dimension
* @param piglinSafe Whether piglins and hoglins are safe from conversion in this dimension. * @param piglinSafe Whether piglins and hoglins are safe from conversion in this dimension.
* This controls if they have the shaking effect applied in the dimension. * This controls if they have the shaking effect applied in the dimension.
*/ */
public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale) { public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale, int bedrockId, boolean isNetherLike) {
public static JavaDimension read(RegistryEntryContext entry) { public static JavaDimension read(RegistryEntryContext entry) {
NbtMap dimension = entry.data(); NbtMap dimension = entry.data();
@ -46,6 +48,22 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double world
// Load world coordinate scale for the world border // Load world coordinate scale for the world border
double coordinateScale = dimension.getDouble("coordinate_scale"); double coordinateScale = dimension.getDouble("coordinate_scale");
return new JavaDimension(minY, maxY, piglinSafe, coordinateScale); boolean isNetherLike;
// Cache the Bedrock version of this dimension, and base it off the ID - THE ID CAN CHANGE!!!
// https://github.com/GeyserMC/Geyser/issues/4837
int bedrockId;
Key id = entry.id();
if ("minecraft".equals(id.namespace())) {
String identifier = id.asString();
bedrockId = DimensionUtils.javaToBedrock(identifier);
isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(identifier);
} else {
// Effects should give is a clue on how this (custom) dimension is supposed to look like
String effects = dimension.getString("effects");
bedrockId = DimensionUtils.javaToBedrock(effects);
isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(effects);
}
return new JavaDimension(minY, maxY, piglinSafe, coordinateScale, bedrockId, isNetherLike);
} }
} }

View file

@ -381,8 +381,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* The dimension of the player. * The dimension of the player.
* As all entities are in the same world, this can be safely applied to all other entities. * As all entities are in the same world, this can be safely applied to all other entities.
*/ */
@Setter
private int dimension = DimensionUtils.OVERWORLD;
@MonotonicNonNull @MonotonicNonNull
@Setter @Setter
private JavaDimension dimensionType = null; private JavaDimension dimensionType = null;

View file

@ -27,12 +27,14 @@ package org.geysermc.geyser.translator.protocol.bedrock;
import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsRequestPacket; import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsRequestPacket;
import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsResponsePacket; import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsResponsePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket;
import org.geysermc.cumulus.form.CustomForm; import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.impl.FormDefinitions; import org.geysermc.cumulus.form.impl.FormDefinitions;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.SettingsUtils; import org.geysermc.geyser.util.SettingsUtils;
import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -47,6 +49,14 @@ public class BedrockServerSettingsRequestTranslator extends PacketTranslator<Ser
return; return;
} }
// Peaceful difficulty allows always eating food - hence, we just do not send it to Bedrock.
// However, in order for server settings to show it properly, let's revert while we are in the menu!
if (session.getWorldCache().getDifficulty() == Difficulty.PEACEFUL) {
SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket();
setDifficultyPacket.setDifficulty(Difficulty.PEACEFUL.ordinal());
session.sendUpstreamPacket(setDifficultyPacket);
}
CustomForm form = SettingsUtils.buildForm(session); CustomForm form = SettingsUtils.buildForm(session);
int formId = session.getFormCache().addForm(form); int formId = session.getFormCache().addForm(form);

View file

@ -25,21 +25,29 @@
package org.geysermc.geyser.translator.protocol.java; package org.geysermc.geyser.translator.protocol.java;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundChangeDifficultyPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundChangeDifficultyPacket;
@Translator(packet = ClientboundChangeDifficultyPacket.class) @Translator(packet = ClientboundChangeDifficultyPacket.class)
public class JavaChangeDifficultyTranslator extends PacketTranslator<ClientboundChangeDifficultyPacket> { public class JavaChangeDifficultyTranslator extends PacketTranslator<ClientboundChangeDifficultyPacket> {
@Override @Override
public void translate(GeyserSession session, ClientboundChangeDifficultyPacket packet) { public void translate(GeyserSession session, ClientboundChangeDifficultyPacket packet) {
Difficulty difficulty = packet.getDifficulty();
session.getWorldCache().setDifficulty(difficulty);
// Peaceful difficulty allows always eating food - hence, we just do not send it to Bedrock.
if (difficulty == Difficulty.PEACEFUL) {
difficulty = Difficulty.EASY;
}
SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket(); SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket();
setDifficultyPacket.setDifficulty(packet.getDifficulty().ordinal()); setDifficultyPacket.setDifficulty(difficulty.ordinal());
session.sendUpstreamPacket(setDifficultyPacket); session.sendUpstreamPacket(setDifficultyPacket);
session.getWorldCache().setDifficulty(packet.getDifficulty());
} }
} }

View file

@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.protocol.java;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import org.geysermc.erosion.Constants; import org.geysermc.erosion.Constants;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo;
import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket;
@ -65,12 +66,15 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
} }
PlayerSpawnInfo spawnInfo = packet.getCommonPlayerSpawnInfo(); PlayerSpawnInfo spawnInfo = packet.getCommonPlayerSpawnInfo();
JavaDimension newDimension = session.getRegistryCache().dimensions().byId(spawnInfo.getDimension());
boolean forceDimSwitch = false;
// If the player is already initialized and a join game packet is sent, they // If the player is already initialized and a join game packet is sent, they
// are swapping servers // are swapping servers
if (session.isSpawned()) { if (session.isSpawned()) {
int fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), spawnInfo.getDimension()); int fakeDim = DimensionUtils.getTemporaryDimension(session.getDimensionType().bedrockId(), newDimension.bedrockId());
DimensionUtils.switchDimension(session, fakeDim); DimensionUtils.fastSwitchDimension(session, fakeDim);
forceDimSwitch = true;
session.getWorldCache().removeScoreboard(); session.getWorldCache().removeScoreboard();
@ -84,13 +88,12 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
session.setWorldName(spawnInfo.getWorldName()); session.setWorldName(spawnInfo.getWorldName());
session.setLevels(Arrays.stream(packet.getWorldNames()).map(Key::asString).toArray(String[]::new)); session.setLevels(Arrays.stream(packet.getWorldNames()).map(Key::asString).toArray(String[]::new));
session.setGameMode(spawnInfo.getGameMode()); session.setGameMode(spawnInfo.getGameMode());
int newDimension = spawnInfo.getDimension();
boolean needsSpawnPacket = !session.isSentSpawnPacket(); boolean needsSpawnPacket = !session.isSentSpawnPacket();
if (needsSpawnPacket) { if (needsSpawnPacket) {
// The player has yet to spawn so let's do that using some of the information in this Java packet // 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(newDimension);
DimensionUtils.setBedrockDimension(session, newDimension); DimensionUtils.setBedrockDimension(session, newDimension.bedrockId());
session.connect(); session.connect();
// It is now safe to send these packets // It is now safe to send these packets
@ -124,9 +127,9 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
} }
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8)));
if (newDimension != session.getDimension()) { if (newDimension != session.getDimensionType() || forceDimSwitch) {
DimensionUtils.switchDimension(session, newDimension); DimensionUtils.switchDimension(session, newDimension);
} else if (DimensionUtils.isCustomBedrockNetherId() && newDimension == DimensionUtils.NETHER) { } else if (DimensionUtils.isCustomBedrockNetherId() && newDimension.isNetherLike()) {
// If the player is spawning into the "fake" nether, send them some fog // If the player is spawning into the "fake" nether, send them some fog
session.camera().sendFog(DimensionUtils.BEDROCK_FOG_HELL); session.camera().sendFog(DimensionUtils.BEDROCK_FOG_HELL);
} }

View file

@ -31,6 +31,7 @@ import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -92,12 +93,12 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
session.setThunder(false); session.setThunder(false);
} }
int newDimension = spawnInfo.getDimension(); JavaDimension newDimension = session.getRegistryCache().dimensions().byId(spawnInfo.getDimension());
if (session.getDimension() != newDimension || !spawnInfo.getWorldName().equals(session.getWorldName())) { if (session.getDimensionType() != newDimension || !spawnInfo.getWorldName().equals(session.getWorldName())) {
// Switching to a new world (based off the world name change or new dimension); send a fake dimension change // 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)) { if (session.getDimensionType().bedrockId() == newDimension.bedrockId()) {
int fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension); int fakeDim = DimensionUtils.getTemporaryDimension(session.getDimensionType().bedrockId(), newDimension.bedrockId());
DimensionUtils.switchDimension(session, fakeDim); DimensionUtils.fastSwitchDimension(session, fakeDim);
} }
session.setWorldName(spawnInfo.getWorldName()); session.setWorldName(spawnInfo.getWorldName());
DimensionUtils.switchDimension(session, newDimension); DimensionUtils.switchDimension(session, newDimension);

View file

@ -253,7 +253,8 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
// We can get the correct order for button pressing // We can get the correct order for button pressing
data.getValue().sort(Comparator.comparing((stoneCuttingRecipeData -> data.getValue().sort(Comparator.comparing((stoneCuttingRecipeData ->
Registries.JAVA_ITEMS.get().get(stoneCuttingRecipeData.getResult().getId()) Registries.JAVA_ITEMS.get().get(stoneCuttingRecipeData.getResult().getId())
.javaIdentifier()))); // See RecipeManager#getRecipesFor as of 1.21
.translationKey())));
// Now that it's sorted, let's translate these recipes // Now that it's sorted, let's translate these recipes
int buttonId = 0; int buttonId = 0;

View file

@ -92,7 +92,7 @@ public class JavaAnimateTranslator extends PacketTranslator<ClientboundAnimatePa
// Spawn custom particle // Spawn custom particle
SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket(); SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket();
stringPacket.setIdentifier("geyseropt:enchanted_hit_multiple"); stringPacket.setIdentifier("geyseropt:enchanted_hit_multiple");
stringPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); stringPacket.setDimensionId(DimensionUtils.javaToBedrock(session));
stringPacket.setPosition(Vector3f.ZERO); stringPacket.setPosition(Vector3f.ZERO);
stringPacket.setUniqueEntityId(entity.getGeyserId()); stringPacket.setUniqueEntityId(entity.getGeyserId());
stringPacket.setMolangVariablesJson(Optional.empty()); stringPacket.setMolangVariablesJson(Optional.empty());

View file

@ -28,7 +28,6 @@ package org.geysermc.geyser.translator.protocol.java.level;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.ParticleType;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
@ -481,7 +480,7 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
} }
private static void spawnOminousTrialSpawnerParticles(GeyserSession session, Vector3f pos) { private static void spawnOminousTrialSpawnerParticles(GeyserSession session, Vector3f pos) {
int dimensionId = DimensionUtils.javaToBedrock(session.getDimension()); int dimensionId = DimensionUtils.javaToBedrock(session);
SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket(); SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket();
stringPacket.setIdentifier("minecraft:trial_spawner_detection_ominous"); stringPacket.setIdentifier("minecraft:trial_spawner_detection_ominous");
stringPacket.setDimensionId(dimensionId); stringPacket.setDimensionId(dimensionId);

View file

@ -191,7 +191,7 @@ public class JavaLevelParticlesTranslator extends PacketTranslator<ClientboundLe
return packet; return packet;
}; };
} else if (particleMapping.identifier() != null) { } else if (particleMapping.identifier() != null) {
int dimensionId = DimensionUtils.javaToBedrock(session.getDimension()); int dimensionId = DimensionUtils.javaToBedrock(session);
return (position) -> { return (position) -> {
SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket(); SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket();
stringPacket.setIdentifier(particleMapping.identifier()); stringPacket.setIdentifier(particleMapping.identifier());

View file

@ -46,7 +46,7 @@ public class JavaMapItemDataTranslator extends PacketTranslator<ClientboundMapIt
org.cloudburstmc.protocol.bedrock.packet.ClientboundMapItemDataPacket mapItemDataPacket = new org.cloudburstmc.protocol.bedrock.packet.ClientboundMapItemDataPacket(); org.cloudburstmc.protocol.bedrock.packet.ClientboundMapItemDataPacket mapItemDataPacket = new org.cloudburstmc.protocol.bedrock.packet.ClientboundMapItemDataPacket();
mapItemDataPacket.setUniqueMapId(packet.getMapId()); mapItemDataPacket.setUniqueMapId(packet.getMapId());
mapItemDataPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); mapItemDataPacket.setDimensionId(DimensionUtils.javaToBedrock(session));
mapItemDataPacket.setLocked(packet.isLocked()); mapItemDataPacket.setLocked(packet.isLocked());
mapItemDataPacket.setOrigin(Vector3i.ZERO); // Required since 1.19.20 mapItemDataPacket.setOrigin(Vector3i.ZERO); // Required since 1.19.20
mapItemDataPacket.setScale(packet.getScale()); mapItemDataPacket.setScale(packet.getScale());

View file

@ -39,7 +39,7 @@ public class JavaSetDefaultSpawnPositionTranslator extends PacketTranslator<Clie
public void translate(GeyserSession session, ClientboundSetDefaultSpawnPositionPacket packet) { public void translate(GeyserSession session, ClientboundSetDefaultSpawnPositionPacket packet) {
SetSpawnPositionPacket spawnPositionPacket = new SetSpawnPositionPacket(); SetSpawnPositionPacket spawnPositionPacket = new SetSpawnPositionPacket();
spawnPositionPacket.setBlockPosition(packet.getPosition()); spawnPositionPacket.setBlockPosition(packet.getPosition());
spawnPositionPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); spawnPositionPacket.setDimensionId(DimensionUtils.javaToBedrock(session));
spawnPositionPacket.setSpawnType(SetSpawnPositionPacket.Type.WORLD_SPAWN); spawnPositionPacket.setSpawnType(SetSpawnPositionPacket.Type.WORLD_SPAWN);
session.sendUpstreamPacket(spawnPositionPacket); session.sendUpstreamPacket(spawnPositionPacket);
} }

View file

@ -203,8 +203,7 @@ public class ChunkUtils {
* This must be done after the player has switched dimensions so we know what their dimension is * This must be done after the player has switched dimensions so we know what their dimension is
*/ */
public static void loadDimension(GeyserSession session) { public static void loadDimension(GeyserSession session) {
JavaDimension dimension = session.getRegistryCache().dimensions().byId(session.getDimension()); JavaDimension dimension = session.getDimensionType();
session.setDimensionType(dimension);
int minY = dimension.minY(); int minY = dimension.minY();
int maxY = dimension.maxY(); int maxY = dimension.maxY();
@ -223,7 +222,7 @@ public class ChunkUtils {
session.getGeyser().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.translator.chunk.out_of_bounds", session.getGeyser().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.translator.chunk.out_of_bounds",
String.valueOf(bedrockDimension.minY()), String.valueOf(bedrockDimension.minY()),
String.valueOf(bedrockDimension.height()), String.valueOf(bedrockDimension.height()),
session.getDimension())); session.getRegistryCache().dimensions().byValue(session.getDimensionType())));
} }
session.getChunkCache().setMinY(minY); session.getChunkCache().setMinY(minY);

View file

@ -29,9 +29,15 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType; import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.bedrock.packet.ChangeDimensionPacket;
import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.MobEffectPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
import org.cloudburstmc.protocol.bedrock.packet.StopSoundPacket;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
@ -44,22 +50,18 @@ public class DimensionUtils {
public static final String BEDROCK_FOG_HELL = "minecraft:fog_hell"; public static final String BEDROCK_FOG_HELL = "minecraft:fog_hell";
/** public static final String NETHER_IDENTIFIER = "minecraft:the_nether";
* String reference to vanilla Java overworld dimension identifier
*/
public static final int OVERWORLD = 0;
/**
* String reference to vanilla Java nether dimension identifier
*/
public static final int NETHER = 3;
/**
* String reference to vanilla Java end dimension identifier
*/
public static final int THE_END = 2;
public static void switchDimension(GeyserSession session, int javaDimension) { private static final int BEDROCK_OVERWORLD_ID = 0;
int bedrockDimension = javaToBedrock(javaDimension); // new bedrock dimension private static final int BEDROCK_DEFAULT_NETHER_ID = 1;
int previousDimension = session.getDimension(); // previous java dimension private static final int BEDROCK_END_ID = 2;
public static void switchDimension(GeyserSession session, JavaDimension javaDimension) {
switchDimension(session, javaDimension, javaDimension.bedrockId());
}
public static void switchDimension(GeyserSession session, JavaDimension javaDimension, int bedrockDimension) {
JavaDimension previousDimension = session.getDimensionType(); // previous java dimension
Entity player = session.getPlayerEntity(); Entity player = session.getPlayerEntity();
@ -70,35 +72,9 @@ public class DimensionUtils {
session.getPistonCache().clear(); session.getPistonCache().clear();
session.getSkullCache().clear(); session.getSkullCache().clear();
if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) { changeDimension(session, bedrockDimension);
// 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
// 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(32);
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
// Will be re-adjusted on spawn
}
Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0); session.setDimensionType(javaDimension);
ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket();
changeDimensionPacket.setDimension(bedrockDimension);
changeDimensionPacket.setRespawn(true);
changeDimensionPacket.setPosition(pos);
session.sendUpstreamPacket(changeDimensionPacket);
session.setDimension(javaDimension);
setBedrockDimension(session, javaDimension);
player.setPosition(pos);
session.setSpawned(false);
session.setLastChunkPosition(null);
Set<Effect> entityEffects = session.getEffectCache().getEntityEffects(); Set<Effect> entityEffects = session.getEffectCache().getEntityEffects();
for (Effect effect : entityEffects) { for (Effect effect : entityEffects) {
@ -125,6 +101,60 @@ public class DimensionUtils {
session.sendUpstreamPacket(stopThunderPacket); session.sendUpstreamPacket(stopThunderPacket);
session.setThunder(false); session.setThunder(false);
finalizeDimensionSwitch(session, player);
// If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension,
// we check if the player is entering the nether and apply the nether fog to fake the fact that the client
// thinks they are in the end dimension.
if (isCustomBedrockNetherId()) {
if (javaDimension.isNetherLike()) {
session.camera().sendFog(BEDROCK_FOG_HELL);
} else if (previousDimension.isNetherLike()) {
session.camera().removeFog(BEDROCK_FOG_HELL);
}
}
}
/**
* Switch dimensions without clearing internal logic.
*/
public static void fastSwitchDimension(GeyserSession session, int bedrockDimension) {
changeDimension(session, bedrockDimension);
finalizeDimensionSwitch(session, session.getPlayerEntity());
}
private static void changeDimension(GeyserSession session, int bedrockDimension) {
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
// 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(32);
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
// Will be re-adjusted on spawn
}
Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0);
ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket();
changeDimensionPacket.setDimension(bedrockDimension);
changeDimensionPacket.setRespawn(true);
changeDimensionPacket.setPosition(pos);
session.sendUpstreamPacket(changeDimensionPacket);
setBedrockDimension(session, bedrockDimension);
session.getPlayerEntity().setPosition(pos);
session.setSpawned(false);
session.setLastChunkPosition(null);
}
private static void finalizeDimensionSwitch(GeyserSession session, Entity player) {
//let java server handle portal travel sound //let java server handle portal travel sound
StopSoundPacket stopSoundPacket = new StopSoundPacket(); StopSoundPacket stopSoundPacket = new StopSoundPacket();
stopSoundPacket.setStoppingAllSound(true); stopSoundPacket.setStoppingAllSound(true);
@ -145,23 +175,12 @@ public class DimensionUtils {
// TODO - fix this hack of a fix by sending the final dimension switching logic after sections have been sent. // 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. // The client wants sections sent to it before it can successfully respawn.
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
// If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension,
// we check if the player is entering the nether and apply the nether fog to fake the fact that the client
// thinks they are in the end dimension.
if (isCustomBedrockNetherId()) {
if (NETHER == javaDimension) {
session.camera().sendFog(BEDROCK_FOG_HELL);
} else if (NETHER == previousDimension) {
session.camera().removeFog(BEDROCK_FOG_HELL);
}
}
} }
public static void setBedrockDimension(GeyserSession session, int javaDimension) { public static void setBedrockDimension(GeyserSession session, int bedrockDimension) {
session.getChunkCache().setBedrockDimension(switch (javaDimension) { session.getChunkCache().setBedrockDimension(switch (bedrockDimension) {
case DimensionUtils.THE_END -> BedrockDimension.THE_END; case BEDROCK_END_ID -> BedrockDimension.THE_END;
case DimensionUtils.NETHER -> DimensionUtils.isCustomBedrockNetherId() ? BedrockDimension.THE_END : BedrockDimension.THE_NETHER; case BEDROCK_DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled.
default -> BedrockDimension.OVERWORLD; default -> BedrockDimension.OVERWORLD;
}); });
} }
@ -170,26 +189,12 @@ public class DimensionUtils {
if (dimension == BedrockDimension.THE_NETHER) { if (dimension == BedrockDimension.THE_NETHER) {
return BEDROCK_NETHER_ID; return BEDROCK_NETHER_ID;
} else if (dimension == BedrockDimension.THE_END) { } else if (dimension == BedrockDimension.THE_END) {
return 2; return BEDROCK_END_ID;
} else { } else {
return 0; return BEDROCK_OVERWORLD_ID;
} }
} }
/**
* Map the Java edition dimension IDs to Bedrock edition
*
* @param javaDimension Dimension ID to convert
* @return Converted Bedrock edition dimension ID
*/
public static int javaToBedrock(int javaDimension) {
return switch (javaDimension) {
case NETHER -> BEDROCK_NETHER_ID;
case THE_END -> 2;
default -> 0;
};
}
/** /**
* Map the Java edition dimension IDs to Bedrock edition * Map the Java edition dimension IDs to Bedrock edition
* *
@ -198,12 +203,23 @@ public class DimensionUtils {
*/ */
public static int javaToBedrock(String javaDimension) { public static int javaToBedrock(String javaDimension) {
return switch (javaDimension) { return switch (javaDimension) {
case "minecraft:the_nether" -> BEDROCK_NETHER_ID; case NETHER_IDENTIFIER -> BEDROCK_NETHER_ID;
case "minecraft:the_end" -> 2; case "minecraft:the_end" -> 2;
default -> 0; default -> 0;
}; };
} }
/**
* Gets the Bedrock dimension ID, with a safety check if a packet is created before the player is logged/spawned in.
*/
public static int javaToBedrock(GeyserSession session) {
JavaDimension dimension = session.getDimensionType();
if (dimension == null) {
return BEDROCK_OVERWORLD_ID;
}
return dimension.bedrockId();
}
/** /**
* The Nether dimension in Bedrock does not permit building above Y128 - the Bedrock above the dimension. * The Nether dimension in Bedrock does not permit building above Y128 - the Bedrock above the dimension.
* This workaround sets the Nether as the End dimension to ignore this limit. * This workaround sets the Nether as the End dimension to ignore this limit.
@ -212,28 +228,28 @@ public class DimensionUtils {
*/ */
public static void changeBedrockNetherId(boolean isAboveNetherBedrockBuilding) { public static void changeBedrockNetherId(boolean isAboveNetherBedrockBuilding) {
// Change dimension ID to the End to allow for building above Bedrock // Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1; BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? BEDROCK_END_ID : BEDROCK_DEFAULT_NETHER_ID;
} }
/** /**
* Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional * Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional
* dimension switch. * dimension switch.
* *
* @param currentDimension the current dimension of the player * @param currentBedrockDimension the current dimension of the player
* @param newDimension the new dimension that the player will be transferred to * @param newBedrockDimension the new dimension that the player will be transferred to
* @return the fake dimension to transfer to * @return the Bedrock fake dimension to transfer to
*/ */
public static int getTemporaryDimension(int currentDimension, int newDimension) { public static int getTemporaryDimension(int currentBedrockDimension, int newBedrockDimension) {
if (isCustomBedrockNetherId()) { if (isCustomBedrockNetherId()) {
// Prevents rare instances of Bedrock locking up // Prevents rare instances of Bedrock locking up
return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER; return newBedrockDimension == BEDROCK_END_ID ? BEDROCK_OVERWORLD_ID : BEDROCK_END_ID;
} }
// Check current Bedrock dimension and not just the Java dimension. // Check current Bedrock dimension and not just the Java dimension.
// Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161 // Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161
return javaToBedrock(currentDimension) == 0 ? NETHER : OVERWORLD; return currentBedrockDimension == BEDROCK_OVERWORLD_ID ? BEDROCK_DEFAULT_NETHER_ID : BEDROCK_OVERWORLD_ID;
} }
public static boolean isCustomBedrockNetherId() { public static boolean isCustomBedrockNetherId() {
return BEDROCK_NETHER_ID == 2; return BEDROCK_NETHER_ID == BEDROCK_END_ID;
} }
} }

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.util; package org.geysermc.geyser.util;
import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket;
import org.geysermc.cumulus.component.DropdownComponent; import org.geysermc.cumulus.component.DropdownComponent;
import org.geysermc.cumulus.form.CustomForm; import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
@ -33,6 +34,7 @@ import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty;
public class SettingsUtils { public class SettingsUtils {
/** /**
@ -96,6 +98,7 @@ public class SettingsUtils {
} }
builder.validResultHandler((response) -> { builder.validResultHandler((response) -> {
applyDifficultyFix(session);
if (showClientSettings) { if (showClientSettings) {
// Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config.
if (showCoordinates) { if (showCoordinates) {
@ -134,9 +137,21 @@ public class SettingsUtils {
} }
}); });
builder.closedOrInvalidResultHandler($ -> applyDifficultyFix(session));
return builder.build(); return builder.build();
} }
private static void applyDifficultyFix(GeyserSession session) {
// Peaceful difficulty allows always eating food - hence, we just do not send it to Bedrock.
// Since we sent the real difficulty before opening the server settings form, let's restore it to our workaround here
if (session.getWorldCache().getDifficulty() == Difficulty.PEACEFUL) {
SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket();
setDifficultyPacket.setDifficulty(Difficulty.EASY.ordinal());
session.sendUpstreamPacket(setDifficultyPacket);
}
}
private static String translateEntry(String key, String locale) { private static String translateEntry(String key, String locale) {
if (key.startsWith("%")) { if (key.startsWith("%")) {
// Bedrock will translate // Bedrock will translate

View file

@ -107,7 +107,7 @@ public class StatisticsUtils {
for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) { for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) {
if (entry.getKey() instanceof BreakItemStatistic statistic) { if (entry.getKey() instanceof BreakItemStatistic statistic) {
String item = itemRegistry.get(statistic.getId()).javaIdentifier(); Item item = itemRegistry.get(statistic.getId());
content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue());
} }
} }
@ -117,7 +117,7 @@ public class StatisticsUtils {
for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) { for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) {
if (entry.getKey() instanceof CraftItemStatistic statistic) { if (entry.getKey() instanceof CraftItemStatistic statistic) {
String item = itemRegistry.get(statistic.getId()).javaIdentifier(); Item item = itemRegistry.get(statistic.getId());
content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue());
} }
} }
@ -127,7 +127,7 @@ public class StatisticsUtils {
for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) { for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) {
if (entry.getKey() instanceof UseItemStatistic statistic) { if (entry.getKey() instanceof UseItemStatistic statistic) {
String item = itemRegistry.get(statistic.getId()).javaIdentifier(); Item item = itemRegistry.get(statistic.getId());
content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue());
} }
} }
@ -137,7 +137,7 @@ public class StatisticsUtils {
for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) { for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) {
if (entry.getKey() instanceof PickupItemStatistic statistic) { if (entry.getKey() instanceof PickupItemStatistic statistic) {
String item = itemRegistry.get(statistic.getId()).javaIdentifier(); Item item = itemRegistry.get(statistic.getId());
content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue());
} }
} }
@ -147,7 +147,7 @@ public class StatisticsUtils {
for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) { for (Object2IntMap.Entry<Statistic> entry : session.getStatistics().object2IntEntrySet()) {
if (entry.getKey() instanceof DropItemStatistic statistic) { if (entry.getKey() instanceof DropItemStatistic statistic) {
String item = itemRegistry.get(statistic.getId()).javaIdentifier(); Item item = itemRegistry.get(statistic.getId());
content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue());
} }
} }
@ -208,14 +208,8 @@ public class StatisticsUtils {
* @param language the language to search in * @param language the language to search in
* @return the full name of the item * @return the full name of the item
*/ */
private static String getItemTranslateKey(String item, String language) { private static String getItemTranslateKey(Item item, String language) {
item = item.replace("minecraft:", "item.minecraft."); return MinecraftLocale.getLocaleString(item.translationKey(), language);
String translatedItem = MinecraftLocale.getLocaleString(item, language);
if (translatedItem.equals(item)) {
// Didn't translate; must be a block
translatedItem = MinecraftLocale.getLocaleString(item.replace("item.", "block."), language);
}
return translatedItem;
} }
private static String translate(String keys, String locale) { private static String translate(String keys, String locale) {