diff --git a/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java b/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java index cf5b82606..d22be46bd 100644 --- a/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java +++ b/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java @@ -19,10 +19,9 @@ public static void main(String[] args) throws IOException { } Path destDir = Path.of(args[0]); - genLattiCG(destDir); + genPlayerLattiCG(destDir); } - - private static void genLattiCG(Path destDir) throws IOException { + private static void genPlayerLattiCG(Path destDir) throws IOException { ProgramBuilder program = Program.builder(LCG.JAVA); program.skip(-CCrackRng.NUM_THROWS * 4); for (int i = 0; i < CCrackRng.NUM_THROWS; i++) { diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 07dd5c813..2fbb59887 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -169,6 +169,7 @@ public static void registerCommands(CommandDispatcher UsageTreeCommand.register(dispatcher); UuidCommand.register(dispatcher); VarCommand.register(dispatcher); + VillagerCommand.register(dispatcher, context); WeatherCommand.register(dispatcher); WhisperEncryptedCommand.register(dispatcher); WikiCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/Configs.java b/src/main/java/net/earthcomputer/clientcommands/Configs.java index ddccaee72..e15b24431 100644 --- a/src/main/java/net/earthcomputer/clientcommands/Configs.java +++ b/src/main/java/net/earthcomputer/clientcommands/Configs.java @@ -173,4 +173,13 @@ public enum PacketDumpMethod { @Config public static int maximumPacketFieldDepth = 10; + + @Config(temporary = true, setter = @Config.Setter("setMaxVillagerBruteForceSimulationCalls")) + public static int maxVillagerBruteForceSimulationCalls = 12000; + public static void setMaxVillagerBruteForceSimulationCalls(int maxVillagerBruteForceSimulationCalls) { + Configs.maxVillagerBruteForceSimulationCalls = Mth.clamp(maxVillagerBruteForceSimulationCalls, 0, 1_000_000); + } + + @Config(temporary = true) + public static int villagerAdjustment = 0; } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java index 676a39eb2..96e5a2103 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java @@ -12,6 +12,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import java.util.HashMap; @@ -101,4 +102,18 @@ public static String registerCode(Runnable code) { runnables.put(randomString, code); return randomString; } + + public static void updateOverlayProgressBar(int current, int total, int width, int time) { + MutableComponent builder = Component.empty(); + int color = Mth.hsvToRgb(current / (total * 3.0f), 1.0f, 1.0f); + builder.append(Component.literal("[").withColor(0xAAAAAA)); + builder.append(Component.literal("~" + Math.round(100.0 * current / total) + "%").withColor(color)); + builder.append(Component.literal("] ").withColor(0xAAAAAA)); + int filledWidth = (int) Math.round((double) width * current / total); + int unfilledWidth = width - filledWidth; + builder.append(Component.literal("|".repeat(filledWidth)).withColor(color)); + builder.append(Component.literal("|".repeat(unfilledWidth)).withColor(0xAAAAAA)); + + addOverlayMessage(builder, time); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java new file mode 100644 index 000000000..f4cd74b64 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java @@ -0,0 +1,357 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.datafixers.util.Pair; +import dev.xpple.clientarguments.arguments.CRangeArgument; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.features.FishingCracker; +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.earthcomputer.clientcommands.interfaces.IVillager; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.arguments.item.ItemParser; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.EnchantmentTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.*; +import static dev.xpple.clientarguments.arguments.CBlockPosArgument.*; +import static dev.xpple.clientarguments.arguments.CEntityArgument.*; +import static dev.xpple.clientarguments.arguments.CItemPredicateArgument.*; +import static dev.xpple.clientarguments.arguments.CRangeArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.ItemAndEnchantmentsPredicateArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.WithStringArgument.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class VillagerCommand { + private static final SimpleCommandExceptionType NOT_A_VILLAGER_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.notAVillager")); + private static final SimpleCommandExceptionType NO_CRACKED_VILLAGER_PRESENT_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.noCrackedVillagerPresent")); + private static final SimpleCommandExceptionType NO_PROFESSION_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.noProfession")); + private static final SimpleCommandExceptionType NOT_LEVEL_1_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.notLevel1")); + private static final SimpleCommandExceptionType NO_GOALS_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.listGoals.noGoals")); + private static final SimpleCommandExceptionType ALREADY_BRUTE_FORCING_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.alreadyBruteForcing")); + private static final Dynamic2CommandExceptionType INVALID_GOAL_INDEX_EXCEPTION = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("commands.cvillager.removeGoal.invalidIndex", a, b)); + private static final Dynamic2CommandExceptionType ITEM_QUANTITY_OUT_OF_RANGE_EXCEPTION = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("commands.cvillager.itemCountOutOfRange", a, b)); + private static final List goals = new ArrayList<>(); + + public static void register(CommandDispatcher dispatcher, CommandBuildContext context) { + dispatcher.register(literal("cvillager") + .then(literal("add-two-item-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-count", intRange()) + .then(argument("result-item", withString(itemPredicate(context))) + .then(argument("result-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "first-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "first-count"), null, null, getWithString(ctx, "result-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "result-count")))))))) + .then(literal("add-three-item-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-count", intRange()) + .then(argument("second-item", withString(itemPredicate(context))) + .then(argument("second-count", intRange()) + .then(argument("result-item", withString(itemPredicate(context))) + .then(argument("result-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "first-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "first-count"), getWithString(ctx, "second-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "second-count"), getWithString(ctx, "result-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "result-count")))))))))) + .then(literal("add-two-item-enchanted-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-count", intRange()) + .then(argument("result-item", withString(itemAndEnchantmentsPredicate(context).withEnchantmentPredicate((item, enchantment) -> enchantment.is(EnchantmentTags.TRADEABLE)))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "first-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "first-count"), null, null, getWithString(ctx, "result-item", ItemAndEnchantmentsPredicate.class), MinMaxBounds.Ints.between(1, 1))))))) + .then(literal("add-three-item-enchanted-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-count", intRange()) + .then(argument("second-item", withString(itemPredicate(context))) + .then(argument("second-count", intRange()) + .then(argument("result-item", withString(itemAndEnchantmentsPredicate(context).withEnchantmentPredicate((item, enchantment) -> enchantment.is(EnchantmentTags.TRADEABLE)))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "first-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "first-count"), getWithString(ctx, "second-item", CItemStackPredicateArgument.class), CRangeArgument.Ints.getRangeArgument(ctx, "second-count"), getWithString(ctx, "result-item", ItemAndEnchantmentsPredicate.class), MinMaxBounds.Ints.between(1, 1))))))))) + .then(literal("list-goals") + .executes(ctx -> listGoals(ctx.getSource()))) + .then(literal("remove-goal") + .then(argument("index", integer(1)) + .executes(ctx -> removeGoal(ctx.getSource(), getInteger(ctx, "index"))))) + .then(literal("target") + .executes(ctx -> setVillagerTarget(null)) + .then(argument("entity", entity()) + .executes(ctx -> setVillagerTarget(getEntity(ctx, "entity"))))) + .then(literal("clock") + .executes(ctx -> getClockPos()) + .then(argument("pos", blockPos()) + .executes(ctx -> setClockPos(ctx.getSource(), getBlockPos(ctx, "pos"))))) + .then(literal("reset-cracker") + .executes(ctx -> resetCracker())) + .then(literal("brute-force") + .executes(ctx -> bruteForce(false)) + .then(literal("first-level") + .executes(ctx -> bruteForce(false))) + .then(literal("next-level") + .executes(ctx -> bruteForce(true))))); + } + + private static int addGoal(FabricClientCommandSource ctx, Result> first, MinMaxBounds.Ints firstCount, @Nullable Result> second, @Nullable MinMaxBounds.Ints secondCount, Result> result, MinMaxBounds.Ints resultCount) throws CommandSyntaxException { + HolderLookup.Provider registries = ctx.getWorld().registryAccess(); + String firstString = displayPredicate(first, firstCount, registries); + String secondString = second == null || secondCount == null ? null : displayPredicate(second, secondCount, registries); + String resultString = displayPredicate(result, resultCount, registries); + + goals.add(new Goal( + firstString, + item -> first.value().test(item) && firstCount.matches(item.getCount()), + + secondString, + second == null || secondCount == null ? null : item -> second.value().test(item) && secondCount.matches(item.getCount()), + + resultString, + item -> result.value().test(item) && resultCount.matches(item.getCount()))); + + ctx.sendFeedback(Component.translatable("commands.cvillager.goalAdded")); + return Command.SINGLE_SUCCESS; + } + + private static int listGoals(FabricClientCommandSource source) { + if (goals.isEmpty()) { + source.sendFeedback(Component.translatable("commands.cvillager.listGoals.noGoals").withStyle(style -> style.withColor(ChatFormatting.RED))); + } else { + if (goals.size() == 1) { + source.sendFeedback(Component.translatable("commands.cvillager.listGoals.success.one")); + } else { + source.sendFeedback(Component.translatable("commands.cvillager.listGoals.success", FishingCracker.goals.size() + 1)); + } + for (int i = 0; i < goals.size(); i++) { + Goal goal = goals.get(i); + source.sendFeedback(Component.literal((i + 1) + ": " + goal.toString())); + } + } + return Command.SINGLE_SUCCESS; + } + + private static int removeGoal(FabricClientCommandSource source, int index) throws CommandSyntaxException { + index = index - 1; + if (index < goals.size()) { + Goal goal = goals.remove(index); + source.sendFeedback(Component.translatable("commands.cvillager.removeGoal.success", goal.toString())); + } else { + throw INVALID_GOAL_INDEX_EXCEPTION.create(index + 1, goals.size()); + } + return Command.SINGLE_SUCCESS; + } + + private static int setVillagerTarget(@Nullable Entity target) throws CommandSyntaxException { + if (target instanceof Villager villager) { + VillagerCracker.setTargetVillager(villager); + ClientCommandHelper.sendFeedback("commands.cvillager.target.set"); + } else if (target == null) { + VillagerCracker.setTargetVillager(null); + ClientCommandHelper.sendFeedback("commands.cvillager.target.cleared"); + } else { + throw NOT_A_VILLAGER_EXCEPTION.create(); + } + + return Command.SINGLE_SUCCESS; + } + + private static int getClockPos() { + GlobalPos pos = VillagerCracker.getClockPos(); + if (pos == null) { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.cleared"); + } else { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set", pos.pos().getX(), pos.pos().getY(), pos.pos().getZ(), pos.dimension().location()); + } + return Command.SINGLE_SUCCESS; + } + + private static int setClockPos(FabricClientCommandSource ctx, BlockPos pos) { + ResourceKey dimension = ctx.getWorld().dimension(); + VillagerCracker.setClockPos(pos == null ? null : new GlobalPos(dimension, pos)); + if (pos == null) { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set.cleared"); + } else { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set", pos.getX(), pos.getY(), pos.getZ(), dimension.location()); + } + return Command.SINGLE_SUCCESS; + } + + private static int resetCracker() { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager instanceof IVillager iVillager) { + iVillager.clientcommands_getVillagerRngSimulator().reset(); + ClientCommandHelper.sendFeedback("commands.cvillager.resetCracker"); + } + + return Command.SINGLE_SUCCESS; + } + + private static int bruteForce(boolean levelUp) throws CommandSyntaxException { + Villager targetVillager = VillagerCracker.getVillager(); + + if (goals.isEmpty()) { + throw NO_GOALS_EXCEPTION.create(); + } + + if (!(targetVillager instanceof IVillager iVillager) || !iVillager.clientcommands_getVillagerRngSimulator().getCrackedState().isCracked()) { + throw NO_CRACKED_VILLAGER_PRESENT_EXCEPTION.create(); + } + + VillagerProfession profession = targetVillager.getVillagerData().getProfession(); + if (profession == VillagerProfession.NONE) { + throw NO_PROFESSION_EXCEPTION.create(); + } + + if (iVillager.clientcommands_getVillagerRngSimulator().isCracking()) { + throw ALREADY_BRUTE_FORCING_EXCEPTION.create(); + } + + int currentLevel = targetVillager.getVillagerData().getLevel(); + if (!levelUp && currentLevel != 1) { + throw NOT_LEVEL_1_EXCEPTION.create(); + } + + int crackedLevel = levelUp ? currentLevel + 1 : currentLevel; + + VillagerTrades.ItemListing[] listings = VillagerTrades.TRADES.get(profession).getOrDefault(crackedLevel, new VillagerTrades.ItemListing[0]); + int adjustmentTicks = 1 + (levelUp ? -40 : 0); + Pair pair = iVillager.clientcommands_getVillagerRngSimulator().bruteForceOffers(listings, levelUp ? 240 : 10, Configs.maxVillagerBruteForceSimulationCalls, offer -> VillagerCommand.goals.stream().anyMatch(goal -> goal.matches(offer))).mapFirst(x -> x + adjustmentTicks * 2); + int calls = pair.getFirst(); + Offer offer = pair.getSecond(); + if (calls < 0) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.bruteForce.failed", Configs.maxVillagerBruteForceSimulationCalls).withStyle(ChatFormatting.RED), 100); + } else { + String price; + if (offer.second() == null) { + price = displayText(offer.first(), false); + } else { + price = displayText(offer.first(), false) + " + " + displayText(offer.second(), false); + } + ClientCommandHelper.sendFeedback(Component.translatable("commands.cvillager.bruteForce.success", displayText(offer.result(), false), price, calls).withStyle(ChatFormatting.GREEN)); + VillagerCracker.targetOffer = offer; + iVillager.clientcommands_getVillagerRngSimulator().setCallsUntilToggleGui(calls); + } + + return Command.SINGLE_SUCCESS; + } + + public record Goal(String firstString, Predicate first, @Nullable String secondString, @Nullable Predicate second, String resultString, Predicate result) { + public boolean matches(Offer offer) { + return first.test(offer.first) + && ((second == null && offer.second == null) || offer.second != null && second != null && second.test(offer.second)) + && result.test(offer.result); + } + + @Override + public String toString() { + if (secondString == null) { + return String.format("%s = %s", firstString, resultString); + } else { + return String.format("%s + %s = %s", firstString, secondString, resultString); + } + } + } + + public record Offer(ItemStack first, @Nullable ItemStack second, ItemStack result) { + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Offer other)) { + return false; + } + return ItemStack.isSameItemSameComponents(this.first, other.first) && this.first.getCount() == other.first.getCount() + && (this.second == other.second || this.second != null && other.second != null && ItemStack.isSameItemSameComponents(this.second, other.second) && this.second.getCount() == other.second.getCount()) + && ItemStack.isSameItemSameComponents(this.result, other.result) && this.result.getCount() == other.result.getCount(); + } + + @Override + public String toString() { + if (second == null) { + return String.format("%s = %s", displayText(first, false), displayText(result, false)); + } else { + return String.format("%s + %s = %s", displayText(first, false), displayText(second, false), displayText(result, false)); + } + } + } + + private static String displayPredicate(Result> item, MinMaxBounds.Ints count, HolderLookup.Provider registries) throws CommandSyntaxException { + String name; + int maxCount = 64; + if (item.value() instanceof ItemAndEnchantmentsPredicate itemAndEnchantmentsPredicate) { + name = item.value().toString(); + maxCount = itemAndEnchantmentsPredicate.item().getDefaultMaxStackSize(); + } else { + try { + ItemParser.ItemResult firstItemResult = new ItemParser(registries).parse(new StringReader(item.string())); + name = displayText(new ItemStack(firstItemResult.item(), 1, firstItemResult.components()), true); + maxCount = firstItemResult.item().value().getDefaultMaxStackSize(); + } catch (CommandSyntaxException e) { + name = item.string(); + } + } + + @Nullable + String rangeString = displayRange(maxCount, count); + if (rangeString == null) { + throw ITEM_QUANTITY_OUT_OF_RANGE_EXCEPTION.create(count.min().map(Object::toString).orElse("") + ".." + count.max().map(Object::toString).orElse(""), maxCount); + } + + return rangeString + name; + } + + @Nullable + public static String displayRange(int maxCount, MinMaxBounds.Ints range) { + if (range.max().isPresent() && range.max().get() > maxCount || range.min().isPresent() && range.min().get() > maxCount) { + return null; + } + + if (maxCount == 1) { + return ""; + } + + String string = ""; + if ((range.min().isEmpty() || range.min().get() == 1) && (range.max().isPresent() && range.max().get() == maxCount)) { + string = "*"; + } else if (range.min().equals(range.max()) && range.min().isPresent()) { + string = range.min().get().toString(); + } else { + if (range.min().isPresent()) { + string = string + range.min().get(); + } + if (!string.equals(" ") || range.max().isPresent()) { + string = string + ".."; + } + if (range.max().isPresent()) { + string = string + range.max().get(); + } + } + + return string; + } + + public static String displayText(ItemStack stack, boolean hideCount) { + String quantityPrefix = hideCount || stack.getCount() == 1 ? "" : stack.getCount() + " "; + List lines = stack.getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL); + String itemDescription = lines.stream().skip(1).map(Component::getString).collect(Collectors.joining(", ")); + if (lines.size() == 1) { + return quantityPrefix + lines.getFirst().getString(); + } else { + return quantityPrefix + lines.getFirst().getString() + " (" + itemDescription + ")"; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java index 32812c0a4..0119b26c6 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java @@ -9,6 +9,7 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.earthcomputer.clientcommands.command.VillagerCommand; import net.earthcomputer.clientcommands.util.MultiVersionCompat; import net.minecraft.advancements.critereon.MinMaxBounds; import net.minecraft.commands.SharedSuggestionProvider; @@ -38,6 +39,9 @@ import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class ItemAndEnchantmentsPredicateArgument implements ArgumentType { @@ -126,7 +130,7 @@ public ItemAndEnchantmentsPredicate parse(StringReader reader) throws CommandSyn return true; }; - return new ItemAndEnchantmentsPredicate(parser.item, predicate, parser.with.size()); + return new ItemAndEnchantmentsPredicate(parser.item, predicate, parser.with, parser.without, parser.exact, parser.ordered); } @Override @@ -152,17 +156,41 @@ public Collection getExamples() { return EXAMPLES; } - public record ItemAndEnchantmentsPredicate(Item item, Predicate> predicate, int numEnchantments) implements Predicate { + public record ItemAndEnchantmentsPredicate(Item item, Predicate> predicate, List with, List without, boolean exact, boolean ordered) implements Predicate { @Override public boolean test(ItemStack stack) { if (item != stack.getItem() && (item != Items.BOOK || stack.getItem() != Items.ENCHANTED_BOOK)) { return false; } - List enchantments = stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream() + List enchantments = Stream.concat(stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream(), stack.getOrDefault(DataComponents.STORED_ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream()) .map(entry -> new EnchantmentInstance(entry.getKey(), entry.getIntValue())) .toList(); return predicate.test(enchantments); } + + @Override + public String toString() { + String itemName = item.getName(new ItemStack(item)).getString(); + + StringBuilder flagsBuilder = new StringBuilder(); + if (exact) { + flagsBuilder.append("Exactly"); + } + if (ordered) { + flagsBuilder.append("Ordered"); + } + String flags = flagsBuilder.isEmpty() ? "" : " [" + flagsBuilder + ')'; + + String enchantments = Stream.concat( + with.stream().filter(enchantment -> enchantment.enchantment().unwrapKey().isPresent()).map(EnchantmentInstancePredicate::toString), + without.stream().filter(enchantment -> enchantment.enchantment().unwrapKey().isPresent()).map(enchantment -> "!" + enchantment) + ).collect(Collectors.joining(", ")); + if (!enchantments.isEmpty()) { + enchantments = '(' + enchantments + ')'; + } + + return String.format("%s%s %s", itemName, flags, enchantments); + } } private class Parser { @@ -455,10 +483,15 @@ private void suggestOption() { } } - private record EnchantmentInstancePredicate(Holder enchantment, MinMaxBounds.Ints level) implements Predicate { + public record EnchantmentInstancePredicate(Holder enchantment, MinMaxBounds.Ints level) implements Predicate { @Override public boolean test(EnchantmentInstance enchInstance) { return enchantment.equals(enchInstance.enchantment) && level.matches(enchInstance.level); } + + @Override + public String toString() { + return Component.translatable(enchantment.unwrapKey().get().location().toLanguageKey("enchantment")).getString() + " " + Objects.requireNonNullElse(VillagerCommand.displayRange(enchantment.value().getMaxLevel(), level), "*"); + } } } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java index 8c5043167..6d8097a90 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java @@ -8,6 +8,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; import net.earthcomputer.clientcommands.util.MultiVersionCompat; import net.earthcomputer.clientcommands.task.ItemThrowTask; import net.earthcomputer.clientcommands.task.LongTask; @@ -30,7 +31,6 @@ import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; @@ -113,7 +113,6 @@ public class EnchantmentCracker { */ public static final Logger LOGGER = LogUtils.getLogger(); - private static final int PROGRESS_BAR_WIDTH = 50; // RENDERING /* @@ -480,17 +479,7 @@ public void onCompleted() { @Override protected void onItemThrown(int current, int total) { - MutableComponent builder = Component.empty(); - int color = Mth.hsvToRgb(current / (total * 3.0f), 1.0f, 1.0f); - builder.append(Component.literal("[").withColor(0xAAAAAA)); - builder.append(Component.literal("~" + Math.round(100.0 * current / total) + "%").withColor(color)); - builder.append(Component.literal("] ").withColor(0xAAAAAA)); - int filledWidth = (int) Math.round((double) PROGRESS_BAR_WIDTH * current / total); - int unfilledWidth = PROGRESS_BAR_WIDTH - filledWidth; - builder.append(Component.literal("|".repeat(filledWidth)).withColor(color)); - builder.append(Component.literal("|".repeat(unfilledWidth)).withColor(0xAAAAAA)); - - Minecraft.getInstance().gui.setOverlayMessage(builder, false); + ClientCommandHelper.updateOverlayProgressBar(current, total, 50, 60); } }); } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java index fd7865a32..266faeca3 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java @@ -425,7 +425,7 @@ record ErrorEntry(int tick, boolean isBox) {} LootTable fishingLootTable = MCLootTables.FISHING.get().apply(SeedfindingUtil.getMCVersion()); for (var goal : goals) { if (goal.value() instanceof ClientItemPredicateArgument.EnchantedItemPredicate predicate) { - if (predicate.isEnchantedBook() && predicate.predicate.numEnchantments() >= 2) { + if (predicate.isEnchantedBook() && predicate.predicate.with().size() >= 2) { if (!hasWarnedMultipleEnchants) { ClientCommandHelper.sendHelp(Component.translatable("commands.cfish.help.tooManyEnchants")); hasWarnedMultipleEnchants = true; diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java new file mode 100644 index 000000000..b869b79e4 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java @@ -0,0 +1,129 @@ +package net.earthcomputer.clientcommands.features; + +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.VillagerCommand; +import net.earthcomputer.clientcommands.interfaces.IVillager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundAddExperienceOrbPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.npc.Villager; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.UUID; + +public class VillagerCracker { + // This value was computed by brute forcing all seeds + public static final float MAX_ERROR = 5 * 0x1.0p-24f; + + @Nullable + private static UUID villagerUuid = null; + @Nullable + private static WeakReference cachedVillager = null; + @Nullable + private static GlobalPos clockPos = null; + @Nullable + public static VillagerCommand.Offer targetOffer = null; + @Nullable + private static Long lastServerTick = null; + private static boolean receivedClockRateWarning = false; + + @Nullable + public static Villager getVillager() { + if (villagerUuid == null) { + cachedVillager = null; + return null; + } + if (cachedVillager != null) { + Villager villager = cachedVillager.get(); + if (villager != null && !villager.isRemoved()) { + return villager; + } + } + ClientLevel level = Minecraft.getInstance().level; + if (level != null) { + for (Entity entity : level.entitiesForRendering()) { + if (entity.getUUID() == villagerUuid && entity instanceof Villager villager) { + cachedVillager = new WeakReference<>(villager); + return villager; + } + } + } + return null; + } + + @Nullable + public static GlobalPos getClockPos() { + return clockPos; + } + + public static void setTargetVillager(@Nullable Villager villager) { + Villager oldVillager = getVillager(); + if (oldVillager != null) { + ((IVillager) oldVillager).clientcommands_getVillagerRngSimulator().reset(); + } + + if (clockPos == null) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.noClock")); + } + + ClientLevel level = Minecraft.getInstance().level; + + if (level.getDayTime() % 24000 < 12000) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.day")); + } + + VillagerCracker.cachedVillager = new WeakReference<>(villager); + VillagerCracker.villagerUuid = villager == null ? null : villager.getUUID(); + } + + public static void setClockPos(@Nullable GlobalPos pos) { + VillagerCracker.clockPos = pos; + } + + public static void onSoundEventPlayed(ClientboundSoundPacket packet) { + Villager targetVillager = getVillager(); + if (targetVillager == null || getClockPos() == null) { + return; + } + + switch (packet.getSound().value().getLocation().toString()) { + case "minecraft:entity.villager.ambient", "minecraft:entity.villager.trade" -> ((IVillager) targetVillager).clientcommands_onAmbientSoundPlayed(packet.getPitch()); + case "minecraft:entity.villager.no" -> ((IVillager) targetVillager).clientcommands_onNoSoundPlayed(packet.getPitch()); + case "minecraft:entity.villager.yes" -> ((IVillager) targetVillager).clientcommands_onYesSoundPlayed(packet.getPitch()); + case "minecraft:entity.generic.splash" -> ((IVillager) targetVillager).clientcommands_onSplashSoundPlayed(packet.getPitch()); + } + } + + public static void onXpOrbSpawned(ClientboundAddExperienceOrbPacket packet) { + Villager targetVillager = getVillager(); + if (targetVillager == null) { + return; + } + + ((IVillager) targetVillager).clientcommands_onXpOrbSpawned(packet.getValue()); + } + + public static void onServerTick() { + long now = System.currentTimeMillis(); + + Villager targetVillager = getVillager(); + if (targetVillager == null) { + lastServerTick = now; + return; + } + + if (lastServerTick != null && now - lastServerTick > 80L && !receivedClockRateWarning) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.tooSlow")); + receivedClockRateWarning = true; + } + + ((IVillager) targetVillager).clientcommands_onServerTick(); + + lastServerTick = now; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java new file mode 100644 index 000000000..cea183765 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java @@ -0,0 +1,492 @@ +package net.earthcomputer.clientcommands.features; + +import com.mojang.datafixers.util.Pair; +import com.seedfinding.latticg.math.component.BigFraction; +import com.seedfinding.latticg.math.component.BigMatrix; +import com.seedfinding.latticg.math.component.BigVector; +import com.seedfinding.latticg.math.lattice.enumerate.EnumerateRt; +import com.seedfinding.latticg.math.optimize.Optimize; +import com.seedfinding.mcseed.rand.JRand; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.VillagerCommand; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.LongStream; + +public class VillagerRngSimulator { + private static final BigMatrix[] LATTICES; + private static final BigMatrix[] INVERSE_LATTICES; + private static final BigVector[] OFFSETS; + + @Nullable + private JRand random; + private long prevRandomSeed = 0; + private int ambientSoundTime = -80; + private int prevAmbientSoundTime = -80; + private boolean madeSound = false; + private int totalAmbientSounds = 0; + private int callsAtStartOfBruteForce = 0; + private int callsInBruteForce = 0; + private int totalCalls = 0; + private int prevTotalCalls = 0; + private float firstPitch = Float.NaN; + private int ticksBetweenSounds = 0; + private float secondPitch = Float.NaN; + @Nullable + private long[] seedsFromTwoPitches = null; + + static { + try { + CompoundTag root = NbtIo.read(new DataInputStream(Objects.requireNonNull(VillagerRngSimulator.class.getResourceAsStream("/villager_lattice_data.nbt")))); + ListTag lattices = root.getList("lattices", Tag.TAG_LONG_ARRAY); + LATTICES = new BigMatrix[lattices.size()]; + ListTag lattice_inverses = root.getList("lattice_inverses", Tag.TAG_LONG_ARRAY); + INVERSE_LATTICES = new BigMatrix[lattices.size()]; + ListTag offsets = root.getList("offsets", Tag.TAG_LONG_ARRAY); + OFFSETS = new BigVector[offsets.size()]; + for (int i = 0; i < lattices.size(); i++) { + long[] lattice = lattices.getLongArray(i); + BigMatrix matrix = new BigMatrix(3, 3); + matrix.set(0, 0, new BigFraction(lattice[0])); + matrix.set(0, 1, new BigFraction(lattice[1])); + matrix.set(0, 2, new BigFraction(lattice[2])); + matrix.set(1, 0, new BigFraction(lattice[3])); + matrix.set(1, 1, new BigFraction(lattice[4])); + matrix.set(1, 2, new BigFraction(lattice[5])); + matrix.set(2, 0, new BigFraction(lattice[6])); + matrix.set(2, 1, new BigFraction(lattice[7])); + matrix.set(2, 2, new BigFraction(lattice[8])); + LATTICES[i] = matrix; + } + for (int i = 0; i < lattice_inverses.size(); i++) { + long[] lattice_inverse = lattice_inverses.getLongArray(i); + BigMatrix matrix = new BigMatrix(3, 3); + matrix.set(0, 0, new BigFraction(lattice_inverse[0], 1L << 48)); + matrix.set(0, 1, new BigFraction(lattice_inverse[1], 1L << 48)); + matrix.set(0, 2, new BigFraction(lattice_inverse[2], 1L << 48)); + matrix.set(1, 0, new BigFraction(lattice_inverse[3], 1L << 48)); + matrix.set(1, 1, new BigFraction(lattice_inverse[4], 1L << 48)); + matrix.set(1, 2, new BigFraction(lattice_inverse[5], 1L << 48)); + matrix.set(2, 0, new BigFraction(lattice_inverse[6], 1L << 48)); + matrix.set(2, 1, new BigFraction(lattice_inverse[7], 1L << 48)); + matrix.set(2, 2, new BigFraction(lattice_inverse[8], 1L << 48)); + INVERSE_LATTICES[i] = matrix; + } + for (int i = 0; i < offsets.size(); i++) { + long[] offset = offsets.getLongArray(i); + OFFSETS[i] = new BigVector(0, offset[0], offset[1]); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public VillagerRngSimulator(@Nullable JRand random) { + this.random = random; + } + + public VillagerRngSimulator copy() { + VillagerRngSimulator that = new VillagerRngSimulator(random == null ? null : random.copy()); + that.ambientSoundTime = this.ambientSoundTime; + that.prevAmbientSoundTime = this.prevAmbientSoundTime; + that.madeSound = this.madeSound; + that.totalAmbientSounds = this.totalAmbientSounds; + that.totalCalls = this.totalCalls; + that.prevTotalCalls = this.prevTotalCalls; + return that; + } + + public void simulateTick() { + // called on receiving clock packet at the beginning of the tick, simulates the rest of the tick + + if (random == null) { + ambientSoundTime++; + return; + } + + prevRandomSeed = random.getSeed(); + prevAmbientSoundTime = ambientSoundTime; + prevTotalCalls = totalCalls; + + simulateBaseTick(); + simulateServerAiStep(); + + if (callsInBruteForce > 0) { + updateProgressBar(); + } + } + + private void revertSimulatedTick() { + random.setSeed(prevRandomSeed, false); + ambientSoundTime = prevAmbientSoundTime; + totalCalls = prevTotalCalls; + } + + public boolean shouldInteractWithVillager() { + boolean shouldInteractWithVillager = totalCalls - callsAtStartOfBruteForce >= callsInBruteForce - Configs.villagerAdjustment * 2 && callsInBruteForce > 0; + if (shouldInteractWithVillager) { + reset(); + } + return shouldInteractWithVillager; + } + + private void simulateBaseTick() { + // we have the server receiving ambient noise tell us if we have to do this to increment the random, this is so that our ambient sound time is synced up. + totalCalls += 1; + if (random.nextInt(1000) < ambientSoundTime++ && totalAmbientSounds > 0) { + random.nextFloat(); + random.nextFloat(); + totalCalls += 2; + ambientSoundTime = -80; + madeSound = true; + } else { + madeSound = false; + } + } + + private void simulateServerAiStep() { + random.nextInt(100); + totalCalls += 1; + } + + public void updateProgressBar() { + ClientCommandHelper.updateOverlayProgressBar(Math.min(callsInBruteForce - Configs.villagerAdjustment * 2, totalCalls - callsAtStartOfBruteForce), callsInBruteForce - Configs.villagerAdjustment * 2, 50, 60); + } + + @Nullable + public VillagerCommand.Offer anyOffersMatch(VillagerTrades.ItemListing[] listings, Entity trader, Predicate predicate) { + if (!getCrackedState().isCracked()) { + return null; + } + + RandomSource rand = new LegacyRandomSource(random.getSeed() ^ 0x5deece66dL);; + + ArrayList newListings = new ArrayList<>(List.of(listings)); + int i = 0; + while (i < 2 && !newListings.isEmpty()) { + VillagerTrades.ItemListing listing = newListings.remove(rand.nextInt(newListings.size())); + MerchantOffer offer = listing.getOffer(trader, rand); + if (offer != null) { + VillagerCommand.Offer x = new VillagerCommand.Offer(offer.getBaseCostA(), offer.getItemCostB().map(ItemCost::itemStack).orElse(null), offer.getResult()); + if (predicate.test(x)) { + return x; + } else { + i++; + } + } + } + return null; + } + + public void setCallsUntilToggleGui(int calls) { + callsAtStartOfBruteForce = totalCalls; + callsInBruteForce = calls; + } + + public int getTotalCalls() { + return totalCalls; + } + + public CrackedState getCrackedState() { + if (random == null || totalAmbientSounds < 2) { + return CrackedState.UNCRACKED; + } + + return CrackedState.CRACKED; + } + + public boolean isCracking() { + return callsInBruteForce > 0; + } + + public void reset() { + random = null; + prevRandomSeed = 0; + prevAmbientSoundTime = 0; + prevTotalCalls = 0; + totalAmbientSounds = 0; + totalCalls = 0; + callsAtStartOfBruteForce = 0; + callsInBruteForce = 0; + firstPitch = Float.NaN; + ticksBetweenSounds = 0; + secondPitch = Float.NaN; + seedsFromTwoPitches = null; + } + + @Override + public String toString() { + return "VillagerRngSimulator[seed=" + (random == null ? "null" : random.getSeed()) + ']'; + } + + public void onAmbientSoundPlayed(float pitch) { + boolean justReset = false; + if (totalAmbientSounds == 2 && !madeSound) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.ambient").withStyle(ChatFormatting.RED), 100); + reset(); + justReset = true; + } + + if (totalAmbientSounds == 0) { + totalAmbientSounds++; + firstPitch = pitch; + ambientSoundTime = -80; + if (!justReset) { + ClientCommandHelper.addOverlayMessage(((MutableComponent) getCrackedState().getMessage(false)).withStyle(ChatFormatting.RED), 100); + } + return; + } + + if (totalAmbientSounds == 1) { + totalAmbientSounds++; + ticksBetweenSounds = ambientSoundTime - (-80); + secondPitch = pitch; + ambientSoundTime = -80; + + if (seedsFromTwoPitches != null) { + int matchingSeeds = 0; + long matchingSeed = 0; + nextSeed: for (long seed : seedsFromTwoPitches) { + JRand rand = JRand.ofInternalSeed(seed); + rand.nextInt(100); + for (int i = -80; i < ticksBetweenSounds - 80 - 1; i++) { + if (rand.nextInt(1000) < i) { + continue nextSeed; + } + rand.nextInt(100); + } + if (rand.nextInt(1000) >= ticksBetweenSounds - 80 - 1) { + continue; + } + float simulatedThirdPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + if (simulatedThirdPitch == pitch) { + matchingSeeds++; + matchingSeed = rand.getSeed(); + } + } + seedsFromTwoPitches = null; + if (matchingSeeds == 1) { + random = JRand.ofInternalSeed(matchingSeed); + random.nextInt(100); + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.success", Long.toHexString(matchingSeed)).withStyle(ChatFormatting.GREEN), 100); + return; + } + } + + long[] seeds = crackSeed(); + if (seeds.length == 1) { + random = JRand.ofInternalSeed(seeds[0]); + random.nextInt(100); + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.success", Long.toHexString(seeds[0])).withStyle(ChatFormatting.GREEN), 100); + } else { + totalAmbientSounds = 1; + firstPitch = pitch; + secondPitch = Float.NaN; + seedsFromTwoPitches = seeds.length > 0 ? seeds : null; + ambientSoundTime = -80; + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.failed", seeds.length).withStyle(ChatFormatting.RED), 100); + } + } + } + + public void onNoSoundPlayed(float pitch, boolean fromGuiInteract) { + // the last received action before the next tick's clock + // played both when interacting with a villager without a profession and when using the villager gui + + if (random != null) { + totalCalls += 2; + if (fromGuiInteract) { + ambientSoundTime = -80; + } + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.2f + 1.0f; + if (pitch != simulatedPitch) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.no").withStyle(ChatFormatting.RED), 100); + reset(); + } + } + } + + public void onYesSoundPlayed(float pitch) { + // the last received action before the next tick's clock + // played when using the villager gui + + if (random != null) { + totalCalls += 2; + ambientSoundTime = -80; + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.2f + 1.0f; + if (pitch != simulatedPitch) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.yes").withStyle(ChatFormatting.RED), 100); + reset(); + } + } + } + + public void onSplashSoundPlayed(float pitch) { + // the first received action after this tick's clock + + if (random != null) { + // simulateTick() was already called for this tick assuming no splash happened, so revert it and rerun it with the splash + revertSimulatedTick(); + + totalCalls += 2; + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.4f + 1.0f; + if (pitch != simulatedPitch) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.splash").withStyle(ChatFormatting.RED), 100); + reset(); + return; + } + + int iterations = Mth.ceil(1.0f + EntityType.VILLAGER.getDimensions().width() * 20.0f); + totalCalls += iterations * 10; + random.advance(iterations * 10L); + + simulateTick(); + } + } + + public void onXpOrbSpawned(int value) { + // the last received action before the next tick's clock + + if (random != null) { + totalCalls += 1; + ambientSoundTime = -80; + int simulatedValue = 3 + this.random.nextInt(4); + boolean leveledUp = value > 3 + 3; + if (leveledUp) simulatedValue += 5; + if (value != simulatedValue) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.xpOrb").withStyle(ChatFormatting.RED), 100); + reset(); + } + } + } + + public Pair bruteForceOffers(VillagerTrades.ItemListing[] listings, int minTicks, int maxCalls, Predicate predicate) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null && getCrackedState().isCracked()) { + VillagerRngSimulator rng = this.copy(); + int startingCalls = rng.getTotalCalls(); + + for (int i = 0; i < minTicks; i++) { + rng.simulateTick(); + } + + while (rng.getTotalCalls() < maxCalls + startingCalls) { + VillagerRngSimulator randomBranch = rng.copy(); + randomBranch.simulateTick(); + VillagerCommand.Offer offer = randomBranch.anyOffersMatch(listings, targetVillager, predicate); + if (offer != null) { + // we do the calls before this ticks processing so that since with 0ms ping, the server reads it next tick + return Pair.of(rng.getTotalCalls() - startingCalls, offer); + } + rng.simulateTick(); + } + } + + return Pair.of(-1_000_000, null); + } + + public long[] crackSeed() { + if (!(80 <= ticksBetweenSounds && ticksBetweenSounds - 80 < LATTICES.length)) { + return new long[0]; + } + + BigMatrix lattice = LATTICES[ticksBetweenSounds - 80]; + BigMatrix inverseLattice = INVERSE_LATTICES[ticksBetweenSounds - 80]; + BigVector offset = OFFSETS[ticksBetweenSounds - 80]; + + float firstMin = Math.max(-1.0f + 0x1.0p-24f, (firstPitch - 1.0f) / 0.2f - VillagerCracker.MAX_ERROR); + float firstMax = Math.min(1.0f - 0x1.0p-24f, (firstPitch - 1.0f) / 0.2f + VillagerCracker.MAX_ERROR); + float secondMin = Math.max(-1.0f + 0x1.0p-24f, (secondPitch - 1.0f) / 0.2f - VillagerCracker.MAX_ERROR); + float secondMax = Math.min(1.0f - 0x1.0p-24f, (secondPitch - 1.0f) / 0.2f + VillagerCracker.MAX_ERROR); + + firstMax = Math.nextUp(firstMax); + secondMax = Math.nextUp(secondMax); + + long firstMinLong = (long) Math.ceil(firstMin * 0x1.0p24f); + long firstMaxLong = (long) Math.ceil(firstMax * 0x1.0p24f) - 1; + long secondMinLong = (long) Math.ceil(secondMin * 0x1.0p24f); + long secondMaxLong = (long) Math.ceil(secondMax * 0x1.0p24f) - 1; + + long firstMinSeedDiff = (firstMinLong << 24) - 0xFFFFFF; + long firstMaxSeedDiff = (firstMaxLong << 24) + 0xFFFFFF; + long secondMinSeedDiff = (secondMinLong << 24) - 0xFFFFFF; + long secondMaxSeedDiff = (secondMaxLong << 24) + 0xFFFFFF; + + long firstCombinationModMin = firstMinSeedDiff & 0xFFFFFFFFFFFFL; + long firstCombinationModMax = firstMaxSeedDiff & 0xFFFFFFFFFFFFL; + long secondCombinationModMin = secondMinSeedDiff & 0xFFFFFFFFFFFFL; + long secondCombinationModMax = secondMaxSeedDiff & 0xFFFFFFFFFFFFL; + + firstCombinationModMax = firstCombinationModMax < firstCombinationModMin ? firstCombinationModMax + (1L << 48) : firstCombinationModMax; + secondCombinationModMax = secondCombinationModMax < secondCombinationModMin ? secondCombinationModMax + (1L << 48) : secondCombinationModMax; + + Optimize optimize = Optimize.Builder.ofSize(3) + .withLowerBound(0, 0) + .withUpperBound(0, 0xFFFFFFFFFFFFL) + .withLowerBound(1, firstCombinationModMin) + .withUpperBound(1, firstCombinationModMax) + .withLowerBound(2, secondCombinationModMin) + .withUpperBound(2, secondCombinationModMax) + .build(); + + return EnumerateRt.enumerate(lattice, offset, optimize, inverseLattice, inverseLattice.multiply(offset)).mapToLong(vec -> vec.get(0).getNumerator().longValue() & ((1L << 48) - 1)).flatMap(seed -> { + JRand rand = JRand.ofInternalSeed(seed); + float simulatedFirstPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + rand.nextInt(100); + for (int i = -80; i < ticksBetweenSounds - 80 - 1; i++) { + if (rand.nextInt(1000) < i) { + return LongStream.empty(); + } + rand.nextInt(100); + } + if (rand.nextInt(1000) >= ticksBetweenSounds - 80 - 1) { + return LongStream.empty(); + } + float simulatedSecondPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + if (simulatedFirstPitch == firstPitch && simulatedSecondPitch == secondPitch) { + return LongStream.of(rand.getSeed()); + } else { + return LongStream.empty(); + } + }).toArray(); + } + + public enum CrackedState { + UNCRACKED, + CRACKED; + + public boolean isCracked() { + return this == CRACKED; + } + + public Component getMessage(boolean addColor) { + return switch (this) { + case UNCRACKED -> Component.translatable("commands.cvillager.partiallyCracked").withStyle(addColor ? ChatFormatting.RED : ChatFormatting.RESET); + case CRACKED -> Component.translatable("commands.cvillager.inSync").withStyle(addColor ? ChatFormatting.GREEN : ChatFormatting.RESET); + }; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java b/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java new file mode 100644 index 000000000..6c10c0f19 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java @@ -0,0 +1,19 @@ +package net.earthcomputer.clientcommands.interfaces; + +import net.earthcomputer.clientcommands.features.VillagerRngSimulator; + +public interface IVillager { + VillagerRngSimulator clientcommands_getVillagerRngSimulator(); + + void clientcommands_onAmbientSoundPlayed(float pitch); + + void clientcommands_onNoSoundPlayed(float pitch); + + void clientcommands_onYesSoundPlayed(float pitch); + + void clientcommands_onSplashSoundPlayed(float pitch); + + void clientcommands_onXpOrbSpawned(int value); + + void clientcommands_onServerTick(); +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/VillagerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/VillagerMixin.java new file mode 100644 index 000000000..1ec26cd3d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/VillagerMixin.java @@ -0,0 +1,73 @@ +package net.earthcomputer.clientcommands.mixin.commands.villager; + +import net.earthcomputer.clientcommands.features.VillagerRngSimulator; +import net.earthcomputer.clientcommands.interfaces.IVillager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.AbstractVillager; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Villager.class) +public abstract class VillagerMixin extends AbstractVillager implements IVillager { + public VillagerMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + @Unique + VillagerRngSimulator rng = new VillagerRngSimulator(null); + + @Override + public void clientcommands_onAmbientSoundPlayed(float pitch) { + rng.onAmbientSoundPlayed(pitch); + } + + @Override + public void clientcommands_onNoSoundPlayed(float pitch) { + VillagerProfession profession = ((Villager) (Object) this).getVillagerData().getProfession(); + rng.onNoSoundPlayed(pitch, profession != VillagerProfession.NONE && profession != VillagerProfession.NITWIT); + } + + @Override + public void clientcommands_onYesSoundPlayed(float pitch) { + rng.onYesSoundPlayed(pitch); + } + + @Override + public void clientcommands_onSplashSoundPlayed(float pitch) { + rng.onSplashSoundPlayed(pitch); + } + + @Override + public void clientcommands_onXpOrbSpawned(int value) { + rng.onXpOrbSpawned(value); + } + + @Override + public void clientcommands_onServerTick() { + rng.simulateTick(); + + if (rng.shouldInteractWithVillager()) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.screen instanceof MerchantScreen) { + minecraft.screen.onClose(); + } else { + InteractionResult result = minecraft.gameMode.interact(minecraft.player, this, InteractionHand.MAIN_HAND); + if (result.consumesAction() && result.shouldSwing()) { + minecraft.player.swing(InteractionHand.MAIN_HAND); + } + } + } + } + + @Override + public VillagerRngSimulator clientcommands_getVillagerRngSimulator() { + return rng; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java index cd0ff704f..5d11ec83d 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java @@ -1,15 +1,30 @@ package net.earthcomputer.clientcommands.mixin.rngevents; import com.mojang.brigadier.StringReader; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; import net.earthcomputer.clientcommands.features.PlayerRandCracker; +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.*; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; 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 abstract class ClientPacketListenerMixin { + @Shadow + private ClientLevel level; + @Inject(method = "sendCommand", at = @At("HEAD")) private void onSendCommand(String command, CallbackInfo ci) { StringReader reader = new StringReader(command); @@ -18,4 +33,49 @@ private void onSendCommand(String command, CallbackInfo ci) { PlayerRandCracker.onGiveCommand(); } } + + @Inject(method = "handleSoundEvent", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleSoundEvent(ClientboundSoundPacket packet, CallbackInfo ci) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null && new Vec3(packet.getX(), packet.getY(), packet.getZ()).distanceToSqr(targetVillager.position()) <= 0.1f) { + VillagerCracker.onSoundEventPlayed(packet); + } + } + + @Inject(method = "handleChunkBlocksUpdate", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleChunkBlocksUpdate(ClientboundSectionBlocksUpdatePacket packet, CallbackInfo ci) { + if (Minecraft.getInstance().level != null) { + ResourceKey key = Minecraft.getInstance().level.dimension(); + packet.runUpdates((pos, state) -> { + if (new GlobalPos(key, pos).equals(VillagerCracker.getClockPos())) { + VillagerCracker.onServerTick(); + } + }); + } + } + + @Inject(method = "handleAddExperienceOrb", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleAddExperienceOrb(ClientboundAddExperienceOrbPacket packet, CallbackInfo ci) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null && new Vec3(packet.getX(), packet.getY() - 0.5, packet.getZ()).distanceToSqr(targetVillager.position()) <= 0.1f) { + VillagerCracker.onXpOrbSpawned(packet); + } + } + + @Inject(method = "handleBlockUpdate", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleBlockUpdate(ClientboundBlockUpdatePacket packet, CallbackInfo ci) { + if (Minecraft.getInstance().level != null && new GlobalPos(Minecraft.getInstance().level.dimension(), packet.getPos()).equals(VillagerCracker.getClockPos())) { + VillagerCracker.onServerTick(); + } + } + + @Inject(method = "handleSetTime", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void handleSetTime(ClientboundSetTimePacket packet, CallbackInfo ci) { + if (level.getDayTime() < 12000 && packet.getDayTime() >= 12000) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.day")); + } + } + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/MerchantScreenMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/MerchantScreenMixin.java new file mode 100644 index 000000000..8dd3adde3 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/MerchantScreenMixin.java @@ -0,0 +1,52 @@ +package net.earthcomputer.clientcommands.mixin.rngevents; + +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.VillagerCommand; +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.earthcomputer.clientcommands.interfaces.IVillager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.item.trading.ItemCost; +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(MerchantScreen.class) +public abstract class MerchantScreenMixin extends AbstractContainerScreen { + public MerchantScreenMixin(MerchantMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Inject(method = "render", at = @At("HEAD")) + private void onRender(CallbackInfo ci) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null) { + if (Minecraft.getInstance().player.distanceToSqr(targetVillager) > 2.0) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.outOfSync.distance").withStyle(ChatFormatting.RED), 100); + ((IVillager) targetVillager).clientcommands_getVillagerRngSimulator().reset(); + return; + } + + if (VillagerCracker.targetOffer != null) { + if (menu.getOffers().stream().map(offer -> new VillagerCommand.Offer(offer.getBaseCostA(), offer.getItemCostB().map(ItemCost::itemStack).orElse(null), offer.getResult())).anyMatch(offer -> offer.equals(VillagerCracker.targetOffer))) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.success", Configs.villagerAdjustment * 50).withStyle(ChatFormatting.GREEN), 100); + minecraft.player.playNotifySound(SoundEvents.NOTE_BLOCK_PLING.value(), SoundSource.PLAYERS, 1.0f, 2.0f); + } else { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.failure", Configs.villagerAdjustment * 50).withStyle(ChatFormatting.RED), 100); + minecraft.player.playNotifySound(SoundEvents.NOTE_BLOCK_BASS.value(), SoundSource.PLAYERS, 1.0f, 1.0f); + } + VillagerCracker.targetOffer = null; + } + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java index 00b96af14..885a4e479 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java @@ -260,14 +260,6 @@ class DebugRandomSourcePanel extends JPanel { add(tabbedPane, BorderLayout.CENTER); JPanel bottomPanel = new JPanel(); - JButton dumpStackTraceButton = new JButton("Dump stack trace"); - dumpStackTraceButton.addActionListener(e -> { - if (selectedStackTrace >= 0 && selectedStackTrace < DebugRandom.stackTraceById.size()) { - DebugRandom.LOGGER.info(DebugRandom.stackTraceById.get(selectedStackTrace)); - } - }); - bottomPanel.add(dumpStackTraceButton); - add(bottomPanel, BorderLayout.SOUTH); } diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index ec1169c12..fea7eee08 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -242,6 +242,42 @@ "commands.cvar.list.empty": "No available variables", "commands.cvar.list": "Available variables: %s", + "commands.cvillager.alreadyBruteForcing": "Cannot brute-force villager RNG since your target villager is already brute forcing", + "commands.cvillager.bruteForce.failed": "Could not find a match for any goals within %d calls", + "commands.cvillager.bruteForce.success": "Found a match for %s priced at %s. Will open villager gui in %d calls", + "commands.cvillager.clock.cleared": "Your clock is cleared", + "commands.cvillager.clock.set": "Clock set to %d %d %d (%s)", + "commands.cvillager.clock.set.cleared": "Your clock is now cleared", + "commands.cvillager.crack.failed": "Failed to crack villager seed (found %d seeds), re-cracking...", + "commands.cvillager.crack.success": "Villager RNG cracked: %d", + "commands.cvillager.failure": "Got the incorrect trade with correction of %dms", + "commands.cvillager.goalAdded": "Added goal successfully.", + "commands.cvillager.help.day": "Help: Villager RNG manipulation is likely to break if it's not night-time", + "commands.cvillager.help.noClock": "Help: Villager RNG manipulation requires a 20 Hz clock", + "commands.cvillager.help.tooSlow": "Help: Villager RNG manipulation requires a 20 Hz clock, please be advised that your clock seems to be running much slower", + "commands.cvillager.inSync": "Your villager's RNG is cracked", + "commands.cvillager.listGoals.noGoals": "There are no villager goals", + "commands.cvillager.listGoals.success": "There are %d villager goals:", + "commands.cvillager.listGoals.success.one": "There is 1 villager goal:", + "commands.cvillager.noCrackedVillagerPresent": "There was no cracked villager available to use", + "commands.cvillager.noProfession": "The targeted villager has no profession", + "commands.cvillager.notAVillager": "Target was not a villager", + "commands.cvillager.notLevel1": "The targeted villager has level above 1", + "commands.cvillager.outOfSync.ambient": "Your villager's hrmm sound was incorrectly timed and or incorrectly pitched, re-cracking", + "commands.cvillager.outOfSync.distance": "Your villager was too far away from you, re-cracking", + "commands.cvillager.outOfSync.no": "Your villager's no sound was incorrectly pitched, re-cracking", + "commands.cvillager.outOfSync.splash": "Your villager's splash sound was incorrectly pitched, re-cracking", + "commands.cvillager.outOfSync.xpOrb": "Your villager's xp orb was incorrectly sized, re-cracking", + "commands.cvillager.outOfSync.yes": "Your villager's yes sound was incorrectly pitched, re-cracking", + "commands.cvillager.partiallyCracked": "Your villager's RNG is partially cracked (~83m seeds remain)", + "commands.cvillager.removeGoal.invalidIndex": "Unable to remove goal %d, you only have %d goals", + "commands.cvillager.itemCountOutOfRange": "The specified item count range (%s) overlaps into an item count outside the maximum (%d)", + "commands.cvillager.removeGoal.success": "Successfully removed goal %s", + "commands.cvillager.resetCracker": "Successfully reset cracked villager RNG", + "commands.cvillager.success": "Got the correct trade with correction of %dms", + "commands.cvillager.target.cleared": "Target entity cleared", + "commands.cvillager.target.set": "Target entity set", + "commands.cwe.playerNotFound": "Player not found", "commands.cweather.reset": "Stopped overriding weather", diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index a7ff82f6d..02dde2796 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -18,6 +18,7 @@ "commands.relog.ClientPacketListenerMixin", "commands.render.EntityRendererDispatcherMixin", "commands.time.ClientLevelDataMixin", + "commands.villager.VillagerMixin", "commands.weather.LevelMixin", "debug.EntityMixin", "debug.ServerLevelMixin", @@ -69,6 +70,7 @@ "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin", "lengthextender.ChatScreenMixin", + "rngevents.MerchantScreenMixin", "scrambletitle.MinecraftMixin", "serverbrand.ClientCommonPacketListenerImplMixin", "suggestionshook.ClientPacketListenerMixin" diff --git a/src/main/resources/villager_lattice_data.nbt b/src/main/resources/villager_lattice_data.nbt new file mode 100644 index 000000000..c29cf2375 Binary files /dev/null and b/src/main/resources/villager_lattice_data.nbt differ