diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 1ce694cb..726e5fa6 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -160,6 +160,7 @@ public static void registerCommands(CommandDispatcher RelogCommand.register(dispatcher); RenderCommand.register(dispatcher); ReplyCommand.register(dispatcher); + ServerSeedCommand.register(dispatcher); ShrugCommand.register(dispatcher); SignSearchCommand.register(dispatcher); SnakeCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ServerSeedCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/ServerSeedCommand.java new file mode 100644 index 00000000..382b6d45 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/ServerSeedCommand.java @@ -0,0 +1,64 @@ +package net.earthcomputer.clientcommands.command; + +import com.google.common.hash.HashCode; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.seedfinding.mccore.rand.seed.WorldSeed; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +import static com.mojang.brigadier.arguments.LongArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class ServerSeedCommand { + + private static final SimpleCommandExceptionType IMPOSSIBLE_SEED_COMBINATION_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cserverseed.hashedSeed.impossibleSeedCombination")); + private static final SimpleCommandExceptionType ENCHANTMENT_SEED_NOT_SENT_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cserverseed.enchantmentSeedNotSent")); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("cserverseed") + .then(literal("enchantmentSeed") + .executes(ctx -> enchantmentSeed(ctx.getSource()))) + .then(literal("hashedSeed") + .executes(ctx -> hashedSeed(ctx.getSource())) + .then(argument("structureSeed", longArg()) + .executes(ctx -> fromStructureSeed(ctx.getSource(), getLong(ctx, "structureSeed")))))); + } + + private static int enchantmentSeed(FabricClientCommandSource source) throws CommandSyntaxException { + int seed = source.getPlayer().getEnchantmentSeed(); + if (seed == 0) { + throw ENCHANTMENT_SEED_NOT_SENT_EXCEPTION.create(); + } + // The enchantment seed is a Java int, which is 32 bits. + // However, the server sends the enchantment seed as short to the client. + // This causes the 16 higher bits to be ignored, leaving only the 16 lower bits. + seed &= 0x0000ffff; + source.sendFeedback(Component.translatable("commands.cserverseed.enchantmentSeed", ComponentUtils.copyOnClickText(StringUtils.leftPad(Integer.toBinaryString(seed), 16, '0')))); + return seed; + } + + private static int hashedSeed(FabricClientCommandSource source) { + long hashedSeed = source.getWorld().getBiomeManager().biomeZoomSeed; + HashCode seedHash = HashCode.fromLong(hashedSeed); + source.sendFeedback(Component.translatable("commands.cserverseed.hashedSeed", ComponentUtils.copyOnClickText(seedHash.toString()), ComponentUtils.copyOnClickText(String.valueOf(seedHash.asLong())))); + return (int) hashedSeed; + } + + private static int fromStructureSeed(FabricClientCommandSource source, long structureSeed) throws CommandSyntaxException { + long hashedSeed = source.getWorld().getBiomeManager().biomeZoomSeed; + List seeds = WorldSeed.fromHash(structureSeed, hashedSeed); + if (seeds.isEmpty()) { + throw IMPOSSIBLE_SEED_COMBINATION_EXCEPTION.create(); + } + // Collision is extremely unlikely, so just call `getFirst` + long worldSeed = seeds.getFirst(); + source.sendFeedback(Component.translatable("commands.cserverseed.hashedSeed.fromStructureSeed", ComponentUtils.copyOnClickText(String.valueOf(worldSeed)))); + return (int) worldSeed; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/serverseed/ClientPacketListenerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/serverseed/ClientPacketListenerMixin.java new file mode 100644 index 00000000..fa0ae977 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/serverseed/ClientPacketListenerMixin.java @@ -0,0 +1,21 @@ +package net.earthcomputer.clientcommands.mixin.commands.serverseed; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.EnchantmentMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin { + @Inject(method = "handleContainerSetData", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/inventory/AbstractContainerMenu;setData(II)V", shift = At.Shift.AFTER)) + private void onContainerData(ClientboundContainerSetDataPacket packet, CallbackInfo ci, @Local Player player) { + if (player.containerMenu instanceof EnchantmentMenu menu) { + player.enchantmentSeed = menu.getEnchantmentSeed(); + } + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index a0f6884b..43f833ac 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -224,6 +224,12 @@ "commands.creply.messageTooLong": "Your reply was too long (maximum: %s, given: %s)", "commands.creply.noTargetFound": "Could not find a target to reply to", + "commands.cserverseed.enchantmentSeed": "Lower 16 bits of the enchantment seed: %s", + "commands.cserverseed.enchantmentSeedNotSent": "The server has not sent the partial enchantment seed yet!", + "commands.cserverseed.hashedSeed": "First 8 bytes of the hashed world seed: %s (decimal: %s)", + "commands.cserverseed.hashedSeed.fromStructureSeed": "Found world seed: %s", + "commands.cserverseed.hashedSeed.impossibleSeedCombination": "Impossible seed combination!", + "commands.csignsearch.starting": "Searching signs", "commands.csnap.airborne": "You cannot snap while airborne", diff --git a/src/main/resources/clientcommands.aw b/src/main/resources/clientcommands.aw index 7e78137d..8806ee53 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -24,6 +24,10 @@ accessible method net/minecraft/world/entity/projectile/FishingHook canHitEntity accessible method net/minecraft/world/entity/player/Inventory addResource (ILnet/minecraft/world/item/ItemStack;)I accessible method net/minecraft/world/entity/player/Inventory hasRemainingSpaceForItem (Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z +# cserverseed +accessible field net/minecraft/world/entity/player/Player enchantmentSeed I +accessible field net/minecraft/world/level/biome/BiomeManager biomeZoomSeed J + # chat accessible method net/minecraft/client/Minecraft openChatScreen (Ljava/lang/String;)V diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 3777cf45..70cf41fd 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -70,6 +70,7 @@ "commands.generic.CommandSuggestionsMixin", "commands.glow.LivingEntityRenderStateMixin", "commands.reply.ClientPacketListenerMixin", + "commands.serverseed.ClientPacketListenerMixin", "commands.snap.MinecraftMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin",