diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index ef179bac..a4f8bd39 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -168,7 +168,7 @@ public static void registerCommands(CommandDispatcher UsageTreeCommand.register(dispatcher); UuidCommand.register(dispatcher); VarCommand.register(dispatcher); - VillagerCommand.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 1af26d7a..fbf073e3 100644 --- a/src/main/java/net/earthcomputer/clientcommands/Configs.java +++ b/src/main/java/net/earthcomputer/clientcommands/Configs.java @@ -169,4 +169,10 @@ public enum PacketDumpMethod { @Config public static int maximumPacketFieldDepth = 10; + + @Config(temporary = true, setter = @Config.Setter("setMaxVillagerSimulationTicks")) + public static int maxVillagerSimulationTicks = 16384; + public static void setMaxVillagerSimulationTicks(int maxVillagerSimulationTicks) { + Configs.maxVillagerSimulationTicks = Mth.clamp(maxVillagerSimulationTicks, 0, 65536); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java index 9db385ac..f4271d9b 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java @@ -4,45 +4,162 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.arguments.PredicatedRangeArgument; +import net.earthcomputer.clientcommands.command.arguments.WithStringArgument; +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.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ai.village.poi.PoiTypes; 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.BlockItem; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; import static dev.xpple.clientarguments.arguments.CBlockPosArgument.*; import static dev.xpple.clientarguments.arguments.CEntityArgument.*; +import static dev.xpple.clientarguments.arguments.CItemPredicateArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.PredicatedRangeArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.PredicatedRangeArgument.Ints.*; +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 SELECTED_ITEM_NOT_WORKSTATION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.selected_item_not_workstation")); + private static final SimpleCommandExceptionType NO_CRACKED_VILLAGER_PRESENT = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.no_cracked_villager_present")); + public static final List goals = new ArrayList<>(); - public static void register(CommandDispatcher dispatcher) { - dispatcher.register( - literal("cvillager") - .then(literal("clock") - .then(argument("pos", blockPos()) - .executes(ctx -> setClockBlockPos(ctx.getSource(), getBlockPos(ctx, "pos"))))) - .then(literal("target") - .then(argument("entity", entity()) - .executes(ctx -> setVillagerTarget(ctx.getSource(), getEntity(ctx, "entity")))))); + public static void register(CommandDispatcher dispatcher, CommandBuildContext context) { + dispatcher.register(literal("cvillager") + .then(literal("add-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-item-quantity", withString(intRange(1, 64))) + .then(argument("result-item", withString(itemPredicate(context))) + .then(argument("result-item-quantity", withString(intRange(1, 64))) + .executes(ctx -> addGoal(getWithString(ctx, "first-item", CItemStackPredicateArgument.class), getWithString(ctx, "first-item-quantity", MinMaxBounds.Ints.class), null, null, getWithString(ctx, "result-item", CItemStackPredicateArgument.class), getWithString(ctx, "result-item-quantity", MinMaxBounds.Ints.class)))))))) + .then(literal("add-three-item-goal") + .then(argument("first-item", withString(itemPredicate(context))) + .then(argument("first-item-quantity", withString(intRange(1, 64))) + .then(argument("second-item", withString(itemPredicate(context))) + .then(argument("second-item-quantity", withString(intRange(1, 64))) + .then(argument("result-item", withString(itemPredicate(context))) + .then(argument("result-item-quantity", withString(intRange(1, 64))) + .executes(ctx -> addGoal(getWithString(ctx, "first-item", CItemStackPredicateArgument.class), getWithString(ctx, "first-item-quantity", MinMaxBounds.Ints.class), getWithString(ctx, "second-item", CItemStackPredicateArgument.class), getWithString(ctx, "second-item-quantity", MinMaxBounds.Ints.class), getWithString(ctx, "result-item", CItemStackPredicateArgument.class), getWithString(ctx, "result-item-quantity", MinMaxBounds.Ints.class)))))))))) + .then(literal("list-goals") + .executes(ctx -> listGoals(ctx.getSource()))) + .then(literal("clock") + .then(argument("pos", blockPos()) + .executes(ctx -> setClockBlockPos(getBlockPos(ctx, "pos"))))) + .then(literal("target") + .executes(ctx -> setVillagerTarget(null)) + .then(argument("entity", entity()) + .executes(ctx -> setVillagerTarget(getEntity(ctx, "entity"))))) + .then(literal("crack") + .executes(ctx -> crack(ctx.getSource())))); } - private static int setClockBlockPos(FabricClientCommandSource source, BlockPos pos) { + private static int setClockBlockPos(BlockPos pos) { VillagerCracker.clockBlockPos = pos; - source.getPlayer().sendSystemMessage(Component.translatable("commands.cvillager.clockSet", pos.getX(), pos.getY(), pos.getZ())); + ClientCommandHelper.sendFeedback("commands.cvillager.clockSet", pos.getX(), pos.getY(), pos.getZ()); return Command.SINGLE_SUCCESS; } - private static int setVillagerTarget(FabricClientCommandSource source, Entity target) throws CommandSyntaxException { - if (!(target instanceof Villager villager)) { + private static int setVillagerTarget(@Nullable Entity target) throws CommandSyntaxException { + if (!(target instanceof Villager) && target != null) { throw NOT_A_VILLAGER_EXCEPTION.create(); } - VillagerCracker.setTargetVillager(villager); - source.getPlayer().sendSystemMessage(Component.translatable("commands.cvillager.targetSet")); + VillagerCracker.setTargetVillager((Villager) target); + if (target == null) { + ClientCommandHelper.sendFeedback("commands.cvillager.targetCleared"); + } else { + ClientCommandHelper.sendFeedback("commands.cvillager.targetSet"); + } + + 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 { + source.sendFeedback(Component.translatable("commands.cvillager.listGoals.success", FishingCracker.goals.size())); + for (int i = 0; i < goals.size(); i++) { + Goal goal = goals.get(i); + if (goal.secondPredicate != null) { + source.sendFeedback(Component.literal(String.format("%d: %s + %s = %s", i + 1, goal.firstString, goal.secondString, goal.resultString))); + } else { + source.sendFeedback(Component.literal(String.format("%d: %s = %s", i + 1, goal.firstString, goal.resultString))); + } + } + } + return Command.SINGLE_SUCCESS; + } + + private static int crack(FabricClientCommandSource source) throws CommandSyntaxException { + Villager targetVillager = VillagerCracker.getVillager(); + + if (!(targetVillager instanceof IVillager iVillager) || iVillager.clientcommands_getCrackedRandom() == null) { + throw NO_CRACKED_VILLAGER_PRESENT.create(); + } + + ItemStack selectedItem = source.getPlayer().getInventory().getSelected(); + if (selectedItem.getItem() instanceof BlockItem blockItem) { + VillagerProfession profession = PoiTypes.forState(blockItem.getBlock().defaultBlockState()) + .flatMap(poi -> BuiltInRegistries.VILLAGER_PROFESSION.stream().filter(p -> p.heldJobSite().test(poi)).findAny()) + .orElseThrow(SELECTED_ITEM_NOT_WORKSTATION::create); + VillagerTrades.ItemListing[] listings = VillagerTrades.TRADES.get(profession).getOrDefault(1, new VillagerTrades.ItemListing[0]); + int i = iVillager.clientcommands_bruteForceOffers(listings, profession, Configs.maxVillagerSimulationTicks, offer -> VillagerCommand.goals.stream().anyMatch(goal -> goal.matches(offer.first(), offer.second(), offer.result()))); + if (i == -1) { + LogUtils.getLogger().info("Could not find any matches"); + } else { + LogUtils.getLogger().info("Found a match at {} ticks in", i); + } + } else { + throw SELECTED_ITEM_NOT_WORKSTATION.create(); + } + + return Command.SINGLE_SUCCESS; + } + + private static int addGoal(WithStringArgument.Result firstPredicate, WithStringArgument.Result firstItemQuantityRange, @Nullable WithStringArgument.Result secondPredicate, @Nullable WithStringArgument.Result secondItemQuantityRange, WithStringArgument.Result resultPredicate, WithStringArgument.Result resultItemQuantityRange) { + goals.add(new Goal( + String.format("[%s] %s", firstItemQuantityRange.string(), firstPredicate.string()), + item -> firstPredicate.value().test(item) && firstItemQuantityRange.value().matches(item.getCount()), + + secondPredicate == null ? null : String.format("[%s] %s", secondItemQuantityRange.string(), secondPredicate.string()), + secondPredicate == null ? null : item -> secondPredicate.value().test(item) && secondItemQuantityRange.value().matches(item.getCount()), + + String.format("[%s] %s", resultItemQuantityRange.string(), resultPredicate.string()), + item -> resultPredicate.value().test(item) && resultItemQuantityRange.value().matches(item.getCount()))); + ClientCommandHelper.sendFeedback("commands.cvillager.goalAdded"); return Command.SINGLE_SUCCESS; } + + public record Goal(String firstString, Predicate firstPredicate, @Nullable String secondString, @Nullable Predicate secondPredicate, String resultString, Predicate resultPredicate) { + public boolean matches(ItemStack firstItem, @Nullable ItemStack secondItem, ItemStack result) { + return firstPredicate.test(firstItem) + && ((secondPredicate == null && secondItem == null) || secondItem != null && secondPredicate != null && secondPredicate.test(secondItem)) + && resultPredicate.test(result); + } + } + + public record Offer(ItemStack first, @Nullable ItemStack second, ItemStack result) {} } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/PredicatedRangeArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/PredicatedRangeArgument.java new file mode 100644 index 00000000..81cb1d3d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/PredicatedRangeArgument.java @@ -0,0 +1,82 @@ +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.Dynamic2CommandExceptionType; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +public interface PredicatedRangeArgument> extends ArgumentType { + Dynamic2CommandExceptionType INTEGER_VALUE_TOO_LOW = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("argument.integer.low", a, b)); + Dynamic2CommandExceptionType INTEGER_VALUE_TOO_HIGH = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("argument.integer.low", a, b)); + Dynamic2CommandExceptionType FLOAT_VALUE_TOO_LOW = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("argument.float.low", a, b)); + Dynamic2CommandExceptionType FLOAT_VALUE_TOO_HIGH = new Dynamic2CommandExceptionType((a, b) -> Component.translatable("argument.float.low", a, b)); + + static Ints intRange(@Nullable Integer min, @Nullable Integer max) { + return new Ints(min, max); + } + + static Floats floatRange(@Nullable Double min, @Nullable Double max) { + return new Floats(min, max); + } + + class Ints implements PredicatedRangeArgument { + @Nullable + private final Integer min; + @Nullable + private final Integer max; + + public Ints(@Nullable Integer min, @Nullable Integer max) { + this.min = min; + this.max = max; + } + + public static MinMaxBounds.Ints getRangeArgument(final CommandContext context, final String name) { + return context.getArgument(name, MinMaxBounds.Ints.class); + } + + @Override + public MinMaxBounds.Ints parse(StringReader reader) throws CommandSyntaxException { + MinMaxBounds.Ints range = MinMaxBounds.Ints.fromReader(reader); + if (min != null && (range.min().isEmpty() || range.min().get() < min)) { + throw INTEGER_VALUE_TOO_LOW.create(min, range.min().orElse(Integer.MIN_VALUE)); + } + if (max != null && (range.max().isEmpty() || range.max().get() > max)) { + throw INTEGER_VALUE_TOO_HIGH.create(max, range.max().orElse(Integer.MAX_VALUE)); + } + return range; + } + } + + class Floats implements PredicatedRangeArgument { + @Nullable + private final Double min; + @Nullable + private final Double max; + + public Floats(@Nullable Double min, @Nullable Double max) { + this.min = min; + this.max = max; + } + + public static MinMaxBounds.Doubles getRangeArgument(final CommandContext context, final String name) { + return context.getArgument(name, MinMaxBounds.Doubles.class); + } + + @Override + public MinMaxBounds.Doubles parse(StringReader reader) throws CommandSyntaxException { + MinMaxBounds.Doubles range = MinMaxBounds.Doubles.fromReader(reader); + if (min != null && (range.min().isEmpty() || range.min().get() < min)) { + throw FLOAT_VALUE_TOO_LOW.create(min, range.min().orElse(-Double.MAX_VALUE)); + } + if (max != null && (range.max().isEmpty() || range.max().get() < max)) { + throw FLOAT_VALUE_TOO_HIGH.create(max, range.max().orElse(Double.MAX_VALUE)); + } + return range; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java index b932ac03..f5527531 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java @@ -44,9 +44,14 @@ public static Villager getVillager() { return null; } - public static void setTargetVillager(Villager villager) { + public static void setTargetVillager(@Nullable Villager villager) { + Villager oldVillager = getVillager(); + if (oldVillager != null) { + ((IVillager) oldVillager).clientcommands_setCrackedRandom(null); + } + VillagerCracker.cachedVillager = new WeakReference<>(villager); - VillagerCracker.villagerUuid = villager.getUUID(); + VillagerCracker.villagerUuid = villager == null ? null : villager.getUUID(); } public static void onSoundEventPlayed(ClientboundSoundPacket packet) { @@ -62,7 +67,7 @@ public static void onSoundEventPlayed(ClientboundSoundPacket packet) { ClientCommandHelper.sendError(Component.translatable("commands.cvillager.crackFailed")); } else { ((IVillager) targetVillager).clientcommands_setCrackedRandom(RandomSource.create(possible[0] ^ 0x5deece66dL)); - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.crackSuccess", Long.toHexString(possible[0]))); + ClientCommandHelper.sendFeedback("commands.cvillager.crackSuccess", Long.toHexString(possible[0])); } } @@ -79,4 +84,5 @@ public static void onServerTick() { ((IVillager) targetVillager).clientcommands_onServerTick(); } + } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java index 764eb649..912b44d9 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java @@ -1,25 +1,18 @@ package net.earthcomputer.clientcommands.features; -import com.mojang.logging.LogUtils; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import net.minecraft.client.Minecraft; -import net.minecraft.network.chat.Component; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.VillagerCommand; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.npc.Villager; -import net.minecraft.world.entity.npc.VillagerData; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.item.trading.MerchantOffer; -import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.level.levelgen.LegacyRandomSource; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; public class VillagerRngSimulator { - private static final Logger LOGGER = LogUtils.getLogger(); - @Nullable private LegacyRandomSource random; private int ambientSoundTime; @@ -32,8 +25,7 @@ public VillagerRngSimulator(@Nullable LegacyRandomSource random, int ambientSoun this.ambientSoundTime = ambientSoundTime; } - @Override - public VillagerRngSimulator clone() { + public VillagerRngSimulator copy() { VillagerRngSimulator that = new VillagerRngSimulator(random == null ? null : new LegacyRandomSource(random.seed.get() ^ 0x5deece66dL), ambientSoundTime); that.waitingTicks = this.waitingTicks; that.madeSound = this.madeSound; @@ -42,13 +34,20 @@ public VillagerRngSimulator clone() { } public void simulateTick() { + if (random == null) { + return; + } + + simulateBaseTick(); + simulateServerAiStep(); + } + + public void simulateBaseTick() { if (waitingTicks > 0) { waitingTicks--; return; } - LOGGER.info("Client (pre-tick): {}", this); - if (random == null) { return; } @@ -62,39 +61,35 @@ public void simulateTick() { } else { madeSound = false; } - - random.nextInt(100); } - @Nullable - public MerchantOffers simulateTrades(Villager villager) { - VillagerData villagerData = villager.getVillagerData(); - Int2ObjectMap map = VillagerTrades.TRADES.get(villagerData.getProfession()); - - if (map == null || map.isEmpty()) { - return null; + public void simulateServerAiStep() { + if (random == null) { + return; } - return simulateOffers(map.get(villagerData.getLevel()), villager); + random.nextInt(100); } - private MerchantOffers simulateOffers(VillagerTrades.ItemListing[] listings, Entity trader) { - if (random == null) { - return null; + public boolean anyOffersMatch(VillagerTrades.ItemListing[] listings, Entity trader, Predicate predicate) { + if (!isCracked()) { + return false; } - MerchantOffers offers = new MerchantOffers(); ArrayList newListings = new ArrayList<>(List.of(listings)); int i = 0; while (i < 2 && !newListings.isEmpty()) { VillagerTrades.ItemListing listing = newListings.remove(random.nextInt(newListings.size())); MerchantOffer offer = listing.getOffer(trader, random); if (offer != null) { - offers.add(offer); - i++; + if (predicate.test(new VillagerCommand.Offer(offer.getBaseCostA(), offer.getCostB(), offer.getResult()))) { + return true; + } else { + i++; + } } } - return offers; + return false; } @Nullable @@ -102,6 +97,10 @@ public LegacyRandomSource getRandom() { return random; } + public boolean isCracked() { + return random != null && !firstAmbientNoise; + } + public void setRandom(@Nullable LegacyRandomSource random) { this.random = random; } @@ -128,31 +127,30 @@ public void onAmbientSoundPlayed() { // is in sync if (madeSound) { - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.perfectlyInSync")); + ClientCommandHelper.sendFeedback("commands.cvillager.perfectlyInSync"); return; } // is not in sync, needs to be re-synced - VillagerRngSimulator clone = clone(); + VillagerRngSimulator copy = copy(); int i = 0; - while (!clone.madeSound) { + while (!copy.madeSound) { i++; - clone.simulateTick(); + copy.simulateTick(); } - // todo, use ping if it's meant to be used (the idea here is to sync up to when the server says the villager makes a noise) if (0 < i && i < 30) { // in this case, it's a believable jump that we're less than 30 ticks behind, so we'll advance by the amount we calculated to be what this tick should've been - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.tooManyTicksBehind", i)); - this.random = clone.random; - this.ambientSoundTime = clone.ambientSoundTime; - this.waitingTicks = clone.waitingTicks; - this.madeSound = clone.madeSound; + ClientCommandHelper.sendFeedback("commands.cvillager.tooManyTicksBehind", i); + this.random = copy.random; + this.ambientSoundTime = copy.ambientSoundTime; + this.waitingTicks = copy.waitingTicks; + this.madeSound = copy.madeSound; } else if (i > 30) { // in this case, it took so many ticks to advance to rsync, that it's safe to assume we are ahead of the server, so we'll let the server catch up by 30 ticks - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.tooManyTicksAhead")); + ClientCommandHelper.sendFeedback("commands.cvillager.tooManyTicksAhead"); waitingTicks += 30; } else { - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.perfectlyInSync")); + ClientCommandHelper.sendFeedback("commands.cvillager.perfectlyInSync"); } } } diff --git a/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java b/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java index 3cdeae93..ac97f03c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java +++ b/src/main/java/net/earthcomputer/clientcommands/interfaces/IVillager.java @@ -1,14 +1,22 @@ package net.earthcomputer.clientcommands.interfaces; +import net.earthcomputer.clientcommands.command.VillagerCommand; import net.earthcomputer.clientcommands.features.VillagerRngSimulator; import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerTrades; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; public interface IVillager { - void clientcommands_setCrackedRandom(RandomSource random); + void clientcommands_setCrackedRandom(@Nullable RandomSource random); VillagerRngSimulator clientcommands_getCrackedRandom(); void clientcommands_onAmbientSoundPlayed(); void clientcommands_onServerTick(); + + int clientcommands_bruteForceOffers(VillagerTrades.ItemListing[] listings, VillagerProfession profession, int maxTicks, Predicate predicate); } 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 index 3acdd61e..25383b16 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/VillagerMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/VillagerMixin.java @@ -1,36 +1,25 @@ package net.earthcomputer.clientcommands.mixin.commands.villager; -import com.mojang.logging.LogUtils; -import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.earthcomputer.clientcommands.command.VillagerCommand; import net.earthcomputer.clientcommands.features.VillagerRngSimulator; import net.earthcomputer.clientcommands.interfaces.IVillager; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.network.chat.Component; import net.minecraft.util.RandomSource; 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.item.Item; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.item.trading.MerchantOffer; -import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.entity.npc.*; import net.minecraft.world.level.Level; import net.minecraft.world.level.levelgen.LegacyRandomSource; -import org.slf4j.Logger; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.stream.Collectors; +import java.util.function.Predicate; @Mixin(Villager.class) public abstract class VillagerMixin extends AbstractVillager implements IVillager { - @Unique - private static final Logger LOGGER = LogUtils.getLogger(); + @Shadow public abstract VillagerData getVillagerData(); + + @Shadow public abstract void setVillagerData(VillagerData data); public VillagerMixin(EntityType entityType, Level level) { super(entityType, level); @@ -40,7 +29,7 @@ public VillagerMixin(EntityType entityType, Level le VillagerRngSimulator rng = new VillagerRngSimulator(null, -80); @Override - public void clientcommands_setCrackedRandom(RandomSource random) { + public void clientcommands_setCrackedRandom(@Nullable RandomSource random) { rng.setRandom((LegacyRandomSource) random); } @@ -54,56 +43,63 @@ public void clientcommands_onAmbientSoundPlayed() { rng.onAmbientSoundPlayed(); } +// public void clientcommands_onProfessionUpdate(VillagerProfession newProfession) { +// Villager targetVillager = VillagerCracker.getVillager(); +// if (targetVillager instanceof IVillager iVillager && iVillager.clientcommands_getCrackedRandom().isCracked()) { +// +// if (offers == null) { +// return; +// } +// for (MerchantOffer offer : offers) { +// if (offer.getItemCostB().isPresent()) { +// LOGGER.info("[x{}] {} + [x{}] {} = [x{}] {} ({})", +// offer.getItemCostA().count(), +// Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostA().item().value()).getPath()).getString(), +// offer.getItemCostB().get().count(), +// Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostB().get().item().value()).getPath()).getString(), +// offer.getResult().getCount(), +// I18n.get(BuiltInRegistries.ITEM.getKey(offer.getResult().getItem()).getPath()), +// offer.getResult().getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL).stream().map(Component::getString).skip(1).collect(Collectors.joining(", "))); +// } else { +// LOGGER.info("[x{}] {} = [x{}] {} ({})", +// offer.getItemCostA().count(), +// Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostA().item().value()).getPath()).getString(), +// offer.getResult().getCount(), +// Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getResult().getItem()).getPath()).getString(), +// offer.getResult().getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL).stream().map(Component::getString).skip(1).collect(Collectors.joining(", "))); +// } +// } +// } +// } + @Override public void clientcommands_onServerTick() { rng.simulateTick(); } - @Inject(method = "updateTrades", at = @At("HEAD")) - public void onUpdateTrades(CallbackInfo ci) { - if (!level().isClientSide) { - LOGGER.info("Server Seed (b4 trade): {}", ((LegacyRandomSource) random).seed.get()); + @Override + public int clientcommands_bruteForceOffers(VillagerTrades.ItemListing[] listings, VillagerProfession profession, int maxTicks, Predicate predicate) { + if (this instanceof IVillager iVillager && iVillager.clientcommands_getCrackedRandom().isCracked()) { + VillagerProfession oldProfession = getVillagerData().getProfession(); + setVillagerData(getVillagerData().setProfession(profession)); - Villager targetVillager = VillagerCracker.getVillager(); - if (targetVillager != null && this.getUUID().equals(targetVillager.getUUID()) && ((IVillager) targetVillager).clientcommands_getCrackedRandom() != null) { - VillagerRngSimulator randomBranch = ((IVillager) targetVillager).clientcommands_getCrackedRandom().clone(); - LOGGER.info("Client Seed (pre-interact): {}", randomBranch); - randomBranch.simulateTick(); - LOGGER.info("Client Seed (post-tick): {}", randomBranch); - targetVillager.setVillagerData(targetVillager.getVillagerData().setProfession(VillagerProfession.LIBRARIAN)); - MerchantOffers offers = randomBranch.simulateTrades(targetVillager); - targetVillager.setVillagerData(targetVillager.getVillagerData().setProfession(VillagerProfession.NONE)); - if (offers == null) { - return; + VillagerRngSimulator rng = this.rng.copy(); + int i = 0; + while (i < maxTicks) { + VillagerRngSimulator randomBranch = rng.copy(); + randomBranch.simulateBaseTick(); + if (randomBranch.anyOffersMatch(listings, (Villager) (Object) this, predicate)) { + setVillagerData(getVillagerData().setProfession(oldProfession)); + return i; } - for (MerchantOffer offer : offers) { - if (offer.getItemCostB().isPresent()) { - LOGGER.info("[x{}] {} + [x{}] {} = [x{}] {} ({})", - offer.getItemCostA().count(), - Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostA().item().value()).getPath()).getString(), - offer.getItemCostB().get().count(), - Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostB().get().item().value()).getPath()).getString(), - offer.getResult().getCount(), - I18n.get(BuiltInRegistries.ITEM.getKey(offer.getResult().getItem()).getPath()), - offer.getResult().getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL).stream().map(Component::getString).skip(1).collect(Collectors.joining(", "))); - } else { - LOGGER.info("[x{}] {} = [x{}] {} ({})", - offer.getItemCostA().count(), - Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getItemCostA().item().value()).getPath()).getString(), - offer.getResult().getCount(), - Component.translatable(BuiltInRegistries.ITEM.getKey(offer.getResult().getItem()).getPath()).getString(), - offer.getResult().getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL).stream().map(Component::getString).skip(1).collect(Collectors.joining(", "))); - } - } - LOGGER.info("Client Seed (post-interact): {}", randomBranch); + randomBranch.simulateServerAiStep(); + rng.simulateTick(); + i++; } - } - } - @Inject(method = "tick", at = @At("HEAD")) - private void startTick(CallbackInfo ci) { - if (!level().isClientSide) { - LOGGER.info("Server Seed (pre-tick): {}", ((LegacyRandomSource) random).seed.get()); + setVillagerData(getVillagerData().setProfession(oldProfession)); } + + return -1; } } diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index b719b031..10f63c68 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -46,10 +46,16 @@ "commands.ccrackrng.success": "Player RNG cracked: %d", "commands.cvillager.notAVillager": "Target was not a villager", + "commands.cvillager.selected_item_not_workstation": "Your selected item was not a valid workstation block", + "commands.cvillager.no_cracked_villager_present": "There was no cracked villager available to use", + "commands.cvillager.listGoals.noGoals": "There are no villager goals.", + "commands.cvillager.listGoals.success": "There are %d villager goals:", "commands.cvillager.targetSet": "Target entity set", + "commands.cvillager.targetCleared": "Target entity cleared", "commands.cvillager.clockSet": "Clock set to %d %d %d", "commands.cvillager.crackFailed": "Failed to crack villager seed", - "commands.cvillager.crackSuccess": "Successfully cracked villager seed: %d", + "commands.cvillager.crackSuccess": "Villager RNG cracked: %d", + "commands.cvillager.goalAdded": "Added goal successfully.", "commands.cvillager.tooManyTicksAhead": "Was too many ticks ahead of villager cracking, awaiting the next 30 ticks of processing. Will likely need another ambient sound to resync", "commands.cvillager.tooManyTicksBehind": "Was too many ticks behind of villager cracking, simulating by %d ticks, should be in sync.", "commands.cvillager.perfectlyInSync": "Your villager's random is perfectly in sync",