1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

Merge remote-tracking branch 'upstream/master' into feature/1.21.6

# Conflicts:
#	README.md
#	core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
#	core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
#	core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
#	core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java
#	core/src/main/resources/bedrock/entity_identifiers.dat
#	gradle.properties
This commit is contained in:
onebeastchris
2025-06-17 17:42:57 +02:00
50 changed files with 20498 additions and 402 deletions

View File

@@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
## Supported Versions
Geyser is currently supporting Minecraft Bedrock 1.21.50 - 1.21.80 and Minecraft Java 1.21.6. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
Geyser is currently supporting Minecraft Bedrock 1.21.50 - 1.21.90 and Minecraft Java 1.21.6. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
## Setting Up
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.

View File

@@ -113,4 +113,14 @@ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent {
* @since 2.1.1
*/
public abstract boolean unregister(@NonNull UUID uuid);
/**
* Whether to forcefully disable vibrant visuals for joining clients.
* While vibrant visuals are nice to look at, they can cause issues with
* some resource packs.
*
* @param enabled Whether vibrant visuals are allowed. This is true by default.
* @since 2.7.2
*/
public abstract void allowVibrantVisuals(boolean enabled);
}

View File

@@ -479,6 +479,8 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::platformName));
metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale));
metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION));
metrics.addCustomChart(new Metrics.SimplePie("javaHaProxyProtocol", () -> String.valueOf(config.getRemote().isUseProxyProtocol())));
metrics.addCustomChart(new Metrics.SimplePie("bedrockHaProxyProtocol", () -> String.valueOf(config.getBedrock().isEnableProxyProtocol())));
metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : sessionManager.getAllSessions()) {

View File

@@ -99,6 +99,9 @@ public class CommandRegistry implements EventRegistrar {
private static final String GEYSER_ROOT_PERMISSION = "geyser.command";
public final static boolean STANDALONE_COMMAND_MANAGER = GeyserImpl.getInstance().getPlatformType() == PlatformType.STANDALONE ||
GeyserImpl.getInstance().getPlatformType() == PlatformType.VIAPROXY;
protected final GeyserImpl geyser;
private final CommandManager<GeyserCommandSource> cloud;
private final boolean applyRootPermission;
@@ -279,12 +282,15 @@ public class CommandRegistry implements EventRegistrar {
cloud.command(builder.handler(context -> {
GeyserCommandSource source = context.sender();
if (!source.hasPermission(help.permission())) {
// delegate if possible - otherwise we have nothing else to offer the user.
if (source.hasPermission(help.permission())) {
// Delegate to help if possible
help.execute(source);
} else if (STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) {
// If we are on an appropriate platform, forward the command to the backend
session.sendCommand(context.rawInput().input());
} else {
source.sendLocaleString(ExceptionHandlers.PERMISSION_FAIL_LANG_KEY);
return;
}
help.execute(source);
}));
}

View File

@@ -28,10 +28,12 @@ package org.geysermc.geyser.command;
import io.leangen.geantyref.GenericTypeReflector;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.exception.ArgumentParseException;
import org.incendo.cloud.exception.CommandExecutionException;
import org.incendo.cloud.exception.InvalidCommandSenderException;
@@ -71,36 +73,51 @@ final class ExceptionHandlers {
controller.clearHandlers();
registerExceptionHandler(InvalidSyntaxException.class,
(src, e) -> src.sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax()));
(ctx, e) -> ctx.sender().sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax()));
registerExceptionHandler(InvalidCommandSenderException.class, (src, e) -> {
registerExceptionHandler(InvalidCommandSenderException.class, (ctx, e) -> {
// We currently don't use cloud sender type requirements anywhere.
// This can be implemented better in the future if necessary.
Type type = e.requiredSenderTypes().iterator().next(); // just grab the first
String typeString = GenericTypeReflector.getTypeName(type);
src.sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), typeString);
ctx.sender().sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), typeString);
});
registerExceptionHandler(NoPermissionException.class, ExceptionHandlers::handleNoPermission);
registerExceptionHandler(NoSuchCommandException.class,
(src, e) -> src.sendLocaleString("geyser.command.not_found"));
(ctx, e) -> {
// Let backend server receive & handle the command
if (CommandRegistry.STANDALONE_COMMAND_MANAGER && ctx.sender() instanceof GeyserSession session) {
session.sendCommand(ctx.rawInput().input());
} else {
ctx.sender().sendLocaleString("geyser.command.not_found");
}
});
registerExceptionHandler(ArgumentParseException.class,
(src, e) -> src.sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage()));
(ctx, e) -> ctx.sender().sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage()));
registerExceptionHandler(CommandExecutionException.class,
(src, e) -> handleUnexpectedThrowable(src, e.getCause()));
(ctx, e) -> handleUnexpectedThrowable(ctx.sender(), e.getCause()));
registerExceptionHandler(Throwable.class,
(src, e) -> handleUnexpectedThrowable(src, e.getCause()));
(ctx, e) -> handleUnexpectedThrowable(ctx.sender(), e.getCause()));
}
private <E extends Throwable> void registerExceptionHandler(Class<E> type, BiConsumer<GeyserCommandSource, E> handler) {
controller.registerHandler(type, context -> handler.accept(context.context().sender(), context.exception()));
private <E extends Throwable> void registerExceptionHandler(Class<E> type, BiConsumer<CommandContext<GeyserCommandSource>, E> handler) {
controller.registerHandler(type, context -> handler.accept(context.context(), context.exception()));
}
private static void handleNoPermission(GeyserCommandSource source, NoPermissionException exception) {
private static void handleNoPermission(CommandContext<GeyserCommandSource> context, NoPermissionException exception) {
GeyserCommandSource source = context.sender();
// Let backend server receive & handle the command
if (CommandRegistry.STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) {
session.sendCommand(context.rawInput().input());
return;
}
// custom handling if the source can't use the command because of additional requirements
if (exception.permissionResult() instanceof GeyserPermission.Result result) {
if (result.meta() == GeyserPermission.Result.Meta.NOT_BEDROCK) {

View File

@@ -56,6 +56,8 @@ public class AreaEffectCloudEntity extends Entity {
dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_RADIUS, 3.0f);
dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_CHANGE_ON_PICKUP, Float.MIN_VALUE);
//noinspection deprecation - still needed for these to show up
dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_CHANGE_RATE, Float.MIN_VALUE);
setFlag(EntityFlag.FIRE_IMMUNE, true);
}

View File

@@ -25,6 +25,7 @@
package org.geysermc.geyser.entity.type;
import lombok.Getter;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
@@ -42,7 +43,12 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen
import java.util.Locale;
import java.util.UUID;
@Getter
public class ThrowableEggEntity extends ThrowableItemEntity {
// Used for egg break particles
private GeyserItemStack itemStack;
public ThrowableEggEntity(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);
}
@@ -58,6 +64,7 @@ public class ThrowableEggEntity extends ThrowableItemEntity {
GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue());
propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, getVariantOrFallback(session, stack));
updateBedrockEntityProperties();
this.itemStack = stack;
}
private static String getVariantOrFallback(GeyserSession session, GeyserItemStack stack) {
@@ -71,6 +78,6 @@ public class ThrowableEggEntity extends ThrowableItemEntity {
}
}
return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE.name().toLowerCase(Locale.ROOT);
return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE.toBedrock();
}
}

View File

@@ -54,13 +54,17 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity implements V
@Override
public void setBedrockVariant(BuiltInVariant variant) {
propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.name().toLowerCase(Locale.ROOT));
propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.toBedrock());
updateBedrockEntityProperties();
}
public enum BuiltInVariant implements VariantHolder.BuiltIn {
COLD,
TEMPERATE,
WARM
WARM;
public String toBedrock() {
return name().toLowerCase(Locale.ROOT);
}
}
}

View File

@@ -70,8 +70,11 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
*/
private final Map<UUID, OptionHolder> sessionPackOptionOverrides;
private final GeyserSession session;
public SessionLoadResourcePacksEventImpl(GeyserSession session) {
super(session);
this.session = session;
this.packs = new Object2ObjectLinkedOpenHashMap<>(Registries.RESOURCE_PACKS.get());
this.sessionPackOptionOverrides = new Object2ObjectOpenHashMap<>();
}
@@ -160,6 +163,11 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
return packs.remove(uuid) != null;
}
@Override
public void allowVibrantVisuals(boolean enabled) {
session.setAllowVibrantVisuals(enabled);
}
private void attemptRegisterOptions(@NonNull GeyserResourcePack pack, @Nullable ResourcePackOption<?>... options) {
if (options == null) {
return;

View File

@@ -139,8 +139,8 @@ public abstract class Inventory {
public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot);
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
if (slot > this.size) {
session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this);
if (slot < 0 || slot >= this.size) {
session.getGeyser().getLogger().debug("Tried to set an item out of bounds (slot was " + slot + ")! " + this);
return;
}
GeyserItemStack oldItem = items[slot];

View File

@@ -160,8 +160,10 @@ public final class InventoryHolder<T extends Inventory> {
@Override
public String toString() {
return "InventoryHolder[" +
"session=" + session + ", " +
"session=" + session.bedrockUsername() + ", " +
"inventory=" + inventory + ", " +
"translator=" + translator + ']';
"pending= " + pending + ", " +
"containerOpenAttempts=" + containerOpenAttempts + ", " +
"translator=" + translator.getClass().getSimpleName() + ']';
}
}

View File

@@ -46,7 +46,6 @@ public class StoredItemMappings {
private final ItemMapping carrotOnAStick;
private final ItemMapping compass;
private final ItemMapping crossbow;
private final ItemMapping egg;
private final ItemMapping glassBottle;
private final ItemMapping milkBucket;
private final ItemMapping powderSnowBucket;
@@ -65,7 +64,6 @@ public class StoredItemMappings {
this.carrotOnAStick = load(itemMappings, Items.CARROT_ON_A_STICK);
this.compass = load(itemMappings, Items.COMPASS);
this.crossbow = load(itemMappings, Items.CROSSBOW);
this.egg = load(itemMappings, Items.EGG);
this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE);
this.milkBucket = load(itemMappings, Items.MILK_BUCKET);
this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET);

View File

@@ -259,8 +259,6 @@ class CodecProcessor {
.updateSerializer(CreatePhotoPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(NpcRequestPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(PhotoInfoRequestPacket.class, ILLEGAL_SERIALIZER)
// Unused serverbound packets for featured servers, which is for some reason still occasionally sent
.updateSerializer(PurchaseReceiptPacket.class, IGNORED_SERIALIZER)
// Illegal unused serverbound packets that are deprecated
.updateSerializer(ClientCheatAbilityPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(CraftingEventPacket.class, ILLEGAL_SERIALIZER)
@@ -276,7 +274,6 @@ class CodecProcessor {
.updateSerializer(MapInfoRequestPacket.class, IGNORED_SERIALIZER)
.updateSerializer(SettingsCommandPacket.class, IGNORED_SERIALIZER)
.updateSerializer(AnvilDamagePacket.class, IGNORED_SERIALIZER)
.updateSerializer(RefreshEntitlementsPacket.class, IGNORED_SERIALIZER)
// Illegal when serverbound due to Geyser specific setup
.updateSerializer(InventoryContentPacket.class, INVENTORY_CONTENT_SERIALIZER_V748)
.updateSerializer(InventorySlotPacket.class, INVENTORY_SLOT_SERIALIZER_V748)
@@ -308,6 +305,11 @@ class CodecProcessor {
.updateSerializer(PlayerInputPacket.class, ILLEGAL_SERIALIZER);
}
if (!Boolean.getBoolean("Geyser.ReceiptPackets")) {
codecBuilder.updateSerializer(RefreshEntitlementsPacket.class, IGNORED_SERIALIZER);
codecBuilder.updateSerializer(PurchaseReceiptPacket.class, IGNORED_SERIALIZER);
}
return codecBuilder.build();
}

View File

@@ -27,8 +27,11 @@ package org.geysermc.geyser.network;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766;
import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec;
@@ -47,8 +50,8 @@ 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 BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v800.CODEC.toBuilder()
.minecraftVersion("1.21.80")
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v818.CODEC.toBuilder()
.minecraftVersion("1.21.90")
.build());
/**
@@ -63,15 +66,18 @@ public final class GameProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static {
//SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v766.CODEC.toBuilder()
// .minecraftVersion("1.21.50 - 1.21.51")
// .build()));
//SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v776.CODEC.toBuilder()
// .minecraftVersion("1.21.60 - 1.21.62")
// .build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v766.CODEC.toBuilder()
.minecraftVersion("1.21.50 - 1.21.51")
.build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v776.CODEC.toBuilder()
.minecraftVersion("1.21.60 - 1.21.62")
.build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v786.CODEC.toBuilder()
.minecraftVersion("1.21.70 - 1.21.73")
.build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v800.CODEC.toBuilder()
.minecraftVersion("1.21.80 - 1.21.84")
.build()));
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
}
@@ -107,8 +113,8 @@ public final class GameProtocol {
return session.protocolVersion() >= Bedrock_v800.CODEC.getProtocolVersion();
}
public static boolean is1_21_80(GeyserSession session) {
return session.protocolVersion() == Bedrock_v800.CODEC.getProtocolVersion();
public static boolean is1_21_90orHigher(GeyserSession session) {
return session.protocolVersion() >= Bedrock_v818.CODEC.getProtocolVersion();
}
/**

View File

@@ -208,6 +208,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries());
resourcePacksInfo.setVibrantVisualsForceDisabled(!session.isAllowVibrantVisuals());
resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks());
resourcePacksInfo.setWorldTemplateId(UUID.randomUUID());
@@ -241,11 +242,13 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
stackPacket.setGameVersion(session.getClientData().getGameVersion());
stackPacket.getResourcePacks().addAll(this.resourcePackLoadEvent.orderedPacks());
// Allows Vibrant Visuals to be toggled in the settings
stackPacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
// Enables 2025 Content Drop 2 features
stackPacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
if (session.isAllowVibrantVisuals() && !GameProtocol.is1_21_90orHigher(session)) {
stackPacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
}
if (GameProtocol.is1_21_80(session)) {
// Support happy ghasts in .80
stackPacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
// Enables the locator bar for 1.21.80 clients
stackPacket.getExperiments().add(new ExperimentData("locator_bar", true));
}

View File

@@ -73,17 +73,11 @@ public class GeyserUrlPackCodec extends UrlPackCodec {
@Override
protected GeyserResourcePack.@NonNull Builder createBuilder() {
if (this.fallback == null) {
try {
ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> {
if (throwable != null) {
throw new IllegalArgumentException(throwable);
} else if (pack != null) {
this.fallback = pack;
}
ResourcePackLoader.downloadPack(url, false)
.thenAccept(pack -> this.fallback = pack)
.exceptionally(throwable -> {
throw new IllegalStateException(throwable.getCause());
}).join(); // Needed to ensure that we don't attempt to read a pack before downloading/checking it
} catch (Exception e) {
throw new IllegalArgumentException("Failed to download pack from the url %s (%s)!".formatted(url, e.getMessage()));
}
}
return ResourcePackLoader.readPack(this);

View File

@@ -82,6 +82,11 @@ public class BlockRegistries {
*/
public static final MappedRegistry<String, Integer, Object2IntMap<String>> JAVA_IDENTIFIER_TO_ID = MappedRegistry.create(RegistryLoaders.empty(Object2IntOpenHashMap::new));
/**
* A registry containing non-vanilla block IDS.
*/
public static final SimpleRegistry<BitSet> NON_VANILLA_BLOCK_IDS = SimpleRegistry.create(RegistryLoaders.empty(BitSet::new));
/**
* A registry containing all the waterlogged blockstates.
* Properties.WATERLOGGED should not be relied on for two reasons:

View File

@@ -213,7 +213,6 @@ public final class Registries {
PARTICLES.load();
// load potion mixes later
//RECIPES.load();
RESOURCE_PACKS.load();
SOUNDS.load();
SOUND_LEVEL_EVENTS.load();
SOUND_TRANSLATORS.load();

View File

@@ -28,7 +28,7 @@ package org.geysermc.geyser.registry.loader;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent;
import org.geysermc.geyser.api.pack.PathPackCodec;
@@ -61,6 +61,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -288,10 +289,6 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
}
if (pathPackCodec == null) {
return; // Already warned about
}
GeyserResourcePack newPack = readPack(pathPackCodec.path()).build();
if (newPack.uuid().equals(packId)) {
if (packVersion.equals(newPack.manifest().header().version().toString())) {
@@ -337,13 +334,13 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
}
public static CompletableFuture<@Nullable PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException {
public static CompletableFuture<@NonNull PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException {
return CompletableFuture.supplyAsync(() -> {
Path path = WebUtils.downloadRemotePack(url, testing);
// Already warned about these above
if (path == null) {
return null;
Path path;
try {
path = WebUtils.downloadRemotePack(url, testing);
} catch (Throwable e) {
throw new CompletionException(e);
}
// Check if the pack is a .zip or .mcpack file

View File

@@ -303,6 +303,7 @@ public class CustomBlockRegistryPopulator {
BlockRegistries.JAVA_BLOCKS.registerWithAnyIndex(javaBlockState.stateGroupId(), block, Blocks.AIR);
BlockRegistries.JAVA_IDENTIFIER_TO_ID.register(javaId, stateRuntimeId);
BlockRegistries.NON_VANILLA_BLOCK_IDS.register(set -> set.set(stateRuntimeId));
// TODO register different collision types?
BoundingBox[] geyserCollisions = Arrays.stream(javaBlockState.collision())

View File

@@ -35,6 +35,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.item.type.Item;
@@ -71,7 +72,8 @@ public final class TagRegistryPopulator {
// ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()),
ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()),
// Not a typo, they're the same file
ObjectIntPair.of("1_21_70", Bedrock_v800.CODEC.getProtocolVersion())
ObjectIntPair.of("1_21_70", Bedrock_v800.CODEC.getProtocolVersion()),
ObjectIntPair.of("1_21_70", Bedrock_v818.CODEC.getProtocolVersion())
);
Type type = new TypeToken<Map<String, List<String>>>() {}.getType();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
* Copyright (c) 2024-2025 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
@@ -61,44 +61,42 @@ public class BelownameDisplaySlot extends DisplaySlot {
// remove is handled in #remove()
if (updateType == UpdateType.ADD) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
playerRegistered(player);
}
session.getEntityCache().forEachPlayerEntity(this::playerRegistered);
return;
}
if (updateType == UpdateType.UPDATE) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
session.getEntityCache().forEachPlayerEntity(player -> {
setBelowNameText(player, scoreFor(player.getUsername()));
}
});
updateType = UpdateType.NOTHING;
return;
}
for (var score : displayScores.values()) {
// we don't have to worry about a score not existing, because that's handled by both
// this method when an objective is added and addScore/playerRegistered.
// we only have to update them, if they have changed
// (or delete them, if the score no longer exists)
if (!score.shouldUpdate()) {
continue;
}
synchronized (displayScores) {
for (var score : displayScores.values()) {
// we don't have to worry about a score not existing, because that's handled by both
// this method when an objective is added and addScore/playerRegistered.
// we only have to update them, if they have changed
// (or delete them, if the score no longer exists)
if (!score.shouldUpdate()) {
continue;
}
if (score.referenceRemoved()) {
clearBelowNameText(score.player());
continue;
}
if (score.referenceRemoved()) {
clearBelowNameText(score.player());
continue;
}
score.markUpdated();
setBelowNameText(score.player(), score.reference());
score.markUpdated();
setBelowNameText(score.player(), score.reference());
}
}
}
@Override
public void remove() {
updateType = UpdateType.REMOVE;
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
clearBelowNameText(player);
}
session.getEntityCache().forEachPlayerEntity(this::clearBelowNameText);
}
@Override
@@ -119,7 +117,9 @@ public class BelownameDisplaySlot extends DisplaySlot {
@Override
public void playerRemoved(PlayerEntity player) {
displayScores.remove(player.getGeyserId());
synchronized (displayScores) {
displayScores.remove(player.getGeyserId());
}
}
private void addDisplayScore(ScoreReference reference) {
@@ -131,7 +131,9 @@ public class BelownameDisplaySlot extends DisplaySlot {
private BelownameDisplayScore addDisplayScore(PlayerEntity player, ScoreReference reference) {
var score = new BelownameDisplayScore(this, objective.getScoreboard().nextId(), reference, player);
displayScores.put(player.getGeyserId(), score);
synchronized (displayScores) {
displayScores.put(player.getGeyserId(), score);
}
return score;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
* Copyright (c) 2024-2025 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
@@ -26,7 +26,6 @@
package org.geysermc.geyser.scoreboard.display.slot;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
@@ -41,8 +40,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public class PlayerlistDisplaySlot extends DisplaySlot {
private final Long2ObjectMap<PlayerlistDisplayScore> displayScores =
Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
private final Long2ObjectMap<PlayerlistDisplayScore> displayScores = new Long2ObjectOpenHashMap<>();
private final List<PlayerlistDisplayScore> removedScores = Collections.synchronizedList(new ArrayList<>());
public PlayerlistDisplaySlot(GeyserSession session, Objective objective) {
@@ -71,35 +69,37 @@ public class PlayerlistDisplaySlot extends DisplaySlot {
removedScores.clear();
}
for (var score : displayScores.values()) {
if (score.referenceRemoved()) {
ScoreInfo cachedInfo = score.cachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
synchronized (displayScores) {
for (var score : displayScores.values()) {
if (score.referenceRemoved()) {
ScoreInfo cachedInfo = score.cachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
}
continue;
}
continue;
}
//todo does an animated title exist on tab?
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
//todo does an animated title exist on tab?
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.cachedInfo());
}
if (add) {
addScores.add(score.cachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && objectiveNothing) {
removeScores.add(score.cachedInfo());
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && objectiveNothing) {
removeScores.add(score.cachedInfo());
}
}
}
@@ -124,16 +124,17 @@ public class PlayerlistDisplaySlot extends DisplaySlot {
players.add(selfPlayer);
}
for (PlayerEntity player : players) {
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
synchronized (displayScores) {
for (PlayerEntity player : players) {
var score = new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
}
}
}
private void registerExisting() {
playerRegistered(session.getPlayerEntity());
session.getEntityCache().getAllPlayerEntities().forEach(this::playerRegistered);
session.getEntityCache().forEachPlayerEntity(this::playerRegistered);
}
@Override
@@ -142,14 +143,20 @@ public class PlayerlistDisplaySlot extends DisplaySlot {
if (reference == null) {
return;
}
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
var score = new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
synchronized (displayScores) {
displayScores.put(player.getGeyserId(), score);
}
}
@Override
public void playerRemoved(PlayerEntity player) {
var score = displayScores.remove(player.getGeyserId());
PlayerlistDisplayScore score;
synchronized (displayScores) {
score = displayScores.remove(player.getGeyserId());
}
if (score == null) {
return;
}

View File

@@ -49,6 +49,11 @@ public final class SidebarDisplaySlot extends DisplaySlot {
.thenComparing(ScoreReference::name, String.CASE_INSENSITIVE_ORDER);
private List<SidebarDisplayScore> displayScores = new ArrayList<>(SCORE_DISPLAY_LIMIT);
/// A copy of displayScores which can be modified by the render0 method for its calculation of the scores to
/// display. This was done to not add locks to displayScores, as that list can be used by multiple threads because
/// of the setTeamFor method. Additionally, there is a brief period in render0 where scores are not present in the
/// list which could lead to bugs that are hard to reproduce.
private final List<SidebarDisplayScore> displayScoresCopy = new ArrayList<>(SCORE_DISPLAY_LIMIT);
public SidebarDisplaySlot(GeyserSession session, Objective objective, ScoreboardPosition position) {
super(session, objective, position);
@@ -56,8 +61,8 @@ public final class SidebarDisplaySlot extends DisplaySlot {
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
// while one could argue that we may not have to do this fancy Java filter when there are fewer scores than the
// line limit, we would lose the correct order of the scores if we don't
// While one could argue that we may not have to do this fancy Java filter when there are fewer scores than the
// line limit, it is also responsible for making sure that the scores are in the correct order.
var newDisplayScores =
objective.getScores().values().stream()
.filter(score -> !score.hidden())
@@ -65,7 +70,7 @@ public final class SidebarDisplaySlot extends DisplaySlot {
.limit(SCORE_DISPLAY_LIMIT)
.map(reference -> {
// pretty much an ArrayList#remove
var iterator = this.displayScores.iterator();
var iterator = displayScoresCopy.iterator();
while (iterator.hasNext()) {
var score = iterator.next();
if (score.name().equals(reference.name())) {
@@ -78,32 +83,42 @@ public final class SidebarDisplaySlot extends DisplaySlot {
return new SidebarDisplayScore(this, objective.getScoreboard().nextId(), reference);
}).collect(Collectors.toList());
// in newDisplayScores we removed the items that were already present from displayScores,
// meaning that the items that remain are items that are no longer displayed
for (var score : this.displayScores) {
// Make sure that we set the displayScores as early as possible, because setTeamFor relies on these potential
// changes. And even if no scores were added or removed, the order could've changed.
displayScores = newDisplayScores;
// In newDisplayScores we removed the items that were already present from displayScoresCopy,
// meaning that the items that remain are items that are no longer displayed.
for (var score : displayScoresCopy) {
removeScores.add(score.cachedInfo());
}
// preserves the new order
this.displayScores = newDisplayScores;
// The newDisplayScores have to be copied over to displayScoresCopy for the next render.
for (int i = 0; i < newDisplayScores.size(); i++) {
if (i < displayScoresCopy.size()) {
displayScoresCopy.set(i, newDisplayScores.get(i));
} else {
displayScoresCopy.add(newDisplayScores.get(i));
}
}
// fixes ordering issues with multiple entries with same score
if (!this.displayScores.isEmpty()) {
if (!displayScores.isEmpty()) {
SidebarDisplayScore lastScore = null;
int count = 0;
for (var score : this.displayScores) {
for (var score : displayScores) {
if (lastScore == null) {
lastScore = score;
continue;
}
if (score.score() == lastScore.score()) {
// something to keep in mind is that Bedrock doesn't support some legacy color codes and adds some
// codes as well, so if the line limit is every increased keep that in mind
// Bedrock doesn't support some legacy color codes and adds some codes as well.
// Keep this in mind if the line limit is ever increased.
if (count == 0) {
lastScore.order(ChatColor.styleOrder(count++));
lastScore.order(ChatColor.colorDisplayOrder(count++));
}
score.order(ChatColor.styleOrder(count++));
score.order(ChatColor.colorDisplayOrder(count++));
} else {
if (count == 0) {
lastScore.order(null);
@@ -121,7 +136,7 @@ public final class SidebarDisplaySlot extends DisplaySlot {
boolean objectiveAdd = updateType == UpdateType.ADD;
boolean objectiveUpdate = updateType == UpdateType.UPDATE;
for (var score : this.displayScores) {
for (var score : displayScores) {
Team team = score.team();
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();

View File

@@ -715,6 +715,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private int stepTicks = 0;
@Setter
private boolean allowVibrantVisuals = true;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop tickEventLoop) {
this.geyser = geyser;
this.upstream = new UpstreamSession(bedrockServerSession);
@@ -1342,7 +1345,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
private void switchPose(boolean value, EntityFlag flag, Pose pose) {
this.pose = value ? pose : Pose.STANDING;
this.pose = value ? pose : this.pose == pose ? Pose.STANDING : this.pose;
playerEntity.setDimensionsFromPose(this.pose);
playerEntity.setFlag(flag, value);
playerEntity.updateBedrockMetadata();
@@ -1693,10 +1696,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// Needed for certain molang queries used in blocks and items
startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true));
// Allows Vibrant Visuals to appear in the settings menu
startGamePacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
if (allowVibrantVisuals && !GameProtocol.is1_21_90orHigher(this)) {
startGamePacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
}
// Enables 2025 Content Drop 2 features
startGamePacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
if (GameProtocol.is1_21_80(this)) {
startGamePacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
// Enables the locator bar for 1.21.80 clients
startGamePacket.getExperiments().add(new ExperimentData("locator_bar", true));
}
@@ -1717,6 +1722,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setServerId("");
startGamePacket.setWorldId("");
startGamePacket.setScenarioId("");
startGamePacket.setOwnerId("");
upstream.sendPacket(startGamePacket);
}
@@ -1759,7 +1765,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Queue a packet to be sent to player.
*
* @param packet the bedrock packet from the NukkitX protocol lib
* @param packet the bedrock packet from the Cloudburst protocol lib
*/
public void sendUpstreamPacket(BedrockPacket packet) {
upstream.sendPacket(packet);
@@ -1768,7 +1774,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Send a packet immediately to the player.
*
* @param packet the bedrock packet from the NukkitX protocol lib
* @param packet the bedrock packet from the Cloudburst protocol lib
*/
public void sendUpstreamPacketImmediately(BedrockPacket packet) {
upstream.sendPacketImmediately(packet);
@@ -2266,6 +2272,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Override
public int ping() {
// Can otherwise cause issues if the player isn't logged in yet / already left
if (!getUpstream().isInitialized() || getUpstream().isClosed()) {
return 0;
}
RakSessionCodec rakSessionCodec = ((RakChildChannel) getUpstream().getSession().getPeer().getChannel()).rakPipeline().get(RakSessionCodec.class);
return (int) Math.floor(rakSessionCodec.getPing());
}

View File

@@ -55,12 +55,12 @@ import java.util.UUID;
public class GeyserSessionAdapter extends SessionAdapter {
private final GeyserImpl geyser;
private final GeyserSession geyserSession;
private final GeyserSession session;
private final boolean floodgate;
private final String locale;
public GeyserSessionAdapter(GeyserSession session) {
this.geyserSession = session;
this.session = session;
this.floodgate = session.remoteServer().authType() == AuthType.FLOODGATE;
this.geyser = GeyserImpl.getInstance();
this.locale = session.locale();
@@ -69,7 +69,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
@Override
public void packetSending(PacketSendingEvent event) {
if (event.getPacket() instanceof ClientIntentionPacket intentionPacket) {
BedrockClientData clientData = geyserSession.getClientData();
BedrockClientData clientData = session.getClientData();
String addressSuffix;
if (floodgate) {
@@ -79,7 +79,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
FloodgateCipher cipher = geyser.getCipher();
String bedrockAddress = geyserSession.getUpstream().getAddress().getAddress().getHostAddress();
String bedrockAddress = session.getUpstream().getAddress().getAddress().getHostAddress();
// both BungeeCord and Velocity remove the IPv6 scope (if there is one) for Spigot
int ipv6ScopeIndex = bedrockAddress.indexOf('%');
if (ipv6ScopeIndex != -1) {
@@ -88,8 +88,8 @@ public class GeyserSessionAdapter extends SessionAdapter {
encryptedData = cipher.encryptFromString(BedrockData.of(
clientData.getGameVersion(),
geyserSession.bedrockUsername(),
geyserSession.xuid(),
session.bedrockUsername(),
session.xuid(),
clientData.getDeviceOs().ordinal(),
clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(),
@@ -100,7 +100,7 @@ public class GeyserSessionAdapter extends SessionAdapter {
).toString());
} catch (Exception e) {
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
geyserSession.disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encrypt_fail", locale));
session.disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encrypt_fail", locale));
return;
}
@@ -122,38 +122,38 @@ public class GeyserSessionAdapter extends SessionAdapter {
@Override
public void connected(ConnectedEvent event) {
geyserSession.loggingIn = false;
geyserSession.loggedIn = true;
session.loggingIn = false;
session.loggedIn = true;
if (geyserSession.getDownstream().getSession() instanceof LocalSession) {
if (session.getDownstream().getSession() instanceof LocalSession) {
// Connected directly to the server
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect_internal",
geyserSession.bedrockUsername(), geyserSession.getProtocol().getProfile().getName()));
session.bedrockUsername(), session.getProtocol().getProfile().getName()));
} else {
// Connected to an IP address
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect",
geyserSession.bedrockUsername(), geyserSession.getProtocol().getProfile().getName(), geyserSession.remoteServer().address()));
session.bedrockUsername(), session.getProtocol().getProfile().getName(), session.remoteServer().address()));
}
UUID uuid = geyserSession.getProtocol().getProfile().getId();
UUID uuid = session.getProtocol().getProfile().getId();
if (uuid == null) {
// Set what our UUID *probably* is going to be
if (geyserSession.remoteServer().authType() == AuthType.FLOODGATE) {
uuid = new UUID(0, Long.parseLong(geyserSession.xuid()));
if (session.remoteServer().authType() == AuthType.FLOODGATE) {
uuid = new UUID(0, Long.parseLong(session.xuid()));
} else {
uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + geyserSession.getProtocol().getProfile().getName()).getBytes(StandardCharsets.UTF_8));
uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + session.getProtocol().getProfile().getName()).getBytes(StandardCharsets.UTF_8));
}
}
geyserSession.getPlayerEntity().setUuid(uuid);
geyserSession.getPlayerEntity().setUsername(geyserSession.getProtocol().getProfile().getName());
session.getPlayerEntity().setUuid(uuid);
session.getPlayerEntity().setUsername(session.getProtocol().getProfile().getName());
String locale = geyserSession.getClientData().getLanguageCode();
String locale = session.getClientData().getLanguageCode();
// Let the user know there locale may take some time to download
// as it has to be extracted from a JAR
if (locale.equalsIgnoreCase("en_us") && !MinecraftLocale.LOCALE_MAPPINGS.containsKey("en_us")) {
// This should probably be left hardcoded as it will only show for en_us clients
geyserSession.sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
session.sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
}
// Download and load the language for the player
@@ -162,12 +162,12 @@ public class GeyserSessionAdapter extends SessionAdapter {
@Override
public void disconnected(DisconnectedEvent event) {
geyserSession.loggingIn = false;
session.loggingIn = false;
String disconnectMessage, customDisconnectMessage = null;
Throwable cause = event.getCause();
if (cause instanceof UnexpectedEncryptionException) {
if (geyserSession.remoteServer().authType() != AuthType.FLOODGATE) {
if (session.remoteServer().authType() != AuthType.FLOODGATE) {
// Server expects online mode
customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale);
// Explain that they may be looking for Floodgate.
@@ -192,10 +192,10 @@ public class GeyserSessionAdapter extends SessionAdapter {
// Use our helpful disconnect message whenever possible
disconnectMessage = customDisconnectMessage != null ? customDisconnectMessage : MessageTranslator.convertMessage(event.getReason());;
if (geyserSession.getDownstream().getSession() instanceof LocalSession) {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect_internal", geyserSession.bedrockUsername(), disconnectMessage));
if (session.getDownstream().getSession() instanceof LocalSession) {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect_internal", session.bedrockUsername(), disconnectMessage));
} else {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", geyserSession.bedrockUsername(), geyserSession.remoteServer().address(), disconnectMessage));
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", session.bedrockUsername(), session.remoteServer().address(), disconnectMessage));
}
if (cause != null) {
if (cause.getMessage() != null) {
@@ -207,24 +207,24 @@ public class GeyserSessionAdapter extends SessionAdapter {
cause.printStackTrace();
}
}
if ((!geyserSession.isClosed() && geyserSession.loggedIn) || cause != null) {
if ((!session.isClosed() && session.loggedIn) || cause != null) {
// GeyserSession is disconnected via session.disconnect() called indirectly be the server
// This needs to be "initiated" here when there is an exception, but also when the Netty connection
// is closed without a disconnect packet - in this case, closed will still be false, but loggedIn
// will also be true as GeyserSession#disconnect will not have been called.
if (customDisconnectMessage != null) {
geyserSession.disconnect(customDisconnectMessage);
session.disconnect(customDisconnectMessage);
} else {
geyserSession.disconnect(event.getReason());
session.disconnect(event.getReason());
}
}
geyserSession.loggedIn = false;
session.loggedIn = false;
}
@Override
public void packetReceived(Session session, Packet packet) {
Registries.JAVA_PACKET_TRANSLATORS.translate(packet.getClass(), packet, geyserSession, true);
Registries.JAVA_PACKET_TRANSLATORS.translate(packet.getClass(), packet, this.session, true);
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
* Copyright (c) 2019-2025 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
@@ -32,11 +32,11 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import lombok.Getter;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.Tickable;
@@ -136,10 +136,12 @@ public class EntityCache {
}
public void addPlayerEntity(PlayerEntity entity) {
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
boolean exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
if (exists) {
return;
synchronized (playerEntities) {
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
boolean exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
if (exists) {
return;
}
}
// notify scoreboard for new entity
@@ -148,21 +150,29 @@ public class EntityCache {
}
public PlayerEntity getPlayerEntity(UUID uuid) {
return playerEntities.get(uuid);
synchronized (playerEntities) {
return playerEntities.get(uuid);
}
}
public List<PlayerEntity> getPlayersByName(String name) {
var list = new ArrayList<PlayerEntity>();
for (PlayerEntity player : playerEntities.values()) {
if (name.equals(player.getUsername())) {
list.add(player);
synchronized (playerEntities) {
for (PlayerEntity player : playerEntities.values()) {
if (name.equals(player.getUsername())) {
list.add(player);
}
}
}
return list;
}
public PlayerEntity removePlayerEntity(UUID uuid) {
var player = playerEntities.remove(uuid);
PlayerEntity player;
synchronized (playerEntities) {
player = playerEntities.remove(uuid);
}
if (player != null) {
// notify scoreboard
session.getWorldCache().getScoreboard().playerRemoved(player);
@@ -170,12 +180,20 @@ public class EntityCache {
return player;
}
public Collection<PlayerEntity> getAllPlayerEntities() {
return playerEntities.values();
/**
* Run a specific bit of code for each cached player entity.
* As usual with synchronized, try to minimize the amount of work you because you block the PlayerList collection.
*/
public void forEachPlayerEntity(Consumer<PlayerEntity> player) {
synchronized (playerEntities) {
playerEntities.values().forEach(player);
}
}
public void removeAllPlayerEntities() {
playerEntities.clear();
synchronized (playerEntities) {
playerEntities.clear();
}
}
public void addBossBar(UUID uuid, BossBar bossBar) {

View File

@@ -139,7 +139,8 @@ public final class InputCache {
case PERSIST_SNEAK -> {
// Ignoring start/stop sneaking while in scaffolding on purpose to ensure
// that we don't spam both cases for every block we went down
if (session.getPlayerEntity().isInsideScaffolding()) {
// Consoles would also send persist sneak; but don't send the descend_block flag
if (inputMode == InputMode.TOUCH && session.getPlayerEntity().isInsideScaffolding()) {
return authInputData.contains(PlayerAuthInputData.DESCEND_BLOCK) &&
authInputData.contains(PlayerAuthInputData.SNEAK_CURRENT_RAW);
}

View File

@@ -91,7 +91,8 @@ public final class GeyserHolderSet<T> {
*/
public static <T> GeyserHolderSet<T> fromHolderSet(JavaRegistryKey<T> registry, @NonNull HolderSet holderSet) {
// MCPL HolderSets don't have to support inline elements... for now (TODO CHECK ME)
return new GeyserHolderSet<>(registry, new Tag<>(registry, holderSet.getLocation()), holderSet.getHolders(), null);
Tag<T> tag = holderSet.getLocation() == null ? null : new Tag<>(registry, holderSet.getLocation());
return new GeyserHolderSet<>(registry, tag, holderSet.getHolders(), null);
}
/**

View File

@@ -87,7 +87,7 @@ public class ChatColor {
return string;
}
public static String styleOrder(int index) {
public static String colorDisplayOrder(int index) {
// https://bugs.mojang.com/browse/MCPE-41729
// strikethrough and underlined do not exist on Bedrock
return switch (index) {

View File

@@ -47,15 +47,20 @@ public class MinecraftLocale {
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
private static final Path LOCALE_FOLDER = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales");
// Check instance availability to avoid exception during testing
private static final boolean IN_INSTANCE = GeyserImpl.getInstance() != null;
private static final Path LOCALE_FOLDER = (IN_INSTANCE) ? GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales") : null;
static {
try {
// Create the locales folder
Files.createDirectories(LOCALE_FOLDER);
Files.createDirectories(LOCALE_FOLDER.resolve("overrides"));
} catch (IOException exception) {
throw new RuntimeException("Unable to create locale folders! " + exception.getMessage());
if (IN_INSTANCE) {
try {
// Create the locales folder
Files.createDirectories(LOCALE_FOLDER);
Files.createDirectories(LOCALE_FOLDER.resolve("overrides"));
} catch (IOException exception) {
throw new RuntimeException("Unable to create locale folders! " + exception.getMessage());
}
}
}
@@ -266,4 +271,4 @@ public class MinecraftLocale {
}
return result.toString();
}
}
}

View File

@@ -41,6 +41,7 @@ import java.util.regex.Pattern;
public class MinecraftTranslationRegistry extends TranslatableComponentRenderer<String> {
private final Pattern stringReplacement = Pattern.compile("%s");
private final Pattern positionalStringReplacement = Pattern.compile("%([0-9]+)\\$s");
private final Pattern escapeBraces = Pattern.compile("\\{+['{]+\\{+|\\{+");
// Exists to maintain compatibility with Velocity's older Adventure version
@Override
@@ -66,14 +67,19 @@ public class MinecraftTranslationRegistry extends TranslatableComponentRenderer<
// replace single quote instances which get lost in MessageFormat otherwise
localeString = localeString.replace("'", "''");
// Wrap all curly brackets with single quote inserts - fixes https://github.com/GeyserMC/Geyser/issues/4662
localeString = localeString.replace("{", "'{")
.replace("}", "'}");
// Replace the `%s` with numbered inserts `{0}`
Pattern p = stringReplacement;
// Escape all left curly brackets with single quote - fixes https://github.com/GeyserMC/Geyser/issues/4662
Pattern p = escapeBraces;
Matcher m = p.matcher(localeString);
StringBuilder sb = new StringBuilder();
while (m.find()) {
m.appendReplacement(sb, "'" + m.group() + "'");
}
m.appendTail(sb);
// Replace the `%s` with numbered inserts `{0}`
p = stringReplacement;
m = p.matcher(sb.toString());
sb = new StringBuilder();
int i = 0;
while (m.find()) {
m.appendReplacement(sb, "{" + (i++) + "}");

View File

@@ -44,7 +44,6 @@ import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.util.InventoryUtils;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSetBeaconPacket;
@@ -60,12 +59,9 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator<
}
@Override
public void openInventory(GeyserSession session, Container container) {
if (!container.isUsingRealBlock()) {
InventoryUtils.closeInventory(session, container.getJavaId(), false);
return;
}
super.openInventory(session, container);
public boolean prepareInventory(GeyserSession session, Container container) {
// Virtual beacon inventories aren't possible - we don't want to spawn a whole pyramid!
return super.canUseRealBlock(session, container);
}
}, UIInventoryUpdater.INSTANCE);
}

View File

@@ -27,7 +27,6 @@ package org.geysermc.geyser.translator.protocol.bedrock;
import org.cloudburstmc.protocol.bedrock.packet.CommandRequestPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@@ -44,10 +43,12 @@ public class BedrockCommandRequestTranslator extends PacketTranslator<CommandReq
}
static void handleCommand(GeyserSession session, String command) {
if (session.getGeyser().getPlatformType() == PlatformType.STANDALONE ||
session.getGeyser().getPlatformType() == PlatformType.VIAPROXY) {
// try to handle the command within the standalone/viaproxy command manager
if (MessageTranslator.isTooLong(command, session)) {
return;
}
if (CommandRegistry.STANDALONE_COMMAND_MANAGER) {
// try to handle the command within the standalone/viaproxy command manager
String[] args = command.split(" ");
if (args.length > 0) {
String root = args[0];
@@ -55,15 +56,13 @@ public class BedrockCommandRequestTranslator extends PacketTranslator<CommandReq
CommandRegistry registry = GeyserImpl.getInstance().commandRegistry();
if (registry.rootCommands().contains(root)) {
registry.runCommand(session, command);
return; // don't pass the command to the java server
// don't pass the command to the java server here
// will pass it through later if the user lacks permission
return;
}
}
}
if (MessageTranslator.isTooLong(command, session)) {
return;
}
session.sendCommand(command);
}
}

View File

@@ -33,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
@@ -101,7 +102,7 @@ final class BedrockBlockActions {
SkullCache.Skull skull = session.getSkullCache().getSkulls().get(vector);
session.setBlockBreakStartTime(0);
if (blockStateOverride != null || customItem != null || (skull != null && skull.getBlockDefinition() != null)) {
if (BlockRegistries.NON_VANILLA_BLOCK_IDS.get().get(blockState) || blockStateOverride != null || customItem != null || (skull != null && skull.getBlockDefinition() != null)) {
session.setBlockBreakStartTime(System.currentTimeMillis());
}
startBreak.setData((int) (65535 / breakTime));

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
* Copyright (c) 2024-2025 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
@@ -29,7 +29,6 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.MultiRec
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@@ -59,9 +58,9 @@ public class JavaFinishConfigurationTranslator extends PacketTranslator<Clientbo
public void translate(GeyserSession session, ClientboundFinishConfigurationPacket packet) {
// Clear the player list, as on Java the player list is cleared after transitioning from config to play phase
List<PlayerListPacket.Entry> entries = new ArrayList<>();
for (PlayerEntity otherEntity : session.getEntityCache().getAllPlayerEntities()) {
entries.add(new PlayerListPacket.Entry(otherEntity.getTabListUuid()));
}
session.getEntityCache().forEachPlayerEntity(otherPlayer -> {
entries.add(new PlayerListPacket.Entry(otherPlayer.getTabListUuid()));
});
PlayerListUtils.batchSendPlayerList(session, entries, PlayerListPacket.Action.REMOVE);
session.getEntityCache().removeAllPlayerEntities();

View File

@@ -78,6 +78,11 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
// Remove extra hearts, hunger, etc.
entity.resetAttributes();
entity.resetMetadata();
// Reset inventories; just in case. Might resolve some issues where inventories get stuck?
session.setInventoryHolder(null);
session.setPendingOrCurrentBedrockInventoryId(-1);
session.setClosingInventory(false);
}
session.setDimensionType(newDimension);

View File

@@ -44,12 +44,14 @@ import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.EvokerFangsEntity;
import org.geysermc.geyser.entity.type.FishingHookEntity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.ThrowableEggEntity;
import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity;
import org.geysermc.geyser.entity.type.living.monster.CreakingEntity;
import org.geysermc.geyser.entity.type.living.monster.WardenEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.InventoryUtils;
@@ -99,10 +101,10 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
case LIVING_DEATH:
entityEventPacket.setType(EntityEventType.DEATH);
if (entity.getDefinition() == EntityDefinitions.EGG) {
if (entity instanceof ThrowableEggEntity egg) {
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(ParticleType.ICON_CRACK);
particlePacket.setData(session.getItemMappings().getStoredItems().egg().getBedrockDefinition().getRuntimeId() << 16);
particlePacket.setData(ItemTranslator.getBedrockItemDefinition(session, egg.getItemStack()).getRuntimeId() << 16);
particlePacket.setPosition(entity.getPosition());
for (int i = 0; i < 6; i++) {
session.sendUpstreamPacket(particlePacket);

View File

@@ -70,10 +70,10 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
Inventory inventory = holder.inventory();
int slot = packet.getSlot();
if (slot >= inventory.getSize()) {
if (slot < 0 || slot >= inventory.getSize()) {
GeyserLogger logger = session.getGeyser().getLogger();
logger.warning("ClientboundContainerSetSlotPacket sent to " + session.bedrockUsername()
+ " that exceeds inventory size!");
logger.warning("Slot of ClientboundContainerSetSlotPacket sent to " + session.bedrockUsername()
+ " is out of bounds! Was: " + slot + " for container: " + packet.getContainerId());
if (logger.isDebug()) {
logger.debug(packet.toString());
logger.debug(inventory.toString());

View File

@@ -508,7 +508,7 @@ public class MessageTranslator {
} else {
String translateKey = map.getString("translate", null);
if (translateKey != null) {
String fallback = map.getString("fallback", "");
String fallback = map.getString("fallback", null);
List<Component> args = new ArrayList<>();
Object with = map.get("with");

View File

@@ -29,6 +29,8 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode;
import org.cloudburstmc.protocol.bedrock.data.auth.AuthPayload;
import org.cloudburstmc.protocol.bedrock.data.auth.CertificateChainPayload;
import org.cloudburstmc.protocol.bedrock.packet.LoginPacket;
import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket;
import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult;
@@ -58,14 +60,14 @@ public class LoginEncryptionUtils {
private static boolean HAS_SENT_ENCRYPTION_MESSAGE = false;
public static void encryptPlayerConnection(GeyserSession session, LoginPacket loginPacket) {
encryptConnectionWithCert(session, loginPacket.getExtra(), loginPacket.getChain());
encryptConnectionWithCert(session, loginPacket.getAuthPayload(), loginPacket.getClientJwt());
}
private static void encryptConnectionWithCert(GeyserSession session, String clientData, List<String> certChainData) {
private static void encryptConnectionWithCert(GeyserSession session, AuthPayload authPayload, String jwt) {
try {
GeyserImpl geyser = session.getGeyser();
ChainValidationResult result = EncryptionUtils.validateChain(certChainData);
ChainValidationResult result = EncryptionUtils.validatePayload(authPayload);
geyser.getLogger().debug(String.format("Is player data signed? %s", result.signed()));
@@ -75,19 +77,25 @@ public class LoginEncryptionUtils {
}
IdentityData extraData = result.identityClaims().extraData;
// TODO!!! identity won't persist
session.setAuthData(new AuthData(extraData.displayName, extraData.identity, extraData.xuid));
session.setCertChainData(certChainData);
if (authPayload instanceof CertificateChainPayload certificateChainPayload) {
session.setCertChainData(certificateChainPayload.getChain());
} else {
GeyserImpl.getInstance().getLogger().warning("Received new auth payload!");
session.setCertChainData(List.of());
}
PublicKey identityPublicKey = result.identityClaims().parsedIdentityPublicKey();
byte[] clientDataPayload = EncryptionUtils.verifyClientData(clientData, identityPublicKey);
byte[] clientDataPayload = EncryptionUtils.verifyClientData(jwt, identityPublicKey);
if (clientDataPayload == null) {
throw new IllegalStateException("Client data isn't signed by the given chain data");
}
JsonNode clientDataJson = JSON_MAPPER.readTree(clientDataPayload);
BedrockClientData data = JSON_MAPPER.convertValue(clientDataJson, BedrockClientData.class);
data.setOriginalString(clientData);
data.setOriginalString(jwt);
session.setClientData(data);
try {

View File

@@ -26,6 +26,7 @@
package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
@@ -110,7 +111,7 @@ public class WebUtils {
* @return Path to the downloaded pack file, or null if it was unable to be loaded
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public static @Nullable Path downloadRemotePack(String url, boolean force) {
public static @NonNull Path downloadRemotePack(String url, boolean force) throws IOException {
GeyserLogger logger = GeyserImpl.getInstance().getLogger();
try {
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
@@ -137,6 +138,9 @@ public class WebUtils {
"Bedrock Edition only supports the application/zip content type.", url, type));
}
// Ensure remote pack cache dir exists
Files.createDirectories(REMOTE_PACK_CACHE);
Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata");
Path downloadLocation;
@@ -190,23 +194,20 @@ public class WebUtils {
));
packMetadata.toFile().setLastModified(System.currentTimeMillis());
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage());
Files.delete(packMetadata);
Files.delete(downloadLocation);
return null;
throw new IllegalStateException("Failed to write cached pack metadata: " + e.getMessage());
}
downloadLocation.toFile().setLastModified(System.currentTimeMillis());
logger.debug("Successfully downloaded remote pack! URL: %s (to: %s )".formatted(url, downloadLocation));
return downloadLocation;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s! ".formatted(url));
throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s".formatted(url));
} catch (SocketTimeoutException | ConnectException e) {
logger.error("Unable to download pack from url %s due to network error! ( %s )".formatted(url, e.getMessage()));
logger.debug(e);
} catch (IOException e) {
throw new IllegalStateException("Unable to download and save remote resource pack from: %s ( %s )!".formatted(url, e.getMessage()));
throw new IllegalArgumentException("Unable to download pack from url %s due to network error ( %s )".formatted(url, e.toString()));
}
return null;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -50,9 +50,9 @@
"spawns_warm_variant_frogs"
]
},
"snowy_slopes": {
"temperature": -0.3,
"downfall": 0.9,
"grove": {
"temperature": -0.2,
"downfall": 0.8,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
@@ -70,17 +70,17 @@
"id": null,
"tags": [
"mountains",
"cold",
"monster",
"overworld",
"frozen",
"grove",
"spawns_cold_variant_farm_animals",
"spawns_cold_variant_frogs",
"spawns_snow_foxes",
"spawns_white_rabbits",
"snowy_slopes",
"spawns_cold_variant_farm_animals"
"spawns_white_rabbits"
]
},
"jagged_peaks": {
"frozen_peaks": {
"temperature": -0.7,
"downfall": 0.9,
"redSporeDensity": 0.0,
@@ -103,7 +103,7 @@
"monster",
"overworld",
"frozen",
"jagged_peaks",
"frozen_peaks",
"spawns_cold_variant_farm_animals",
"spawns_cold_variant_frogs",
"spawns_snow_foxes",
@@ -338,6 +338,32 @@
"has_structure_trail_ruins"
]
},
"meadow": {
"temperature": 0.3,
"downfall": 0.8,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"mountains",
"monster",
"overworld",
"meadow",
"bee_habitat"
]
},
"jungle_mutated": {
"temperature": 0.95,
"downfall": 0.9,
@@ -365,6 +391,36 @@
"spawns_warm_variant_farm_animals"
]
},
"jagged_peaks": {
"temperature": -0.7,
"downfall": 0.9,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"mountains",
"monster",
"overworld",
"frozen",
"jagged_peaks",
"spawns_cold_variant_farm_animals",
"spawns_cold_variant_frogs",
"spawns_snow_foxes",
"spawns_white_rabbits"
]
},
"flower_forest": {
"temperature": 0.7,
"downfall": 0.8,
@@ -586,6 +642,32 @@
"spawns_warm_variant_farm_animals"
]
},
"lush_caves": {
"temperature": 0.9,
"downfall": 0.0,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"caves",
"lush_caves",
"overworld",
"monster",
"spawns_tropical_fish_at_any_height"
]
},
"deep_frozen_ocean": {
"temperature": 0.0,
"downfall": 0.5,
@@ -834,33 +916,6 @@
"spawns_cold_variant_farm_animals"
]
},
"crimson_forest": {
"temperature": 2.0,
"downfall": 0.0,
"redSporeDensity": 0.25,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 165,
"r": 144,
"g": 89,
"b": 87
},
"rain": false,
"chunkGenData": null,
"id": null,
"tags": [
"nether",
"netherwart_forest",
"crimson_forest",
"spawn_few_zombified_piglins",
"spawn_piglin",
"spawns_warm_variant_farm_animals"
]
},
"mesa": {
"temperature": 2.0,
"downfall": 0.0,
@@ -998,6 +1053,32 @@
"spawns_cold_variant_farm_animals"
]
},
"warped_forest": {
"temperature": 2.0,
"downfall": 0.0,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.25,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 165,
"r": 144,
"g": 89,
"b": 87
},
"rain": false,
"chunkGenData": null,
"id": null,
"tags": [
"nether",
"netherwart_forest",
"warped_forest",
"spawn_endermen",
"spawns_warm_variant_farm_animals"
]
},
"mesa_plateau_stone": {
"temperature": 2.0,
"downfall": 0.0,
@@ -1385,32 +1466,6 @@
"spawns_warm_variant_farm_animals"
]
},
"meadow": {
"temperature": 0.3,
"downfall": 0.8,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"mountains",
"monster",
"overworld",
"meadow",
"bee_habitat"
]
},
"jungle_hills": {
"temperature": 0.95,
"downfall": 0.9,
@@ -1467,36 +1522,6 @@
"spawns_cold_variant_farm_animals"
]
},
"frozen_peaks": {
"temperature": -0.7,
"downfall": 0.9,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"mountains",
"monster",
"overworld",
"frozen",
"frozen_peaks",
"spawns_cold_variant_farm_animals",
"spawns_cold_variant_frogs",
"spawns_snow_foxes",
"spawns_white_rabbits"
]
},
"taiga": {
"temperature": 0.25,
"downfall": 0.8,
@@ -1633,6 +1658,33 @@
"warm"
]
},
"crimson_forest": {
"temperature": 2.0,
"downfall": 0.0,
"redSporeDensity": 0.25,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 165,
"r": 144,
"g": 89,
"b": 87
},
"rain": false,
"chunkGenData": null,
"id": null,
"tags": [
"nether",
"netherwart_forest",
"crimson_forest",
"spawn_few_zombified_piglins",
"spawn_piglin",
"spawns_warm_variant_farm_animals"
]
},
"ice_plains": {
"temperature": 0.0,
"downfall": 0.5,
@@ -1958,32 +2010,6 @@
"spawns_cold_variant_frogs"
]
},
"warped_forest": {
"temperature": 2.0,
"downfall": 0.0,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.25,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 165,
"r": 144,
"g": 89,
"b": 87
},
"rain": false,
"chunkGenData": null,
"id": null,
"tags": [
"nether",
"netherwart_forest",
"warped_forest",
"spawn_endermen",
"spawns_warm_variant_farm_animals"
]
},
"mesa_plateau_stone_mutated": {
"temperature": 2.0,
"downfall": 0.0,
@@ -2037,9 +2063,9 @@
"spawns_without_patrols"
]
},
"grove": {
"temperature": -0.2,
"downfall": 0.8,
"snowy_slopes": {
"temperature": -0.3,
"downfall": 0.9,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
@@ -2057,14 +2083,14 @@
"id": null,
"tags": [
"mountains",
"cold",
"monster",
"overworld",
"grove",
"spawns_cold_variant_farm_animals",
"frozen",
"spawns_cold_variant_frogs",
"spawns_snow_foxes",
"spawns_white_rabbits"
"spawns_white_rabbits",
"snowy_slopes",
"spawns_cold_variant_farm_animals"
]
},
"warm_ocean": {
@@ -2312,32 +2338,6 @@
"overworld"
]
},
"lush_caves": {
"temperature": 0.9,
"downfall": 0.0,
"redSporeDensity": 0.0,
"blueSporeDensity": 0.0,
"ashDensity": 0.0,
"whiteAshDensity": 0.0,
"depth": 0.1,
"scale": 0.2,
"mapWaterColor": {
"a": 166,
"r": 96,
"g": 183,
"b": 255
},
"rain": true,
"chunkGenData": null,
"id": null,
"tags": [
"caves",
"lush_caves",
"overworld",
"monster",
"spawns_tropical_fish_at_any_height"
]
},
"frozen_ocean": {
"temperature": 0.0,
"downfall": 0.5,

View File

@@ -69,6 +69,12 @@ public class MessageTranslatorTest {
"§e All participants will receive a reward\n" +
"§e and the top 3 will get extra bonus prizes!");
// Escape curly braces in translatable strings (make MessageFormat ignore them)
messages.put("{\"translate\":\"tt{tt%stt}tt\",\"with\":[\"AA\"]}", "tt{ttAAtt}tt");
messages.put("{\"translate\":\"tt{'tt%stt'{tt\",\"with\":[\"AA\"]}", "tt{'ttAAtt'{tt");
messages.put("{\"translate\":\"tt{''{tt\"}", "tt{''{tt");
messages.put("{\"translate\":\"tt{{''}}tt\"}", "tt{{''}}tt");
MessageTranslator.init();
}

View File

@@ -9,9 +9,9 @@ netty = "4.2.1.Final"
guava = "29.0-jre"
gson = "2.3.1" # Provided by Spigot 1.8.8
websocket = "1.5.1"
protocol-connection = "3.0.0.Beta6-20250506.012145-17"
protocol-common = "3.0.0.Beta6-20250506.012145-17"
protocol-codec = "3.0.0.Beta6-20250506.012145-17"
protocol-connection = "3.0.0.Beta7-20250616.124609-6"
protocol-common = "3.0.0.Beta7-20250616.124609-6"
protocol-codec = "3.0.0.Beta7-20250616.124609-6"
raknet = "1.0.0.CR3-20250218.160705-18"
minecraftauth = "4.1.1"
mcprotocollib = "1.21.6-SNAPSHOT"