diff --git a/build.gradle b/build.gradle index 0b57d410e..fd163f761 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import net.earthcomputer.clientcommands.buildscript.CheckLanguageFilesTask +import net.earthcomputer.clientcommands.buildscript.GenerateBuildInfoTask plugins { id 'fabric-loom' version '1.9-SNAPSHOT' @@ -94,6 +95,10 @@ jar { from("LICENSE") } +tasks.register('generateBuildInfo', GenerateBuildInfoTask) { + outputFile = new File(temporaryDir, "build_info.json") +} + processResources { inputs.property "version", project.version inputs.property "mcversion", project.minecraft_version_dependency @@ -108,6 +113,8 @@ processResources { from(sourceSets.main.resources.srcDirs) { exclude "fabric.mod.json" } + + from generateBuildInfo } java { diff --git a/buildSrc/src/main/kotlin/net/earthcomputer/clientcommands/buildscript/GenerateBuildInfoTask.kt b/buildSrc/src/main/kotlin/net/earthcomputer/clientcommands/buildscript/GenerateBuildInfoTask.kt new file mode 100644 index 000000000..27a95be90 --- /dev/null +++ b/buildSrc/src/main/kotlin/net/earthcomputer/clientcommands/buildscript/GenerateBuildInfoTask.kt @@ -0,0 +1,40 @@ +package net.earthcomputer.clientcommands.buildscript + +import com.google.gson.Gson +import com.google.gson.JsonObject +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream +import javax.inject.Inject + +abstract class GenerateBuildInfoTask : DefaultTask() { + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @get:Inject + protected abstract val execOperations: ExecOperations + + init { + outputs.upToDateWhen { false } + } + + @TaskAction + fun run() { + val gitCommitHashBytes = ByteArrayOutputStream() + execOperations.exec { + commandLine("git", "rev-parse", "HEAD") + standardOutput = gitCommitHashBytes + }.rethrowFailure() + val commitHash = gitCommitHashBytes.toByteArray().decodeToString().trim() + + val json = JsonObject() + json.addProperty("commit_hash", commitHash) + + outputFile.asFile.get().writer().use { + Gson().toJson(json, it) + } + } +} diff --git a/docs/villager_rng_setup.png b/docs/villager_rng_setup.png new file mode 100644 index 000000000..43b7d0980 Binary files /dev/null and b/docs/villager_rng_setup.png differ diff --git a/regressionTests/villagerRandomHierarchy.regressiontest b/regressionTests/villagerRandomHierarchy.regressiontest new file mode 100644 index 000000000..f2a0ca71b --- /dev/null +++ b/regressionTests/villagerRandomHierarchy.regressiontest @@ -0,0 +1,133 @@ +net/minecraft/world/entity/Entity. (Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/level/Level;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity. (Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/level/Level;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.doWaterSplashEffect ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/InfestedMobEffect.onMobHurt (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;ILnet/minecraft/world/damagesource/DamageSource;F)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/InfestedMobEffect.onMobHurt (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;ILnet/minecraft/world/damagesource/DamageSource;F)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/InfestedMobEffect.spawnSilverfish (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;DDD)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/OozingMobEffect.onMobRemoved (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;ILnet/minecraft/world/entity/Entity$RemovalReason;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/WeavingMobEffect.onMobRemoved (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;ILnet/minecraft/world/entity/Entity$RemovalReason;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/effect/WindChargedMobEffect.onMobRemoved (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;ILnet/minecraft/world/entity/Entity$RemovalReason;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/CelebrateVillagersSurvivedRaid.tick (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/CrossbowAttack.crossbowAttack (Lnet/minecraft/world/entity/Mob;Lnet/minecraft/world/entity/LivingEntity;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/LocateHidingPlace.create (IFI)Lnet/minecraft/world/entity/ai/behavior/OneShot; <- net/minecraft/world/entity/ai/behavior/LocateHidingPlace.lambda$create$10 <- net/minecraft/world/entity/ai/behavior/LocateHidingPlace.lambda$create$9 <- net/minecraft/world/entity/ai/behavior/LocateHidingPlace.lambda$create$8 <- net/minecraft/world/entity/ai/behavior/LocateHidingPlace.lambda$create$5 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/LongJumpToPreferredBlock.start (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Mob;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/MoveToSkySeeingSpot.getOutdoorPosition (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/RandomLookAround.start (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Mob;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/Swim.tick (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Mob;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/VillagerMakeLove.start (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/VillagerMakeLove.tick (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;J)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/AirAndWaterRandomPos.generateRandomPos (Lnet/minecraft/world/entity/PathfinderMob;IIIDDDZ)Lnet/minecraft/core/BlockPos; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/AirAndWaterRandomPos.generateRandomPos (Lnet/minecraft/world/entity/PathfinderMob;IIIDDDZ)Lnet/minecraft/core/BlockPos; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/DefaultRandomPos.generateRandomPosTowardDirection (Lnet/minecraft/world/entity/PathfinderMob;IZLnet/minecraft/core/BlockPos;)Lnet/minecraft/core/BlockPos; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/DefaultRandomPos.getPos (Lnet/minecraft/world/entity/PathfinderMob;II)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/DefaultRandomPos.lambda$getPos$0 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/DefaultRandomPos.getPosAway (Lnet/minecraft/world/entity/PathfinderMob;IILnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/DefaultRandomPos.lambda$getPosAway$2 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/DefaultRandomPos.getPosTowards (Lnet/minecraft/world/entity/PathfinderMob;IILnet/minecraft/world/phys/Vec3;D)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/DefaultRandomPos.lambda$getPosTowards$1 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/HoverRandomPos.getPos (Lnet/minecraft/world/entity/PathfinderMob;IIDDFII)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/HoverRandomPos.lambda$getPos$1 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/LandRandomPos.generateRandomPosTowardDirection (Lnet/minecraft/world/entity/PathfinderMob;IZLnet/minecraft/core/BlockPos;)Lnet/minecraft/core/BlockPos; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/LandRandomPos.getPos (Lnet/minecraft/world/entity/PathfinderMob;IILjava/util/function/ToDoubleFunction;)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/LandRandomPos.lambda$getPos$0 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/util/LandRandomPos.getPosInDirection (Lnet/minecraft/world/entity/PathfinderMob;IILnet/minecraft/world/phys/Vec3;Z)Lnet/minecraft/world/phys/Vec3; <- net/minecraft/world/entity/ai/util/LandRandomPos.lambda$getPosInDirection$1 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/food/FoodProperties.onConsume (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/component/Consumable;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/CrossbowItem.shootProjectile (Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/entity/projectile/Projectile;IFFFLnet/minecraft/world/entity/LivingEntity;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/ItemStack.onUseTick (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/LivingEntity;I)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/component/Consumable.onConsume (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;)Lnet/minecraft/world/item/ItemStack; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.apply (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)Z <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.apply (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)Z <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.apply (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)Z <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.apply (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)Z <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/Enchantment.modifyDamageFilteredValue (Lnet/minecraft/core/component/DataComponentType;Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lorg/apache/commons/lang3/mutable/MutableFloat;)V <- net/minecraft/world/item/enchantment/Enchantment.lambda$modifyDamageFilteredValue$7 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/Enchantment.modifyEntityFilteredValue (Lnet/minecraft/core/component/DataComponentType;Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/Entity;Lorg/apache/commons/lang3/mutable/MutableFloat;)V <- net/minecraft/world/item/enchantment/Enchantment.lambda$modifyEntityFilteredValue$6 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/Enchantment.modifyDamageProtection (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lorg/apache/commons/lang3/mutable/MutableFloat;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/EnchantmentHelper.getRandomItemWith (Lnet/minecraft/core/component/DataComponentType;Lnet/minecraft/world/entity/LivingEntity;Ljava/util/function/Predicate;)Ljava/util/Optional; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/EnchantmentHelper.getTridentSpinAttackStrength (Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)F <- net/minecraft/world/item/enchantment/EnchantmentHelper.lambda$getTridentSpinAttackStrength$36 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/EnchantmentHelper.modifyCrossbowChargingTime (Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;F)F <- net/minecraft/world/item/enchantment/EnchantmentHelper.lambda$modifyCrossbowChargingTime$35 <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/EnchantmentHelper.processEquipmentDropChance (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/damagesource/DamageSource;F)F <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/ApplyMobEffect.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/DamageEntity.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/PlaySoundEffect.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/ReplaceBlock.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/ReplaceDisk.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/enchantment/effects/SpawnParticlesEffect.apply (Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/item/enchantment/EnchantedItemInUse;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/level/pathfinder/FlyNodeEvaluator.iteratePathfindingStartNodeCandidatePositions (Lnet/minecraft/world/entity/Mob;)Ljava/lang/Iterable; <- net/minecraft/world/entity/Entity.getRandom <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/AgeableMob.aiStep ()V <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.tickEffects ()V <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.handleEntityEvent (B)V <- net/minecraft/world/entity/npc/AbstractVillager.addParticlesAroundSelf <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.handleEntityEvent (B)V <- net/minecraft/world/entity/npc/AbstractVillager.addParticlesAroundSelf <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.handleEntityEvent (B)V <- net/minecraft/world/entity/npc/AbstractVillager.addParticlesAroundSelf <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.handleEntityEvent (B)V <- net/minecraft/world/entity/npc/AbstractVillager.addParticlesAroundSelf <- net/minecraft/world/entity/Entity.getRandomX <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/AgeableMob.aiStep ()V <- net/minecraft/world/entity/Entity.getRandomY <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.getRandomY <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.tickEffects ()V <- net/minecraft/world/entity/Entity.getRandomY <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/AgeableMob.aiStep ()V <- net/minecraft/world/entity/Entity.getRandomZ <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.getRandomZ <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.tickEffects ()V <- net/minecraft/world/entity/Entity.getRandomZ <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.lavaHurt ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.moveTowardsClosestSpace (DDD)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.playAmethystStepSound ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.playEntityOnFireExtinguishedSound ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.playEntityOnFireExtinguishedSound ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.playSwimSound (F)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.playSwimSound (F)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.spawnSprintParticle ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Entity.spawnSprintParticle ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.decreaseAirSupply (I)I <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.hurtServer (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;F)Z <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.playHurtSound (Lnet/minecraft/world/damagesource/DamageSource;)V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.playAmbientSound ()V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/monster/hoglin/HoglinAi.updateActivity (Lnet/minecraft/world/entity/monster/hoglin/Hoglin;)V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/monster/piglin/PiglinAi.updateActivity (Lnet/minecraft/world/entity/monster/piglin/Piglin;)V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/AbstractVillager.notifyTradeUpdated (Lnet/minecraft/world/item/ItemStack;)V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/AbstractVillager.playCelebrateSound ()V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.playWorkSound ()V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.setUnhappy ()V <- net/minecraft/world/entity/LivingEntity.makeSound <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.tick (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/PathfinderMob;J)V <- net/minecraft/world/entity/LivingEntity.getVoicePitch <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleDamageEvent (Lnet/minecraft/world/damagesource/DamageSource;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleDamageEvent (Lnet/minecraft/world/damagesource/DamageSource;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.handleEntityEvent (B)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.makePoofParticles ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.onEquipItem (Lnet/minecraft/world/entity/EquipmentSlot;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.pushEntities ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.breakItem (Lnet/minecraft/world/item/ItemStack;)V <- net/minecraft/world/entity/LivingEntity.spawnItemParticles <- net/minecraft/world/entity/Entity.random +net/minecraft/world/item/component/Consumable.emitParticlesAndSounds (Lnet/minecraft/util/RandomSource;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;I)V <- net/minecraft/world/entity/LivingEntity.spawnItemParticles <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.tickEffects ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.tickEffects ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/LivingEntity.updateFallFlying ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.baseTick ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.checkDespawn ()V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.dropCustomDeathLoot (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.dropCustomDeathLoot (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.dropCustomDeathLoot (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.equipItemIfPossible (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/item/ItemStack;)Lnet/minecraft/world/item/ItemStack; <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.getBaseExperienceReward (Lnet/minecraft/server/level/ServerLevel;)I <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.getBaseExperienceReward (Lnet/minecraft/server/level/ServerLevel;)I <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.getBaseExperienceReward (Lnet/minecraft/server/level/ServerLevel;)I <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/Mob.isSunBurnTick ()Z <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/AbstractVillager.addOffersFromItemListings (Lnet/minecraft/world/item/trading/MerchantOffers;[Lnet/minecraft/world/entity/npc/VillagerTrades$ItemListing;I)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/AbstractVillager.addOffersFromItemListings (Lnet/minecraft/world/item/trading/MerchantOffers;[Lnet/minecraft/world/entity/npc/VillagerTrades$ItemListing;I)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.customServerAiStep (Lnet/minecraft/server/level/ServerLevel;)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.getBreedOffspring (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/AgeableMob;)Lnet/minecraft/world/entity/npc/Villager; <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.gossip (Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;J)V <- net/minecraft/world/entity/Entity.random +net/minecraft/world/entity/npc/Villager.rewardTradeXp (Lnet/minecraft/world/item/trading/MerchantOffer;)V <- net/minecraft/world/entity/Entity.random diff --git a/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java b/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java index cf5b82606..d22be46bd 100644 --- a/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java +++ b/src/codeGen/java/net/earthcomputer/clientcommands/codegen/CodeGenerator.java @@ -19,10 +19,9 @@ public static void main(String[] args) throws IOException { } Path destDir = Path.of(args[0]); - genLattiCG(destDir); + genPlayerLattiCG(destDir); } - - private static void genLattiCG(Path destDir) throws IOException { + private static void genPlayerLattiCG(Path destDir) throws IOException { ProgramBuilder program = Program.builder(LCG.JAVA); program.skip(-CCrackRng.NUM_THROWS * 4); for (int i = 0; i < CCrackRng.NUM_THROWS; i++) { diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 1ce694cbd..0dc11c8c8 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -172,6 +172,7 @@ public static void registerCommands(CommandDispatcher UsageTreeCommand.register(dispatcher); UuidCommand.register(dispatcher); VarCommand.register(dispatcher); + VillagerCommand.register(dispatcher, context); WeatherCommand.register(dispatcher); WhisperEncryptedCommand.register(dispatcher); WikiCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/Configs.java b/src/main/java/net/earthcomputer/clientcommands/Configs.java index 9ad287476..d3ec6597f 100644 --- a/src/main/java/net/earthcomputer/clientcommands/Configs.java +++ b/src/main/java/net/earthcomputer/clientcommands/Configs.java @@ -7,6 +7,7 @@ import net.earthcomputer.clientcommands.features.FishingCracker; import net.earthcomputer.clientcommands.features.PlayerRandCracker; import net.earthcomputer.clientcommands.features.ServerBrandManager; +import net.earthcomputer.clientcommands.features.VillagerCracker; import net.earthcomputer.clientcommands.util.MultiVersionCompat; import net.minecraft.util.Mth; import net.minecraft.util.StringRepresentable; @@ -171,4 +172,20 @@ public enum PacketDumpMethod { public static void setMinimumReplyDelaySeconds(float minimumReplyDelaySeconds) { Configs.minimumReplyDelaySeconds = Math.clamp(minimumReplyDelaySeconds, 0.0f, ReplyCommand.MAXIMUM_REPLY_DELAY_SECONDS); } + + @Config(onChange = "onChangeVillagerManipulation", temporary = true) + public static boolean villagerManipulation = false; + public static void onChangeVillagerManipulation(boolean oldVillagerManipulation, boolean villagerManipulation) { + if (villagerManipulation) { + ServerBrandManager.rngWarning(); + } else { + VillagerCracker.reset(); + } + } + + @Config(setter = @Config.Setter("setMaxVillagerManipulationWaitTicks"), temporary = true) + public static int maxVillagerManipulationWaitTicks = 12000; + public static void setMaxVillagerManipulationWaitTicks(int maxVillagerManipulationWaitTicks) { + Configs.maxVillagerManipulationWaitTicks = Mth.clamp(maxVillagerManipulationWaitTicks, 0, 1_000_000); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java index 318c8acba..6d21a7fc3 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java @@ -12,6 +12,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import java.util.HashMap; @@ -101,4 +102,18 @@ public static String registerCode(Runnable code) { runnables.put(randomString, code); return randomString; } + + public static void updateOverlayProgressBar(int current, int total, int width, int time) { + MutableComponent builder = Component.empty(); + int color = Mth.hsvToRgb(current / (total * 3.0f), 1.0f, 1.0f); + builder.append(Component.literal("[").withColor(0xAAAAAA)); + builder.append(Component.literal("~" + Math.round(100.0 * current / total) + "%").withColor(color)); + builder.append(Component.literal("] ").withColor(0xAAAAAA)); + int filledWidth = (int) Math.round((double) width * current / total); + int unfilledWidth = width - filledWidth; + builder.append(Component.literal("|".repeat(filledWidth)).withColor(color)); + builder.append(Component.literal("|".repeat(unfilledWidth)).withColor(0xAAAAAA)); + + addOverlayMessage(builder, time); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java new file mode 100644 index 000000000..5093027ba --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/VillagerCommand.java @@ -0,0 +1,359 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.arguments.ClientItemPredicateArgument; +import net.earthcomputer.clientcommands.command.arguments.ItemAndEnchantmentsPredicateArgument; +import net.earthcomputer.clientcommands.command.arguments.WithStringArgument; +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.earthcomputer.clientcommands.features.VillagerRngSimulator; +import net.earthcomputer.clientcommands.task.SimpleTask; +import net.earthcomputer.clientcommands.task.TaskManager; +import net.earthcomputer.clientcommands.util.CUtil; +import net.earthcomputer.clientcommands.util.CombinedMedianEM; +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.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.EnchantmentTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.*; +import static dev.xpple.clientarguments.arguments.CBlockPosArgument.*; +import static dev.xpple.clientarguments.arguments.CEntityArgument.*; +import static dev.xpple.clientarguments.arguments.CRangeArgument.*; +import static net.earthcomputer.clientcommands.command.ClientCommandHelper.*; +import static net.earthcomputer.clientcommands.command.arguments.ClientItemPredicateArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.ItemAndEnchantmentsPredicateArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.WithStringArgument.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class VillagerCommand { + private static final SimpleCommandExceptionType NOT_A_VILLAGER_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.notAVillager")); + private static final SimpleCommandExceptionType NO_CRACKED_VILLAGER_PRESENT_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.noCrackedVillagerPresent")); + private static final SimpleCommandExceptionType NO_PROFESSION_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.noProfession")); + private static final SimpleCommandExceptionType NOT_LEVEL_1_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.notLevel1")); + private static final SimpleCommandExceptionType NO_GOALS_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.listGoals.noGoals")); + private static final SimpleCommandExceptionType ALREADY_RUNNING_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.alreadyRunning")); + private static final SimpleCommandExceptionType CANNOT_MODIFY_GOALS_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.cannotModifyGoals")); + private static final Dynamic2CommandExceptionType INVALID_GOAL_INDEX_EXCEPTION = new Dynamic2CommandExceptionType((index, length) -> Component.translatable("commands.cvillager.removeGoal.invalidIndex", index, length)); + private static final Dynamic2CommandExceptionType ITEM_OVERSTACKED_EXCEPTION = new Dynamic2CommandExceptionType((item, stackSize) -> Component.translatable("arguments.item.overstacked", item, stackSize)); + private static final SimpleCommandExceptionType NEED_VILLAGER_MANIPULATION_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cvillager.needVillagerManipulation") + .withStyle(ChatFormatting.RED) + .append(" ") + .append(getCommandTextComponent("commands.client.enable", "/cconfig clientcommands villagerManipulation set true"))); + + public static void register(CommandDispatcher dispatcher, CommandBuildContext context) { + dispatcher.register(literal("cvillager") + .then(literal("add-goal") + .then(argument("result", withString(clientItemPredicate(context))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY)) + .then(argument("result-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "result-count"), null, MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY)) + .then(argument("input", withString(clientItemPredicate(context))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "result-count"), getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY)) + .then(argument("input-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "result-count"), getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), null, MinMaxBounds.Ints.ANY)) + .then(argument("second-input", withString(clientItemPredicate(context))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "result-count"), getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), getWithString(ctx, "second-input", ClientItemPredicateArgument.ClientItemPredicate.class), MinMaxBounds.Ints.ANY)) + .then(argument("second-input-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "result-count"), getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), getWithString(ctx, "second-input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "second-input-count")))))))))) + .then(literal("add-enchanted-goal") + .then(argument("result", withString(itemAndEnchantmentsPredicate(context).withSuffix("from").withEnchantmentPredicate((item, ench) -> ench.is(EnchantmentTags.TRADEABLE)))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ItemAndEnchantmentsPredicateArgument.ItemAndEnchantmentsPredicate.class).map(ClientItemPredicateArgument.EnchantedItemPredicate::new), MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY)) + .then(argument("input", withString(clientItemPredicate(context))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ItemAndEnchantmentsPredicateArgument.ItemAndEnchantmentsPredicate.class).map(ClientItemPredicateArgument.EnchantedItemPredicate::new), MinMaxBounds.Ints.ANY, getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), MinMaxBounds.Ints.ANY, null, MinMaxBounds.Ints.ANY)) + .then(argument("input-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ItemAndEnchantmentsPredicateArgument.ItemAndEnchantmentsPredicate.class).map(ClientItemPredicateArgument.EnchantedItemPredicate::new), MinMaxBounds.Ints.ANY, getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), null, MinMaxBounds.Ints.ANY)) + .then(argument("second-input", withString(clientItemPredicate(context))) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ItemAndEnchantmentsPredicateArgument.ItemAndEnchantmentsPredicate.class).map(ClientItemPredicateArgument.EnchantedItemPredicate::new), MinMaxBounds.Ints.ANY, getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), getWithString(ctx, "second-input", ClientItemPredicateArgument.ClientItemPredicate.class), MinMaxBounds.Ints.ANY)) + .then(argument("second-input-count", intRange()) + .executes(ctx -> addGoal(ctx.getSource(), getWithString(ctx, "result", ItemAndEnchantmentsPredicateArgument.ItemAndEnchantmentsPredicate.class).map(ClientItemPredicateArgument.EnchantedItemPredicate::new), MinMaxBounds.Ints.ANY, getWithString(ctx, "input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "input-count"), getWithString(ctx, "second-input", ClientItemPredicateArgument.ClientItemPredicate.class), Ints.getRangeArgument(ctx, "second-input-count"))))))))) + .then(literal("list-goals") + .executes(ctx -> listGoals(ctx.getSource()))) + .then(literal("remove-goal") + .then(argument("index", integer(1)) + .executes(ctx -> removeGoal(ctx.getSource(), getInteger(ctx, "index"))))) + .then(literal("target") + .executes(ctx -> setVillagerTarget(null)) + .then(argument("entity", entity()) + .executes(ctx -> setVillagerTarget(getEntity(ctx, "entity"))))) + .then(literal("clock") + .executes(ctx -> getClockPos()) + .then(argument("pos", blockPos()) + .executes(ctx -> setClockPos(ctx.getSource(), getBlockPos(ctx, "pos"))))) + .then(literal("start") + .executes(ctx -> start(false)) + .then(literal("first-level") + .executes(ctx -> start(false))) + .then(literal("next-level") + .executes(ctx -> start(true)))) + .then(literal("reset") + .executes(ctx -> reset())) + .then(literal("reset-correction") + .executes(ctx -> resetCorrection(-25)) + .then(argument("correction", integer()) + .executes(ctx -> resetCorrection(getInteger(ctx, "correction")))))); + } + + private static int addGoal( + FabricClientCommandSource ctx, + Result result, + MinMaxBounds.Ints resultCount, + @Nullable WithStringArgument.Result first, + MinMaxBounds.Ints firstCount, + @Nullable WithStringArgument.Result second, + MinMaxBounds.Ints secondCount + ) throws CommandSyntaxException { + checkStackSize(result, resultCount); + if (first != null) { + checkStackSize(first, firstCount); + } + if (second != null) { + checkStackSize(second, secondCount); + } + + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + if (VillagerCracker.isRunning()) { + throw CANNOT_MODIFY_GOALS_EXCEPTION.create(); + } + + String firstString = first == null ? null : first.string() + " " + CUtil.boundsToString(firstCount); + String secondString = second == null ? null : second.string() + " " + CUtil.boundsToString(secondCount); + String resultString = (result.string().endsWith(" from") ? result.string().substring(0, result.string().length() - " from".length()) : result.string()) + " " + CUtil.boundsToString(resultCount); + + VillagerCracker.goals.add(new VillagerCracker.Goal( + resultString, + item -> result.value().test(item) && resultCount.matches(item.getCount()), + + firstString, + first == null ? null : item -> first.value().test(item) && firstCount.matches(item.getCount()), + + secondString, + second == null ? null : item -> second.value().test(item) && secondCount.matches(item.getCount()) + )); + + ctx.sendFeedback(Component.translatable("commands.cvillager.goalAdded")); + return Command.SINGLE_SUCCESS; + } + + private static void checkStackSize(WithStringArgument.Result itemPredicate, MinMaxBounds.Ints count) throws CommandSyntaxException { + int maxCount = itemPredicate.value().getPossibleItems().stream().mapToInt(Item::getDefaultMaxStackSize).max().orElse(Item.DEFAULT_MAX_STACK_SIZE); + if (count.min().isPresent() && count.min().get() > maxCount) { + throw ITEM_OVERSTACKED_EXCEPTION.create(itemPredicate.string(), maxCount); + } + if (count.max().isPresent() && count.max().get() > maxCount) { + throw ITEM_OVERSTACKED_EXCEPTION.create(itemPredicate.string(), maxCount); + } + } + + private static int listGoals(FabricClientCommandSource source) throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + if (VillagerCracker.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", VillagerCracker.goals.size())); + for (int i = 0; i < VillagerCracker.goals.size(); i++) { + VillagerCracker.Goal goal = VillagerCracker.goals.get(i); + source.sendFeedback(Component.literal((i + 1) + ": " + goal.toString())); + } + } + return Command.SINGLE_SUCCESS; + } + + private static int removeGoal(FabricClientCommandSource source, int index) throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + if (VillagerCracker.isRunning()) { + throw CANNOT_MODIFY_GOALS_EXCEPTION.create(); + } + + index = index - 1; + if (index < VillagerCracker.goals.size()) { + VillagerCracker.Goal goal = VillagerCracker.goals.remove(index); + source.sendFeedback(Component.translatable("commands.cvillager.removeGoal.success", goal.toString())); + } else { + throw INVALID_GOAL_INDEX_EXCEPTION.create(index + 1, VillagerCracker.goals.size()); + } + return Command.SINGLE_SUCCESS; + } + + private static int setVillagerTarget(@Nullable Entity target) throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + if (target instanceof Villager villager) { + VillagerCracker.setTargetVillager(villager); + ClientCommandHelper.sendFeedback("commands.cvillager.target.set"); + } else if (target == null) { + VillagerCracker.setTargetVillager(null); + ClientCommandHelper.sendFeedback("commands.cvillager.target.cleared"); + } else { + throw NOT_A_VILLAGER_EXCEPTION.create(); + } + + return Command.SINGLE_SUCCESS; + } + + private static int getClockPos() throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + GlobalPos pos = VillagerCracker.getClockPos(); + if (pos == null) { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.cleared"); + } else { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set", pos.pos().getX(), pos.pos().getY(), pos.pos().getZ(), String.valueOf(pos.dimension().location())); + } + return Command.SINGLE_SUCCESS; + } + + private static int setClockPos(FabricClientCommandSource ctx, BlockPos pos) throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + ResourceKey dimension = ctx.getWorld().dimension(); + VillagerCracker.setClockPos(pos == null ? null : new GlobalPos(dimension, pos)); + if (pos == null) { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set.cleared"); + } else { + ClientCommandHelper.sendFeedback("commands.cvillager.clock.set", pos.getX(), pos.getY(), pos.getZ(), String.valueOf(dimension.location())); + } + return Command.SINGLE_SUCCESS; + } + + private static int start(boolean levelUp) throws CommandSyntaxException { + if (!Configs.villagerManipulation) { + throw NEED_VILLAGER_MANIPULATION_EXCEPTION.create(); + } + + Villager targetVillager = VillagerCracker.getVillager(); + + if (VillagerCracker.goals.isEmpty()) { + throw NO_GOALS_EXCEPTION.create(); + } + + if (targetVillager == null || !VillagerCracker.simulator.isCracked()) { + throw NO_CRACKED_VILLAGER_PRESENT_EXCEPTION.create(); + } + + VillagerProfession profession = targetVillager.getVillagerData().getProfession(); + if (profession == VillagerProfession.NONE) { + throw NO_PROFESSION_EXCEPTION.create(); + } + + if (VillagerCracker.isRunning()) { + throw ALREADY_RUNNING_EXCEPTION.create(); + } + + int currentLevel = targetVillager.getVillagerData().getLevel(); + if (!levelUp && currentLevel != 1) { + throw NOT_LEVEL_1_EXCEPTION.create(); + } + + int crackedLevel = levelUp ? currentLevel + 1 : currentLevel; + + VillagerTrades.ItemListing[] listings = VillagerTrades.TRADES.get(profession).getOrDefault(crackedLevel, new VillagerTrades.ItemListing[0]); + // 39 ticks ahead instead of 40 because the two trade xp calls act as one effective tick. + // this means we have to do our trades for one tick into the future because we'll be re-adjusting for this + int adjustmentTicks = levelUp ? -39 : 0; + VillagerRngSimulator.BruteForceResult result = VillagerCracker.simulator.bruteForceOffers(listings, levelUp ? 240 : 10, Configs.maxVillagerManipulationWaitTicks, offer -> VillagerCracker.goals.stream().anyMatch(goal -> goal.matches(offer))); + if (result == null) { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.bruteForce.failed", Configs.maxVillagerManipulationWaitTicks).withStyle(ChatFormatting.RED), 100); + return Command.SINGLE_SUCCESS; + } + VillagerRngSimulator.SurroundingOffers surroundingOffers = VillagerCracker.simulator.generateSurroundingOffers(listings, result.ticksPassed(), 1000); + assert surroundingOffers != null; + int ticks = result.ticksPassed() + adjustmentTicks; + VillagerCracker.Offer offer = result.offer(); + String price; + if (offer.second() == null) { + price = displayText(offer.first(), false); + } else { + price = displayText(offer.first(), false) + " + " + displayText(offer.second(), false); + } + ClientCommandHelper.sendFeedback(Component.translatable("commands.cvillager.bruteForce.success", displayText(offer.result(), false), price, ticks).withStyle(ChatFormatting.GREEN)); + System.out.println("Expected seed: " + Long.toHexString(result.seed())); + VillagerCracker.targetOffer = offer; + VillagerCracker.surroundingOffers = surroundingOffers; + VillagerCracker.hasQueuedInteractionPackets = false; + VillagerCracker.hasSentInteractionPackets = false; + VillagerCracker.isFirstLevelCrack = !levelUp; + VillagerCracker.simulator.setTicksUntilInteract(ticks); + TaskManager.addTask("cvillagerWaiting", new SimpleTask() { + @Override + public boolean condition() { + return VillagerCracker.isRunning(); + } + + @Override + protected void onTick() { + VillagerCracker.simulator.updateProgressBar(); + } + + @Override + public void onCompleted() { + // check if `onCompleted` was cancelled + if (condition()) { + VillagerCracker.stopRunning(); + } + } + }); + + return Command.SINGLE_SUCCESS; + } + + private static int reset() { + VillagerCracker.reset(); + ClientCommandHelper.sendFeedback("commands.cvillager.reset"); + return Command.SINGLE_SUCCESS; + } + + private static int resetCorrection(int correction) { + VillagerCracker.combinedMedianEM = new CombinedMedianEM(); + VillagerCracker.magicMillisecondCorrection = correction; + ClientCommandHelper.sendFeedback("commands.cvillager.resetCorrection", correction); + return Command.SINGLE_SUCCESS; + } + + public static String displayText(ItemStack stack, boolean hideCount) { + String quantityPrefix = hideCount || stack.getCount() == 1 ? "" : stack.getCount() + " "; + List lines = stack.getTooltipLines(Item.TooltipContext.EMPTY, null, TooltipFlag.NORMAL); + String itemDescription = lines.stream().skip(1).map(Component::getString).collect(Collectors.joining(", ")); + if (lines.size() == 1) { + return quantityPrefix + lines.getFirst().getString(); + } else { + return quantityPrefix + lines.getFirst().getString() + " (" + itemDescription + ")"; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java index 9e676ef98..d9a13dc69 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ItemAndEnchantmentsPredicateArgument.java @@ -39,6 +39,7 @@ import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; public class ItemAndEnchantmentsPredicateArgument implements ArgumentType { @@ -51,6 +52,8 @@ public class ItemAndEnchantmentsPredicateArgument implements ArgumentType itemPredicate = item -> true; private BiPredicate> enchantmentPredicate = (item, ench) -> true; private boolean constrainMaxLevel = false; + @Nullable + private String suffix; private ItemAndEnchantmentsPredicateArgument(HolderLookup.Provider holderLookupProvider) { this.enchantmentLookup = holderLookupProvider.lookupOrThrow(Registries.ENCHANTMENT); @@ -75,6 +78,11 @@ public ItemAndEnchantmentsPredicateArgument constrainMaxLevel() { return this; } + public ItemAndEnchantmentsPredicateArgument withSuffix(String suffix) { + this.suffix = suffix; + return this; + } + public static ItemAndEnchantmentsPredicate getItemAndEnchantmentsPredicate(CommandContext context, String name) { return context.getArgument(name, ItemAndEnchantmentsPredicate.class); } @@ -159,9 +167,10 @@ public boolean test(ItemStack stack) { if (item != stack.getItem() && (item != Items.BOOK || stack.getItem() != Items.ENCHANTED_BOOK)) { return false; } - List enchantments = stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream() - .map(entry -> new EnchantmentInstance(entry.getKey(), entry.getIntValue())) - .toList(); + List enchantments = Stream.concat( + stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream(), + stack.getOrDefault(DataComponents.STORED_ENCHANTMENTS, ItemEnchantments.EMPTY).entrySet().stream() + ).map(entry -> new EnchantmentInstance(entry.getKey(), entry.getIntValue())).toList(); return predicate.test(enchantments); } } @@ -220,6 +229,10 @@ private boolean parseEnchantmentInstancePredicate() throws CommandSyntaxExceptio return false; } + if (option == Option.SUFFIX) { + return false; + } + boolean suggest = reader.canRead(); if (option == Option.EXACT) { exact = true; @@ -256,6 +269,7 @@ private enum Option { WITHOUT, EXACT, ORDERED, + SUFFIX, } @Nullable @@ -267,7 +281,7 @@ private Option parseOption() { case "without" -> Option.WITHOUT; case "exactly" -> exact ? null : Option.EXACT; case "ordered" -> ordered || MultiVersionCompat.INSTANCE.getProtocolVersion() >= MultiVersionCompat.V1_21 ? null : Option.ORDERED; - default -> null; + default -> option.equals(suffix) ? Option.SUFFIX : null; }; } @@ -451,6 +465,9 @@ private void suggestOption() { if (!ordered && MultiVersionCompat.INSTANCE.getProtocolVersion() < MultiVersionCompat.V1_21) { validOptions.add("ordered"); } + if (suffix != null) { + validOptions.add(suffix); + } SharedSuggestionProvider.suggest(validOptions, builder); suggestions.add(builder); }; diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/WithStringArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/WithStringArgument.java index 614decfb0..23a36643e 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/arguments/WithStringArgument.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/WithStringArgument.java @@ -6,10 +6,10 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import org.apache.commons.lang3.tuple.Pair; import java.util.Collection; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; public class WithStringArgument implements ArgumentType> { @@ -46,5 +46,9 @@ public Collection getExamples() { return delegate.getExamples(); } - public record Result(String string, T value) {} + public record Result(String string, T value) { + public Result map(Function mapper) { + return new Result<>(string, mapper.apply(value)); + } + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java index 0418ec90f..e2b9a75eb 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/EnchantmentCracker.java @@ -8,6 +8,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; import net.earthcomputer.clientcommands.util.MultiVersionCompat; import net.earthcomputer.clientcommands.task.ItemThrowTask; import net.earthcomputer.clientcommands.task.LongTask; @@ -29,7 +30,6 @@ import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; @@ -112,7 +112,6 @@ public class EnchantmentCracker { */ public static final Logger LOGGER = LogUtils.getLogger(); - private static final int PROGRESS_BAR_WIDTH = 50; // RENDERING /* @@ -479,17 +478,7 @@ public void onCompleted() { @Override protected void onItemThrown(int current, int total) { - MutableComponent builder = Component.empty(); - int color = Mth.hsvToRgb(current / (total * 3.0f), 1.0f, 1.0f); - builder.append(Component.literal("[").withColor(0xAAAAAA)); - builder.append(Component.literal("~" + Math.round(100.0 * current / total) + "%").withColor(color)); - builder.append(Component.literal("] ").withColor(0xAAAAAA)); - int filledWidth = (int) Math.round((double) PROGRESS_BAR_WIDTH * current / total); - int unfilledWidth = PROGRESS_BAR_WIDTH - filledWidth; - builder.append(Component.literal("|".repeat(filledWidth)).withColor(color)); - builder.append(Component.literal("|".repeat(unfilledWidth)).withColor(0xAAAAAA)); - - Minecraft.getInstance().gui.setOverlayMessage(builder, false); + ClientCommandHelper.updateOverlayProgressBar(current, total, 50, 60); } }); } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java index d8f709f47..fcb738b0b 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/FishingCracker.java @@ -11,6 +11,8 @@ import com.seedfinding.mcfeature.loot.entry.ItemEntry; import com.seedfinding.mcfeature.loot.entry.LootEntry; import com.seedfinding.mcfeature.loot.entry.TableEntry; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; import net.earthcomputer.clientcommands.Configs; import net.earthcomputer.clientcommands.command.ClientCommandHelper; import net.earthcomputer.clientcommands.command.PingCommand; @@ -22,11 +24,11 @@ import net.earthcomputer.clientcommands.task.LongTask; import net.earthcomputer.clientcommands.task.TaskManager; import net.earthcomputer.clientcommands.util.CUtil; +import net.earthcomputer.clientcommands.util.CombinedMedianEM; import net.earthcomputer.clientcommands.util.SeedfindingUtil; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.multiplayer.MultiPlayerGameMode; import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; @@ -73,9 +75,6 @@ import java.util.OptionalLong; import java.util.Set; import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -106,7 +105,7 @@ public class FishingCracker { private static int serverMspt = 50; private static volatile int averageTimeToEndOfTick = 0; private static volatile int magicMillisecondsCorrection = -100; - private static final ScheduledExecutorService DELAY_EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private static final CombinedMedianEM combinedMedianEM = new CombinedMedianEM(); // state public static volatile State state = State.NOT_MANIPULATING; @@ -550,24 +549,24 @@ private static void processExperienceOrbSpawn(double x, double y, double z, int } if (!indices.isEmpty()) { - if (CombinedMedianEM.data.size() >= 10) { - CombinedMedianEM.data.removeFirst(); + if (combinedMedianEM.data.size() >= 10) { + combinedMedianEM.data.removeFirst(); } - ArrayList sample = new ArrayList<>(); + DoubleList sample = new DoubleArrayList(); for (int index : indices) { sample.add((double) (index * serverMspt) + magicMillisecondsCorrection); } - CombinedMedianEM.data.add(sample); + combinedMedianEM.data.add(sample); - CombinedMedianEM.begintime = magicMillisecondsCorrection - serverMspt / 2 - (expectedCatches.length / 2) * serverMspt; - CombinedMedianEM.endtime = magicMillisecondsCorrection + serverMspt / 2 + (expectedCatches.length / 2) * serverMspt; - CombinedMedianEM.width = serverMspt; - CombinedMedianEM.run(); - magicMillisecondsCorrection = (int) Math.round(CombinedMedianEM.mu); + combinedMedianEM.update(serverMspt, expectedCatches.length); + magicMillisecondsCorrection = (int) Math.round(combinedMedianEM.getResult()); } } private static void onTimeSync() { + LocalPlayer player = Minecraft.getInstance().player; + assert player != null; + long time = System.nanoTime(); //noinspection SuspiciousSystemArraycopy System.arraycopy(timeSyncTimes, 1, timeSyncTimes, 0, timeSyncTimes.length - 1); @@ -589,50 +588,39 @@ private static void onTimeSync() { int timeToStartOfTick = serverMspt - averageTimeToEndOfTick; int delay = (totalTicksToWait - estimatedTicksElapsed) * serverMspt - magicMillisecondsCorrection - PingCommand.getLocalPing() - timeToStartOfTick + serverMspt / 2; long targetTime = (delay) * 1000000L + System.nanoTime(); - DELAY_EXECUTOR.schedule(() -> { - if (!Configs.fishingManipulation.isEnabled() || state != State.ASYNC_WAITING_FOR_FISH) { - return; - } - LocalPlayer oldPlayer = Minecraft.getInstance().player; - if (oldPlayer != null) { - ClientPacketListener packetListener = oldPlayer.connection; - while (System.nanoTime() - targetTime < 0) { - if (state != State.ASYNC_WAITING_FOR_FISH) { - return; - } - } - FishingHook oldFishingHook = oldPlayer.fishing; - packetListener.send(new ServerboundUseItemPacket(InteractionHand.MAIN_HAND, 0, oldPlayer.getYRot(), oldPlayer.getXRot())); - synchronized (STATE_LOCK) { - state = State.WAITING_FOR_ITEM; - } - Minecraft.getInstance().schedule(() -> { - LocalPlayer player = Minecraft.getInstance().player; - if (player != null) { - ItemStack oldStack = player.getMainHandItem(); - - // If the player interaction packet gets handled before the next tick, - // then the fish hook would be null and the client would act as if the fishing rod is extending. - // Temporarily set to the previous fishing hook to fix this. - expectedFishingRodUses++; - FishingHook prevFishingHook = player.fishing; - player.fishing = oldFishingHook; - InteractionResult result = oldStack.use(player.level(), player, InteractionHand.MAIN_HAND); - player.fishing = prevFishingHook; - - if (result instanceof InteractionResult.Success successResult) { - if (oldStack != successResult.heldItemTransformedTo()) { - player.setItemInHand(InteractionHand.MAIN_HAND, successResult.heldItemTransformedTo()); - } - if (successResult.swingSource() == InteractionResult.SwingSource.CLIENT) { - player.swing(InteractionHand.MAIN_HAND); - } + FishingHook[] oldFishingHook = {null}; + CUtil.sendAtPreciseTime( + targetTime, + new ServerboundUseItemPacket(InteractionHand.MAIN_HAND, 0, player.getYRot(), player.getXRot()), + () -> { + oldFishingHook[0] = player.fishing; + return Configs.fishingManipulation.isEnabled() && state == State.ASYNC_WAITING_FOR_FISH; + }, + () -> { + if (player == Minecraft.getInstance().player) { + ItemStack oldStack = player.getMainHandItem(); + + // If the player interaction packet gets handled before the next tick, + // then the fish hook would be null and the client would act as if the fishing rod is extending. + // Temporarily set to the previous fishing hook to fix this. + expectedFishingRodUses++; + FishingHook prevFishingHook = player.fishing; + player.fishing = oldFishingHook[0]; + InteractionResult result = oldStack.use(player.level(), player, InteractionHand.MAIN_HAND); + player.fishing = prevFishingHook; + + if (result instanceof InteractionResult.Success successResult) { + if (oldStack != successResult.heldItemTransformedTo()) { + player.setItemInHand(InteractionHand.MAIN_HAND, successResult.heldItemTransformedTo()); + } + if (successResult.swingSource() == InteractionResult.SwingSource.CLIENT) { + player.swing(InteractionHand.MAIN_HAND); } - //networkHandler.sendPacket(new PlayerInteractItemC2SPacket(Hand.MAIN_HAND)); } - }); + //networkHandler.sendPacket(new PlayerInteractItemC2SPacket(Hand.MAIN_HAND)); + } } - }, Math.max(0, delay - 100), TimeUnit.MILLISECONDS); + ); } } } @@ -730,152 +718,6 @@ public String toString() { // endregion - // region NETWORK DELAY ESTIMATION - - /** - * Taken from: https://gist.github.com/pseudogravity/294f12225c18bf319e4c1923dd664bd5 - * - * @author MC (PseudoGravity) - */ - private static class CombinedMedianEM { - - // a list of samples - // each sample is actually a list of points - // for each interval, convert it to a point by taking the median value - // or... break the 50ms intervals into 2 25ms intervals for better accuracy - static ArrayList> data = new ArrayList<>(); - - // width of the intervals - static double width = 50; - - // width of time window considered - // all of the data points for all samples are within this window - static int begintime = -1000; - static int endtime = 1000; - - // three parameters and 2 constraints - static double packetlossrate = 0.2; - static double maxpacketlossrate = 0.5; - static double minpacketlossrate = 0.01; - static double mu = 0.0; - static double sigma = 500; - static double maxsigma = 1000; - static double minsigma = 10; - - public static void run() { - ArrayList droprate = new ArrayList<>(); - - for (ArrayList sample : data) { - droprate.add(sample.size() * width / (endtime - begintime)); - } - - ArrayList times = new ArrayList<>(); - for (int i = begintime; i <= endtime; i += 10) { - times.add(i * 1.0); - } - - double besttime = 0; - double bestscore = Double.MAX_VALUE; - - for (double time : times) { - // find score for each option for time - double score = 0; - for (int i = 0; i < data.size(); i++) { - // for each sample, find the point which adds the least to the score - ArrayList sample = data.get(i); - double lambda = droprate.get(i) / width; - double bestsubscore = Double.MAX_VALUE; - for (Double x : sample) { - double absdev = Math.abs(x - time); - absdev = (1 - Math.exp(-lambda * absdev)) / lambda; // further curbing outlier effects - if (absdev < bestsubscore) { - bestsubscore = absdev; - } - } - score += bestsubscore; - } - if (score < bestscore) { - bestscore = score; - besttime = time; - } - } - - mu = besttime; - sigma = bestscore / data.size(); - sigma = Math.max(Math.min(sigma, maxsigma), minsigma); - - for (int repeat = 0; repeat < 1; repeat++) { - // E step - // calculate weights (and classifications) - var masses = new ArrayList>(); - for (int i = 0; i < data.size(); i++) { - - ArrayList sample = data.get(i); - - // process each sample - double sum = 0; - for (double x : sample) { - sum += mass(x); - } - double pXandNorm = Math.min(sum, 1) * (1 - packetlossrate); // cap at 1 - - double pXandUnif = droprate.get(i) * packetlossrate; - - double pNorm = pXandNorm / (pXandNorm + pXandUnif); - - ArrayList mass = new ArrayList<>(); - for (double x : sample) { - mass.add(mass(x) / sum * pNorm); - } - - masses.add(mass); - } - - // M step - // compute new best estimate for parameters - double weightedsum = 0; - double sumofweights = 0; - for (int i = 0; i < data.size(); i++) { - ArrayList sample = data.get(i); - ArrayList mass = masses.get(i); - for (int j = 0; j < sample.size(); j++) { - weightedsum += sample.get(j) * mass.get(j); - sumofweights += mass.get(j); - } - } - double muNext = weightedsum / sumofweights; - - double weightedsumofsquaredeviations = 0; - for (int i = 0; i < data.size(); i++) { - ArrayList sample = data.get(i); - ArrayList mass = masses.get(i); - for (int j = 0; j < sample.size(); j++) { - weightedsumofsquaredeviations += Math.pow(sample.get(j) - muNext, 2) * mass.get(j); - } - } - double sigmaNext = Math.sqrt(weightedsumofsquaredeviations / sumofweights); - sigmaNext = Math.max(Math.min(sigmaNext, maxsigma), minsigma); - - double packetlossrateNext = (data.size() - sumofweights) / data.size(); - packetlossrateNext = Math.max(Math.min(packetlossrateNext, maxpacketlossrate), minpacketlossrate); - - mu = muNext; - sigma = sigmaNext; - packetlossrate = packetlossrateNext; - } - } - - public static double mass(double x) { - // should be cdf(x+width/2)-cdf(x-width/2) but is simplified to pdf(x)*width and - // capped at 1 - // to avoid pesky erf() functions - double pdf = 1 / (sigma * Math.sqrt(2 * Math.PI)) * Math.exp(-Math.pow((x - mu) / sigma, 2) / 2); - return Math.min(pdf * width, 1); - } - } - - // endregion - // region FISHING BOBBER SIMULATION private static class SimulatedFishingBobber { diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java new file mode 100644 index 000000000..a23cd25ab --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerCracker.java @@ -0,0 +1,559 @@ +package net.earthcomputer.clientcommands.features; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.earthcomputer.clientcommands.Configs; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.PingCommand; +import net.earthcomputer.clientcommands.command.VillagerCommand; +import net.earthcomputer.clientcommands.event.ClientConnectionEvents; +import net.earthcomputer.clientcommands.event.MoreClientEvents; +import net.earthcomputer.clientcommands.util.BuildInfo; +import net.earthcomputer.clientcommands.util.CUtil; +import net.earthcomputer.clientcommands.util.CombinedMedianEM; +import net.minecraft.ChatFormatting; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundAddExperienceOrbPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; +import net.minecraft.network.protocol.game.ServerboundInteractPacket; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BedPart; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.function.Predicate; + +public class VillagerCracker { + // This value was computed by brute forcing all seeds + public static final float MAX_ERROR = 5 * 0x1.0p-24f; + + @Nullable + private static UUID villagerUuid = null; + @Nullable + private static WeakReference cachedVillager = null; + public static final VillagerRngSimulator simulator = new VillagerRngSimulator(null); + @Nullable + private static GlobalPos clockPos = null; + private static boolean isNewClock; + public static final List goals = new ArrayList<>(); + @Nullable + public static Offer targetOffer = null; + public static VillagerRngSimulator.SurroundingOffers surroundingOffers = null; + private static int clockTicksSinceLastTimeSync = 0; + private static long lastClockRateWarning = 0; + public static boolean hasSentInteractionPackets = false; + public static boolean hasQueuedInteractionPackets = false; + public static boolean isFirstLevelCrack = true; + + private static long lastTimeSyncTime; + public static int serverMspt = SharedConstants.MILLIS_PER_TICK; + public static int magicMillisecondCorrection = 25; + public static int maxTicksBefore = 10; + public static int maxTicksAfter = 10; + public static CombinedMedianEM combinedMedianEM = new CombinedMedianEM(); + + static { + ClientConnectionEvents.DISCONNECT.register(VillagerCracker::onDisconnect); + MoreClientEvents.TIME_SYNC.register(packet -> onTimeSync()); + } + + @Nullable + public static Villager getVillager() { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return null; + } + + if (villagerUuid == null) { + cachedVillager = null; + return null; + } + if (cachedVillager != null) { + Villager villager = cachedVillager.get(); + if (villager != null && !villager.isRemoved() && villager.level() == level) { + return villager; + } + } + for (Entity entity : level.entitiesForRendering()) { + if (villagerUuid.equals(entity.getUUID()) && entity instanceof Villager villager) { + cachedVillager = new WeakReference<>(villager); + return villager; + } + } + return null; + } + + @Nullable + public static GlobalPos getClockPos() { + return clockPos; + } + + public static void setTargetVillager(@Nullable Villager villager) { + Villager oldVillager = getVillager(); + if (oldVillager != null) { + simulator.reset(); + } + + if (clockPos == null) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.noClock")); + } + + ClientLevel level = Minecraft.getInstance().level; + + if (level != null && level.getDayTime() % 24000 < 12000) { + simulator.onBadSetup("day"); + ClientCommandHelper.sendHelp(Component.translatable("villagerManip.help.day")); + } + + if (villager != null) { + cachedVillager = new WeakReference<>(villager); + villagerUuid = villager.getUUID(); + } else { + reset(); + } + } + + public static void setClockPos(@Nullable GlobalPos pos) { + clockPos = pos; + if (pos != null) { + isNewClock = true; + } + } + + public static void onSoundEventPlayed(ClientboundSoundPacket packet, Vec3 pos) { + Villager targetVillager = getVillager(); + if (targetVillager == null || getClockPos() == null || pos.distanceToSqr(targetVillager.position()) > 0.1f) { + return; + } + + SoundEvent soundEvent = packet.getSound().value(); + if (soundEvent == SoundEvents.VILLAGER_AMBIENT || soundEvent == SoundEvents.VILLAGER_TRADE) { + simulator.onAmbientSoundPlayed(packet.getPitch()); + } else if (soundEvent == SoundEvents.VILLAGER_NO) { + VillagerProfession profession = targetVillager.getVillagerData().getProfession(); + simulator.onNoSoundPlayed(packet.getPitch(), profession != VillagerProfession.NONE && profession != VillagerProfession.NITWIT); + } else if (soundEvent == SoundEvents.VILLAGER_YES) { + simulator.onYesSoundPlayed(packet.getPitch()); + } else if (soundEvent == SoundEvents.GENERIC_SPLASH) { + simulator.onSplashSoundPlayed(packet.getPitch()); + } else if (BuiltInRegistries.SOUND_EVENT.getKey(soundEvent).getPath().startsWith("item.armor.equip_")) { + simulator.onBadSetup("itemEquipped"); + } + } + + public static void onXpOrbSpawned(ClientboundAddExperienceOrbPacket packet) { + Villager targetVillager = getVillager(); + if (targetVillager == null) { + return; + } + + simulator.onXpOrbSpawned(packet.getValue()); + } + + private static void onTimeSync() { + long now = System.nanoTime(); + + if (Configs.villagerManipulation && clockPos != null && !isNewClock && clockTicksSinceLastTimeSync != 20) { + if (now - lastClockRateWarning >= 60_000_000_000L) { + if (clockTicksSinceLastTimeSync < 20) { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.tooSlow")); + } else { + ClientCommandHelper.sendHelp(Component.translatable("commands.cvillager.help.tooFast")); + } + lastClockRateWarning = now; + } + } + isNewClock = false; + clockTicksSinceLastTimeSync = 0; + + serverMspt = (3 * serverMspt + (int) ((now - lastTimeSyncTime) / 20_000_000)) / 4; + lastTimeSyncTime = now; + } + + public static void onServerTick() { + clockTicksSinceLastTimeSync++; + + final Villager targetVillager = getVillager(); + if (targetVillager == null) { + return; + } + + if (simulator.isAtLeastPartiallyCracked()) { + checkVillagerStateSetup(); + } + + simulator.simulateTick(); + + if (isRunning() && checkVillagerWaitingStateSetup() && !hasQueuedInteractionPackets && simulator.isCracked()) { + trySendInteraction(); + } + } + + private static void trySendInteraction() { + final Minecraft mc = Minecraft.getInstance(); + final Villager targetVillager = getVillager(); + final long nanoTime = System.nanoTime(); + int millisecondsUntilInteract = simulator.getTicksRemaining() * serverMspt - PingCommand.getLocalPing() + magicMillisecondCorrection; + if (millisecondsUntilInteract < 200) { + LocalPlayer oldPlayer = mc.player; + assert oldPlayer != null; + + if (isFirstLevelCrack) { + CUtil.sendAtPreciseTime( + nanoTime + millisecondsUntilInteract * 1_000_000L, + ServerboundInteractPacket.createInteractionPacket(targetVillager, false, InteractionHand.MAIN_HAND), + VillagerCracker::isRunning, + () -> { + LocalPlayer player = mc.player; + if (player == oldPlayer) { + player.swing(InteractionHand.MAIN_HAND); + } + hasSentInteractionPackets = true; + } + ); + } else { + // todo: could potentially still break if a "hrmm" happens between this and the interaction + if (!(mc.screen instanceof MerchantScreen screen)) { + simulator.onBadSetup("wrongScreen"); + return; + } + + screen.slotClicked(screen.getMenu().slots.get(2), 2, 0, ClickType.QUICK_MOVE); + + CUtil.sendAtPreciseTime( + nanoTime + millisecondsUntilInteract * 1_000_000L, + new ServerboundContainerClosePacket(oldPlayer.containerMenu.containerId), + VillagerCracker::isRunning, + () -> { + LocalPlayer player = mc.player; + player.clientSideCloseContainer(); + hasSentInteractionPackets = true; + } + ); + + // debug + CUtil.sendAtPreciseTime( + nanoTime + (millisecondsUntilInteract + 41L * serverMspt) * 1_000_000L, + ServerboundInteractPacket.createInteractionPacket(targetVillager, false, InteractionHand.MAIN_HAND), + VillagerCracker::isRunning, + () -> { + LocalPlayer player = mc.player; + if (player == oldPlayer) { + player.swing(InteractionHand.MAIN_HAND); + } + } + ); + } + + simulator.resetWaitingState(); + hasQueuedInteractionPackets = true; + } + } + + private static boolean checkVillagerStateSetup() { + final Minecraft mc = Minecraft.getInstance(); + final Villager targetVillager = getVillager(); + + if (targetVillager == null) { + return false; + } + + if (!isResting(targetVillager.level().dayTime())) { + simulator.onBadSetup("day"); + ClientCommandHelper.sendHelp(Component.translatable("villagerManip.help.day")); + return false; + } else if (targetVillager.isInWater() && targetVillager.getFluidHeight(FluidTags.WATER) > targetVillager.getFluidJumpThreshold() || targetVillager.isInLava()) { + simulator.onBadSetup("swim"); + return false; + } else if (!targetVillager.getActiveEffects().isEmpty()) { + simulator.onBadSetup("potion"); + return false; + } else if (!mc.player.getItemInHand(InteractionHand.MAIN_HAND).isEmpty()) { + simulator.onBadSetup("itemInMainHand"); + return false; + } else { + Level level = targetVillager.level(); + Vec3 pos = targetVillager.position(); + BlockPos blockPos = targetVillager.blockPosition(); + + int villagersNearVillager = level.getEntities(EntityTypeTest.forExactClass(Villager.class), AABB.ofSize(pos, 10.0, 10.0, 10.0), entity -> entity.position().distanceToSqr(pos) <= 5.0 * 5.0).size(); + if (villagersNearVillager > 1) { + simulator.onBadSetup("gossip"); + return false; + } + + List bedHeadPositions = BlockPos.betweenClosedStream(blockPos.offset(-16, -16, -16), blockPos.offset(16, 16, 16)).map(BlockPos::new).filter(p -> p.distSqr(blockPos) <= 16.0 * 16.0).filter(p -> level.getBlockState(p).is(BlockTags.BEDS) && level.getBlockState(p).getValue(BedBlock.OCCUPIED) == Boolean.FALSE && level.getBlockState(p).getValue(BedBlock.PART) == BedPart.HEAD).toList(); + if (bedHeadPositions.size() != 1) { + simulator.onBadSetup("invalidBedPosition"); + sendInvalidSetupHelp(); + return false; + } + + for (Direction direction : Direction.Plane.HORIZONTAL) { + BlockPos trapdoorPos = blockPos.relative(direction).above(); + BlockState trapdoorPosState = level.getBlockState(trapdoorPos); + if (!(trapdoorPosState.is(BlockTags.TRAPDOORS) && trapdoorPosState.getValue(TrapDoorBlock.HALF) == Half.TOP && trapdoorPosState.getValue(TrapDoorBlock.HALF) == Half.TOP && trapdoorPosState.getValue(TrapDoorBlock.OPEN) == Boolean.FALSE)) { + simulator.onBadSetup("invalidCage"); + sendInvalidSetupHelp(); + return false; + } + } + } + + return true; + } + + private static boolean checkVillagerWaitingStateSetup() { + if (!Configs.villagerManipulation) { + simulator.onBadSetup("manipInactive"); + stopRunning(); + return false; + } + + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager == null || !VillagerCracker.simulator.isCracked()) { + simulator.onBadSetup("uncrackedMisc"); + stopRunning(); + return false; + } + + VillagerProfession profession = targetVillager.getVillagerData().getProfession(); + if (profession == VillagerProfession.NONE) { + simulator.onBadSetup("professionLost"); + stopRunning(); + return false; + } + + return true; + } + + private static void sendInvalidSetupHelp() { + ClientCommandHelper.sendHelp( + Component.translatable("villagerManip.help.setup", + Component.translatable("villagerManip.help.setup.link").withStyle(style -> style + .withUnderlined(true) + .withClickEvent(new ClickEvent( + ClickEvent.Action.OPEN_URL, + "https://github.com/Earthcomputer/clientcommands/blob/%s/docs/villager_rng_setup.png?raw=true".formatted(BuildInfo.INSTANCE.shortCommitHash(10)) + )) + ) + ) + ); + } + + private static void onDisconnect() { + if (Relogger.isRelogging) { + UUID prevVillagerUuid = villagerUuid; + GlobalPos prevClockPos = clockPos; + Relogger.relogSuccessTasks.add(() -> { + villagerUuid = prevVillagerUuid; + setClockPos(prevClockPos); + }); + } + + reset(); + } + + private static int[] possibleTicksAhead(Offer[] actualOffers, VillagerRngSimulator.SurroundingOffers surroundingOffers) { + IntList ticksAhead = new IntArrayList(); + + for (int i = 0; i < surroundingOffers.before().size(); i++) { + Offer[] offers = surroundingOffers.before().get(i); + if (Arrays.equals(offers, actualOffers)) { + // we need to adjust by 1 to get it to not be 0 for the last value in `beforeOffers` + ticksAhead.add((surroundingOffers.before().size() - 1 - i) + 1); + } + } + + if (Arrays.equals(surroundingOffers.middle(), actualOffers)) { + ticksAhead.add(0); + } + + for (int i = 0; i < surroundingOffers.after().size(); i++) { + Offer[] offers = surroundingOffers.after().get(i); + if (Arrays.equals(offers, actualOffers)) { + // we need to adjust by 1 to get it to not be 0 for the first value in `afterOffers` + ticksAhead.add(-(i + 1)); + } + } + + int[] result = ticksAhead.toIntArray(); + ArrayUtils.reverse(result); + return result; + } + + public static void onGuiOpened(List actualOffersList) { + final LocalPlayer player = Minecraft.getInstance().player; + assert player != null; + + Villager villager = getVillager(); + if (villager == null) { + return; + } + + // potentially not required? + if (player.distanceTo(villager) > 2.0) { + simulator.onBadSetup("distance"); + stopRunning(); + return; + } + + if ((VillagerCracker.isFirstLevelCrack && isRunning()) || (!VillagerCracker.isFirstLevelCrack && VillagerCracker.hasSentInteractionPackets)) { + int[] possibleTicksAhead = possibleTicksAhead(actualOffersList.toArray(Offer[]::new), surroundingOffers); + // chop possible ticks ahead into a limited reasonable range + final int consideredTickRange = 21; + int lowerIndex = Arrays.binarySearch(possibleTicksAhead, -consideredTickRange / 2); + if (lowerIndex < 0) { + lowerIndex = -lowerIndex - 1; + } + int upperIndex = Arrays.binarySearch(possibleTicksAhead, consideredTickRange / 2); + if (upperIndex < 0) { + upperIndex = -upperIndex - 2; + } + possibleTicksAhead = Arrays.copyOfRange(possibleTicksAhead, lowerIndex, upperIndex + 1); + + int prevCorrection = magicMillisecondCorrection; + if (possibleTicksAhead.length > 0) { + if (combinedMedianEM.data.size() >= 10) { + combinedMedianEM.data.removeFirst(); + } + DoubleList possibleMillisecondsAhead = new DoubleArrayList(possibleTicksAhead.length); + for (int ticksAhead : possibleTicksAhead) { + possibleMillisecondsAhead.add(ticksAhead * serverMspt + magicMillisecondCorrection); + } + combinedMedianEM.data.add(possibleMillisecondsAhead); + maxTicksBefore = Math.max(maxTicksBefore, -possibleTicksAhead[0]); + maxTicksAfter = Math.max(maxTicksAfter, possibleTicksAhead[possibleTicksAhead.length - 1]); + combinedMedianEM.update(serverMspt, consideredTickRange); + magicMillisecondCorrection = (int) Math.round(combinedMedianEM.getResult()); + } + + if (actualOffersList.contains(targetOffer)) { + ClientCommandHelper.sendFeedback(Component.translatable("commands.cvillager.success", prevCorrection).withStyle(ChatFormatting.GREEN)); + player.playNotifySound(SoundEvents.NOTE_BLOCK_PLING.value(), SoundSource.PLAYERS, 1.0f, 2.0f); + } else { + ClientCommandHelper.sendFeedback(Component.translatable("commands.cvillager.failure", prevCorrection, magicMillisecondCorrection).withStyle(ChatFormatting.RED)); + player.playNotifySound(SoundEvents.NOTE_BLOCK_BASS.value(), SoundSource.PLAYERS, 1.0f, 1.0f); + } + + simulator.reset(); + stopRunning(); + hasQueuedInteractionPackets = false; + hasSentInteractionPackets = false; + isFirstLevelCrack = true; + } + } + + public static void reset() { + simulator.reset(); + isFirstLevelCrack = true; + hasSentInteractionPackets = false; + hasQueuedInteractionPackets = false; + clockPos = null; + villagerUuid = null; + cachedVillager = new WeakReference<>(null); + stopRunning(); + } + + public static boolean isRunning() { + return targetOffer != null; + } + + public static void stopRunning() { + targetOffer = null; + } + + public static boolean isResting(long dayTime) { + long timeOfDay = dayTime % 24_000; + return timeOfDay < 10 || timeOfDay >= 12_000; + } + + public record Goal( + String resultString, + Predicate result, + @Nullable String firstString, + @Nullable Predicate first, + @Nullable String secondString, + @Nullable Predicate second + ) { + public boolean matches(Offer offer) { + return result.test(offer.result) + && (first == null || first.test(offer.first)) + && (second == null || (offer.second != null && second.test(offer.second))); + } + + @Override + public String toString() { + if (firstString == null) { + return resultString; + } else if (secondString == null) { + return String.format("%s = %s", firstString, resultString); + } else { + return String.format("%s + %s = %s", firstString, secondString, resultString); + } + } + } + + public record Offer(ItemStack first, @Nullable ItemStack second, ItemStack result) { + public Offer(MerchantOffer offer) { + this(offer.getBaseCostA(), offer.getItemCostB().map(ItemCost::itemStack).orElse(null), offer.getResult()); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Offer(ItemStack offerFirst, ItemStack offerSecond, ItemStack offerResult))) { + return false; + } + return ItemStack.isSameItemSameComponents(this.first, offerFirst) && this.first.getCount() == offerFirst.getCount() + && (this.second == offerSecond || this.second != null && offerSecond != null && ItemStack.isSameItemSameComponents(this.second, offerSecond) && this.second.getCount() == offerSecond.getCount()) + && ItemStack.isSameItemSameComponents(this.result, offerResult) && this.result.getCount() == offerResult.getCount(); + } + + @Override + public String toString() { + if (second == null) { + return String.format("%s = %s", VillagerCommand.displayText(first, false), VillagerCommand.displayText(result, false)); + } else { + return String.format("%s + %s = %s", VillagerCommand.displayText(first, false), VillagerCommand.displayText(second, false), VillagerCommand.displayText(result, false)); + } + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java new file mode 100644 index 000000000..3ded7c7ae --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/VillagerRngSimulator.java @@ -0,0 +1,538 @@ +package net.earthcomputer.clientcommands.features; + +import com.demonwav.mcdev.annotations.Translatable; +import com.seedfinding.latticg.math.component.BigFraction; +import com.seedfinding.latticg.math.component.BigMatrix; +import com.seedfinding.latticg.math.component.BigVector; +import com.seedfinding.latticg.math.lattice.enumerate.EnumerateRt; +import com.seedfinding.latticg.math.optimize.Optimize; +import com.seedfinding.mcseed.rand.JRand; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.LongStream; + +public class VillagerRngSimulator { + private static final BigMatrix[] LATTICES; + private static final BigMatrix[] INVERSE_LATTICES; + private static final BigVector[] OFFSETS; + + private JRand random; + private long prevRandomSeed = 0; + private int ambientSoundTime = -80; + private int prevAmbientSoundTime = -80; + private boolean madeSound = false; + private int totalAmbientSounds = 0; + private int tickCount = 0; + private int totalTicksWaiting = 0; + private float firstPitch = Float.NaN; + private int ticksBetweenSounds = 0; + private float secondPitch = Float.NaN; + private long @Nullable [] seedsFromTwoPitches = null; + + static { + try { + CompoundTag root = NbtIo.read(new DataInputStream(Objects.requireNonNull(VillagerRngSimulator.class.getResourceAsStream("/villager_lattice_data.nbt")))); + ListTag lattices = root.getList("lattices", Tag.TAG_LONG_ARRAY); + LATTICES = new BigMatrix[lattices.size()]; + ListTag latticeInverses = root.getList("lattice_inverses", Tag.TAG_LONG_ARRAY); + INVERSE_LATTICES = new BigMatrix[lattices.size()]; + ListTag offsets = root.getList("offsets", Tag.TAG_LONG_ARRAY); + OFFSETS = new BigVector[offsets.size()]; + for (int i = 0; i < lattices.size(); i++) { + long[] lattice = lattices.getLongArray(i); + BigMatrix matrix = new BigMatrix(3, 3); + matrix.set(0, 0, new BigFraction(lattice[0])); + matrix.set(0, 1, new BigFraction(lattice[1])); + matrix.set(0, 2, new BigFraction(lattice[2])); + matrix.set(1, 0, new BigFraction(lattice[3])); + matrix.set(1, 1, new BigFraction(lattice[4])); + matrix.set(1, 2, new BigFraction(lattice[5])); + matrix.set(2, 0, new BigFraction(lattice[6])); + matrix.set(2, 1, new BigFraction(lattice[7])); + matrix.set(2, 2, new BigFraction(lattice[8])); + LATTICES[i] = matrix; + } + for (int i = 0; i < latticeInverses.size(); i++) { + long[] lattice_inverse = latticeInverses.getLongArray(i); + BigMatrix matrix = new BigMatrix(3, 3); + matrix.set(0, 0, new BigFraction(lattice_inverse[0], 1L << 48)); + matrix.set(0, 1, new BigFraction(lattice_inverse[1], 1L << 48)); + matrix.set(0, 2, new BigFraction(lattice_inverse[2], 1L << 48)); + matrix.set(1, 0, new BigFraction(lattice_inverse[3], 1L << 48)); + matrix.set(1, 1, new BigFraction(lattice_inverse[4], 1L << 48)); + matrix.set(1, 2, new BigFraction(lattice_inverse[5], 1L << 48)); + matrix.set(2, 0, new BigFraction(lattice_inverse[6], 1L << 48)); + matrix.set(2, 1, new BigFraction(lattice_inverse[7], 1L << 48)); + matrix.set(2, 2, new BigFraction(lattice_inverse[8], 1L << 48)); + INVERSE_LATTICES[i] = matrix; + } + for (int i = 0; i < offsets.size(); i++) { + long[] offset = offsets.getLongArray(i); + OFFSETS[i] = new BigVector(0, offset[0], offset[1]); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public VillagerRngSimulator(@Nullable JRand random) { + this.random = random; + } + + public VillagerRngSimulator copy() { + VillagerRngSimulator that = new VillagerRngSimulator(random == null ? null : random.copy()); + that.ambientSoundTime = this.ambientSoundTime; + that.prevAmbientSoundTime = this.prevAmbientSoundTime; + that.madeSound = this.madeSound; + that.totalAmbientSounds = this.totalAmbientSounds; + return that; + } + + public void simulateTick() { + // called on receiving clock packet at the beginning of the tick, simulates the rest of the tick + + if (random == null) { + ambientSoundTime++; + return; + } + + prevRandomSeed = random.getSeed(); + prevAmbientSoundTime = ambientSoundTime; + + simulateBaseTick(); + simulateServerAiStep(); + + tickCount++; + } + + private void untick() { + random.setSeed(prevRandomSeed, false); + ambientSoundTime = prevAmbientSoundTime; + tickCount--; + } + + public int getTicksRemaining() { + return totalTicksWaiting - tickCount; + } + + private void simulateBaseTick() { + // we have the server receiving ambient noise tell us if we have to do this to increment the random, this is so that our ambient sound time is synced up. + if (random.nextInt(1000) < ambientSoundTime++ && totalAmbientSounds > 0) { + random.nextFloat(); + random.nextFloat(); + ambientSoundTime = -80; + madeSound = true; + } else { + madeSound = false; + } + } + + private void simulateServerAiStep() { + random.nextInt(100); + } + + public void updateProgressBar() { + if (totalTicksWaiting > 0) { + ClientCommandHelper.updateOverlayProgressBar(tickCount, totalTicksWaiting, 50, 60); + } + } + + @Nullable + public VillagerCracker.Offer anyOffersMatch(VillagerTrades.ItemListing[] listings, Entity trader, Predicate predicate) { + if (!isCracked()) { + return null; + } + + RandomSource rand = new LegacyRandomSource(random.getSeed() ^ 0x5deece66dL); + ArrayList newListings = new ArrayList<>(List.of(listings)); + int i = 0; + while (i < 2 && !newListings.isEmpty()) { + VillagerTrades.ItemListing listing = newListings.remove(rand.nextInt(newListings.size())); + MerchantOffer offer = listing.getOffer(trader, rand); + if (offer != null) { + VillagerCracker.Offer x = new VillagerCracker.Offer(offer); + if (predicate.test(x)) { + return x; + } else { + i++; + } + } + } + return null; + } + + @Nullable + public VillagerCracker.Offer[] generateOffers(VillagerTrades.ItemListing[] listings, Entity trader) { + if (!isCracked()) { + return null; + } + + VillagerCracker.Offer[] offers = new VillagerCracker.Offer[Math.min(listings.length, 2)]; + + RandomSource rand = new LegacyRandomSource(random.getSeed() ^ 0x5deece66dL); + ArrayList newListings = new ArrayList<>(List.of(listings)); + + for (int i = 0; i < offers.length; i++) { + VillagerTrades.ItemListing listing = newListings.remove(rand.nextInt(newListings.size())); + MerchantOffer offer = listing.getOffer(trader, rand); + if (offer != null) { + offers[i] = new VillagerCracker.Offer(offer); + } + } + + return offers; + } + + public void setTicksUntilInteract(int ticks) { + tickCount = 0; + totalTicksWaiting = ticks; + } + + public CrackedState getCrackedState() { + if (totalAmbientSounds == 0) { + return CrackedState.UNCRACKED; + } else if (totalAmbientSounds > 0 && random == null) { + return CrackedState.PARTIALLY_CRACKED; + } + + return CrackedState.CRACKED; + } + + public boolean isCracked() { + return getCrackedState() == CrackedState.CRACKED; + } + + public boolean isAtLeastPartiallyCracked() { + return getCrackedState() != CrackedState.UNCRACKED; + } + + public void reset() { + random = null; + prevRandomSeed = 0; + prevAmbientSoundTime = 0; + totalAmbientSounds = 0; + tickCount = 0; + totalTicksWaiting = 0; + firstPitch = Float.NaN; + ticksBetweenSounds = 0; + secondPitch = Float.NaN; + seedsFromTwoPitches = null; + } + + public void resetWaitingState() { + totalTicksWaiting = 0; + tickCount = 0; + } + + @Override + public String toString() { + return "VillagerRngSimulator[seed=" + (random == null ? "null" : random.getSeed()) + ']'; + } + + public void onAmbientSoundPlayed(float pitch) { + boolean justReset = false; + if (totalAmbientSounds == 2 && !madeSound) { + onBadSetup("ambient"); + justReset = true; + } + + if (totalAmbientSounds == 0) { + totalAmbientSounds++; + firstPitch = pitch; + ambientSoundTime = -80; + if (!justReset) { + ClientCommandHelper.addOverlayMessage(getCrackedState().getMessage(false).withStyle(ChatFormatting.RED), 100); + } + return; + } + + if (totalAmbientSounds == 1) { + totalAmbientSounds++; + ticksBetweenSounds = ambientSoundTime - (-80); + secondPitch = pitch; + ambientSoundTime = -80; + + if (seedsFromTwoPitches != null) { + int matchingSeeds = 0; + long matchingSeed = 0; + nextSeed: for (long seed : seedsFromTwoPitches) { + JRand rand = JRand.ofInternalSeed(seed); + rand.nextInt(100); + for (int i = -80; i < ticksBetweenSounds - 80 - 1; i++) { + if (rand.nextInt(1000) < i) { + continue nextSeed; + } + rand.nextInt(100); + } + if (rand.nextInt(1000) >= ticksBetweenSounds - 80 - 1) { + continue; + } + float simulatedThirdPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + if (simulatedThirdPitch == pitch) { + matchingSeeds++; + matchingSeed = rand.getSeed(); + } + } + seedsFromTwoPitches = null; + if (matchingSeeds == 1) { + random = JRand.ofInternalSeed(matchingSeed); + random.nextInt(100); + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.success", Long.toHexString(matchingSeed)).withStyle(ChatFormatting.GREEN), 100); + return; + } + } + + long[] seeds = crackSeed(); + if (seeds.length == 1) { + random = JRand.ofInternalSeed(seeds[0]); + random.nextInt(100); + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.success", Long.toHexString(seeds[0])).withStyle(ChatFormatting.GREEN), 100); + } else { + totalAmbientSounds = 1; + firstPitch = pitch; + secondPitch = Float.NaN; + seedsFromTwoPitches = seeds.length > 0 ? seeds : null; + ambientSoundTime = -80; + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.crack.failed", seeds.length).withStyle(ChatFormatting.RED), 100); + } + } + } + + public void onBadSetup(@Translatable(prefix = "villagerManip.reset.") String reason) { + ClientCommandHelper.sendError(Component.translatable("villagerManip.reset", Component.translatable("villagerManip.reset." + reason))); + reset(); + } + + public void onNoSoundPlayed(float pitch, boolean fromGuiInteract) { + // the last received action before the next tick's clock + // played both when interacting with a villager without a profession and when using the villager gui + + if (random != null) { + if (fromGuiInteract) { + ambientSoundTime = -80; + } + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.2f + 1.0f; + if (pitch != simulatedPitch) { + onBadSetup("no"); + } else { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.inSync", Long.toHexString(random.getSeed())).withStyle(ChatFormatting.GREEN), 100); + } + } + } + + public void onYesSoundPlayed(float pitch) { + // the last received action before the next tick's clock + // played when using the villager gui + + if (random != null) { + ambientSoundTime = -80; + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.2f + 1.0f; + if (pitch != simulatedPitch) { + onBadSetup("yes"); + } else { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.inSync", Long.toHexString(random.getSeed())).withStyle(ChatFormatting.GREEN), 100); + } + } + } + + public void onSplashSoundPlayed(float pitch) { + // the first received action after this tick's clock + + if (random != null) { + // simulateTick() was already called for this tick assuming no splash happened, so revert it and rerun it with the splash + untick(); + + float simulatedPitch = (random.nextFloat() - random.nextFloat()) * 0.4f + 1.0f; + if (pitch == simulatedPitch) { + int iterations = Mth.ceil(1.0f + EntityType.VILLAGER.getDimensions().width() * 20.0f); + random.advance(iterations * 10L); + simulateTick(); + + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.inSync", Long.toHexString(random.getSeed())).withStyle(ChatFormatting.GREEN), 100); + } else { + onBadSetup("splash"); + } + } + } + + public void onXpOrbSpawned(int value) { + // the last received action before the next tick's clock + + if (random != null) { + ambientSoundTime = -80; + int simulatedValue = 3 + this.random.nextInt(4); + boolean leveledUp = value > 3 + 3; + if (leveledUp) simulatedValue += 5; + if (value != simulatedValue) { + onBadSetup("xpOrb"); + } else { + ClientCommandHelper.addOverlayMessage(Component.translatable("commands.cvillager.inSync", Long.toHexString(random.getSeed())).withStyle(ChatFormatting.GREEN), 100); + } + } + } + + @Nullable + public VillagerRngSimulator.BruteForceResult bruteForceOffers(VillagerTrades.ItemListing[] listings, int minTicks, int maxTicks, Predicate predicate) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null && isCracked()) { + VillagerRngSimulator branchedSimulator = this.copy(); + int ticksPassed = 0; + + for (int i = 0; i < minTicks; i++) { + branchedSimulator.simulateTick(); + ticksPassed++; + } + + while (ticksPassed < maxTicks) { + VillagerRngSimulator offerSimulator = branchedSimulator.copy(); + offerSimulator.simulateTick(); + ticksPassed++; + VillagerCracker.Offer offer = offerSimulator.anyOffersMatch(listings, targetVillager, predicate); + if (offer != null) { + // we do the calls before this ticks processing so that since with 0ms ping, the server reads it next tick + return new BruteForceResult(ticksPassed, offer, offerSimulator.random.getSeed()); + } + branchedSimulator.simulateTick(); + } + } + + return null; + } + + @Nullable + public SurroundingOffers generateSurroundingOffers(VillagerTrades.ItemListing[] listings, int centerTicks, int radius) { + Villager targetVillager = VillagerCracker.getVillager(); + + if (targetVillager == null || !isCracked()) { + return null; + } + + List before = new ArrayList<>(radius); + List after = new ArrayList<>(radius); + + VillagerRngSimulator branchedSimulator = this.copy(); + for (int i = 0; i < Math.max(0, centerTicks - radius); i++) { + branchedSimulator.simulateTick(); + } + for (int i = Math.max(0, centerTicks - radius); i < centerTicks - 1; i++) { + branchedSimulator.simulateTick(); + before.add(branchedSimulator.generateOffers(listings, targetVillager)); + } + branchedSimulator.simulateTick(); + VillagerCracker.Offer[] middle = branchedSimulator.generateOffers(listings, targetVillager); + for (int i = 0; i < radius; i++) { + branchedSimulator.simulateTick(); + after.add(branchedSimulator.generateOffers(listings, targetVillager)); + } + + return new SurroundingOffers(before, middle, after); + } + + public long[] crackSeed() { + if (!(80 <= ticksBetweenSounds && ticksBetweenSounds - 80 < LATTICES.length)) { + return new long[0]; + } + + BigMatrix lattice = LATTICES[ticksBetweenSounds - 80]; + BigMatrix inverseLattice = INVERSE_LATTICES[ticksBetweenSounds - 80]; + BigVector offset = OFFSETS[ticksBetweenSounds - 80]; + + float firstMin = Math.max(-1.0f + 0x1.0p-24f, (firstPitch - 1.0f) / 0.2f - VillagerCracker.MAX_ERROR); + float firstMax = Math.min(1.0f - 0x1.0p-24f, (firstPitch - 1.0f) / 0.2f + VillagerCracker.MAX_ERROR); + float secondMin = Math.max(-1.0f + 0x1.0p-24f, (secondPitch - 1.0f) / 0.2f - VillagerCracker.MAX_ERROR); + float secondMax = Math.min(1.0f - 0x1.0p-24f, (secondPitch - 1.0f) / 0.2f + VillagerCracker.MAX_ERROR); + + firstMax = Math.nextUp(firstMax); + secondMax = Math.nextUp(secondMax); + + long firstMinLong = (long) Math.ceil(firstMin * 0x1.0p24f); + long firstMaxLong = (long) Math.ceil(firstMax * 0x1.0p24f) - 1; + long secondMinLong = (long) Math.ceil(secondMin * 0x1.0p24f); + long secondMaxLong = (long) Math.ceil(secondMax * 0x1.0p24f) - 1; + + long firstMinSeedDiff = (firstMinLong << 24) - 0xFFFFFF; + long firstMaxSeedDiff = (firstMaxLong << 24) + 0xFFFFFF; + long secondMinSeedDiff = (secondMinLong << 24) - 0xFFFFFF; + long secondMaxSeedDiff = (secondMaxLong << 24) + 0xFFFFFF; + + long firstCombinationModMin = firstMinSeedDiff & 0xFFFFFFFFFFFFL; + long firstCombinationModMax = firstMaxSeedDiff & 0xFFFFFFFFFFFFL; + long secondCombinationModMin = secondMinSeedDiff & 0xFFFFFFFFFFFFL; + long secondCombinationModMax = secondMaxSeedDiff & 0xFFFFFFFFFFFFL; + + firstCombinationModMax = firstCombinationModMax < firstCombinationModMin ? firstCombinationModMax + (1L << 48) : firstCombinationModMax; + secondCombinationModMax = secondCombinationModMax < secondCombinationModMin ? secondCombinationModMax + (1L << 48) : secondCombinationModMax; + + Optimize optimize = Optimize.Builder.ofSize(3) + .withLowerBound(0, 0) + .withUpperBound(0, 0xFFFFFFFFFFFFL) + .withLowerBound(1, firstCombinationModMin) + .withUpperBound(1, firstCombinationModMax) + .withLowerBound(2, secondCombinationModMin) + .withUpperBound(2, secondCombinationModMax) + .build(); + + return EnumerateRt.enumerate(lattice, offset, optimize, inverseLattice, inverseLattice.multiply(offset)).mapToLong(vec -> vec.get(0).getNumerator().longValue() & ((1L << 48) - 1)).flatMap(seed -> { + JRand rand = JRand.ofInternalSeed(seed); + float simulatedFirstPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + rand.nextInt(100); + for (int i = -80; i < ticksBetweenSounds - 80 - 1; i++) { + if (rand.nextInt(1000) < i) { + return LongStream.empty(); + } + rand.nextInt(100); + } + if (rand.nextInt(1000) >= ticksBetweenSounds - 80 - 1) { + return LongStream.empty(); + } + float simulatedSecondPitch = (rand.nextFloat() - rand.nextFloat()) * 0.2f + 1.0f; + if (simulatedFirstPitch == firstPitch && simulatedSecondPitch == secondPitch) { + return LongStream.of(rand.getSeed()); + } else { + return LongStream.empty(); + } + }).toArray(); + } + + public enum CrackedState { + UNCRACKED, + PARTIALLY_CRACKED, + CRACKED; + + public MutableComponent getMessage(boolean addColor) { + return switch (this) { + case UNCRACKED -> Component.translatable("commands.cvillager.uncracked").withStyle(addColor ? ChatFormatting.RED : ChatFormatting.RESET); + case PARTIALLY_CRACKED -> Component.translatable("commands.cvillager.partiallyCracked").withStyle(addColor ? ChatFormatting.RED : ChatFormatting.RESET); + case CRACKED -> Component.translatable("commands.cvillager.inSync").withStyle(addColor ? ChatFormatting.GREEN : ChatFormatting.RESET); + }; + } + } + + public record BruteForceResult(int ticksPassed, VillagerCracker.Offer offer, long seed) { + } + + public record SurroundingOffers(List before, VillagerCracker.Offer[] middle, List after) { + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/VillagerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/VillagerMixin.java new file mode 100644 index 000000000..910ad87df --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/VillagerMixin.java @@ -0,0 +1,23 @@ +package net.earthcomputer.clientcommands.mixin; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Villager.class) +public abstract class VillagerMixin extends Entity { + public VillagerMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + @Inject(method = "updateTrades", at = @At("HEAD")) + private void updateTrades(CallbackInfo ci) { + System.out.println("Actual seed: " + Long.toHexString(((LegacyRandomSource) random).seed.get())); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/ClientPacketListenerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/ClientPacketListenerMixin.java new file mode 100644 index 000000000..0833adc2c --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/ClientPacketListenerMixin.java @@ -0,0 +1,66 @@ +package net.earthcomputer.clientcommands.mixin.commands.villager; + +import com.llamalad7.mixinextras.sugar.Local; +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.protocol.game.ClientboundAddExperienceOrbPacket; +import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundDamageEventPacket; +import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin { + @Inject(method = "handleSoundEvent", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleSoundEvent(ClientboundSoundPacket packet, CallbackInfo ci) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null) { + VillagerCracker.onSoundEventPlayed(packet, new Vec3(packet.getX(), packet.getY(), packet.getZ())); + } + } + + @Inject(method = "handleChunkBlocksUpdate", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleChunkBlocksUpdate(ClientboundSectionBlocksUpdatePacket packet, CallbackInfo ci) { + if (Minecraft.getInstance().level != null) { + ResourceKey key = Minecraft.getInstance().level.dimension(); + packet.runUpdates((pos, state) -> { + if (new GlobalPos(key, pos).equals(VillagerCracker.getClockPos())) { + VillagerCracker.onServerTick(); + } + }); + } + } + + @Inject(method = "handleAddExperienceOrb", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleAddExperienceOrb(ClientboundAddExperienceOrbPacket packet, CallbackInfo ci) { + Villager targetVillager = VillagerCracker.getVillager(); + if (targetVillager != null && new Vec3(packet.getX(), packet.getY() - 0.5, packet.getZ()).distanceToSqr(targetVillager.position()) <= 0.1f) { + VillagerCracker.onXpOrbSpawned(packet); + } + } + + @Inject(method = "handleBlockUpdate", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/util/thread/BlockableEventLoop;)V")) + private void onHandleBlockUpdate(ClientboundBlockUpdatePacket packet, CallbackInfo ci) { + if (Minecraft.getInstance().level != null && new GlobalPos(Minecraft.getInstance().level.dimension(), packet.getPos()).equals(VillagerCracker.getClockPos())) { + VillagerCracker.onServerTick(); + } + } + + @Inject(method = "handleDamageEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;handleDamageEvent(Lnet/minecraft/world/damagesource/DamageSource;)V")) + private void onHandleDamageEvent(ClientboundDamageEventPacket packet, CallbackInfo ci, @Local Entity entity) { + if (entity == VillagerCracker.getVillager() && VillagerCracker.simulator.isAtLeastPartiallyCracked()) { + VillagerCracker.simulator.onBadSetup("mobHurt"); + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/MerchantScreenMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/MerchantScreenMixin.java new file mode 100644 index 000000000..59ccf3b57 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/villager/MerchantScreenMixin.java @@ -0,0 +1,24 @@ +package net.earthcomputer.clientcommands.mixin.commands.villager; + +import net.earthcomputer.clientcommands.features.VillagerCracker; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.MerchantMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MerchantScreen.class) +public abstract class MerchantScreenMixin extends AbstractContainerScreen { + public MerchantScreenMixin(MerchantMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Inject(method = "render", at = @At("HEAD")) + private void onRender(CallbackInfo ci) { + VillagerCracker.onGuiOpened(menu.getOffers().stream().map(VillagerCracker.Offer::new).toList()); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/util/BuildInfo.java b/src/main/java/net/earthcomputer/clientcommands/util/BuildInfo.java new file mode 100644 index 000000000..76dca6aac --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/util/BuildInfo.java @@ -0,0 +1,48 @@ +package net.earthcomputer.clientcommands.util; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.logging.LogUtils; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.Util; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.nio.file.Files; +import java.nio.file.Path; + +public record BuildInfo(String commitHash) { + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final BuildInfo UNKNOWN = new BuildInfo("refs/heads/fabric"); + + public static final BuildInfo INSTANCE = Util.make(() -> { + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + Path buildInfoPath = FabricLoader.getInstance().getModContainer("clientcommands") + .orElseThrow() + .findPath("build_info.json") + .orElse(null); + if (buildInfoPath == null) { + LOGGER.warn("Couldn't find build_info.json"); + return UNKNOWN; + } + + try (BufferedReader reader = Files.newBufferedReader(buildInfoPath)) { + BuildInfo result = gson.fromJson(reader, BuildInfo.class); + if (result == null) { + LOGGER.warn("build_info.json was null or empty"); + return UNKNOWN; + } + return result; + } catch (Throwable e) { + LOGGER.warn("Couldn't read build_info.json", e); + return UNKNOWN; + } + }); + + public String shortCommitHash(int length) { + return this == UNKNOWN || length >= commitHash.length() ? commitHash : commitHash.substring(0, length); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/util/CUtil.java b/src/main/java/net/earthcomputer/clientcommands/util/CUtil.java index 77bd2efbe..f3cc0c971 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/CUtil.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/CUtil.java @@ -1,12 +1,24 @@ package net.earthcomputer.clientcommands.util; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.datafixers.util.Either; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.Registries; +import net.minecraft.network.PacketEncoder; +import net.minecraft.network.PacketListener; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; import net.minecraft.resources.ResourceKey; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; @@ -17,10 +29,15 @@ import java.util.Arrays; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.regex.Pattern; public final class CUtil { + private static final ScheduledExecutorService PRECISE_PACKET_TIME_EXECUTOR = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setNameFormat("Clientcommands precise packet sender #%d").build()); private static final DynamicCommandExceptionType REGEX_TOO_SLOW_EXCEPTION = new DynamicCommandExceptionType(arg -> Component.translatable("commands.client.regexTooSlow", arg)); private CUtil() { @@ -51,6 +68,14 @@ public static void forEither(Either either, Consumer lef }); } + public static String boundsToString(MinMaxBounds bounds) { + StringBuilder sb = new StringBuilder(); + bounds.min().ifPresent(sb::append); + sb.append(".."); + bounds.max().ifPresent(sb::append); + return sb.toString(); + } + /** @noinspection OptionalIsPresent - no boxing! */ public static int getEnchantmentLevel(RegistryAccess registryAccess, ResourceKey enchantment, ItemStack stack) { Optional> enchHolder = registryAccess.lookupOrThrow(Registries.ENCHANTMENT).get(enchantment); @@ -66,6 +91,47 @@ public static int getEnchantmentLevel(ResourceKey enchantment, Livi return Arrays.stream(EquipmentSlot.values()).mapToInt(slot -> entity.getItemBySlot(slot).getEnchantments().getLevel(enchHolder.get())).max().orElse(0); } + public static void sendAtPreciseTime(long nanoTime, Packet packet, BooleanSupplier shouldStillSend, Runnable mainThreadCallback) { + sendAtPreciseTimeImpl(nanoTime, packet, shouldStillSend, mainThreadCallback); + } + + private static void sendAtPreciseTimeImpl(long nanoTime, Packet packet, BooleanSupplier shouldStillSend, Runnable mainThreadCallback) { + ClientPacketListener connection = Minecraft.getInstance().getConnection(); + if (connection == null) { + return; + } + + ChannelPipeline pipeline = connection.getConnection().channel.pipeline(); + @SuppressWarnings("unchecked") + PacketEncoder encoder = (PacketEncoder) pipeline.get("encoder"); + if (encoder == null) { + return; + } + ChannelHandlerContext encoderContext = pipeline.context("encoder"); + + // Pre-encode the packet with context that's safe to access from the main thread + ByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), connection.registryAccess()); + encoder.protocolInfo.codec().encode(buf, packet); + + PRECISE_PACKET_TIME_EXECUTOR.schedule(() -> { + while (System.nanoTime() - nanoTime < 0) { + if (!shouldStillSend.getAsBoolean()) { + buf.release(); + return; + } + } + + if (!shouldStillSend.getAsBoolean()) { + buf.release(); + return; + } + + encoderContext.writeAndFlush(buf); + + Minecraft.getInstance().schedule(mainThreadCallback); + }, Math.max(0, nanoTime - System.nanoTime() - 2000000), TimeUnit.NANOSECONDS); + } + private static class FusedRegexInput implements CharSequence { private static final long FUSE_LENGTH = 50_000_000; // 50ms should be more than enough for a normal regex to do its matching diff --git a/src/main/java/net/earthcomputer/clientcommands/util/CombinedMedianEM.java b/src/main/java/net/earthcomputer/clientcommands/util/CombinedMedianEM.java new file mode 100644 index 000000000..f39396011 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/util/CombinedMedianEM.java @@ -0,0 +1,153 @@ +package net.earthcomputer.clientcommands.util; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.SharedConstants; +import net.minecraft.util.Mth; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Taken from PseudoGravity's code + * + * @author MC (PseudoGravity) + */ +public final class CombinedMedianEM { + // a list of samples + // each sample is actually a list of points + // for each interval, convert it to a point by taking the median value + // or... break the 50ms intervals into 2 25ms intervals for better accuracy + public final List data = new ArrayList<>(); + + // width of the intervals + private int width = SharedConstants.MILLIS_PER_TICK; + + private static final double MIN_PACKET_LOSS_RATE = 0.01; + private static final double MAX_PACKET_LOSS_RATE = 0.5; + private static final double MIN_SIGMA = 10; + private static final double MAX_SIGMA = 1000; + + // three parameters and 2 constraints + private double packetLossRate = 0.2; + private double mu = 0.0; + private double sigma = 500; + + public void update(int mspt, int consideredTickRange) { + width = mspt; + + // width of time window considered + // all of the data points for all samples are within this window + int beginTime = Mth.floor(mu + 0.5) - width / 2 - (consideredTickRange / 2) * width; + int endTime = Mth.floor(mu + 0.5) + width / 2 + (consideredTickRange / 2) * width; + + double[] dropRate = new double[data.size()]; + Arrays.setAll(dropRate, i -> (double) data.get(i).size() * width / (endTime - beginTime)); + + int bestTime = 0; + double bestScore = Double.POSITIVE_INFINITY; + + for (int time = beginTime; time <= endTime; time += 10) { + // find score for each option for time + double score = 0; + for (int i = 0; i < data.size(); i++) { + // for each sample, find the point which adds the least to the score + DoubleList sample = data.get(i); + double lambda = dropRate[i] / width; + double bestSubScore = Double.POSITIVE_INFINITY; + for (int j = 0; j < sample.size(); j++) { + double x = sample.getDouble(j); + double absDev = Math.abs(x - time); + absDev = (1 - Math.exp(-lambda * absDev)) / lambda; // further curbing outlier effects + if (absDev < bestSubScore) { + bestSubScore = absDev; + } + } + score += bestSubScore; + } + if (score < bestScore) { + bestScore = score; + bestTime = time; + } + } + + mu = bestTime; + sigma = bestScore / data.size(); + sigma = Math.max(Math.min(sigma, MAX_SIGMA), MIN_SIGMA); + + for (int repeat = 0; repeat < 1; repeat++) { + // E step + // calculate weights (and classifications) + var masses = new ArrayList(); + for (int i = 0; i < data.size(); i++) { + + DoubleList sample = data.get(i); + + // process each sample + double sum = 0; + for (int j = 0; j < sample.size(); j++) { + double x = sample.getDouble(j); + sum += mass(x); + } + double pXandNorm = Math.min(sum, 1) * (1 - packetLossRate); // cap at 1 + + double pXandUnif = dropRate[i] * packetLossRate; + + double pNorm = pXandNorm / (pXandNorm + pXandUnif); + + DoubleList mass = new DoubleArrayList(); + for (int j = 0; j < sample.size(); j++) { + double x = sample.getDouble(j); + mass.add(mass(x) / sum * pNorm); + } + + masses.add(mass); + } + + // M step + // compute new best estimate for parameters + double weightedsum = 0; + double sumofweights = 0; + for (int i = 0; i < data.size(); i++) { + DoubleList sample = data.get(i); + DoubleList mass = masses.get(i); + for (int j = 0; j < sample.size(); j++) { + weightedsum += sample.getDouble(j) * mass.getDouble(j); + sumofweights += mass.getDouble(j); + } + } + double muNext = weightedsum / sumofweights; + + double weightedsumofsquaredeviations = 0; + for (int i = 0; i < data.size(); i++) { + DoubleList sample = data.get(i); + DoubleList mass = masses.get(i); + for (int j = 0; j < sample.size(); j++) { + weightedsumofsquaredeviations += Math.pow(sample.getDouble(j) - muNext, 2) * mass.getDouble(j); + } + } + double sigmaNext = Math.sqrt(weightedsumofsquaredeviations / sumofweights); + sigmaNext = Math.max(Math.min(sigmaNext, MAX_SIGMA), MIN_SIGMA); + + double packetlossrateNext = (data.size() - sumofweights) / data.size(); + packetlossrateNext = Math.max(Math.min(packetlossrateNext, MAX_PACKET_LOSS_RATE), MIN_PACKET_LOSS_RATE); + + mu = muNext; + sigma = sigmaNext; + packetLossRate = packetlossrateNext; + } + } + + private double mass(double x) { + // should be cdf(x+width/2)-cdf(x-width/2) but is simplified to pdf(x)*width and + // capped at 1 + // to avoid pesky erf() functions + double pdf = 1 / (sigma * Math.sqrt(2 * Math.PI)) * Math.exp(-Math.pow((x - mu) / sigma, 2) / 2); + return Math.min(pdf * width, 1); + } + + public double getResult() { + return mu; + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 67fc071c3..264c42039 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -283,6 +283,38 @@ "commands.cvar.remove.success": "Successfully removed variable \"%s\"", "commands.cvar.saveFile.failed": "Could not save variables file", + "commands.cvillager.alreadyRunning": "Cannot brute-force villager RNG since villager RNG manipulation is already running", + "commands.cvillager.bruteForce.failed": "Could not find a match for any goals within %s ticks", + "commands.cvillager.bruteForce.success": "Found a match for %s priced at %s. Will open villager gui in %s ticks", + "commands.cvillager.cannotModifyGoals": "Cannot modify goals while villager is currently being cracked", + "commands.cvillager.clock.cleared": "Your clock is cleared", + "commands.cvillager.clock.set": "Clock set to %s %s %s (%s)", + "commands.cvillager.clock.set.cleared": "Your clock is now cleared", + "commands.cvillager.crack.failed": "Failed to crack villager seed (found %s seeds), re-cracking...", + "commands.cvillager.crack.success": "Villager RNG cracked: %s", + "commands.cvillager.failure": "Got the incorrect trade with correction of %sms; re-adjusting to %sms", + "commands.cvillager.goalAdded": "Added goal successfully.", + "commands.cvillager.help.noClock": "Help: villager RNG manipulation requires a 20 Hz clock", + "commands.cvillager.help.tooFast": "Help: villager RNG manipulation requires a 20 Hz clock, please be advised that your clock seems to be running faster", + "commands.cvillager.help.tooSlow": "Help: villager RNG manipulation requires a 20 Hz clock, please be advised that your clock seems to be running slower", + "commands.cvillager.inSync": "Your villager's RNG is cracked", + "commands.cvillager.listGoals.noGoals": "There are no villager goals", + "commands.cvillager.listGoals.success": "There are %s villager goals:", + "commands.cvillager.needVillagerManipulation": "Villager manipulation is not enabled", + "commands.cvillager.noCrackedVillagerPresent": "There was no cracked villager available to use", + "commands.cvillager.noProfession": "The targeted villager has no profession", + "commands.cvillager.notAVillager": "Target was not a villager", + "commands.cvillager.notLevel1": "The targeted villager has level above 1", + "commands.cvillager.partiallyCracked": "Your villager's RNG is partially cracked (~83m seeds remain)", + "commands.cvillager.removeGoal.invalidIndex": "Unable to remove goal %s, you only have %s goals", + "commands.cvillager.removeGoal.success": "Successfully removed goal %s", + "commands.cvillager.reset": "Successfully reset villager cracking", + "commands.cvillager.resetCorrection": "Reset timing correction to %sms", + "commands.cvillager.success": "Got the correct trade with correction of %sms", + "commands.cvillager.target.cleared": "Target entity cleared", + "commands.cvillager.target.set": "Target entity set", + "commands.cvillager.uncracked": "Your villager's RNG is not cracked (~18 quintillion seeds remain)", + "commands.cwe.playerNotFound": "Player not found", "commands.cweather.reset": "Stopped overriding weather", @@ -385,5 +417,30 @@ "twoPlayerGame.clickToMakeYourMove": "Click here to make your move", "twoPlayerGame.incoming": "%s has made a move in %s", "twoPlayerGame.noGameWithPlayer": "Currently not playing a game with that player", - "twoPlayerGame.playerNotFound": "Player not found" + "twoPlayerGame.playerNotFound": "Player not found", + + "villagerManip.help.day": "Help: Villager RNG manipulation can only be done at night-time", + "villagerManip.help.setup": "Help: Trap the villager with trapdoors at their head with a bed's head between 3 and 16 blocks away, as seen in %s image", + "villagerManip.help.setup.link": "this", + "villagerManip.reset": "Restarting villager RNG manipulation. Reason: %s", + "villagerManip.reset.ambient": "Bad Ambient Noise", + "villagerManip.reset.day": "Daytime", + "villagerManip.reset.distance": "Too far from villager", + "villagerManip.reset.gossip": "Gossip", + "villagerManip.reset.invalidBedPosition": "Invalid Bed Position", + "villagerManip.reset.invalidCage": "Invalid Villager Cage", + "villagerManip.reset.itemEquipped": "Item Equipped", + "villagerManip.reset.itemInMainHand": "Item in Main Hand", + "villagerManip.reset.manipInactive": "Manipulation deactivated", + "villagerManip.reset.mobHurt": "Mob Hurt", + "villagerManip.reset.no": "Bad Unhappy Noise", + "villagerManip.reset.potion": "Potion Effect Active", + "villagerManip.reset.professionLost": "Profession lost", + "villagerManip.reset.splash": "Bad Splash Noise", + "villagerManip.reset.swim": "Swum", + "villagerManip.reset.tooManyBeds": "Too many beds within 16 block radius of villager", + "villagerManip.reset.uncrackedMisc": "Lost cracked state", + "villagerManip.reset.wrongScreen": "No villager GUI open", + "villagerManip.reset.xpOrb": "Bad XP Orb Size", + "villagerManip.reset.yes": "Bad Happy Noise" } diff --git a/src/main/resources/clientcommands.aw b/src/main/resources/clientcommands.aw index e601a6953..86be7dce3 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -42,6 +42,9 @@ accessible field net/minecraft/client/gui/components/EditBox maxLength I accessible class net/minecraft/client/renderer/RenderType$CompositeState accessible class net/minecraft/client/renderer/RenderStateShard$LineStateShard +# Villager Manipulation +accessible method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen slotClicked (Lnet/minecraft/world/inventory/Slot;IILnet/minecraft/world/inventory/ClickType;)V + # RNG Events accessible method net/minecraft/world/entity/Entity isInvulnerableToBase (Lnet/minecraft/world/damagesource/DamageSource;)Z accessible field net/minecraft/world/entity/LivingEntity lastHurt F diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 3777cf456..cd93592c5 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -3,6 +3,7 @@ "package": "net.earthcomputer.clientcommands.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ + "VillagerMixin", "commands.enchant.EnchantmentScreenMixin", "commands.findblock.LevelChunkMixin", "commands.fish.FishingHookMixin", @@ -71,6 +72,8 @@ "commands.glow.LivingEntityRenderStateMixin", "commands.reply.ClientPacketListenerMixin", "commands.snap.MinecraftMixin", + "commands.villager.ClientPacketListenerMixin", + "commands.villager.MerchantScreenMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin", "lengthextender.ChatScreenMixin", diff --git a/src/main/resources/villager_lattice_data.nbt b/src/main/resources/villager_lattice_data.nbt new file mode 100644 index 000000000..c29cf2375 Binary files /dev/null and b/src/main/resources/villager_lattice_data.nbt differ diff --git a/src/test/java/net/earthcomputer/clientcommands/test/CallHierarchyWalker.java b/src/test/java/net/earthcomputer/clientcommands/test/CallHierarchyWalker.java index 4d1ae5680..52d67cb66 100644 --- a/src/test/java/net/earthcomputer/clientcommands/test/CallHierarchyWalker.java +++ b/src/test/java/net/earthcomputer/clientcommands/test/CallHierarchyWalker.java @@ -33,14 +33,18 @@ public abstract sealed class CallHierarchyWalker { private final Set methodsToRecurseThrough = new HashSet<>(); // the reference <- referenced-from edges that have already been visited, used to prevent infinite recursion private final Set> visitedEdges = new HashSet<>(); - private String runtimeOwnerType = "java/lang/Object"; + String runtimeOwnerType = "java/lang/Object"; public static CallHierarchyWalker fromField(String owner, String name, String desc) { return new Field(owner, name, desc); } public static CallHierarchyWalker fromMethod(String owner, String name, String desc) { - return new Method(owner, name, desc); + return new Method(owner, name, desc, -1); + } + + public static CallHierarchyWalker fromMethod(String owner, String name, String desc, int ownerArgIndex) { + return new Method(owner, name, desc, ownerArgIndex); } public CallHierarchyWalker recurseThrough(String owner, String name, String desc) { @@ -239,21 +243,28 @@ private static final class Method extends CallHierarchyWalker { private final String owner; private final String name; private final String desc; + private final int ownerArgIndex; - private Method(String owner, String name, String desc) { + private Method(String owner, String name, String desc, int ownerArgIndex) { this.owner = owner; this.name = name; this.desc = desc; + this.ownerArgIndex = ownerArgIndex; } @Override public void walk(ReferenceConsumer referenceConsumer) { int argumentCount = Type.getArgumentCount(desc); + if (ownerArgIndex >= argumentCount) { + throw new IllegalArgumentException("ownerArgIndex (" + ownerArgIndex + ") >= argumentCount (" + argumentCount + ")"); + } + int stackDepth = argumentCount - 1 - ownerArgIndex; + handleReferences( List.of(new ReferencesFinder.OwnerNameAndDesc(owner, name, desc)), finder.findMethodReferences(owner, name, desc), - insn -> ((MethodInsnNode) insn).owner, - insn -> insn.getOpcode() == Opcodes.INVOKESTATIC ? -1 : argumentCount, + insn -> ownerArgIndex == -1 ? ((MethodInsnNode) insn).owner : runtimeOwnerType, + insn -> ownerArgIndex == -1 && insn.getOpcode() == Opcodes.INVOKESTATIC ? -1 : stackDepth, (containingClass, method) -> finder.findCallsToMethodInMethod(containingClass, method, owner, name, desc), referenceConsumer ); diff --git a/src/test/java/net/earthcomputer/clientcommands/test/EntityRandomCallHierarchyTest.java b/src/test/java/net/earthcomputer/clientcommands/test/EntityRandomCallHierarchyTest.java index c2c9afee2..b6a341f4f 100644 --- a/src/test/java/net/earthcomputer/clientcommands/test/EntityRandomCallHierarchyTest.java +++ b/src/test/java/net/earthcomputer/clientcommands/test/EntityRandomCallHierarchyTest.java @@ -1,9 +1,15 @@ package net.earthcomputer.clientcommands.test; import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; import java.io.PrintWriter; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; public final class EntityRandomCallHierarchyTest { @@ -30,6 +36,29 @@ public void testPlayer() { }); } + @Test + public void testVillager() { + TestUtil.regressionTest("villagerRandomHierarchy", out -> { + CallHierarchyWalker.fromField("net/minecraft/world/entity/Entity", "random", "Lnet/minecraft/util/RandomSource;") + .runtimeOwnerType("net/minecraft/world/entity/npc/Villager") + .recurseThrough("net/minecraft/world/entity/Entity", "getRandom", "()Lnet/minecraft/util/RandomSource;") + .recurseThrough("net/minecraft/world/entity/Entity", "getRandomX", "(D)D") + .recurseThrough("net/minecraft/world/entity/Entity", "getRandomY", "()D") + .recurseThrough("net/minecraft/world/entity/Entity", "getRandomZ", "(D)D") + .recurseThrough("net/minecraft/world/entity/LivingEntity", "spawnItemParticles", "(Lnet/minecraft/world/item/ItemStack;I)V") + .recurseThrough("net/minecraft/world/entity/LivingEntity", "makeSound", "(Lnet/minecraft/sounds/SoundEvent;)V") + .recurseThrough("net/minecraft/world/entity/LivingEntity", "getSoundVolume", "()F") + .recurseThrough("net/minecraft/world/entity/LivingEntity", "getVoicePitch", "()F") + .recurseThrough("net/minecraft/world/entity/LivingEntity", "getVoicePitch", "()F") + .recurseThrough("net/minecraft/world/entity/npc/AbstractVillager", "addParticlesAroundSelf", "(Lnet/minecraft/core/particles/ParticleOptions;)V") + .walk((reference, callStack) -> { + if (filterAiGoal(reference.owner(), "net/minecraft/world/entity/npc/Villager")) { + printReference(out, reference, callStack); + } + }); + }); + } + @Test public void testTeleportRandomly() { TestUtil.regressionTest("teleportRandomlyHierarchy", out -> { @@ -79,7 +108,61 @@ public void testEnchantmentSpawnParticlesEffect() { }); } - private void printReference(PrintWriter out, ReferencesFinder.OwnerNameAndDesc reference, List callStack) { + private static void printReference(PrintWriter out, ReferencesFinder.OwnerNameAndDesc reference, List callStack) { out.printf("%s.%s %s <- %s%n", reference.owner(), reference.name(), reference.desc(), callStack.reversed().stream().map(method -> method.owner() + "." + method.name()).collect(Collectors.joining(" <- "))); } + + // TODO: add a way to filter AI behaviors too + private static boolean filterAiGoal(String owner, String entityClass) { + return filterAiGoal(owner, entityClass, new HashSet<>()); + } + + private static boolean filterAiGoal(String owner, String entityClass, Set alreadyChecked) { + ReferencesFinder finder = ReferencesFinder.getInstance(); + if (!finder.isAssignable("net/minecraft/world/entity/ai/goal/Goal", owner)) { + return true; + } + + if (!alreadyChecked.add(owner)) { + return false; + } + + ClassNode clazz = finder.getClassNode(owner); + Objects.requireNonNull(clazz); + for (MethodNode method : clazz.methods) { + if (!"".equals(method.name)) { + continue; + } + + Type[] argumentTypes = Type.getArgumentTypes(method.desc); + int ownerIndex = -1; + for (int i = 0; i < argumentTypes.length; i++) { + Type argType = argumentTypes[i]; + if (argType.getSort() == Type.OBJECT && finder.isAssignable("net/minecraft/world/entity/Entity", argType.getInternalName())) { + ownerIndex = i; + break; + } + } + if (ownerIndex == -1) { + continue; + } + if (!finder.isAssignable(argumentTypes[ownerIndex].getInternalName(), entityClass) && !finder.isAssignable(entityClass, argumentTypes[ownerIndex].getInternalName())) { + continue; + } + + boolean[] foundAnyReferences = { false }; + CallHierarchyWalker.fromMethod(owner, "", method.desc, ownerIndex) + .runtimeOwnerType(entityClass) + .walk((reference, callStack) -> { + if (filterAiGoal(reference.owner(), entityClass, alreadyChecked)) { + foundAnyReferences[0] = true; + } + }); + if (foundAnyReferences[0]) { + return true; + } + } + + return false; + } } diff --git a/src/test/java/net/earthcomputer/clientcommands/test/ReferencesFinder.java b/src/test/java/net/earthcomputer/clientcommands/test/ReferencesFinder.java index 379e0a7cf..f099bc796 100644 --- a/src/test/java/net/earthcomputer/clientcommands/test/ReferencesFinder.java +++ b/src/test/java/net/earthcomputer/clientcommands/test/ReferencesFinder.java @@ -15,6 +15,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; @@ -106,6 +107,13 @@ private boolean getClassData(String className, ClassVisitor classVisitor, int re return false; } + @Nullable + public ClassNode getClassNode(String className) { + ClassNode classNode = new ClassNode(); + boolean exists = getClassData(className, classNode, ClassReader.SKIP_FRAMES); + return exists ? classNode : null; + } + // should only be called while indexing! @Nullable private ClassInfo getOrCreateClassInfo(Interner interner, String className) {