diff --git a/build.gradle b/build.gradle index 3aaca5cbd..1a1690245 100644 --- a/build.gradle +++ b/build.gradle @@ -61,8 +61,7 @@ dependencies { includedLibrary "com.seedfinding:mc_core:${project.seedfinding_core_version}" includedLibrary "com.seedfinding:mc_seed:${project.seedfinding_seed_version}" - includedLibrary "com.seedfinding:latticg:${project.latticg_version}:rt" - codeGenImplementation "com.seedfinding:latticg:${project.latticg_version}" + includedLibrary "com.seedfinding:latticg:${project.latticg_version}" compileOnly 'com.demonwav.mcdev:annotations:2.0.0' diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index fcdf9e61e..60dde6110 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -171,6 +171,7 @@ public static void registerCommands(CommandDispatcher WeatherCommand.register(dispatcher); WhisperEncryptedCommand.register(dispatcher); WikiCommand.register(dispatcher); + CrackVillagerRNGCommand.register(dispatcher, context); Calendar calendar = Calendar.getInstance(); boolean registerChatCommand = calendar.get(Calendar.MONTH) == Calendar.APRIL && calendar.get(Calendar.DAY_OF_MONTH) == 1; diff --git a/src/main/java/net/earthcomputer/clientcommands/command/CrackVillagerRNGCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/CrackVillagerRNGCommand.java new file mode 100644 index 000000000..29a735ce1 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/CrackVillagerRNGCommand.java @@ -0,0 +1,176 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.earthcomputer.clientcommands.features.CCrackVillager; +import net.earthcomputer.clientcommands.features.VillagerRNGSim; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.arguments.item.ItemInput; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentHelper; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static net.earthcomputer.clientcommands.command.arguments.DynamicIntegerArgument.integer; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; +import static dev.xpple.clientarguments.arguments.CBlockPosArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.CombinedArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.EnchantmentArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.DynamicIntegerArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.WithStringArgument.*; + +import static net.earthcomputer.clientcommands.command.arguments.CachedItemArgument.item; + +public class CrackVillagerRNGCommand { + public static void register(CommandDispatcher dispatcher, CommandBuildContext context) { + dispatcher.register(literal("ccrackvillager") + .then(literal("cancel") + .executes(ctx -> cancel(ctx.getSource()))) + .then(literal("clock") + .then(argument("clockpos", blockPos()) + .executes(ctx -> crackVillagerRNG(ctx.getSource(), getBlockPos(ctx, "clockpos"))))) + .then(literal("interval") + .then(argument("ticks", IntegerArgumentType.integer(0, 20)) + .executes(ctx -> setInterval(ctx.getSource(), getInteger(ctx, "ticks"))))) + .then(literal("add-goal") + .then(genFirst(context)) + .then(genSecond(context)) + .then(genResult(context))) + .then(literal("list-goals") + .executes(CrackVillagerRNGCommand::listGoals)) + .then(literal("remove-goal") + .then(argument("index", integer(1,CCrackVillager.goalOffers::size)) + .executes(CrackVillagerRNGCommand::removeGoal))) + .then(literal("clear-goals").executes(CrackVillagerRNGCommand::clearGoals)) + .then(literal("run").executes(CrackVillagerRNGCommand::doRun))); + } + + private static int doRun(CommandContext context) { + CCrackVillager.findingOffers = true; + if(CCrackVillager.goalOffers.isEmpty()) { + context.getSource().sendFeedback(Component.translatable("commands.ccrackvillager.emptyGoals")); + } + return Command.SINGLE_SUCCESS; + } + + private static int clearGoals(CommandContext context) { + CCrackVillager.goalOffers.clear(); + return Command.SINGLE_SUCCESS; + } + + private static int removeGoal(CommandContext context) { + context.getSource().sendFeedback(Component.translatable("commands.ccrackvillager.removeGoal", CCrackVillager.goalOffers.remove(getInteger(context, "index") - 1))); + return Command.SINGLE_SUCCESS; + } + + private static int listGoals(CommandContext context) { + for(var i = 0; i < CCrackVillager.goalOffers.size(); i++) { + var offer = CCrackVillager.goalOffers.get(i); + context.getSource().sendFeedback(Component.translatable("commands.ccrackvillager.listGoal", i + 1, offer)); + } + return Command.SINGLE_SUCCESS; + } + + private static int crackVillagerRNG(FabricClientCommandSource source, BlockPos pos) throws CommandSyntaxException { + CCrackVillager.clockPos = pos; + VillagerRNGSim.commandSource = source; + CCrackVillager.crackVillager(source.getPlayer(), seed -> { + source.sendFeedback(Component.translatable("commands.ccrackvillager.success", Long.toHexString(seed))); + }); + return Command.SINGLE_SUCCESS; + } + + private static int cancel(FabricClientCommandSource source) { + CCrackVillager.cancel(); + CCrackVillager.findingOffers = false; + source.sendFeedback(Component.translatable("commands.ccrackvillager.cancel")); + return Command.SINGLE_SUCCESS; + } + + private static int setInterval(FabricClientCommandSource source, int interval) throws CommandSyntaxException { + CCrackVillager.setInterval(interval); + return Command.SINGLE_SUCCESS; + } + + private static Combined, String> createPredicate(CommandContext context, String argName) { + try { + Result>> result = getWithString(context, argName, null); + var combined = result.value(); + var item = combined.first().getItem(); + var min = combined.second().first(); + var max = combined.second().second(); + return new Combined<>((stack) -> stack.is(item) + && ((min > item.getDefaultMaxStackSize() || min <= stack.getCount()) + && (max > item.getDefaultMaxStackSize() || stack.getCount() <= max)), + result.string().replaceAll("\\* \\*","*").replaceAll("([\\d*]+) ([\\d*]+)$", "$1-$2")); + } catch (IllegalArgumentException ignored) { } + return null; + } + + private static int setGoal(CommandContext context) { + CCrackVillager.Offer offer = new CCrackVillager.Offer(); + + var combined = createPredicate(context, "firstitem"); + if(combined != null) offer.withFirst(combined.first(), combined.second()); + combined = createPredicate(context, "seconditem"); + if(combined != null) offer.withSecond(combined.first(), combined.second()); + combined = createPredicate(context, "resultitem"); + if(combined != null) offer.withResult(combined.first(), combined.second()); + + try { + Result> result2 = getWithString(context, "enchantment", null); + var enchantment = result2.value().first(); + offer.andEnchantment((stack) -> { + var enchantments = EnchantmentHelper.getEnchantmentsForCrafting(stack); + var level = enchantments.getLevel(enchantment); + return (result2.value().second() > enchantment.getMaxLevel() && level > 0) || result2.value().second() == level; + }, result2.string()); + } catch (IllegalArgumentException ignored) { } + + context.getSource().sendFeedback(Component.literal((CCrackVillager.goalOffers.size()+1) + ": " + offer)); + + CCrackVillager.goalOffers.add(offer); + + return Command.SINGLE_SUCCESS; + } + + + + static LiteralArgumentBuilder genFirst(CommandBuildContext context) { + var item = item(context); + Supplier supplier = () -> item.lastItem.getItem().getDefaultMaxStackSize(); + return literal("first") + .then(argument("firstitem", withString(combined(item, combined(integer(1, supplier).allowAny(), integer(1, supplier).allowAny())))) + .executes(CrackVillagerRNGCommand::setGoal) + .then(genSecond(context)) .then(genResult(context))); + } + + static LiteralArgumentBuilder genSecond(CommandBuildContext context) { + var item = item(context); + Supplier supplier = () -> item.lastItem.getItem().getDefaultMaxStackSize(); + return literal("second") + .then(argument("seconditem", withString(combined(item, combined(integer(1, supplier).allowAny(), integer(1, supplier).allowAny())))) + .executes(CrackVillagerRNGCommand::setGoal) .then(genResult(context))); + } + + static LiteralArgumentBuilder genResult(CommandBuildContext context) { + var enchantment = enchantment(); + var item = item(context); + Supplier supplier = () -> item.lastItem.getItem().getDefaultMaxStackSize(); + return literal("result") + .then(argument("resultitem", withString(combined(item, combined(integer(1, supplier).allowAny(), integer(1, supplier).allowAny())))) + .executes(CrackVillagerRNGCommand::setGoal) .then(literal("enchant") + .then(argument("enchantment", withString(combined(enchantment, integer(() -> enchantment.lastParsed.getMaxLevel()).allowAny()))) + .executes(CrackVillagerRNGCommand::setGoal)))); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/CachedItemArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/CachedItemArgument.java new file mode 100644 index 000000000..1bb81303f --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/CachedItemArgument.java @@ -0,0 +1,31 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.earthcomputer.clientcommands.command.CrackVillagerRNGCommand; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.arguments.item.ItemArgument; +import net.minecraft.commands.arguments.item.ItemInput; + +public class CachedItemArgument extends ItemArgument { + public ItemInput lastItem; + + public CachedItemArgument(CommandBuildContext context) { + super(context); + } + + public static CachedItemArgument item(CommandBuildContext context) { + return new CachedItemArgument(context); + } + + public static ItemInput getItem(CommandContext context, String name) { + return context.getArgument(name, ItemInput.class); + } + + @Override + public ItemInput parse(StringReader reader) throws CommandSyntaxException { + return lastItem = super.parse(reader); + + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/CombinedArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/CombinedArgument.java new file mode 100644 index 000000000..bde396fe9 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/CombinedArgument.java @@ -0,0 +1,69 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.network.chat.Component; + +import java.util.concurrent.CompletableFuture; + +public class CombinedArgument implements ArgumentType> { + ArgumentType firstArgument; + ArgumentType secondArgument; + private static final SimpleCommandExceptionType TOO_FEW_ARGUMENTS_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.client.tooFewArguments")); + + private CombinedArgument(ArgumentType first, ArgumentType second) { + firstArgument = first; + secondArgument = second; + } + + public static CombinedArgument combined(ArgumentType first, ArgumentType second) { + return new CombinedArgument<>(first, second); + } + + public static Combined getCombined(CommandContext context, String name) { + return context.getArgument(name, Combined.class); + } + + @Override + public Combined parse(StringReader reader) throws CommandSyntaxException { + A first = firstArgument.parse(reader); + if(!reader.canRead()) throw TOO_FEW_ARGUMENTS_EXCEPTION.create(); + reader.expect(' '); + if(!reader.canRead()) throw TOO_FEW_ARGUMENTS_EXCEPTION.create(); + B second = secondArgument.parse(reader); + return new Combined<>(first, second); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + StringReader reader = new StringReader(builder.getInput()); + reader.setCursor(builder.getStart()); + int readAmount = 0; + int cursor = reader.getCursor(); + try { + if(reader.canRead()) { + firstArgument.parse(reader); + if(reader.canRead()) { + reader.expect(' '); + readAmount++; + cursor = reader.getCursor(); + secondArgument.parse(reader); + } + } + } catch (CommandSyntaxException ignored) { + } + if(readAmount == 0) { + return firstArgument.listSuggestions(context, builder.createOffset(cursor)); + } else { + return secondArgument.listSuggestions(context, builder.createOffset(cursor)); + } + } + + public record Combined(A first, B second) { + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/DynamicIntegerArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/DynamicIntegerArgument.java new file mode 100644 index 000000000..72d1c46ad --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/DynamicIntegerArgument.java @@ -0,0 +1,77 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class DynamicIntegerArgument implements ArgumentType { + Supplier low; + Supplier high; + boolean allowAny = false; + + DynamicIntegerArgument(Supplier low, Supplier high) { + this.low = low; + this.high = high; + } + + public DynamicIntegerArgument allowAny() { + allowAny = true; + return this; + } + + public static int getInteger(CommandContext context, String name) { + return context.getArgument(name, int.class); + } + + public static DynamicIntegerArgument integer(Supplier low, Supplier high) { + return new DynamicIntegerArgument(low, high); + } + + public static DynamicIntegerArgument integer(int low, Supplier high) { + return integer(() -> low, high); + } + public static DynamicIntegerArgument integer(Supplier low, int high) { + return integer(low, () -> high); + } + + public static DynamicIntegerArgument integer(Supplier limit) { + return integer(0, limit); + } + + @Override + public Integer parse(final StringReader reader) throws CommandSyntaxException { + final int start = reader.getCursor(); + if (allowAny && reader.canRead() && reader.peek() == '*') { + reader.read(); + return Integer.MAX_VALUE; + } + final int result = reader.readInt(); + + var min = low.get(); + var max = high.get(); + + if (result < min) { + reader.setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.integerTooLow().createWithContext(reader, result, min); + } + if (result > max) { + reader.setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.integerTooHigh().createWithContext(reader, result, max); + } + return result; + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + if (allowAny) { + builder.suggest("*"); + } + return builder.buildFuture(); + } +} \ No newline at end of file diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/EnchantmentArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/EnchantmentArgument.java new file mode 100644 index 000000000..e7fb6c5a2 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/EnchantmentArgument.java @@ -0,0 +1,39 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.enchantment.Enchantment; + +import java.util.concurrent.CompletableFuture; + +public class EnchantmentArgument implements ArgumentType { + public Enchantment lastParsed = null; + + @Override + public Enchantment parse(StringReader reader) throws CommandSyntaxException { + return lastParsed = BuiltInRegistries.ENCHANTMENT.get(ResourceLocation.read(reader)); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + SharedSuggestionProvider.suggestResource(BuiltInRegistries.ENCHANTMENT.keySet(), builder); + + return builder.buildFuture(); + } + + public static EnchantmentArgument enchantment() { + return new EnchantmentArgument(); + } + + + public static Enchantment getEnchantment(CommandContext context, String name) { + return context.getArgument(name, Enchantment.class); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/CCrackVillager.java b/src/main/java/net/earthcomputer/clientcommands/features/CCrackVillager.java new file mode 100644 index 000000000..905166c98 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/CCrackVillager.java @@ -0,0 +1,207 @@ +package net.earthcomputer.clientcommands.features; + +import com.seedfinding.latticg.reversal.DynamicProgram; +import com.seedfinding.latticg.util.LCG; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.block.Blocks; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class CCrackVillager { + + public static WeakReference targetVillager = null; + + static Consumer onCrackFinished; + + static List measurements = new ArrayList<>(); + static boolean cracked = false; + static int validMeasures = 0; + static boolean cracking = false; + static int interval = 0; + public static BlockPos clockPos = null; + + public static List goalOffers = new ArrayList<>(); + public static boolean findingOffers = false; + + + + public static void cancel() { + targetVillager = null; + } + + public static void setInterval(int i) { + interval = i; + } + + public static void onClockUpdate() { + VillagerRNGSim.INSTANCE.onTick(); + if(!cracked && validMeasures > 0) { + measurements.add(Measurement.skip(2)); + } + } + + public static void onAmethyst(ClientboundSoundPacket packet) { + Villager villager; + if(targetVillager != null && (villager = targetVillager.get()) != null) { + if(villager.distanceToSqr(packet.getX(), packet.getY(), packet.getZ()) < 1.0) { + if(cracked) { + VillagerRNGSim.INSTANCE.onAmethyst(packet); + } else { + new Thread(() -> crack(packet)).start(); + } + } + } + } + + public static void onAmbient(ClientboundSoundPacket packet) { + Villager villager; + if(targetVillager != null && (villager = targetVillager.get()) != null) { + if(villager.distanceToSqr(packet.getX(), packet.getY(), packet.getZ()) < 1.0) { + VillagerRNGSim.INSTANCE.onAmbient(); + if(!cracked && validMeasures > 0) { + measurements.add(Measurement.skip(2)); + } + } + } + } + + static void crack(ClientboundSoundPacket packet) { + var lastChimeIntensity1_2 = (packet.getVolume() - 0.1f); + var nextFloat = (packet.getPitch() - 0.5f) / lastChimeIntensity1_2; + + measurements.add(Measurement.nextFloat(nextFloat, 0.0015f)); + validMeasures++; + + if(validMeasures > 6 && !cracking) { + cracking = true; + var cachedMeasurements = new ArrayList<>(measurements); + DynamicProgram program = DynamicProgram.create(new LCG(25214903917L, 11, 1L<<48)); + for(var measurement : cachedMeasurements) + measurement.apply(program); + var seeds = program.reverse().toArray(); + if(seeds.length == 1) { + cracked = true; + reset(); + VillagerRNGSim.INSTANCE.setSeed(seeds[0]); + for(var measurement : cachedMeasurements) { + measurement.apply(VillagerRNGSim.INSTANCE.random); + } + onCrackFinished.accept(VillagerRNGSim.INSTANCE.getSeed()); + } else { + var gui = Minecraft.getInstance().gui; + if(clockPos == null) { + gui.getChat().addMessage(Component.translatable("commands.ccrackvillager.noClock")); + } + gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.fail"), false); + reset(); + } + cracking = false; + } else { + Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.progress", validMeasures), false); + } + } + + public static void crackVillager(LocalPlayer player, Consumer callback) { + var world = player.getCommandSenderWorld(); + var villagers = world.getNearbyEntities(Villager.class, TargetingConditions.forNonCombat().selector( + (villager -> villager.getBlockStateOn().is(Blocks.AMETHYST_BLOCK)) + ), player, player.getBoundingBox().deflate(30)); + if(!villagers.isEmpty()) { + Villager target = null; + double distance = 100; + for(var villager : villagers) { + var tmpDistance = villager.distanceToSqr(player); + if(tmpDistance < distance) { + target = villager; + distance = tmpDistance; + } + } + if(target != null) { + targetVillager = new WeakReference<>(target); + onCrackFinished = callback; + reset(); + cracked = false; + VillagerRNGSim.INSTANCE.lastAmbientCracked = false; + } + } + } + + static void reset() { + measurements.clear(); + validMeasures = 0; + } + + public static class Offer implements Predicate { + Predicate first = null; + Predicate second = null; + Predicate result = null; + + String firstDescription = ""; + String secondDescription = ""; + String resultDescription = ""; + String enchantmentDescription = ""; + + public Offer withFirst(Predicate predicate, String description) { + first = predicate; + firstDescription = description; + return this; + } + + public Offer withSecond(Predicate predicate, String description) { + second = predicate; + secondDescription = description; + return this; + } + + public Offer withResult(Predicate predicate, String description) { + result = predicate; + resultDescription = description; + return this; + } + + public Offer andEnchantment(Predicate predicate, String description) { + result = result.and(predicate); + enchantmentDescription = description; + return this; + } + + @Override + public boolean test(MerchantOffer offer) { + if (first != null && !first.test(offer.getItemCostA().itemStack())) return false; + if (second != null && !second.test(offer.getItemCostB().isPresent() ? offer.getItemCostB().get().itemStack() : null)) { + return false; + } + return result == null || result.test(offer.getResult()); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(firstDescription); + if (!secondDescription.isEmpty() && !result.isEmpty()) { + result.append(" + "); + } + result.append(secondDescription); + if (!resultDescription.isEmpty() && !result.isEmpty()) { + result.append(" => "); + } + result.append(resultDescription); + if (!enchantmentDescription.isEmpty()) { + result.append(" with ").append(enchantmentDescription); + } + + return result.toString(); + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/Measurement.java b/src/main/java/net/earthcomputer/clientcommands/features/Measurement.java new file mode 100644 index 000000000..3342269db --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/Measurement.java @@ -0,0 +1,121 @@ +package net.earthcomputer.clientcommands.features; + +import com.seedfinding.latticg.reversal.DynamicProgram; +import com.seedfinding.latticg.reversal.calltype.CallType; +import com.seedfinding.latticg.reversal.calltype.java.JavaCalls; +import com.seedfinding.latticg.util.Range; +import net.minecraft.util.RandomSource; + + +public abstract class Measurement { + public abstract void apply(DynamicProgram program); + + public abstract void apply(RandomSource random); + + public static Measurement nextFloat(float value) { + return new FloatMeasurement(value); + } + + public static Measurement nextFloat(float value, float range) { + return new FloatRangedMeasurement(value, range); + } + + public static Measurement skip() { + return skip(1); + } + + public static Measurement skip(int count) { + return new SkipMeasurement(count); + } + + public static Measurement nextInt(int bound, int value) { + return new IntMeasurement(bound, value); + } + + static class SkipMeasurement extends Measurement { + int count; + SkipMeasurement(int count) { + this.count = count; + } + + @Override + public void apply(DynamicProgram program) { + program.skip(count); + } + + @Override + public void apply(RandomSource random) { + for(var i = 0; i < count; i++) { + random.nextFloat(); + } + } + } + + static class FloatMeasurement extends Measurement { + float value; + + static CallType> callType = JavaCalls.nextFloat().ranged(0.04f); + + FloatMeasurement(float value) { + this.value = value; + } + + CallType> getCallType() { + return callType; + } + + float getRange() { + return 0.02f; + } + + @Override + public void apply(DynamicProgram program) { + program.add(getCallType(), Range.of(value-getRange(), value+getRange())); + } + + @Override + public void apply(RandomSource random) { + random.nextFloat(); + } + } + + static class FloatRangedMeasurement extends FloatMeasurement { + float range; + + FloatRangedMeasurement(float value, float range) { + super(value); + this.range = range; + } + + @Override + CallType> getCallType() { + return JavaCalls.nextFloat().ranged(this.range * 2f); + } + + @Override + public float getRange() { + return range; + } + } + + static class IntMeasurement extends Measurement { + CallType callType; + int bound; + int value; + IntMeasurement(int bound, int value) { + this.value = value; + this.bound = bound; + callType = JavaCalls.nextInt(bound); + } + + @Override + public void apply(DynamicProgram program) { + program.add(callType, this.value); + } + + @Override + public void apply(RandomSource random) { + random.nextInt(bound); + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRNGSim.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRNGSim.java new file mode 100644 index 000000000..244462412 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRNGSim.java @@ -0,0 +1,339 @@ +package net.earthcomputer.clientcommands.features; + +import com.google.common.collect.Lists; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.EnchantmentInstance; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.levelgen.LegacyRandomSource; + +import java.util.ArrayList; +import java.util.List; + +public class VillagerRNGSim { + public static VillagerRNGSim INSTANCE = new VillagerRNGSim(); + public static FabricClientCommandSource commandSource = null; + + LegacyRandomSource random = new LegacyRandomSource(0); + + static List nextOffers = new ArrayList<>(); + static List nextOffersWithBooks = new ArrayList<>(); + + int errorCount = 0; + long lastAmbient = 0; + boolean lastAmbientCracked = false; + boolean justAmbient = false; + long lastAmethyst = 0; + static long amethystInterval = 0; + int ticksToWait = 0; + boolean synced = false; + + public static long lastBruteForce = 0; + public static long bruteForceResult = 0; + + long tickCounter = 0; + + public void onAmethyst(ClientboundSoundPacket packet) { + var lastChimeIntensity1_2 = (packet.getVolume() - 0.1f); + var nextFloat = (packet.getPitch() - 0.5f) / lastChimeIntensity1_2; + + var forSync = clone(); + var ticks = 0; + var predicted = forSync.nextFloat(); + while (Math.abs(nextFloat - predicted) > 0.0001 && ticks++ < 30) { + predicted = forSync.nextFloat(); + } + + assert Minecraft.getInstance().level != null; + amethystInterval = lastAmethyst; + lastAmethyst = tickCounter; + amethystInterval = lastAmethyst - amethystInterval; + + synced = false; + if (ticks < 30) { + setSeed(forSync.getSeed()); + justAmbient = forSync.justAmbient; + lastAmbient = forSync.lastAmbient; + ticksToWait = forSync.ticksToWait; + lastAmethyst = forSync.lastAmethyst; + + errorCount = 0; + if (ticks == 0) { + if ((tickCounter - lastBruteForce > 500 || tickCounter > bruteForceResult + 10) && CCrackVillager.findingOffers) { + lastBruteForce = tickCounter; + bruteForce(); + } + synced = true; + if (CCrackVillager.findingOffers) { + Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.synced"), false); + } else { + Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.syncedNotCracking"), false); + } + } else { + Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.maintain"), false); + } + Minecraft.getInstance().gui.overlayMessageTime = 20; + } else { + ticksToWait = 10; + errorCount++; + Minecraft.getInstance().gui.overlayMessageTime = 0; + if (errorCount > 2) { + errorCount = 0; + CCrackVillager.cracked = false; + lastAmbientCracked = false; + lastBruteForce = 0; + Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("commands.ccrackvillager.maintainFail"), false); + } + } + } + + public void onAmbient() { + if (!lastAmbientCracked) { + if (!CCrackVillager.cracked) return; + + lastAmbient = -80; + nextFloat(); + nextFloat(); + justAmbient = true; + } + + if (justAmbient) return; + + var forSync = clone(); + var ticks = 0; + while (!forSync.justAmbient) { + ticks++; + forSync.onTick(true); + } + + if (ticks < 30) { + setSeed(forSync.getSeed()); + justAmbient = forSync.justAmbient; + lastAmbient = forSync.lastAmbient; + ticksToWait = forSync.ticksToWait; + lastAmethyst = forSync.lastAmethyst;; + tickCounter = forSync.tickCounter; + } else { + ticksToWait = 10; + } + } + + public long getSeed() { + return random.seed.get(); + } + + public void setSeed(long seed) { + random.setSeed(seed ^ 25214903917L); + } + + public void onTick() { + onTick(false); + } + + public void onTick(boolean sim) { + if (ticksToWait-- > 0) { + return; + } + tickCounter++; + + if (sim && tickCounter - lastAmethyst >= amethystInterval) { + nextFloat(); + } + + if (nextInt(1000) < lastAmbient++ && CCrackVillager.cracked) { + nextFloat(); + nextFloat(); + lastAmbient = -80; + lastAmbientCracked = true; + justAmbient = true; + } else { + justAmbient = false; + } + nextInt(100); + + if (CCrackVillager.cracked && !sim && CCrackVillager.findingOffers && synced) { + var player = Minecraft.getInstance().player; + var villager = CCrackVillager.targetVillager.get(); + if (player == null || player.distanceTo(villager) > 5) return; + var simulate = clone(); + for (var i = 0; i < CCrackVillager.interval; i++) { + simulate.onTick(true); + } + var offers = simulate.predictOffers(); + if (offers == null) return; + for (var offer : offers) { + if (CCrackVillager.goalOffers.stream().anyMatch(goalOffer -> goalOffer.test(offer))) { + assert Minecraft.getInstance().gameMode != null; + printNextTrades(); + Minecraft.getInstance().gameMode.interact(player, villager, InteractionHand.MAIN_HAND); + CCrackVillager.findingOffers = false; + break; + } + } + } + } + + void bruteForce() { + var player = Minecraft.getInstance().player; + var villager = CCrackVillager.targetVillager.get(); + if (player == null || player.distanceTo(villager) > 5) return; + var simulate = clone(); + + for (var i = 0; i < CCrackVillager.interval; i++) { + simulate.onTick(true); + } + for (var step = 0; step < 600; step++) { + simulate.onTick(true); + var sim2 = simulate.clone(); + + var offers = sim2.predictOffers(); + if (offers == null) return; + for (var offer : offers) { + for (var goal : CCrackVillager.goalOffers) { + if (goal.test(offer)) { + bruteForceResult = sim2.tickCounter; + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.match", step)); + return; + } + } + } + } + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.noMatch")); + } + + void randomCalled() { + } + + float nextFloat() { + randomCalled(); + return random.nextFloat(); + } + + int nextInt(int bound) { + randomCalled(); + return random.nextInt(bound); + } + + public List predictOffers() { + var villager = CCrackVillager.targetVillager.get(); + if (villager == null) return null; + var map = VillagerTrades.TRADES.get(villager.getVillagerData().getProfession()); + if (map == null || map.isEmpty()) return null; + var items = map.get(villager.getVillagerData().getLevel()); + if (items == null) return null; + var itemList = Lists.newArrayList(items); + List offers = new ArrayList<>(); + var i = 0; + while (i < 2) { + MerchantOffer offer = itemList.remove(nextInt(itemList.size())).getOffer(villager, this.random); + if (offer == null) continue; + offers.add(offer); + ++i; + } + return offers; + } + + public void printNextTrades() { + nextOffers.clear(); + nextOffersWithBooks.clear(); + for (var i = 0; i < 15; i++) { + var sim = clone(); + for (var tick = 0; tick < i; tick++) { + sim.onTick(true); + } + var offers = sim.predictOffers(); + var bookIndex = -1; + for (var index = 0; index < 2; index++) { + var offer = offers.get(index); + nextOffers.add(offer); + if (offer.getResult().is(Items.ENCHANTED_BOOK)) { + bookIndex = index; + } + } + nextOffersWithBooks.add(bookIndex); + } + + } + + EnchantmentInstance getEnchantment(ItemStack stack) { + var enchantmentsForCrafting = EnchantmentHelper.getEnchantmentsForCrafting(stack); + if (enchantmentsForCrafting.isEmpty()) return null; + var holder = enchantmentsForCrafting.keySet().iterator().next(); + return new EnchantmentInstance(holder.value(), EnchantmentHelper.getEnchantmentsForCrafting(stack).getLevel(holder.value())); + } + + public void syncOffer(ClientboundMerchantOffersPacket packet) { + var offers = packet.getOffers(); + var hasBook = -1; + for (var i = 0; i < 2; i++) { + if (offers.get(i).getResult().is(Items.ENCHANTED_BOOK)) hasBook = i; + } + if (hasBook != -1) { + for (var i = 0; i < nextOffers.size(); i+=2) { + var i2 = nextOffersWithBooks.get(i/2); + if (i2 != -1) { + if (i2 == hasBook && checkItem(offers.get(hasBook).getResult(), nextOffers.get(i+i2).getResult())) { + CCrackVillager.interval = i/2; + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.syncWithLag", CCrackVillager.interval)); + return; + } + } + } + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.maintainFail")); + } else { + for (var i = 0; i < nextOffers.size(); i+=2) { + var same = true; + for (var j = 0; j < 2; j++) { + var offer = nextOffers.get(i+j); + var offer2 = offers.get(j); + if (checkItem(offer.getResult(), offer2.getResult()) + && checkItem(offer.getCostA(), offer2.getCostA()) + && checkItem(offer.getCostB(), offer2.getCostB())) + continue; + same = false; + } + if (same) { + CCrackVillager.interval = i/2; + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.syncWithLag", CCrackVillager.interval)); + return; + } + } + commandSource.sendFeedback(Component.translatable("commands.ccrackvillager.maintainFail")); + } + } + + boolean checkItem(ItemStack stack1, ItemStack stack2){ + if (stack1.getItem() == stack2.getItem() && stack1.getCount() == stack2.getCount()) { + var enchantment1 = getEnchantment(stack1); + var enchantment2 = getEnchantment(stack2); + if ((enchantment1 == null) == (enchantment2 == null)) { + if (enchantment1 == null) return true; + return enchantment1.enchantment.getDescriptionId().equals(enchantment2.enchantment.getDescriptionId()) + && enchantment1.level == enchantment2.level; + } + return false; + } + return false; + } + + @Override + public VillagerRNGSim clone() { + var result = new VillagerRNGSim(); + result.setSeed(getSeed()); + result.lastAmbient = lastAmbient; + result.lastAmethyst = lastAmethyst; + result.lastAmbientCracked = lastAmbientCracked; + result.justAmbient = justAmbient; + result.tickCounter = tickCounter; + + return result; + } +} \ No newline at end of file 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..8a078f5bc 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java @@ -1,8 +1,15 @@ package net.earthcomputer.clientcommands.mixin.rngevents; import com.mojang.brigadier.StringReader; +import net.earthcomputer.clientcommands.features.CCrackVillager; import net.earthcomputer.clientcommands.features.PlayerRandCracker; +import net.earthcomputer.clientcommands.features.VillagerRNGSim; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket; +import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.sounds.SoundEvents; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -18,4 +25,34 @@ private void onSendCommand(String command, CallbackInfo ci) { PlayerRandCracker.onGiveCommand(); } } + + @Inject(method = "handleSoundEvent", at = @At("TAIL")) + private void onSoundEvent(ClientboundSoundPacket packet, CallbackInfo ci) { + if (packet.getSound().is(SoundEvents.VILLAGER_AMBIENT.getLocation())) { + CCrackVillager.onAmbient(packet); + } else if(packet.getSound().is(SoundEvents.AMETHYST_BLOCK_CHIME.getLocation())) { + CCrackVillager.onAmethyst(packet); + } + } + + @Inject(method = "handleBlockUpdate", at = @At("TAIL")) + void onBlockUpdate(ClientboundBlockUpdatePacket packet, CallbackInfo ci){ + if(packet.getPos().equals(CCrackVillager.clockPos)) { + CCrackVillager.onClockUpdate(); + } + } + + @Inject(method = "handleChunkBlocksUpdate", at = @At("TAIL")) + void onBlockUpdate(ClientboundSectionBlocksUpdatePacket packet, CallbackInfo ci){ + packet.runUpdates((pos, block) -> { + if(pos.equals(CCrackVillager.clockPos)) { + CCrackVillager.onClockUpdate(); + } + }); + } + + @Inject(method = "handleMerchantOffers", at = @At("TAIL")) + void onOffers(ClientboundMerchantOffersPacket packet, CallbackInfo ci) { + VillagerRNGSim.INSTANCE.syncOffer(packet); + } } diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index ec1169c12..1eba2f901 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -263,6 +263,23 @@ "commands.client.requiresRestart": "This change will take effect after you restart your client", "commands.client.tooFewArguments": "Too few arguments", + "commands.ccrackvillager.cancel": "Crack RNG cancelled", + "commands.ccrackvillager.emptyGoals": "warning: you haven't added any goals", + "commands.ccrackvillager.fail": "Crack Fail, retrying...", + "commands.ccrackvillager.listGoal": "%d: %s", + "commands.ccrackvillager.maintain": "Villager RNG maintain success", + "commands.ccrackvillager.maintainFail": "Villager RNG maintain fail", + "commands.ccrackvillager.match": "there might have a desired trade in %d ticks later", + "commands.ccrackvillager.noClock": "warning: clock position hasn't be set", + "commands.ccrackvillager.noMatch": "cannot find any trades match any goals in 30 seconds", + "commands.ccrackvillager.progress": "crack RNG progress: %d/7", + "commands.ccrackvillager.removeGoal": "remove goal: %s", + "commands.ccrackvillager.showEnchOnTick": "tick: %d => %s", + "commands.ccrackvillager.success": "Villager RNG cracked: %d", + "commands.ccrackvillager.synced": "Villager RNG synced", + "commands.ccrackvillager.syncedNotCracking": "Villager RNG synced (not cracking trades)", + "commands.ccrackvillager.syncWithLag": "set delay to %d tick(s)", + "chorusManip.landing.success": "Landing on: %d, %d, %d", "chorusManip.landing.failed": "Landing manipulation not possible", "chorusManip.goalTooFar": "Goal is too far away!",