diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java index 69a50ea4b40..07ec9916f2f 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -34,15 +34,12 @@ import org.cloudburstmc.math.vector.Vector3i; import org.geysermc.erosion.bukkit.BukkitUtils; import org.geysermc.erosion.bukkit.SchedulerUtils; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.level.GameRule; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import java.util.List; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -86,38 +83,6 @@ public boolean hasOwnChunkCache() { return true; } - public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { - org.bukkit.GameRule bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID()); - if (bukkitGameRule == null) { - GeyserImpl.getInstance().getLogger().debug("Unknown game rule " + gameRule.getJavaID()); - return gameRule.getDefaultBooleanValue(); - } - - Player bukkitPlayer = Objects.requireNonNull(Bukkit.getPlayer(session.getPlayerEntity().uuid())); - Object value = bukkitPlayer.getWorld().getGameRuleValue(bukkitGameRule); - if (value instanceof Boolean booleanValue) { - return booleanValue; - } - GeyserImpl.getInstance().getLogger().debug("Expected a bool for " + gameRule + " but got " + value); - return gameRule.getDefaultBooleanValue(); - } - - @Override - public int getGameRuleInt(GeyserSession session, GameRule gameRule) { - org.bukkit.GameRule bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID()); - if (bukkitGameRule == null) { - GeyserImpl.getInstance().getLogger().debug("Unknown game rule " + gameRule.getJavaID()); - return gameRule.getDefaultIntValue(); - } - Player bukkitPlayer = Objects.requireNonNull(Bukkit.getPlayer(session.getPlayerEntity().uuid())); - Object value = bukkitPlayer.getWorld().getGameRuleValue(bukkitGameRule); - if (value instanceof Integer intValue) { - return intValue; - } - GeyserImpl.getInstance().getLogger().debug("Expected an int for " + gameRule + " but got " + value); - return gameRule.getDefaultIntValue(); - } - @Override public GameMode getDefaultGameMode(GeyserSession session) { return GameMode.byId(Bukkit.getDefaultGameMode().ordinal()); diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 17596b3c427..e510e524357 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -262,6 +262,7 @@ public void initialize() { return; } + MinecraftLocale.downloadDeprecations(); MinecraftLocale.ensureEN_US(); String locale = GeyserLocale.getDefaultLocale(); if (!"en_us".equals(locale)) { diff --git a/core/src/main/java/org/geysermc/geyser/Permissions.java b/core/src/main/java/org/geysermc/geyser/Permissions.java index b65a5af7a41..3e04d5e1a2d 100644 --- a/core/src/main/java/org/geysermc/geyser/Permissions.java +++ b/core/src/main/java/org/geysermc/geyser/Permissions.java @@ -39,7 +39,6 @@ public final class Permissions { public static final String CHECK_UPDATE = register("geyser.update"); public static final String SERVER_SETTINGS = register("geyser.settings.server"); - public static final String SETTINGS_GAMERULES = register("geyser.settings.gamerules"); private Permissions() { //no diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java index df97c64627d..85e7dda4d46 100644 --- a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -48,6 +48,7 @@ import org.geysermc.geyser.command.defaults.CustomOptionsCommand; import org.geysermc.geyser.command.defaults.DumpCommand; import org.geysermc.geyser.command.defaults.ExtensionsCommand; +import org.geysermc.geyser.command.defaults.GameruleCommand; import org.geysermc.geyser.command.defaults.HelpCommand; import org.geysermc.geyser.command.defaults.ListCommand; import org.geysermc.geyser.command.defaults.OffhandCommand; @@ -170,6 +171,7 @@ public CommandRegistry(GeyserImpl geyser, CommandManager cl registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping")); registerBuiltInCommand(new CustomOptionsCommand("options", "geyser.commands.options.desc", "geyser.command.options")); registerBuiltInCommand(new QuickActionsCommand("quickactions", "geyser.commands.quickactions.desc", "geyser.command.quickactions")); + registerBuiltInCommand(new GameruleCommand("gamerules", "geyser.commands.gamerules.desc", "geyser.command.gamerules")); if (this.geyser.platformType() == PlatformType.STANDALONE) { registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/GameruleCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/GameruleCommand.java new file mode 100644 index 00000000000..8777aceb1c7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/GameruleCommand.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.defaults; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.context.CommandContext; + +public class GameruleCommand extends GeyserCommand { + + public GameruleCommand(@NonNull String name, @NonNull String description, @NonNull String permission) { + super(name, description, permission, TriState.NOT_SET, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = context.sender().connection(); + if (session != null) { + session.getGameRuleHandler().requestGamerules(); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/GameRule.java b/core/src/main/java/org/geysermc/geyser/level/GameRule.java deleted file mode 100644 index fee10cb4d62..00000000000 --- a/core/src/main/java/org/geysermc/geyser/level/GameRule.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.level; - -import lombok.Getter; - -import java.util.Locale; - -/** - * This enum stores each gamerule along with the value type and the default. - * It is used to construct the list for the settings menu - */ -// TODO gamerules from ClientboundGameRuleValuesPacket -public enum GameRule { - ADVANCE_TIME("gamerule.doDaylightCycle", true), - ADVANCE_WEATHER("gamerule.doWeatherCycle", true), - ALLOW_ENTERING_NETHER_USING_PORTALS("gamerule.allowEnteringNetherUsingPortals", true), - BLOCK_DROPS("gamerule.doTileDrops", true), - BLOCK_EXPLOSION_DROP_DECAY("gamerule.blockExplosionDropDecay", true), - COMMAND_BLOCKS_WORK("gamerule.commandBlocksEnabled", true), - COMMAND_BLOCK_OUTPUT("gamerule.commandBlockOutput", true), - DROWNING_DAMAGE("gamerule.drowningDamage", true), - ELYTRA_MOVEMENT_CHECK("gamerule.minecraft.elytra_movement_check", true), - ENDER_PEARLS_VANISH_ON_DEATH("gamerule.enderPearlsVanishOnDeath", true), - ENTITY_DROPS("gamerule.doEntityDrops", true), - FALL_DAMAGE("gamerule.fallDamage", true), - FIRE_DAMAGE("gamerule.fireDamage", true), - FIRE_SPREAD_RADIUS_AROUND_PLAYER("gamerule.minecraft.fire_spread_radius_around_player", 128), - FORGIVE_DEAD_PLAYERS("gamerule.forgiveDeadPlayers", true), - FREEZE_DAMAGE("gamerule.freezeDamage", true), - GLOBAL_SOUND_EVENTS("gamerule.globalSoundEvents", true), - IMMEDIATE_RESPAWN("gamerule.doImmediateRespawn", false), - KEEP_INVENTORY("gamerule.keepInventory", false), - LAVA_SOURCE_CONVERSION("gamerule.lavaSourceConversion", false), - LIMITED_CRAFTING("gamerule.doLimitedCrafting", false), - LOCATOR_BAR("gamerule.locatorBar", true), - LOG_ADMIN_COMMANDS("gamerule.logAdminCommands", true), - MAX_BLOCK_MODIFICATIONS("gamerule.commandModificationBlockLimit", 32768), - MAX_COMMAND_FORKS("gamerule.maxCommandForkCount", 65536), - MAX_COMMAND_SEQUENCE_LENGTH("gamerule.maxCommandChainLength", 65536), - MAX_ENTITY_CRAMMING("gamerule.maxEntityCramming", 24), - MAX_SNOW_ACCUMULATION_HEIGHT("gamerule.snowAccumulationHeight", 1), - MOB_DROPS("gamerule.doMobLoot", true), - MOB_EXPLOSION_DROP_DECAY("gamerule.mobExplosionDropDecay", true), - MOB_GRIEFING("gamerule.mobGriefing", true), - NATURAL_HEALTH_REGENERATION("gamerule.naturalRegeneration", true), - PLAYER_MOVEMENT_CHECK("gamerule.minecraft.player_movement_check", true), - PLAYERS_NETHER_PORTAL_CREATIVE_DELAY("gamerule.playersNetherPortalCreativeDelay", 0), - PLAYERS_NETHER_PORTAL_DEFAULT_DELAY("gamerule.playersNetherPortalDefaultDelay", 80), - PLAYERS_SLEEPING_PERCENTAGE("gamerule.playersSleepingPercentage", 100), - PROJECTILES_CAN_BREAK_BLOCKS("gamerule.projectilesCanBreakBlocks", true), - PVP("gamerule.pvp", true), - RAIDS("gamerule.minecraft.raids", true), - RANDOM_TICK_SPEED("gamerule.randomTickSpeed", 3), - REDUCED_DEBUG_INFO("gamerule.reducedDebugInfo", false), - RESPAWN_RADIUS("gamerule.spawnRadius", 10), - SEND_COMMAND_FEEDBACK("gamerule.sendCommandFeedback", true), - SHOW_ADVANCEMENT_MESSAGES("gamerule.announceAdvancements", true), - SHOW_DEATH_MESSAGES("gamerule.showDeathMessages", true), - SPAWNER_BLOCKS_WORK("gamerule.spawnerBlocksEnabled", true), - SPAWN_MOBS("gamerule.doMobSpawning", true), - SPAWN_MONSTERS("gamerule.spawnMonsters", true), - SPAWN_PATROLS("gamerule.doPatrolSpawning", true), - SPAWN_PHANTOMS("gamerule.doInsomnia", true), - SPAWN_WANDERING_TRADERS("gamerule.doTraderSpawning", true), - SPAWN_WARDENS("gamerule.doWardenSpawning", true), - SPECTATORS_GENERATE_CHUNKS("gamerule.spectatorsGenerateChunks", true), - SPREAD_VINES("gamerule.doVinesSpread", true), - TNT_EXPLODES("gamerule.tntExplodes", true), - TNT_EXPLOSION_DROP_DECAY("gamerule.tntExplosionDropDecay", false), - UNIVERSAL_ANGER("gamerule.universalAnger", false), - WATER_SOURCE_CONVERSION("gamerule.waterSourceConversion", true); - - public static final GameRule[] VALUES = values(); - - @Getter - private final String translation; - - @Getter - private final Class type; - - private final int defaultValue; - - GameRule(String translation, boolean defaultValue) { - this.translation = translation; - this.type = Boolean.class; - this.defaultValue = defaultValue ? 1 : 0; - } - - GameRule(String translation, int defaultValue) { - this.translation = translation; - this.type = Integer.class; - this.defaultValue = defaultValue; - } - - public boolean getDefaultBooleanValue() { - return defaultValue != 0; - } - - public int getDefaultIntValue() { - return defaultValue; - } - - public String getJavaID() { - return name().toLowerCase(Locale.ROOT); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java index ca2ebcb085d..83cc04290d6 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.level; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.cloudburstmc.math.vector.Vector3i; import org.geysermc.erosion.packet.backendbound.BackendboundBatchBlockRequestPacket; import org.geysermc.erosion.packet.backendbound.BackendboundBlockRequestPacket; @@ -38,7 +36,6 @@ import java.util.concurrent.CompletableFuture; public class GeyserWorldManager extends WorldManager { - private final Object2ObjectMap gameruleCache = new Object2ObjectOpenHashMap<>(); @Override public int getBlockAt(GeyserSession session, int x, int y, int z) { @@ -89,32 +86,6 @@ public boolean hasOwnChunkCache() { return false; } - @Override - public void setGameRule(GeyserSession session, String name, Object value) { - super.setGameRule(session, name, value); - gameruleCache.put(name, String.valueOf(value)); - } - - @Override - public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { - String value = gameruleCache.get(gameRule.getJavaID()); - if (value != null) { - return Boolean.parseBoolean(value); - } - - return gameRule.getDefaultBooleanValue(); - } - - @Override - public int getGameRuleInt(GeyserSession session, GameRule gameRule) { - String value = gameruleCache.get(gameRule.getJavaID()); - if (value != null) { - return Integer.parseInt(value); - } - - return gameRule.getDefaultIntValue(); - } - @Override public GameMode getDefaultGameMode(GeyserSession session) { return GameMode.SURVIVAL; diff --git a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java index 48f9734b274..5e845aa7236 100644 --- a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java @@ -25,10 +25,6 @@ package org.geysermc.geyser.level; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3i; @@ -36,19 +32,12 @@ import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponent; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import java.util.function.Function; /** * Class that manages or retrieves various information @@ -118,35 +107,6 @@ public int[] getBlocksAt(GeyserSession session, BlockPositionIterator iter) { */ public abstract boolean hasOwnChunkCache(); - /** - * Updates a gamerule value on the Java server - * - * @param session The session of the user that requested the change - * @param name The gamerule to change - * @param value The new value for the gamerule - */ - public void setGameRule(GeyserSession session, String name, Object value) { - session.sendCommandPacket("gamerule " + name + " " + value); - } - - /** - * Gets a gamerule value as a boolean - * - * @param session The session of the user that requested the value - * @param gameRule The gamerule to fetch the value of - * @return The boolean representation of the value - */ - public abstract boolean getGameRuleBool(GeyserSession session, GameRule gameRule); - - /** - * Get a gamerule value as an integer - * - * @param session The session of the user that requested the value - * @param gameRule The gamerule to fetch the value of - * @return The integer representation of the value - */ - public abstract int getGameRuleInt(GeyserSession session, GameRule gameRule); - /** * Get the default game mode of the server * @@ -188,20 +148,4 @@ public void setDifficulty(GeyserSession session, Difficulty difficulty) { */ public void getDecoratedPotData(GeyserSession session, Vector3i pos, Consumer> apply) { } - - protected static final Function, DataComponents> RAW_TRANSFORMER = map -> { - try { - Map, DataComponent> components = new HashMap<>(); - Int2ObjectMaps.fastForEach(map, entry -> { - DataComponentType type = DataComponentTypes.from(entry.getIntKey()); - ByteBuf buf = Unpooled.wrappedBuffer(entry.getValue()); - DataComponent value = type.readDataComponent(buf); - components.put(type, value); - }); - return new DataComponents(components); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - }; } diff --git a/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRule.java b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRule.java new file mode 100644 index 00000000000..5b7065917fc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRule.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.gamerule; + +import net.kyori.adventure.key.Key; +import org.geysermc.cumulus.component.Component; +import org.geysermc.cumulus.component.InputComponent; +import org.geysermc.cumulus.component.ToggleComponent; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.geyser.util.TypeAdapter; + +/** + * GameRule types, used in the gamerule menu + */ +public interface GameRule { + + /** + * The id of the gamerule; without the minecraft namespace + */ + Key key(); + + /** + * The category of this gamerule + */ + GameRuleCategory category(); + + /** + * The {@link TypeAdapter} used for this gamerule + */ + TypeAdapter adapter(); + + /** + * default value for the gamerule + */ + T defaultValue(); + + /** + * ensures gamerule values are within range + */ + boolean validate(T value); + + Component toComponent(GeyserSession session, String currentValue); + + record Int(Key key, GameRuleCategory category, Integer defaultValue, int max, int min) implements GameRule { + public Int(String key, GameRuleCategory category, Integer defaultValue, int max, int min) { + this(MinecraftKey.key(key), category, defaultValue, max, min); + } + + @Override + public TypeAdapter adapter() { + return TypeAdapter.INTEGER; + } + + @Override + public boolean validate(Integer value) { + return value <= max && value >= min; + } + + @Override + public Component toComponent(GeyserSession session, String currentValue) { + return InputComponent.of(GameRule.translate(session, key), currentValue, currentValue); + } + } + + record Bool(Key key, GameRuleCategory category, Boolean defaultValue) implements GameRule { + public Bool(String key, GameRuleCategory category, Boolean defaultValue) { + this(MinecraftKey.key(key), category, defaultValue); + } + + @Override + public TypeAdapter adapter() { + return TypeAdapter.BOOLEAN; + } + + @Override + public boolean validate(Boolean value) { + return value != null; + } + + @Override + public Component toComponent(GeyserSession session, String currentValue) { + return ToggleComponent.of(GameRule.translate(session, key), adapter().parser().apply(currentValue)); + } + } + + private static String translate(GeyserSession session, Key key) { + String translatable = "gamerule." + key.namespace() + "." + key.value().replace('/', '.'); + return MessageTranslator.convertMessage(net.kyori.adventure.text.Component.translatable(translatable), session.locale()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleCategory.java b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleCategory.java new file mode 100644 index 00000000000..db3695e0a56 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleCategory.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.gamerule; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import org.geysermc.geyser.util.MinecraftKey; + +public enum GameRuleCategory { + PLAYER, + MOBS, + SPAWNING, + DROPS, + UPDATES, + CHAT, + MISC; + + public Key id() { + return MinecraftKey.key(name().toLowerCase()); + } + + public Component label() { + return Component.translatable("gamerule.category." + id().namespace() + "." + id().value()) + .color(NamedTextColor.YELLOW).style(Style.style(TextDecoration.BOLD)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleHandler.java b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleHandler.java new file mode 100644 index 00000000000..02c016b74e5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRuleHandler.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.gamerule; + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import lombok.Getter; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import org.geysermc.cumulus.form.CustomForm; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.MinecraftLocale; +import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.ClientCommand; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundGameRuleValuesPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundSetGameRulePacket; + +import java.util.Comparator; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class GameRuleHandler { + + private static final Component EDIT_TITLE = Component.translatable("editGamerule.title"); + private final GeyserSession session; + @Getter + private State state = State.NOT_REQUESTED; + + public GameRuleHandler(GeyserSession session) { + this.session = session; + } + + public void requestGamerules() { + state = State.WAITING; + // Hacky, but otherwise, the waiting form shows up *after* the edit form if the server responds too quickly + session.scheduleInEventLoop(() -> { + if (state == State.WAITING) { + showWaitingForm(); + } + }, 500, TimeUnit.MILLISECONDS); + session.sendDownstreamGamePacket(new ServerboundClientCommandPacket(ClientCommand.REQUEST_GAMERULE_VALUES)); + } + + public void onGamerulesReceived(ClientboundGameRuleValuesPacket packet) { + if (state == State.WAITING) { + state = State.SHOWN; + + Map, String>> values = new Object2ObjectArrayMap<>(); + for (Map.Entry entry : packet.getValues().entrySet()) { + GameRule gameRule = Registries.GAME_RULES.get(entry.getKey()); + if (gameRule == null) { + GeyserImpl.getInstance().getLogger().debug("Unknown gamerule: " + entry.getKey()); + continue; + } + values.computeIfAbsent(gameRule.category(), (category) -> new Object2ObjectArrayMap<>()).put(gameRule, entry.getValue()); + } + + CustomForm.Builder builder = CustomForm.builder(); + builder.title(MessageTranslator.convertMessage(EDIT_TITLE, session.locale())); + + Map, String> ordered = new Object2ObjectArrayMap<>(packet.getValues().size()); + values.entrySet() + .stream() + .sorted(Map.Entry.comparingByKey(Comparator.comparing(GameRuleCategory::id))) + .forEach(entry -> { + builder.label(MessageTranslator.convertMessage(entry.getKey().label(), session.locale())); + + entry.getValue().entrySet() + .stream() + .sorted(Map.Entry.comparingByKey(Comparator.comparing(GameRule::key))) + .forEach(nested -> { + builder.component(nested.getKey().toComponent(session, nested.getValue())); + ordered.put(nested.getKey(), nested.getValue()); + }); + }); + + builder.validResultHandler((customForm, result) -> { + Map responses = new Object2ObjectArrayMap<>(); + for (var entry : ordered.entrySet()) { + handleUpdate(entry.getKey(), entry.getValue(), result.next(), responses); + } + if (session.getOpPermissionLevel() >= 2 && !responses.isEmpty()) { + session.sendDownstreamGamePacket(new ServerboundSetGameRulePacket(responses)); + } + state = State.NOT_REQUESTED; + }); + builder.closedOrInvalidResultHandler((customForm, result) -> state = State.NOT_REQUESTED); + + session.sendForm(builder); + } + } + + public void showWaitingForm() { + CustomForm waitingForm = CustomForm.builder() + .title("editGamerule.inGame.downloadingGamerules") + .translator(MinecraftLocale::getLocaleString, session.locale()) + .validResultHandler((customForm, result) -> { + // Resend waiting form only on submit; allow closing + showWaitingForm(); + }) + .closedOrInvalidResultHandler((customForm, result) -> { + if (state != State.SHOWN) { + this.state = State.NOT_REQUESTED; + } + }) + .build(); + session.sendForm(waitingForm); + } + + public void handleUpdate(GameRule gameRule, String previous, Object newValue, Map responses) { + T value; + try { + value = gameRule.adapter().parser().apply(newValue); + } catch (Throwable e) { + GeyserImpl.getInstance().getLogger().debug("Failed to parse value for gamerule %s (old value: %s, new value: %s)", gameRule.key(), previous, newValue); + return; + } + + if (!gameRule.validate(value)) { + GeyserImpl.getInstance().getLogger().debug("Got invalid value for gamerule %s (old value: %s, new value: %s)", gameRule.key(), previous, newValue); + return; + } + + T oldValue = gameRule.adapter().parser().apply(previous); + if (Objects.equals(oldValue, value)) { + return; + } + + responses.put(gameRule.key(), value.toString()); + } + + public enum State { + NOT_REQUESTED, + WAITING, + SHOWN + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRules.java b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRules.java new file mode 100644 index 00000000000..39adc8494b2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/gamerule/GameRules.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.gamerule; + +import org.geysermc.geyser.registry.Registries; + +public class GameRules { + public static final GameRule ADVANCE_TIME = register(new GameRule.Bool("advance_time", GameRuleCategory.UPDATES, true)); + public static final GameRule ADVANCE_WEATHER = register(new GameRule.Bool("advance_weather", GameRuleCategory.UPDATES, true)); + public static final GameRule ALLOW_ENTERING_NETHER_USING_PORTALS = register(new GameRule.Bool("allow_entering_nether_using_portals", GameRuleCategory.MISC, true)); + public static final GameRule BLOCK_DROPS = register(new GameRule.Bool("block_drops", GameRuleCategory.DROPS, true)); + public static final GameRule BLOCK_EXPLOSION_DROP_DECAY = register(new GameRule.Bool("block_explosion_drop_decay", GameRuleCategory.DROPS, true)); + public static final GameRule COMMAND_BLOCKS_WORK = register(new GameRule.Bool("command_blocks_work", GameRuleCategory.MISC, true)); + public static final GameRule COMMAND_BLOCK_OUTPUT = register(new GameRule.Bool("command_block_output", GameRuleCategory.CHAT, true)); + public static final GameRule DROWNING_DAMAGE = register(new GameRule.Bool("drowning_damage", GameRuleCategory.PLAYER, true)); + public static final GameRule ELYTRA_MOVEMENT_CHECK = register(new GameRule.Bool("elytra_movement_check", GameRuleCategory.PLAYER, true)); + public static final GameRule ENDER_PEARLS_VANISH_ON_DEATH = register(new GameRule.Bool("ender_pearls_vanish_on_death", GameRuleCategory.PLAYER, true)); + public static final GameRule ENTITY_DROPS = register(new GameRule.Bool("entity_drops", GameRuleCategory.DROPS, true)); + public static final GameRule FALL_DAMAGE = register(new GameRule.Bool("fall_damage", GameRuleCategory.PLAYER, true)); + public static final GameRule FIRE_DAMAGE = register(new GameRule.Bool("fire_damage", GameRuleCategory.PLAYER, true)); + public static final GameRule FIRE_SPREAD_RADIUS_AROUND_PLAYER = register(new GameRule.Int("fire_spread_radius_around_player", GameRuleCategory.UPDATES, -1, Integer.MAX_VALUE, 128)); + public static final GameRule FORGIVE_DEAD_PLAYERS = register(new GameRule.Bool("forgive_dead_players", GameRuleCategory.MOBS, true)); + public static final GameRule FREEZE_DAMAGE = register(new GameRule.Bool("freeze_damage", GameRuleCategory.PLAYER, true)); + public static final GameRule GLOBAL_SOUND_EVENTS = register(new GameRule.Bool("global_sound_events", GameRuleCategory.MISC, true)); + public static final GameRule IMMEDIATE_RESPAWN = register(new GameRule.Bool("immediate_respawn", GameRuleCategory.PLAYER, false)); + public static final GameRule KEEP_INVENTORY = register(new GameRule.Bool("keep_inventory", GameRuleCategory.PLAYER, false)); + public static final GameRule LAVA_SOURCE_CONVERSION = register(new GameRule.Bool("lava_source_conversion", GameRuleCategory.UPDATES, false)); + public static final GameRule LIMITED_CRAFTING = register(new GameRule.Bool("limited_crafting", GameRuleCategory.PLAYER, false)); + public static final GameRule LOCATOR_BAR = register(new GameRule.Bool("locator_bar", GameRuleCategory.PLAYER, true)); + public static final GameRule LOG_ADMIN_COMMANDS = register(new GameRule.Bool("log_admin_commands", GameRuleCategory.CHAT, true)); + public static final GameRule MAX_BLOCK_MODIFICATIONS = register(new GameRule.Int("max_block_modifications", GameRuleCategory.MISC, 1, Integer.MAX_VALUE, 32768)); + public static final GameRule MAX_COMMAND_FORKS = register(new GameRule.Int("max_command_forks", GameRuleCategory.MISC, 0, Integer.MAX_VALUE, 65536)); + public static final GameRule MAX_COMMAND_SEQUENCE_LENGTH = register(new GameRule.Int("max_command_sequence_length", GameRuleCategory.MISC, 0, Integer.MAX_VALUE, 65536)); + public static final GameRule MAX_ENTITY_CRAMMING = register(new GameRule.Int("max_entity_cramming", GameRuleCategory.MOBS, 0, Integer.MAX_VALUE, 24)); + public static final GameRule MAX_MINECART_SPEED = register(new GameRule.Int("max_minecart_speed", GameRuleCategory.MISC, 1, 1000, 8)); + public static final GameRule MAX_SNOW_ACCUMULATION_HEIGHT = register(new GameRule.Int("max_snow_accumulation_height", GameRuleCategory.UPDATES, 0, 8, 1)); + public static final GameRule MOB_DROPS = register(new GameRule.Bool("mob_drops", GameRuleCategory.DROPS, true)); + public static final GameRule MOB_EXPLOSION_DROP_DECAY = register(new GameRule.Bool("mob_explosion_drop_decay", GameRuleCategory.DROPS, true)); + public static final GameRule MOB_GRIEFING = register(new GameRule.Bool("mob_griefing", GameRuleCategory.MOBS, true)); + public static final GameRule NATURAL_HEALTH_REGENERATION = register(new GameRule.Bool("natural_health_regeneration", GameRuleCategory.PLAYER, true)); + public static final GameRule PLAYER_MOVEMENT_CHECK = register(new GameRule.Bool("player_movement_check", GameRuleCategory.PLAYER, true)); + public static final GameRule PLAYERS_NETHER_PORTAL_CREATIVE_DELAY = register(new GameRule.Int("players_nether_portal_creative_delay", GameRuleCategory.PLAYER, 0, Integer.MAX_VALUE, 0)); + public static final GameRule PLAYERS_NETHER_PORTAL_DEFAULT_DELAY = register(new GameRule.Int("players_nether_portal_default_delay", GameRuleCategory.PLAYER, 0, Integer.MAX_VALUE, 80)); + public static final GameRule PLAYERS_SLEEPING_PERCENTAGE = register(new GameRule.Int("players_sleeping_percentage", GameRuleCategory.PLAYER, 0, Integer.MAX_VALUE, 100)); + public static final GameRule PROJECTILES_CAN_BREAK_BLOCKS = register(new GameRule.Bool("projectiles_can_break_blocks", GameRuleCategory.DROPS, true)); + public static final GameRule PVP = register(new GameRule.Bool("pvp", GameRuleCategory.PLAYER, true)); + public static final GameRule RAIDS = register(new GameRule.Bool("raids", GameRuleCategory.MOBS, true)); + public static final GameRule RANDOM_TICK_SPEED = register(new GameRule.Int("random_tick_speed", GameRuleCategory.UPDATES, 0, Integer.MAX_VALUE, 3)); + public static final GameRule REDUCED_DEBUG_INFO = register(new GameRule.Bool("reduced_debug_info", GameRuleCategory.MISC, false)); + public static final GameRule RESPAWN_RADIUS = register(new GameRule.Int("respawn_radius", GameRuleCategory.PLAYER, 0, Integer.MAX_VALUE, 10)); + public static final GameRule SEND_COMMAND_FEEDBACK = register(new GameRule.Bool("send_command_feedback", GameRuleCategory.CHAT, true)); + public static final GameRule SHOW_ADVANCEMENT_MESSAGES = register(new GameRule.Bool("show_advancement_messages", GameRuleCategory.CHAT, true)); + public static final GameRule SHOW_DEATH_MESSAGES = register(new GameRule.Bool("show_death_messages", GameRuleCategory.CHAT, true)); + public static final GameRule SPAWNER_BLOCKS_WORK = register(new GameRule.Bool("spawner_blocks_work", GameRuleCategory.MISC, true)); + public static final GameRule SPAWN_MOBS = register(new GameRule.Bool("spawn_mobs", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPAWN_MONSTERS = register(new GameRule.Bool("spawn_monsters", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPAWN_PATROLS = register(new GameRule.Bool("spawn_patrols", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPAWN_PHANTOMS = register(new GameRule.Bool("spawn_phantoms", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPAWN_WANDERING_TRADERS = register(new GameRule.Bool("spawn_wandering_traders", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPAWN_WARDENS = register(new GameRule.Bool("spawn_wardens", GameRuleCategory.SPAWNING, true)); + public static final GameRule SPECTATORS_GENERATE_CHUNKS = register(new GameRule.Bool("spectators_generate_chunks", GameRuleCategory.PLAYER, true)); + public static final GameRule SPREAD_VINES = register(new GameRule.Bool("spread_vines", GameRuleCategory.UPDATES, true)); + public static final GameRule TNT_EXPLODES = register(new GameRule.Bool("tnt_explodes", GameRuleCategory.MISC, true)); + public static final GameRule TNT_EXPLOSION_DROP_DECAY = register(new GameRule.Bool("tnt_explosion_drop_decay", GameRuleCategory.DROPS, false)); + public static final GameRule UNIVERSAL_ANGER = register(new GameRule.Bool("universal_anger", GameRuleCategory.MOBS, false)); + public static final GameRule WATER_SOURCE_CONVERSION = register(new GameRule.Bool("water_source_conversion", GameRuleCategory.UPDATES, true)); + + public static GameRule register(GameRule gameRule) { + Registries.GAME_RULES.register(gameRule.key(), gameRule); + return gameRule; + } + + public static void init() { + // no-op + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 5594f3ccd7a..6f36251ba26 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -38,6 +38,8 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.level.gamerule.GameRule; +import org.geysermc.geyser.level.gamerule.GameRules; import org.geysermc.geyser.pack.ResourcePackHolder; import org.geysermc.geyser.registry.loader.BiomeIdentifierRegistryLoader; import org.geysermc.geyser.registry.loader.BlockEntityRegistryLoader; @@ -208,6 +210,12 @@ public final class Registries { */ public static final ListDeferredRegistry DANGEROUS_ENTITIES = ListDeferredRegistry.create(UtilMappings::dangerousEntities, RegistryLoaders.UTIL_MAPPINGS_KEYS); + /** + * A registry containing all the Java game rules. + * Loaded through {@link GameRules} + */ + public static final SimpleMappedRegistry> GAME_RULES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + public static void load() { if (loaded) return; loaded = true; @@ -230,6 +238,7 @@ public static void load() { GAME_MASTER_BLOCKS.load(); DANGEROUS_BLOCK_ENTITIES.load(); DANGEROUS_ENTITIES.load(); + GameRules.init(); } public static void populate() { diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 08df79d2fed..b14415344a4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -154,6 +154,7 @@ import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.JavaDimension; +import org.geysermc.geyser.level.gamerule.GameRuleHandler; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.LocalSession; @@ -295,6 +296,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final EntityCache entityCache; private final EntityEffectCache effectCache; private final FormCache formCache; + private final GameRuleHandler gameRuleHandler; private final InputCache inputCache; private final LodestoneCache lodestoneCache; private final PistonCache pistonCache; @@ -652,7 +654,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { /** * The op permission level set by the server */ - @Setter private int opPermissionLevel = 0; /** @@ -844,9 +845,10 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio this.cameraData = new GeyserCameraData(this); this.entityData = new GeyserEntityData(this); - this.worldBorder = new WorldBorder(this); this.collisionManager = new CollisionManager(this); + this.gameRuleHandler = new GameRuleHandler(this); this.blockBreakHandler = new BlockBreakHandler(this); + this.worldBorder = new WorldBorder(this); this.playerEntity = new SessionPlayerEntity(this); collisionManager.updatePlayerBoundingBox(this.playerEntity.position()); @@ -1753,6 +1755,13 @@ public InetSocketAddress getSocketAddress() { return this.upstream.getAddress(); } + public void setOpPermissionLevel(int opPermissionLevel) { + this.opPermissionLevel = opPermissionLevel; + if (gameRuleHandler.getState() == GameRuleHandler.State.SHOWN || gameRuleHandler.getState() == GameRuleHandler.State.WAITING) { + closeForm(); + } + } + @Override public boolean sendForm(@NonNull Form form) { // First close any dialogs that are open. This won't execute the dialog's closing action. @@ -2126,7 +2135,6 @@ public void setShouldClientTickClock(boolean shouldTick) { if (this.shouldClientTickClock == shouldTick) { return; } - System.out.println("ticking clock on client: " + shouldTick); sendGameRule("dodaylightcycle", shouldTick); // Save the value so we don't have to constantly send a daylight cycle gamerule update this.shouldClientTickClock = shouldTick; diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java index 65a62665d55..4654c1eff02 100644 --- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java @@ -40,7 +40,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -48,10 +50,14 @@ public class MinecraftLocale { public static final Map> LOCALE_MAPPINGS = new HashMap<>(); + private static final List REMOVED_KEYS = new ArrayList<>(); + private static final Map REPLACED_KEYS = new HashMap<>(); + // 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; + private static final Path DEPRECATED = LOCALE_FOLDER == null ? null : getPath("deprecated"); static { if (IN_INSTANCE) { @@ -76,6 +82,18 @@ public static void ensureEN_US() { })); } + public static void downloadDeprecations() { + if (!loadDeprecations()) { + AssetUtils.addTask(!Files.exists(DEPRECATED), new AssetUtils.ClientJarTask("assets/minecraft/lang/deprecated.json", + stream -> AssetUtils.saveFile(DEPRECATED, stream), + () -> { + if (!loadDeprecations()) { + GeyserImpl.getInstance().getLogger().warning("Failed to load deprecated locale file: it doesn't exist?"); + } + })); + } + } + /** * Downloads a locale from Mojang if it's not already loaded * @@ -184,6 +202,35 @@ private static boolean loadLocale(String locale) { } } + private static boolean loadDeprecations() { + if (Files.exists(DEPRECATED) && Files.isReadable(DEPRECATED)) { + try (InputStream localeStream = Files.newInputStream(DEPRECATED, StandardOpenOption.READ)) { + // Parse the file as json + JsonObject localeObj = JsonUtils.fromJson(localeStream); + JsonElement removed = localeObj.get("removed"); + if (removed.isJsonArray()) { + for (JsonElement removedElement : removed.getAsJsonArray()) { + REMOVED_KEYS.add(removedElement.getAsString()); + } + } + + JsonElement renamed = localeObj.get("renamed"); + if (renamed.isJsonObject()) { + for (Map.Entry renamedElement : renamed.getAsJsonObject().entrySet()) { + REPLACED_KEYS.put(renamedElement.getKey(), renamedElement.getValue().getAsString()); + } + } + + return true; + } catch (FileNotFoundException e){ + throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", DEPRECATED, e.getMessage())); + } catch (Exception e) { + throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.json", DEPRECATED), e); + } + } + return false; + } + /** * Load and parse a json lang file. * @@ -200,7 +247,10 @@ public static Map parseLangFile(Path localeFile, String locale) // Parse all the locale fields Map langMap = new HashMap<>(); for (Map.Entry entry : localeObj.entrySet()) { - langMap.put(entry.getKey(), entry.getValue().getAsString()); + if (REMOVED_KEYS.contains(entry.getKey())) { + continue; + } + langMap.put(REPLACED_KEYS.getOrDefault(entry.getKey(), entry.getKey()), entry.getValue().getAsString()); } return langMap; } catch (FileNotFoundException e){ diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameRuleValuesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameRuleValuesTranslator.java new file mode 100644 index 00000000000..9bef49f6b5f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameRuleValuesTranslator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.translator.protocol.java; + +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundGameRuleValuesPacket; + +@Translator(packet = ClientboundGameRuleValuesPacket.class) +public class JavaGameRuleValuesTranslator extends PacketTranslator { + @Override + public void translate(GeyserSession session, ClientboundGameRuleValuesPacket packet) { + session.getGameRuleHandler().onGamerulesReceived(packet); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java index 6043efdda21..f7c16d362cc 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java @@ -25,22 +25,13 @@ package org.geysermc.geyser.util; -import net.kyori.adventure.key.Key; import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.geysermc.cumulus.component.DropdownComponent; import org.geysermc.cumulus.form.CustomForm; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.Permissions; -import org.geysermc.geyser.level.GameRule; -import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundSetGameRulePacket; - -import java.util.HashMap; -import java.util.Map; public class SettingsUtils { /** @@ -87,22 +78,6 @@ public static CustomForm buildForm(GeyserSession session) { } } - boolean showGamerules = session.getOpPermissionLevel() >= 2 || session.hasPermission(Permissions.SETTINGS_GAMERULES); - if (showGamerules) { - builder.label("geyser.settings.title.game_rules") - .translator(MinecraftLocale::getLocaleString); // we need translate gamerules next - - WorldManager worldManager = GeyserImpl.getInstance().getWorldManager(); - for (GameRule gamerule : GameRule.VALUES) { - // Add the relevant form item based on the gamerule type - if (Boolean.class.equals(gamerule.getType())) { - builder.toggle(gamerule.getTranslation(), worldManager.getGameRuleBool(session, gamerule)); - } else if (Integer.class.equals(gamerule.getType())) { - builder.input(gamerule.getTranslation(), "", String.valueOf(worldManager.getGameRuleInt(session, gamerule))); - } - } - } - builder.validResultHandler(response -> { applyDifficultyFix(session); if (showClientSettings) { @@ -125,29 +100,6 @@ public static CustomForm buildForm(GeyserSession session) { session.getPreferencesCache().setPrefersCustomSkulls(response.next()); } } - - if (showGamerules) { - // TODO GameRule fetching via ClientboundGameRuleValuesPacket - //Map changedGamerules = new HashMap<>(); - for (GameRule gamerule : GameRule.VALUES) { - if (Boolean.class.equals(gamerule.getType())) { - boolean value = response.next(); - if (value != session.getGeyser().getWorldManager().getGameRuleBool(session, gamerule)) { - //changedGamerules.put(null, String.valueOf(value)); - session.getGeyser().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); - } - } else if (Integer.class.equals(gamerule.getType())) { - int value = Integer.parseInt(response.next()); - if (value != session.getGeyser().getWorldManager().getGameRuleInt(session, gamerule)) { - //changedGamerules.put(null, String.valueOf(value)); - session.getGeyser().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); - } - } - } - //if (!changedGamerules.isEmpty()) { - // session.sendDownstreamGamePacket(new ServerboundSetGameRulePacket(changedGamerules)); - //} - } }); builder.closedOrInvalidResultHandler($ -> applyDifficultyFix(session)); diff --git a/core/src/main/java/org/geysermc/geyser/util/TypeAdapter.java b/core/src/main/java/org/geysermc/geyser/util/TypeAdapter.java new file mode 100644 index 00000000000..abb8e47af17 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/TypeAdapter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.util; + +import java.util.function.Function; + +public record TypeAdapter(Class type, Function parser) { + public static final TypeAdapter BOOLEAN = new TypeAdapter<>(Boolean.class, (object) -> { + if (object instanceof Boolean bool) { + return bool; + } + return Boolean.parseBoolean(object.toString()); + }); + public static final TypeAdapter INTEGER = new TypeAdapter<>(Integer.class, (object) -> { + if (object instanceof Integer intValue) { + return intValue; + } + return Integer.parseInt(object.toString()); + }); +} +