/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.annotations.VisibleForTesting * com.google.common.collect.ImmutableList * com.google.common.collect.ImmutableMap * com.google.common.collect.ImmutableSet * com.mojang.datafixers.util.Pair * com.mojang.logging.LogUtils * com.mojang.serialization.Dynamic * it.unimi.dsi.fastutil.ints.Int2ObjectMap * org.jspecify.annotations.Nullable * org.slf4j.Logger */ package net.minecraft.world.entity.npc; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import com.mojang.serialization.Dynamic; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiPredicate; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponentGetter; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.stats.Stats; import net.minecraft.tags.ItemTags; import net.minecraft.util.Mth; import net.minecraft.util.SpawnUtil; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.Difficulty; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.SimpleContainer; import net.minecraft.world.attribute.EnvironmentAttributes; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.ConversionParams; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ReputationEventHandler; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.ai.Brain; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.behavior.BehaviorControl; import net.minecraft.world.entity.ai.behavior.VillagerGoalPackages; import net.minecraft.world.entity.ai.gossip.GossipContainer; import net.minecraft.world.entity.ai.gossip.GossipType; import net.minecraft.world.entity.ai.memory.MemoryModuleType; import net.minecraft.world.entity.ai.memory.MemoryStatus; import net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities; import net.minecraft.world.entity.ai.sensing.GolemSensor; import net.minecraft.world.entity.ai.sensing.Sensor; import net.minecraft.world.entity.ai.sensing.SensorType; import net.minecraft.world.entity.ai.village.ReputationEventType; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.ai.village.poi.PoiTypes; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.monster.Witch; import net.minecraft.world.entity.npc.AbstractVillager; import net.minecraft.world.entity.npc.InventoryCarrier; import net.minecraft.world.entity.npc.VillagerData; import net.minecraft.world.entity.npc.VillagerDataHolder; import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.entity.npc.VillagerType; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.raid.Raid; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.level.Level; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; import net.minecraft.world.phys.AABB; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class Villager extends AbstractVillager implements VillagerDataHolder, ReputationEventHandler { private static final Logger LOGGER = LogUtils.getLogger(); private static final EntityDataAccessor DATA_VILLAGER_DATA = SynchedEntityData.defineId(Villager.class, EntityDataSerializers.VILLAGER_DATA); public static final int BREEDING_FOOD_THRESHOLD = 12; public static final Map FOOD_POINTS = ImmutableMap.of((Object)Items.BREAD, (Object)4, (Object)Items.POTATO, (Object)1, (Object)Items.CARROT, (Object)1, (Object)Items.BEETROOT, (Object)1); private static final int TRADES_PER_LEVEL = 2; private static final int MAX_GOSSIP_TOPICS = 10; private static final int GOSSIP_COOLDOWN = 1200; private static final int GOSSIP_DECAY_INTERVAL = 24000; private static final int HOW_FAR_AWAY_TO_TALK_TO_OTHER_VILLAGERS_ABOUT_GOLEMS = 10; private static final int HOW_MANY_VILLAGERS_NEED_TO_AGREE_TO_SPAWN_A_GOLEM = 5; private static final long TIME_SINCE_SLEEPING_FOR_GOLEM_SPAWNING = 24000L; @VisibleForTesting public static final float SPEED_MODIFIER = 0.5f; private static final int DEFAULT_XP = 0; private static final byte DEFAULT_FOOD_LEVEL = 0; private static final int DEFAULT_LAST_RESTOCK = 0; private static final int DEFAULT_LAST_GOSSIP_DECAY = 0; private static final int DEFAULT_RESTOCKS_TODAY = 0; private static final boolean DEFAULT_ASSIGN_PROFESSION_WHEN_SPAWNED = false; private int updateMerchantTimer; private boolean increaseProfessionLevelOnUpdate; private @Nullable Player lastTradedPlayer; private boolean chasing; private int foodLevel = 0; private final GossipContainer gossips = new GossipContainer(); private long lastGossipTime; private long lastGossipDecayTime = 0L; private int villagerXp = 0; private long lastRestockGameTime = 0L; private int numberOfRestocksToday = 0; private long lastRestockCheckDay; private boolean assignProfessionWhenSpawned = false; private static final ImmutableList> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.HOME, MemoryModuleType.JOB_SITE, MemoryModuleType.POTENTIAL_JOB_SITE, MemoryModuleType.MEETING_POINT, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.VISIBLE_VILLAGER_BABIES, MemoryModuleType.NEAREST_PLAYERS, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, MemoryModuleType.ITEM_PICKUP_COOLDOWN_TICKS, (Object[])new MemoryModuleType[]{MemoryModuleType.WALK_TARGET, MemoryModuleType.LOOK_TARGET, MemoryModuleType.INTERACTION_TARGET, MemoryModuleType.BREED_TARGET, MemoryModuleType.PATH, MemoryModuleType.DOORS_TO_CLOSE, MemoryModuleType.NEAREST_BED, MemoryModuleType.HURT_BY, MemoryModuleType.HURT_BY_ENTITY, MemoryModuleType.NEAREST_HOSTILE, MemoryModuleType.SECONDARY_JOB_SITE, MemoryModuleType.HIDING_PLACE, MemoryModuleType.HEARD_BELL_TIME, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.LAST_SLEPT, MemoryModuleType.LAST_WOKEN, MemoryModuleType.LAST_WORKED_AT_POI, MemoryModuleType.GOLEM_DETECTED_RECENTLY}); private static final ImmutableList>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.NEAREST_ITEMS, SensorType.NEAREST_BED, SensorType.HURT_BY, SensorType.VILLAGER_HOSTILES, SensorType.VILLAGER_BABIES, SensorType.SECONDARY_POIS, SensorType.GOLEM_DETECTED); public static final Map, BiPredicate>> POI_MEMORIES = ImmutableMap.of(MemoryModuleType.HOME, (villager, poiType) -> poiType.is(PoiTypes.HOME), MemoryModuleType.JOB_SITE, (villager, poiType) -> villager.getVillagerData().profession().value().heldJobSite().test((Holder)poiType), MemoryModuleType.POTENTIAL_JOB_SITE, (villager, poiType) -> VillagerProfession.ALL_ACQUIRABLE_JOBS.test((Holder)poiType), MemoryModuleType.MEETING_POINT, (villager, poiType) -> poiType.is(PoiTypes.MEETING)); public Villager(EntityType type, Level level) { this(type, level, VillagerType.PLAINS); } public Villager(EntityType entityType, Level level, ResourceKey type) { this(entityType, level, level.registryAccess().getOrThrow(type)); } public Villager(EntityType entityType, Level level, Holder type) { super((EntityType)entityType, level); this.getNavigation().setCanOpenDoors(true); this.getNavigation().setCanFloat(true); this.getNavigation().setRequiredPathLength(48.0f); this.setCanPickUpLoot(true); this.setVillagerData(this.getVillagerData().withType(type).withProfession(level.registryAccess(), VillagerProfession.NONE)); } public Brain getBrain() { return super.getBrain(); } protected Brain.Provider brainProvider() { return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); } @Override protected Brain makeBrain(Dynamic input) { Brain brain = this.brainProvider().makeBrain(input); this.registerBrainGoals(brain); return brain; } public void refreshBrain(ServerLevel level) { Brain oldBrain = this.getBrain(); oldBrain.stopAll(level, this); this.brain = oldBrain.copyWithoutBehaviors(); this.registerBrainGoals(this.getBrain()); } private void registerBrainGoals(Brain brain) { Holder profession = this.getVillagerData().profession(); if (this.isBaby()) { brain.setSchedule(EnvironmentAttributes.BABY_VILLAGER_ACTIVITY); brain.addActivity(Activity.PLAY, VillagerGoalPackages.getPlayPackage(0.5f)); } else { brain.setSchedule(EnvironmentAttributes.VILLAGER_ACTIVITY); brain.addActivityWithConditions(Activity.WORK, (ImmutableList>>)VillagerGoalPackages.getWorkPackage(profession, 0.5f), (Set, MemoryStatus>>)ImmutableSet.of((Object)Pair.of(MemoryModuleType.JOB_SITE, (Object)((Object)MemoryStatus.VALUE_PRESENT)))); } brain.addActivity(Activity.CORE, VillagerGoalPackages.getCorePackage(profession, 0.5f)); brain.addActivityWithConditions(Activity.MEET, (ImmutableList>>)VillagerGoalPackages.getMeetPackage(profession, 0.5f), (Set, MemoryStatus>>)ImmutableSet.of((Object)Pair.of(MemoryModuleType.MEETING_POINT, (Object)((Object)MemoryStatus.VALUE_PRESENT)))); brain.addActivity(Activity.REST, VillagerGoalPackages.getRestPackage(profession, 0.5f)); brain.addActivity(Activity.IDLE, VillagerGoalPackages.getIdlePackage(profession, 0.5f)); brain.addActivity(Activity.PANIC, VillagerGoalPackages.getPanicPackage(profession, 0.5f)); brain.addActivity(Activity.PRE_RAID, VillagerGoalPackages.getPreRaidPackage(profession, 0.5f)); brain.addActivity(Activity.RAID, VillagerGoalPackages.getRaidPackage(profession, 0.5f)); brain.addActivity(Activity.HIDE, VillagerGoalPackages.getHidePackage(profession, 0.5f)); brain.setCoreActivities((Set)ImmutableSet.of((Object)Activity.CORE)); brain.setDefaultActivity(Activity.IDLE); brain.setActiveActivityIfPossible(Activity.IDLE); brain.updateActivityFromSchedule(this.level().environmentAttributes(), this.level().getGameTime(), this.position()); } @Override protected void ageBoundaryReached() { super.ageBoundaryReached(); if (this.level() instanceof ServerLevel) { this.refreshBrain((ServerLevel)this.level()); } } public static AttributeSupplier.Builder createAttributes() { return Mob.createMobAttributes().add(Attributes.MOVEMENT_SPEED, 0.5); } public boolean assignProfessionWhenSpawned() { return this.assignProfessionWhenSpawned; } @Override protected void customServerAiStep(ServerLevel level) { Raid raid; ProfilerFiller profiler = Profiler.get(); profiler.push("villagerBrain"); this.getBrain().tick(level, this); profiler.pop(); if (this.assignProfessionWhenSpawned) { this.assignProfessionWhenSpawned = false; } if (!this.isTrading() && this.updateMerchantTimer > 0) { --this.updateMerchantTimer; if (this.updateMerchantTimer <= 0) { if (this.increaseProfessionLevelOnUpdate) { this.increaseMerchantCareer(level); this.increaseProfessionLevelOnUpdate = false; } this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0)); } } if (this.lastTradedPlayer != null) { level.onReputationEvent(ReputationEventType.TRADE, this.lastTradedPlayer, this); level.broadcastEntityEvent(this, (byte)14); this.lastTradedPlayer = null; } if (!this.isNoAi() && this.random.nextInt(100) == 0 && (raid = level.getRaidAt(this.blockPosition())) != null && raid.isActive() && !raid.isOver()) { level.broadcastEntityEvent(this, (byte)42); } if (this.getVillagerData().profession().is(VillagerProfession.NONE) && this.isTrading()) { this.stopTrading(); } super.customServerAiStep(level); } @Override public void tick() { super.tick(); if (this.getUnhappyCounter() > 0) { this.setUnhappyCounter(this.getUnhappyCounter() - 1); } this.maybeDecayGossip(); } @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); if (!itemStack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isSleeping()) { if (this.isBaby()) { this.setUnhappy(); return InteractionResult.SUCCESS; } if (!this.level().isClientSide()) { boolean noOffers = this.getOffers().isEmpty(); if (hand == InteractionHand.MAIN_HAND) { if (noOffers) { this.setUnhappy(); } player.awardStat(Stats.TALKED_TO_VILLAGER); } if (noOffers) { return InteractionResult.CONSUME; } this.startTrading(player); } return InteractionResult.SUCCESS; } return super.mobInteract(player, hand); } private void setUnhappy() { this.setUnhappyCounter(40); if (!this.level().isClientSide()) { this.makeSound(SoundEvents.VILLAGER_NO); } } private void startTrading(Player player) { this.updateSpecialPrices(player); this.setTradingPlayer(player); this.openTradingScreen(player, this.getDisplayName(), this.getVillagerData().level()); } @Override public void setTradingPlayer(@Nullable Player player) { boolean shouldStop = this.getTradingPlayer() != null && player == null; super.setTradingPlayer(player); if (shouldStop) { this.stopTrading(); } } @Override protected void stopTrading() { super.stopTrading(); this.resetSpecialPrices(); } private void resetSpecialPrices() { if (this.level().isClientSide()) { return; } for (MerchantOffer offer : this.getOffers()) { offer.resetSpecialPriceDiff(); } } @Override public boolean canRestock() { return true; } public void restock() { this.updateDemand(); for (MerchantOffer offer : this.getOffers()) { offer.resetUses(); } this.resendOffersToTradingPlayer(); this.lastRestockGameTime = this.level().getGameTime(); ++this.numberOfRestocksToday; } private void resendOffersToTradingPlayer() { MerchantOffers offers = this.getOffers(); Player tradingPlayer = this.getTradingPlayer(); if (tradingPlayer != null && !offers.isEmpty()) { tradingPlayer.sendMerchantOffers(tradingPlayer.containerMenu.containerId, offers, this.getVillagerData().level(), this.getVillagerXp(), this.showProgressBar(), this.canRestock()); } } private boolean needsToRestock() { for (MerchantOffer offer : this.getOffers()) { if (!offer.needsRestock()) continue; return true; } return false; } private boolean allowedToRestock() { return this.numberOfRestocksToday == 0 || this.numberOfRestocksToday < 2 && this.level().getGameTime() > this.lastRestockGameTime + 2400L; } public boolean shouldRestock(ServerLevel level) { long halfDayPassedTime = this.lastRestockGameTime + 12000L; long gameTime = this.level().getGameTime(); boolean isNewDay = gameTime > halfDayPassedTime; long currentDay = level.getDayCount(); boolean bl = this.lastRestockCheckDay > 0L && currentDay > this.lastRestockCheckDay; this.lastRestockCheckDay = currentDay; if (isNewDay |= bl) { this.lastRestockGameTime = gameTime; this.resetNumberOfRestocks(); } return this.allowedToRestock() && this.needsToRestock(); } private void catchUpDemand() { int missedUpdates = 2 - this.numberOfRestocksToday; if (missedUpdates > 0) { for (MerchantOffer offer : this.getOffers()) { offer.resetUses(); } } for (int i = 0; i < missedUpdates; ++i) { this.updateDemand(); } this.resendOffersToTradingPlayer(); } private void updateDemand() { for (MerchantOffer offer : this.getOffers()) { offer.updateDemand(); } } private void updateSpecialPrices(Player player) { int reputation = this.getPlayerReputation(player); if (reputation != 0) { for (MerchantOffer offer : this.getOffers()) { offer.addToSpecialPriceDiff(-Mth.floor((float)reputation * offer.getPriceMultiplier())); } } if (player.hasEffect(MobEffects.HERO_OF_THE_VILLAGE)) { MobEffectInstance effect = player.getEffect(MobEffects.HERO_OF_THE_VILLAGE); int amplifier = effect.getAmplifier(); for (MerchantOffer offer : this.getOffers()) { double modifier = 0.3 + 0.0625 * (double)amplifier; int costReduction = (int)Math.floor(modifier * (double)offer.getBaseCostA().getCount()); offer.addToSpecialPriceDiff(-Math.max(costReduction, 1)); } } } @Override protected void defineSynchedData(SynchedEntityData.Builder entityData) { super.defineSynchedData(entityData); entityData.define(DATA_VILLAGER_DATA, Villager.createDefaultVillagerData()); } public static VillagerData createDefaultVillagerData() { return new VillagerData(BuiltInRegistries.VILLAGER_TYPE.getOrThrow(VillagerType.PLAINS), BuiltInRegistries.VILLAGER_PROFESSION.getOrThrow(VillagerProfession.NONE), 1); } @Override protected void addAdditionalSaveData(ValueOutput output) { super.addAdditionalSaveData(output); output.store("VillagerData", VillagerData.CODEC, this.getVillagerData()); output.putByte("FoodLevel", (byte)this.foodLevel); output.store("Gossips", GossipContainer.CODEC, this.gossips); output.putInt("Xp", this.villagerXp); output.putLong("LastRestock", this.lastRestockGameTime); output.putLong("LastGossipDecay", this.lastGossipDecayTime); output.putInt("RestocksToday", this.numberOfRestocksToday); if (this.assignProfessionWhenSpawned) { output.putBoolean("AssignProfessionWhenSpawned", true); } } @Override protected void readAdditionalSaveData(ValueInput input) { super.readAdditionalSaveData(input); this.entityData.set(DATA_VILLAGER_DATA, input.read("VillagerData", VillagerData.CODEC).orElseGet(Villager::createDefaultVillagerData)); this.foodLevel = input.getByteOr("FoodLevel", (byte)0); this.gossips.clear(); input.read("Gossips", GossipContainer.CODEC).ifPresent(this.gossips::putAll); this.villagerXp = input.getIntOr("Xp", 0); this.lastRestockGameTime = input.getLongOr("LastRestock", 0L); this.lastGossipDecayTime = input.getLongOr("LastGossipDecay", 0L); if (this.level() instanceof ServerLevel) { this.refreshBrain((ServerLevel)this.level()); } this.numberOfRestocksToday = input.getIntOr("RestocksToday", 0); this.assignProfessionWhenSpawned = input.getBooleanOr("AssignProfessionWhenSpawned", false); } @Override public boolean removeWhenFarAway(double distSqr) { return false; } @Override protected @Nullable SoundEvent getAmbientSound() { if (this.isSleeping()) { return null; } if (this.isTrading()) { return SoundEvents.VILLAGER_TRADE; } return SoundEvents.VILLAGER_AMBIENT; } @Override protected SoundEvent getHurtSound(DamageSource source) { return SoundEvents.VILLAGER_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.VILLAGER_DEATH; } public void playWorkSound() { this.makeSound(this.getVillagerData().profession().value().workSound()); } @Override public void setVillagerData(VillagerData data) { VillagerData currentData = this.getVillagerData(); if (!currentData.profession().equals(data.profession())) { this.offers = null; } this.entityData.set(DATA_VILLAGER_DATA, data); } @Override public VillagerData getVillagerData() { return this.entityData.get(DATA_VILLAGER_DATA); } @Override protected void rewardTradeXp(MerchantOffer offer) { int popXp = 3 + this.random.nextInt(4); this.villagerXp += offer.getXp(); this.lastTradedPlayer = this.getTradingPlayer(); if (this.shouldIncreaseLevel()) { this.updateMerchantTimer = 40; this.increaseProfessionLevelOnUpdate = true; popXp += 5; } if (offer.shouldRewardExp()) { this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), popXp)); } } @Override public void setLastHurtByMob(@Nullable LivingEntity hurtBy) { if (hurtBy != null && this.level() instanceof ServerLevel) { ((ServerLevel)this.level()).onReputationEvent(ReputationEventType.VILLAGER_HURT, hurtBy, this); if (this.isAlive() && hurtBy instanceof Player) { this.level().broadcastEntityEvent(this, (byte)13); } } super.setLastHurtByMob(hurtBy); } @Override public void die(DamageSource source) { LOGGER.info("Villager {} died, message: '{}'", (Object)this, (Object)source.getLocalizedDeathMessage(this).getString()); Entity murderer = source.getEntity(); if (murderer != null) { this.tellWitnessesThatIWasMurdered(murderer); } this.releaseAllPois(); super.die(source); } private void releaseAllPois() { this.releasePoi(MemoryModuleType.HOME); this.releasePoi(MemoryModuleType.JOB_SITE); this.releasePoi(MemoryModuleType.POTENTIAL_JOB_SITE); this.releasePoi(MemoryModuleType.MEETING_POINT); } private void tellWitnessesThatIWasMurdered(Entity murderer) { Level level = this.level(); if (!(level instanceof ServerLevel)) { return; } ServerLevel serverLevel = (ServerLevel)level; Optional witnesses = this.brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES); if (witnesses.isEmpty()) { return; } witnesses.get().findAll(ReputationEventHandler.class::isInstance).forEach(witness -> serverLevel.onReputationEvent(ReputationEventType.VILLAGER_KILLED, murderer, (ReputationEventHandler)((Object)witness))); } public void releasePoi(MemoryModuleType memoryType) { if (!(this.level() instanceof ServerLevel)) { return; } MinecraftServer server = ((ServerLevel)this.level()).getServer(); this.brain.getMemory(memoryType).ifPresent(memory -> { ServerLevel poiLevel = server.getLevel(memory.dimension()); if (poiLevel == null) { return; } PoiManager poiManager = poiLevel.getPoiManager(); Optional> type = poiManager.getType(memory.pos()); BiPredicate> poiTypePredicate = POI_MEMORIES.get(memoryType); if (type.isPresent() && poiTypePredicate.test(this, type.get())) { poiManager.release(memory.pos()); poiLevel.debugSynchronizers().updatePoi(memory.pos()); } }); } @Override public boolean canBreed() { return this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; } private boolean hungry() { return this.foodLevel < 12; } private void eatUntilFull() { if (!this.hungry() || this.countFoodPointsInInventory() == 0) { return; } for (int slot = 0; slot < this.getInventory().getContainerSize(); ++slot) { int itemCount; Integer value; ItemStack itemStack = this.getInventory().getItem(slot); if (itemStack.isEmpty() || (value = FOOD_POINTS.get(itemStack.getItem())) == null) continue; for (int count = itemCount = itemStack.getCount(); count > 0; --count) { this.foodLevel += value.intValue(); this.getInventory().removeItem(slot, 1); if (this.hungry()) continue; return; } } } public int getPlayerReputation(Player player) { return this.gossips.getReputation(player.getUUID(), t -> true); } private void digestFood(int amount) { this.foodLevel -= amount; } public void eatAndDigestFood() { this.eatUntilFull(); this.digestFood(12); } public void setOffers(MerchantOffers offers) { this.offers = offers; } private boolean shouldIncreaseLevel() { int currentLevel = this.getVillagerData().level(); return VillagerData.canLevelUp(currentLevel) && this.villagerXp >= VillagerData.getMaxXpPerLevel(currentLevel); } private void increaseMerchantCareer(ServerLevel level) { this.setVillagerData(this.getVillagerData().withLevel(this.getVillagerData().level() + 1)); this.updateTrades(level); } @Override protected Component getTypeName() { return this.getVillagerData().profession().value().name(); } @Override public void handleEntityEvent(byte id) { if (id == 12) { this.addParticlesAroundSelf(ParticleTypes.HEART); } else if (id == 13) { this.addParticlesAroundSelf(ParticleTypes.ANGRY_VILLAGER); } else if (id == 14) { this.addParticlesAroundSelf(ParticleTypes.HAPPY_VILLAGER); } else if (id == 42) { this.addParticlesAroundSelf(ParticleTypes.SPLASH); } else { super.handleEntityEvent(id); } } @Override public @Nullable SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData groupData) { if (spawnReason == EntitySpawnReason.BREEDING) { this.setVillagerData(this.getVillagerData().withProfession(level.registryAccess(), VillagerProfession.NONE)); } if (spawnReason == EntitySpawnReason.COMMAND || spawnReason == EntitySpawnReason.SPAWN_ITEM_USE || EntitySpawnReason.isSpawner(spawnReason) || spawnReason == EntitySpawnReason.DISPENSER) { this.setVillagerData(this.getVillagerData().withType(level.registryAccess(), VillagerType.byBiome(level.getBiome(this.blockPosition())))); } if (spawnReason == EntitySpawnReason.STRUCTURE) { this.assignProfessionWhenSpawned = true; } return super.finalizeSpawn(level, difficulty, spawnReason, groupData); } @Override public @Nullable Villager getBreedOffspring(ServerLevel level, AgeableMob partner) { double random = this.random.nextDouble(); Holder type = random < 0.5 ? level.registryAccess().getOrThrow(VillagerType.byBiome(level.getBiome(this.blockPosition()))) : (random < 0.75 ? this.getVillagerData().type() : ((Villager)partner).getVillagerData().type()); Villager villager = new Villager(EntityType.VILLAGER, (Level)level, type); villager.finalizeSpawn(level, level.getCurrentDifficultyAt(villager.blockPosition()), EntitySpawnReason.BREEDING, null); return villager; } @Override public void thunderHit(ServerLevel level, LightningBolt lightningBolt) { if (level.getDifficulty() != Difficulty.PEACEFUL) { LOGGER.info("Villager {} was struck by lightning {}.", (Object)this, (Object)lightningBolt); Witch witch = this.convertTo(EntityType.WITCH, ConversionParams.single(this, false, false), w -> { w.finalizeSpawn(level, level.getCurrentDifficultyAt(w.blockPosition()), EntitySpawnReason.CONVERSION, null); w.setPersistenceRequired(); this.releaseAllPois(); }); if (witch == null) { super.thunderHit(level, lightningBolt); } } else { super.thunderHit(level, lightningBolt); } } @Override protected void pickUpItem(ServerLevel level, ItemEntity entity) { InventoryCarrier.pickUpItem(level, this, this, entity); } @Override public boolean wantsToPickUp(ServerLevel level, ItemStack itemStack) { Item item = itemStack.getItem(); return (itemStack.is(ItemTags.VILLAGER_PICKS_UP) || this.getVillagerData().profession().value().requestedItems().contains((Object)item)) && this.getInventory().canAddItem(itemStack); } public boolean hasExcessFood() { return this.countFoodPointsInInventory() >= 24; } public boolean wantsMoreFood() { return this.countFoodPointsInInventory() < 12; } private int countFoodPointsInInventory() { SimpleContainer inventory = this.getInventory(); return FOOD_POINTS.entrySet().stream().mapToInt(entry -> inventory.countItem((Item)entry.getKey()) * (Integer)entry.getValue()).sum(); } public boolean hasFarmSeeds() { return this.getInventory().hasAnyMatching(item -> item.is(ItemTags.VILLAGER_PLANTABLE_SEEDS)); } @Override protected void updateTrades(ServerLevel level) { Int2ObjectMap experimentalTrades; VillagerData data = this.getVillagerData(); ResourceKey profession = data.profession().unwrapKey().orElse(null); if (profession == null) { return; } Int2ObjectMap tradesByLevel = this.level().enabledFeatures().contains(FeatureFlags.TRADE_REBALANCE) ? ((experimentalTrades = VillagerTrades.EXPERIMENTAL_TRADES.get(profession)) != null ? experimentalTrades : VillagerTrades.TRADES.get(profession)) : VillagerTrades.TRADES.get(profession); if (tradesByLevel == null || tradesByLevel.isEmpty()) { return; } VillagerTrades.ItemListing[] itemListings = (VillagerTrades.ItemListing[])tradesByLevel.get(data.level()); if (itemListings == null) { return; } MerchantOffers offers = this.getOffers(); this.addOffersFromItemListings(level, offers, itemListings, 2); if (SharedConstants.DEBUG_UNLOCK_ALL_TRADES && data.level() < tradesByLevel.size()) { this.increaseMerchantCareer(level); } } public void gossip(ServerLevel level, Villager target, long timestamp) { if (timestamp >= this.lastGossipTime && timestamp < this.lastGossipTime + 1200L || timestamp >= target.lastGossipTime && timestamp < target.lastGossipTime + 1200L) { return; } this.gossips.transferFrom(target.gossips, this.random, 10); this.lastGossipTime = timestamp; target.lastGossipTime = timestamp; this.spawnGolemIfNeeded(level, timestamp, 5); } private void maybeDecayGossip() { long timestamp = this.level().getGameTime(); if (this.lastGossipDecayTime == 0L) { this.lastGossipDecayTime = timestamp; return; } if (timestamp < this.lastGossipDecayTime + 24000L) { return; } this.gossips.decay(); this.lastGossipDecayTime = timestamp; } public void spawnGolemIfNeeded(ServerLevel level, long timestamp, int villagersNeededToAgree) { if (!this.wantsToSpawnGolem(timestamp)) { return; } AABB villagerSearchBox = this.getBoundingBox().inflate(10.0, 10.0, 10.0); List nearbyVillagers = level.getEntitiesOfClass(Villager.class, villagerSearchBox); List nearbyVillagersThatWantAGolem = nearbyVillagers.stream().filter(villager -> villager.wantsToSpawnGolem(timestamp)).limit(5L).toList(); if (nearbyVillagersThatWantAGolem.size() < villagersNeededToAgree) { return; } if (SpawnUtil.trySpawnMob(EntityType.IRON_GOLEM, EntitySpawnReason.MOB_SUMMONED, level, this.blockPosition(), 10, 8, 6, SpawnUtil.Strategy.LEGACY_IRON_GOLEM, false).isEmpty()) { return; } nearbyVillagers.forEach(GolemSensor::golemDetected); } public boolean wantsToSpawnGolem(long timestamp) { if (!this.golemSpawnConditionsMet(this.level().getGameTime())) { return false; } return !this.brain.hasMemoryValue(MemoryModuleType.GOLEM_DETECTED_RECENTLY); } @Override public void onReputationEventFrom(ReputationEventType type, Entity source) { if (type == ReputationEventType.ZOMBIE_VILLAGER_CURED) { this.gossips.add(source.getUUID(), GossipType.MAJOR_POSITIVE, 20); this.gossips.add(source.getUUID(), GossipType.MINOR_POSITIVE, 25); } else if (type == ReputationEventType.TRADE) { this.gossips.add(source.getUUID(), GossipType.TRADING, 2); } else if (type == ReputationEventType.VILLAGER_HURT) { this.gossips.add(source.getUUID(), GossipType.MINOR_NEGATIVE, 25); } else if (type == ReputationEventType.VILLAGER_KILLED) { this.gossips.add(source.getUUID(), GossipType.MAJOR_NEGATIVE, 25); } } @Override public int getVillagerXp() { return this.villagerXp; } public void setVillagerXp(int value) { this.villagerXp = value; } private void resetNumberOfRestocks() { this.catchUpDemand(); this.numberOfRestocksToday = 0; } public GossipContainer getGossips() { return this.gossips; } public void setGossips(GossipContainer gossips) { this.gossips.putAll(gossips); } @Override public void startSleeping(BlockPos bedPosition) { super.startSleeping(bedPosition); this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level().getGameTime()); this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); this.brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); } @Override public void stopSleeping() { super.stopSleeping(); this.brain.setMemory(MemoryModuleType.LAST_WOKEN, this.level().getGameTime()); } private boolean golemSpawnConditionsMet(long gameTime) { Optional sleepMemory = this.brain.getMemory(MemoryModuleType.LAST_SLEPT); return sleepMemory.filter(aLong -> gameTime - aLong < 24000L).isPresent(); } @Override public @Nullable T get(DataComponentType type) { if (type == DataComponents.VILLAGER_VARIANT) { return Villager.castComponentValue(type, this.getVillagerData().type()); } return super.get(type); } @Override protected void applyImplicitComponents(DataComponentGetter components) { this.applyImplicitComponentIfPresent(components, DataComponents.VILLAGER_VARIANT); super.applyImplicitComponents(components); } @Override protected boolean applyImplicitComponent(DataComponentType type, T value) { if (type == DataComponents.VILLAGER_VARIANT) { Holder variant = Villager.castComponentValue(DataComponents.VILLAGER_VARIANT, value); this.setVillagerData(this.getVillagerData().withType(variant)); return true; } return super.applyImplicitComponent(type, value); } }