diff --git a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java index cf4e3a4c..9db385ac 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java @@ -6,7 +6,6 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import net.earthcomputer.clientcommands.features.VillagerCracker; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; @@ -22,17 +21,17 @@ public class VillagerCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register( literal("cvillager") - .then(literal("timer") - .then(argument("value", blockPos()) - .executes(ctx -> setTimerBlockPos(ctx.getSource(), getBlockPos(ctx, "value"))))) + .then(literal("clock") + .then(argument("pos", blockPos()) + .executes(ctx -> setClockBlockPos(ctx.getSource(), getBlockPos(ctx, "pos"))))) .then(literal("target") - .then(argument("value", entity()) - .executes(ctx -> setVillagerTarget(ctx.getSource(), getEntity(ctx, "value")))))); + .then(argument("entity", entity()) + .executes(ctx -> setVillagerTarget(ctx.getSource(), getEntity(ctx, "entity")))))); } - private static int setTimerBlockPos(FabricClientCommandSource source, BlockPos pos) { - VillagerCracker.timerBlockPos = pos; - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.timerSet", pos.getX(), pos.getY(), pos.getZ())); + private static int setClockBlockPos(FabricClientCommandSource source, BlockPos pos) { + VillagerCracker.clockBlockPos = pos; + source.getPlayer().sendSystemMessage(Component.translatable("commands.cvillager.clockSet", pos.getX(), pos.getY(), pos.getZ())); return Command.SINGLE_SUCCESS; } @@ -42,7 +41,7 @@ private static int setVillagerTarget(FabricClientCommandSource source, Entity ta } VillagerCracker.setTargetVillager(villager); - Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.targetSet")); + source.getPlayer().sendSystemMessage(Component.translatable("commands.cvillager.targetSet")); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java index a136d566..b932ac03 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java @@ -21,7 +21,7 @@ public class VillagerCracker { private static WeakReference cachedVillager = null; @Nullable - public static BlockPos timerBlockPos = null; + public static BlockPos clockBlockPos = null; @Nullable public static Villager getVillager() { diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java index 9829e6db..764eb649 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java @@ -1,12 +1,31 @@ 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.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; public class VillagerRngSimulator { + private static final Logger LOGGER = LogUtils.getLogger(); + @Nullable - private final LegacyRandomSource random; + private LegacyRandomSource random; private int ambientSoundTime; + private int waitingTicks = 0; + private boolean madeSound = false; + private boolean firstAmbientNoise = true; public VillagerRngSimulator(@Nullable LegacyRandomSource random, int ambientSoundTime) { this.random = random; @@ -14,18 +33,28 @@ public VillagerRngSimulator(@Nullable LegacyRandomSource random, int ambientSoun } @Override - protected Object clone() { - return new VillagerRngSimulator(random == null ? null : new LegacyRandomSource(random.seed.get() ^ 0x5deece66dL), ambientSoundTime); + public VillagerRngSimulator clone() { + VillagerRngSimulator that = new VillagerRngSimulator(random == null ? null : new LegacyRandomSource(random.seed.get() ^ 0x5deece66dL), ambientSoundTime); + that.waitingTicks = this.waitingTicks; + that.madeSound = this.madeSound; + that.firstAmbientNoise = this.firstAmbientNoise; + return that; } - public boolean simulateTick() { - boolean madeSound; + public void simulateTick() { + if (waitingTicks > 0) { + waitingTicks--; + return; + } + + LOGGER.info("Client (pre-tick): {}", this); if (random == null) { - return false; + return; } - if (random.nextInt(1000) < ambientSoundTime++) { + // 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. + if (random.nextInt(1000) < ambientSoundTime++ && !firstAmbientNoise) { random.nextFloat(); random.nextFloat(); ambientSoundTime = -80; @@ -34,15 +63,47 @@ public boolean simulateTick() { madeSound = false; } - return madeSound; + 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; + } + + return simulateOffers(map.get(villagerData.getLevel()), villager); } - public LegacyRandomSource random() { + private MerchantOffers simulateOffers(VillagerTrades.ItemListing[] listings, Entity trader) { + if (random == null) { + return null; + } + + 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++; + } + } + return offers; + } + + @Nullable + public LegacyRandomSource getRandom() { return random; } - public int getAmbientSoundTime() { - return ambientSoundTime; + public void setRandom(@Nullable LegacyRandomSource random) { + this.random = random; } @Override @@ -53,10 +114,45 @@ public String toString() { } public void onAmbientSoundPlayed() { - ambientSoundTime = -80; - if (random != null) { + if (firstAmbientNoise) { + if (random == null) { + return; + } + + firstAmbientNoise = false; + ambientSoundTime = -80; random.nextFloat(); random.nextFloat(); + madeSound = true; + } + + // is in sync + if (madeSound) { + Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.perfectlyInSync")); + return; + } + + // is not in sync, needs to be re-synced + VillagerRngSimulator clone = clone(); + int i = 0; + while (!clone.madeSound) { + i++; + clone.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; + } 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")); + waitingTicks += 30; + } else { + Minecraft.getInstance().player.sendSystemMessage(Component.translatable("commands.cvillager.perfectlyInSync")); } } } 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 7d8a5e13..3acdd61e 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,27 +1,47 @@ package net.earthcomputer.clientcommands.mixin.commands.villager; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.features.VillagerCracker; import net.earthcomputer.clientcommands.features.VillagerRngSimulator; import net.earthcomputer.clientcommands.interfaces.IVillager; -import net.minecraft.client.Minecraft; +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.level.Level; import net.minecraft.world.level.levelgen.LegacyRandomSource; +import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; 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; @Mixin(Villager.class) -public class VillagerMixin implements IVillager { +public abstract class VillagerMixin extends AbstractVillager implements IVillager { @Unique - VillagerRngSimulator rng = new VillagerRngSimulator(null, 0); + private static final Logger LOGGER = LogUtils.getLogger(); + + public VillagerMixin(EntityType entityType, Level level) { + super(entityType, level); + } @Unique - boolean hasSetCrackedAmbientSoundTime = false; + VillagerRngSimulator rng = new VillagerRngSimulator(null, -80); @Override public void clientcommands_setCrackedRandom(RandomSource random) { - rng = new VillagerRngSimulator((LegacyRandomSource) random, rng.getAmbientSoundTime()); - hasSetCrackedAmbientSoundTime = false; + rng.setRandom((LegacyRandomSource) random); } @Override @@ -31,16 +51,59 @@ public VillagerRngSimulator clientcommands_getCrackedRandom() { @Override public void clientcommands_onAmbientSoundPlayed() { - if (!hasSetCrackedAmbientSoundTime) { - rng.onAmbientSoundPlayed(); - } + rng.onAmbientSoundPlayed(); } @Override public void clientcommands_onServerTick() { - if (rng.simulateTick()) { - hasSetCrackedAmbientSoundTime = true; - Minecraft.getInstance().player.sendSystemMessage(Component.literal("hrmm")); + 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()); + + 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; + } + 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); + } + } + } + + @Inject(method = "tick", at = @At("HEAD")) + private void startTick(CallbackInfo ci) { + if (!level().isClientSide) { + LOGGER.info("Server Seed (pre-tick): {}", ((LegacyRandomSource) random).seed.get()); } } } 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 08ac67b4..ae7ee2a0 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/rngevents/ClientPacketListenerMixin.java @@ -40,7 +40,7 @@ private void onHandleSoundEvent(ClientboundSoundPacket packet, CallbackInfo ci) @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 (packet.getPos().equals(VillagerCracker.timerBlockPos)) { + if (packet.getPos().equals(VillagerCracker.clockBlockPos)) { VillagerCracker.onServerTick(); } } @@ -48,7 +48,7 @@ private void onHandleBlockUpdate(ClientboundBlockUpdatePacket packet, CallbackIn @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) { packet.runUpdates((pos, state) -> { - if (pos.equals(VillagerCracker.timerBlockPos)) { + if (pos.equals(VillagerCracker.clockBlockPos)) { VillagerCracker.onServerTick(); } }); diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 3725711f..b719b031 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -46,10 +46,13 @@ "commands.ccrackrng.success": "Player RNG cracked: %d", "commands.cvillager.notAVillager": "Target was not a villager", - "commands.cvillager.targetSet": "Target set", - "commands.cvillager.timerSet": "Timer block position set to %d %d %d", + "commands.cvillager.targetSet": "Target entity set", + "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.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", "commands.ccreativetab.addStack.success": "Successfully added %s to \"%s\"", "commands.ccreativetab.addTab.alreadyExists": "Creative tab \"%s\" already exists",