From 6c581418c78b61cc29af1fe7ed341e9f3a5e8216 Mon Sep 17 00:00:00 2001 From: MehVahdJukaar Date: Wed, 27 Dec 2023 18:09:40 +0200 Subject: [PATCH] Trough changes: - no longer ticks - cooldown is per animals - animals eat immediately when they reach one - added give up cooldown so they dont get stuck - improved fake player positioning - fixed animals still targeting trough when it gets empties after they start targeting it - animals will get much closer to the through, done by adjusting the fake player position while they walk --- .../automation/block/FeedingTroughBlock.java | 13 +- .../block/be/FeedingTroughBlockEntity.java | 119 +++--- .../module/FeedingTroughModule.java | 372 +++++++++++------- 3 files changed, 287 insertions(+), 217 deletions(-) diff --git a/src/main/java/org/violetmoon/quark/content/automation/block/FeedingTroughBlock.java b/src/main/java/org/violetmoon/quark/content/automation/block/FeedingTroughBlock.java index b07868e640..abefd09332 100644 --- a/src/main/java/org/violetmoon/quark/content/automation/block/FeedingTroughBlock.java +++ b/src/main/java/org/violetmoon/quark/content/automation/block/FeedingTroughBlock.java @@ -44,7 +44,7 @@ public class FeedingTroughBlock extends ZetaBlock implements EntityBlock { private static final SoundType WOOD_WITH_PLANT_STEP = new ForgeSoundType(1.0F, 1.0F, () -> SoundEvents.WOOD_BREAK, () -> SoundEvents.GRASS_STEP, () -> SoundEvents.WOOD_PLACE, () -> SoundEvents.WOOD_HIT, () -> SoundEvents.WOOD_FALL); - public static BooleanProperty FULL = BooleanProperty.create("full"); + public static final BooleanProperty FULL = BooleanProperty.create("full"); public static final VoxelShape CUBOID_SHAPE = box(0, 0, 0, 16, 8, 16); public static final VoxelShape EMPTY_SHAPE = Shapes.join(CUBOID_SHAPE, box(2, 2, 2, 14, 8, 14), BooleanOp.ONLY_FIRST); @@ -106,8 +106,8 @@ public void fallOn(Level level, @NotNull BlockState state, @NotNull BlockPos pos public void onRemove(BlockState state, @NotNull Level world, @NotNull BlockPos pos, @NotNull BlockState newState, boolean isMoving) { if(state.getBlock() != newState.getBlock()) { BlockEntity tile = world.getBlockEntity(pos); - if(tile instanceof FeedingTroughBlockEntity) { - Containers.dropContents(world, pos, (FeedingTroughBlockEntity) tile); + if(tile instanceof FeedingTroughBlockEntity f) { + Containers.dropContents(world, pos, f); world.updateNeighbourForOutputSignal(pos, this); } @@ -150,7 +150,7 @@ public boolean triggerEvent(@NotNull BlockState state, @NotNull Level world, @No @Nullable public MenuProvider getMenuProvider(@NotNull BlockState state, Level world, @NotNull BlockPos pos) { BlockEntity tile = world.getBlockEntity(pos); - return tile instanceof MenuProvider ? (MenuProvider) tile : null; + return tile instanceof MenuProvider m ? m : null; } @Override @@ -158,9 +158,4 @@ public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState sta return new FeedingTroughBlockEntity(pos, state); } - @Override - public BlockEntityTicker getTicker(@NotNull Level world, @NotNull BlockState state, @NotNull BlockEntityType type) { - return createTickerHelper(type, FeedingTroughModule.blockEntityType, FeedingTroughBlockEntity::tick); - } - } diff --git a/src/main/java/org/violetmoon/quark/content/automation/block/be/FeedingTroughBlockEntity.java b/src/main/java/org/violetmoon/quark/content/automation/block/be/FeedingTroughBlockEntity.java index 4b51d2a9bb..34befe9a67 100644 --- a/src/main/java/org/violetmoon/quark/content/automation/block/be/FeedingTroughBlockEntity.java +++ b/src/main/java/org/violetmoon/quark/content/automation/block/be/FeedingTroughBlockEntity.java @@ -12,14 +12,14 @@ import net.minecraft.sounds.SoundEvent; import net.minecraft.world.ContainerHelper; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.ai.goal.TemptGoal; import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.DispenserMenu; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; @@ -30,6 +30,7 @@ import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.violetmoon.quark.base.handler.MiscUtil; import org.violetmoon.quark.content.automation.block.FeedingTroughBlock; import org.violetmoon.quark.content.automation.module.FeedingTroughModule; @@ -44,11 +45,8 @@ */ public class FeedingTroughBlockEntity extends RandomizableContainerBlockEntity { - private static final GameProfile DUMMY_PROFILE = new GameProfile(UUID.randomUUID(), "[FeedingTrough]"); - private NonNullList stacks; - private int cooldown = 0; private long internalRng = 0; public FeedingTroughBlockEntity(BlockPos pos, BlockState state) { @@ -56,72 +54,61 @@ public FeedingTroughBlockEntity(BlockPos pos, BlockState state) { this.stacks = NonNullList.withSize(9, ItemStack.EMPTY); } - public FakePlayer getFoodHolder(Animal mob, Ingredient temptations) { - FakePlayer foodHolder = null; - if(level instanceof ServerLevel serverLevel) - foodHolder = FakePlayerFactory.get(serverLevel, DUMMY_PROFILE); - - if(foodHolder != null) { - for(int i = 0; i < getContainerSize(); i++) { - ItemStack stack = getItem(i); - if(temptations.test(stack) && mob.isFood(stack)) { - Inventory inventory = foodHolder.getInventory(); - inventory.items.set(inventory.selected, stack); - Vec3 position = new Vec3(worldPosition.getX(), worldPosition.getY(), worldPosition.getZ()).add(0.5, -1, 0.5); - Vec3 direction = mob.position().subtract(position).normalize(); - Vec2 angles = MiscUtil.getMinecraftAngles(direction); - - Vec3 shift = direction.scale(-0.5 / Math.max( - Math.abs(direction.x), Math.max( - Math.abs(direction.y), - Math.abs(direction.z)))); - - Vec3 truePos = position.add(shift); - - foodHolder.moveTo(truePos.x, truePos.y, truePos.z, angles.x, angles.y); - return foodHolder; + public void updateFoodHolder(Animal mob, Ingredient temptations, FakePlayer foodHolder) { + for(int i = 0; i < getContainerSize(); i++) { + ItemStack stack = getItem(i); + if(temptations.test(stack) && mob.isFood(stack)) { + Inventory inventory = foodHolder.getInventory(); + inventory.items.set(inventory.selected, stack); + Vec3 througPos = new Vec3(worldPosition.getX(), worldPosition.getY(), worldPosition.getZ()) + .add(0.5, -1, 0.5); + Vec3 mobPosition = mob.position(); + Vec3 direction = mobPosition.subtract(througPos); + Vec2 angles = MiscUtil.getMinecraftAngles(direction); + + Vec3 newPos = Vec3.ZERO; + // Fake player will always be at most maxDist blocks away from animal. + // If animal is closer to target, then we will be on target itself. + float maxDist = 3; + if(direction.lengthSqr() > (maxDist*maxDist)){ + newPos = mobPosition.add(direction.normalize().scale(-maxDist)); + }else{ + //place slightly behind trough + newPos = througPos.add(direction.normalize().scale(-1)); } + + foodHolder.moveTo(newPos.x, newPos.y, newPos.z, angles.x, angles.y); + return; } } - - return null; } - public static void tick(Level level, BlockPos pos, BlockState state, FeedingTroughBlockEntity be) { - if(level != null && !level.isClientSide) { - if(be.cooldown > 0) - be.cooldown--; - else { - be.cooldown = FeedingTroughModule.cooldown; // minimize aabb calls - List animals = level.getEntitiesOfClass(Animal.class, new AABB(be.worldPosition).inflate(1.5, 0, 1.5).contract(0, 0.75, 0)); - - for(Animal creature : animals) { - if(creature.canFallInLove() && creature.getAge() == 0) { - for(int i = 0; i < be.getContainerSize(); i++) { - ItemStack stack = be.getItem(i); - if(creature.isFood(stack)) { - SoundEvent soundEvent = creature.getEatingSound(stack); - if(soundEvent != null) - creature.playSound(soundEvent, 0.5F + 0.5F * level.random.nextInt(2), (level.random.nextFloat() - level.random.nextFloat()) * 0.2F + 1.0F); - - be.addItemParticles(creature, stack, 16); - - if(be.getSpecialRand().nextDouble() < FeedingTroughModule.loveChance) { - List animalsAround = level.getEntitiesOfClass(Animal.class, new AABB(be.worldPosition).inflate(FeedingTroughModule.range)); - if(animalsAround.size() <= FeedingTroughModule.maxAnimals) - creature.setInLove(null); - } - - stack.shrink(1); - be.setChanged(); - - return; - } - } - } + public enum FeedResult{ + FED,SECS,NONE + } + public FeedResult tryFeedingAnimal(Animal animal) { + for(int i = 0; i < this.getContainerSize(); i++) { + ItemStack stack = this.getItem(i); + if(animal.isFood(stack)) { + SoundEvent soundEvent = animal.getEatingSound(stack); + animal.playSound(soundEvent, 0.5F + 0.5F * level.random.nextInt(2), (level.random.nextFloat() - level.random.nextFloat()) * 0.2F + 1.0F); + + this.addItemParticles(animal, stack, 16); + + stack.shrink(1); + this.setChanged(); + + if(this.getSpecialRand().nextDouble() < FeedingTroughModule.loveChance) { + List animalsAround = level.getEntitiesOfClass(Animal.class, new AABB(this.worldPosition).inflate(FeedingTroughModule.range)); + if(animalsAround.size() <= FeedingTroughModule.maxAnimals) + animal.setInLove(null); + return FeedResult.SECS; } + + return FeedResult.FED; } } + return FeedResult.NONE; } @Override @@ -148,8 +135,8 @@ private void addItemParticles(Entity entity, ItemStack stack, int count) { position = position.xRot(-entity.getXRot() * ((float) Math.PI / 180F)); position = position.yRot(-entity.getYRot() * ((float) Math.PI / 180F)); position = position.add(entityPos.x, entityPos.y + entity.getEyeHeight(), entityPos.z); - if(this.level instanceof ServerLevel) - ((ServerLevel) this.level).sendParticles(new ItemParticleOption(ParticleTypes.ITEM, stack), position.x, position.y, position.z, 1, direction.x, direction.y + 0.05D, direction.z, 0.0D); + if(this.level instanceof ServerLevel serverLevel) + serverLevel.sendParticles(new ItemParticleOption(ParticleTypes.ITEM, stack), position.x, position.y, position.z, 1, direction.x, direction.y + 0.05D, direction.z, 0.0D); else if(this.level != null) this.level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, stack), position.x, position.y, position.z, direction.x, direction.y + 0.05D, direction.z); } @@ -187,7 +174,6 @@ protected Component getDefaultName() { public void load(@NotNull CompoundTag nbt) { super.load(nbt); - this.cooldown = nbt.getInt("Cooldown"); this.internalRng = nbt.getLong("rng"); this.stacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); if(!this.tryLoadLootTable(nbt)) @@ -199,7 +185,6 @@ public void load(@NotNull CompoundTag nbt) { protected void saveAdditional(@NotNull CompoundTag nbt) { super.saveAdditional(nbt); - nbt.putInt("Cooldown", cooldown); nbt.putLong("rng", internalRng); if(!this.trySaveLootTable(nbt)) ContainerHelper.saveAllItems(nbt, this.stacks); diff --git a/src/main/java/org/violetmoon/quark/content/automation/module/FeedingTroughModule.java b/src/main/java/org/violetmoon/quark/content/automation/module/FeedingTroughModule.java index 5114bbb1f2..49f2fbcb2a 100644 --- a/src/main/java/org/violetmoon/quark/content/automation/module/FeedingTroughModule.java +++ b/src/main/java/org/violetmoon/quark/content/automation/module/FeedingTroughModule.java @@ -1,9 +1,7 @@ package org.violetmoon.quark.content.automation.module; -import java.util.Optional; -import java.util.WeakHashMap; - import com.google.common.collect.ImmutableSet; +import com.mojang.authlib.GameProfile; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; @@ -19,7 +17,6 @@ import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.GameRules; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.entity.BlockEntityType; @@ -28,7 +25,7 @@ import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.util.FakePlayer; -import org.jetbrains.annotations.NotNull; +import net.minecraftforge.common.util.FakePlayerFactory; import org.jetbrains.annotations.Nullable; import org.violetmoon.quark.base.Quark; import org.violetmoon.quark.base.config.Config; @@ -44,146 +41,239 @@ import org.violetmoon.zeta.module.ZetaModule; import org.violetmoon.zeta.util.Hint; +import java.util.Objects; +import java.util.UUID; +import java.util.WeakHashMap; + /** * @author WireSegal - * Created at 9:48 AM on 9/20/19. + * Created at 9:48 AM on 9/20/19. */ @ZetaLoadModule(category = "automation") public class FeedingTroughModule extends ZetaModule { - //using a ResourceKey because they're interned, and Holder.Reference#is leverages this for a very efficient implementation - private static final ResourceKey FEEDING_TROUGH_POI_KEY = ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, Quark.asResource("feeding_trough")); - - public static BlockEntityType blockEntityType; - @Hint - Block feeding_trough; - - private static final String TAG_CACHE = "quark:feedingTroughCache"; - - @Config(description = "How long, in game ticks, between animals being able to eat from the trough") - @Config.Min(1) - public static int cooldown = 30; - - @Config(description = "The maximum amount of animals allowed around the trough's range for an animal to enter love mode") - public static int maxAnimals = 32; - - @Config(description = "The chance (between 0 and 1) for an animal to enter love mode when eating from the trough") - @Config.Min(value = 0.0, exclusive = true) - @Config.Max(1.0) - public static double loveChance = 0.333333333; - - @Config - public static double range = 10; - - @Config(description = "Chance that an animal decides to look for a through. Closer it is to 1 the more performance it will take. Decreasing will make animals take longer to find one") - public static double lookChance = 0.005; - - private static final WeakHashMap NEARBY_TROUGH_CACHE = new WeakHashMap<>(); - - private static final ThreadLocal breedingOccurred = ThreadLocal.withInitial(() -> false); - - @PlayEvent - public void onBreed(ZBabyEntitySpawn.Lowest event) { - if(event.getCausedByPlayer() == null && event.getParentA().level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) - breedingOccurred.set(true); - } - - @PlayEvent - public void onOrbSpawn(ZEntityJoinLevel event) { - if(event.getEntity() instanceof ExperienceOrb && breedingOccurred.get()) { - event.setCanceled(true); - breedingOccurred.remove(); - } - } - - // Both TempingSensor and TemptGoal work by keeping track of a nearby player who is holding food. - // The Feeding Trough causes mobs to pathfind to it by injecting a fakeplayer into these AI goals, who stands at the - // location of the Trough and holds food they like. - - // The "realPlayer" parameter represents a real player located by existing TemptingSensor/TemptGoal code. - // If there is a real player, and they are holding food, we don't swap them for a fakeplayer, so that animals path to - // real players before they consider pathing to the Trough. - - public static @Nullable Player modifyTemptingSensor(@Nullable Player realPlayer, TemptingSensor sensor, Animal animal, ServerLevel level) { - return modifyTempt(realPlayer, level, animal, ((AccessorTemptingSensor) sensor).quark$getTemptations()); - } - - public static @Nullable Player modifyTemptGoal(@Nullable Player realPlayer, TemptGoal goal, Animal animal, ServerLevel level) { - return modifyTempt(realPlayer, level, animal, goal.items); - } - - private static @Nullable Player modifyTempt(@Nullable Player realPlayer, ServerLevel level, Animal animal, Ingredient temptations) { - //early-exit conditions - if(!Quark.ZETA.modules.isEnabled(FeedingTroughModule.class) || - !animal.canFallInLove() || - animal.getAge() != 0 - ) { - return realPlayer; - } - - //deference to real players - if(realPlayer != null && (temptations.test(realPlayer.getMainHandItem()) || temptations.test(realPlayer.getOffhandItem()))) - return realPlayer; - - //do we already know about a nearby trough? - TroughPointer pointer = NEARBY_TROUGH_CACHE.get(animal); - if(pointer != null && !pointer.valid(animal)) { //invalid cache - pointer = null; - NEARBY_TROUGH_CACHE.remove(animal); - } - - //there's no cached trough nearby. - //Randomize whether we actually look for a new trough, to hopefully not eat all the tick time. - if(pointer == null && level.random.nextFloat() <= lookChance) { - BlockPos position = animal.blockPosition(); - pointer = level.getPoiManager().findClosest( - holder -> holder.is(FEEDING_TROUGH_POI_KEY), p -> p.distSqr(position) <= range * range, - position, (int) range, PoiManager.Occupancy.ANY) - .flatMap(p -> TroughPointer.find(level, p, animal, temptations)) - .orElse(null); - NEARBY_TROUGH_CACHE.put(animal, pointer); - } - - //did we find one? - if(pointer != null) { - //if the animal can see it, direct the animal to this trough's fakeplayer - BlockPos location = pointer.pos(); - Vec3 eyesPos = animal.position().add(0, animal.getEyeHeight(), 0); - Vec3 targetPos = new Vec3(location.getX(), location.getY(), location.getZ()).add(0.5, 0.0625, 0.5); - BlockHitResult ray = level.clip(new ClipContext(eyesPos, targetPos, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, animal)); - if(ray.getType() == HitResult.Type.BLOCK && ray.getBlockPos().equals(location)) - return pointer.player(); - } - - return realPlayer; - } - - @LoadEvent - public final void register(ZRegister event) { - feeding_trough = new FeedingTroughBlock("feeding_trough", this, - Block.Properties.of().mapColor(MapColor.WOOD).ignitedByLava().strength(0.6F).sound(SoundType.WOOD)); - - blockEntityType = BlockEntityType.Builder.of(FeedingTroughBlockEntity::new, feeding_trough).build(null); - event.getRegistry().register(blockEntityType, "feeding_trough", Registries.BLOCK_ENTITY_TYPE); - - PoiType feedingTroughPoi = new PoiType(ImmutableSet.copyOf(feeding_trough.getStateDefinition().getPossibleStates()), 1, 32); - event.getRegistry().register(feedingTroughPoi, FEEDING_TROUGH_POI_KEY.location(), Registries.POINT_OF_INTEREST_TYPE); - } - - private record TroughPointer(@NotNull BlockPos pos, @NotNull FakePlayer player) { - - static Optional find(Level level, BlockPos pos, Animal mob, Ingredient temptations) { - if(level.getBlockEntity(pos) instanceof FeedingTroughBlockEntity trough) { - FakePlayer fakeplayer = trough.getFoodHolder(mob, temptations); - if(fakeplayer != null) - return Optional.of(new TroughPointer(pos, fakeplayer)); - } - return Optional.empty(); - } - - boolean valid(Animal animal) { - return !player.isRemoved() && player.level() == animal.level() && pos.distSqr(animal.blockPosition()) <= range * range; - } - - } + //using a ResourceKey because they're interned, and Holder.Reference#is leverages this for a very efficient implementation + private static final ResourceKey FEEDING_TROUGH_POI_KEY = ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, Quark.asResource("feeding_trough")); + private static final GameProfile DUMMY_PROFILE = new GameProfile(UUID.randomUUID(), "[FeedingTrough]"); + + public static BlockEntityType blockEntityType; + @Hint + Block feeding_trough; + + @Config(description = "How long, in game ticks, between animals being able to eat from the trough") + @Config.Min(1) + public static int cooldown = 30; + + @Config(description = "The maximum amount of animals allowed around the trough's range for an animal to enter love mode") + public static int maxAnimals = 32; + + @Config(description = "The chance (between 0 and 1) for an animal to enter love mode when eating from the trough") + @Config.Min(value = 0.0, exclusive = true) + @Config.Max(1.0) + public static double loveChance = 0.333333333; + + @Config + public static double range = 10; + + @Config(description = "Chance that an animal decides to look for a through. Closer it is to 1 the more performance it will take. Decreasing will make animals take longer to find one") + public static double lookChance = 0.005; + + private static final WeakHashMap NEARBY_TROUGH_CACHE = new WeakHashMap<>(); + + private static final ThreadLocal breedingOccurred = ThreadLocal.withInitial(() -> false); + + @PlayEvent + public void onBreed(ZBabyEntitySpawn.Lowest event) { + if (event.getCausedByPlayer() == null && event.getParentA().level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) + breedingOccurred.set(true); + } + + @PlayEvent + public void onOrbSpawn(ZEntityJoinLevel event) { + if (event.getEntity() instanceof ExperienceOrb && breedingOccurred.get()) { + event.setCanceled(true); + breedingOccurred.remove(); + } + } + + // Both TempingSensor and TemptGoal work by keeping track of a nearby player who is holding food. + // The Feeding Trough causes mobs to pathfind to it by injecting a fakeplayer into these AI goals, who stands at the + // location of the Trough and holds food they like. + + // The "realPlayer" parameter represents a real player located by existing TemptingSensor/TemptGoal code. + // If there is a real player, and they are holding food, we don't swap them for a fakeplayer, so that animals path to + // real players before they consider pathing to the Trough. + + public static @Nullable Player modifyTemptingSensor(@Nullable Player realPlayer, TemptingSensor sensor, Animal animal, ServerLevel level) { + return modifyTempt(realPlayer, level, animal, ((AccessorTemptingSensor) sensor).quark$getTemptations()); + } + + public static @Nullable Player modifyTemptGoal(@Nullable Player realPlayer, TemptGoal goal, Animal animal, ServerLevel level) { + return modifyTempt(realPlayer, level, animal, goal.items); + } + + private static @Nullable Player modifyTempt(@Nullable Player realPlayer, ServerLevel level, Animal animal, Ingredient temptations) { + //early-exit conditions + if (!Quark.ZETA.modules.isEnabled(FeedingTroughModule.class) || + !animal.canFallInLove() || + animal.getAge() != 0 + ) { + return realPlayer; + } + + //deference to real players + //TODO: this logic should not be here, should just be in vanilla code instead + if (realPlayer != null && (temptations.test(realPlayer.getMainHandItem()) || temptations.test(realPlayer.getOffhandItem()))) + return realPlayer; + + //do we already know about a nearby trough? + NEARBY_TROUGH_CACHE.entrySet().removeIf(p -> !p.getValue().valid(p.getKey())); + + TroughPointer pointer = NEARBY_TROUGH_CACHE.get(animal); + + //there's no cached trough nearby. + //Randomize whether we actually look for a new trough, to hopefully not eat all the tick time. + if (pointer == null && level.random.nextFloat() <= lookChance) { + pointer = TroughPointer.find(level, animal, temptations); + if (pointer != null){ + NEARBY_TROUGH_CACHE.put(animal, pointer); + } + } + + //did we find one? + if (pointer != null) { + pointer.tryEatingOrTickCooldown(animal); + + if (!pointer.isOnCooldown()) { + + //if the animal can see it, direct the animal to this trough's fakeplayer + BlockPos location = pointer.pos; + Vec3 eyesPos = animal.position().add(0, animal.getEyeHeight(), 0); + Vec3 targetPos = new Vec3(location.getX(), location.getY(), location.getZ()).add(0.5, 0.0625, 0.5); + BlockHitResult ray = level.clip(new ClipContext(eyesPos, targetPos, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, animal)); + if (ray.getType() == HitResult.Type.BLOCK && ray.getBlockPos().equals(location)) { + return pointer.foodHolder; + } else { + int error = 0; + } + } + } + + return realPlayer; + } + + + @LoadEvent + public final void register(ZRegister event) { + feeding_trough = new FeedingTroughBlock("feeding_trough", this, + Block.Properties.of().mapColor(MapColor.WOOD).ignitedByLava().strength(0.6F).sound(SoundType.WOOD)); + + blockEntityType = BlockEntityType.Builder.of(FeedingTroughBlockEntity::new, feeding_trough).build(null); + event.getRegistry().register(blockEntityType, "feeding_trough", Registries.BLOCK_ENTITY_TYPE); + + PoiType feedingTroughPoi = new PoiType(ImmutableSet.copyOf(feeding_trough.getStateDefinition().getPossibleStates()), 1, 32); + event.getRegistry().register(feedingTroughPoi, FEEDING_TROUGH_POI_KEY.location(), Registries.POINT_OF_INTEREST_TYPE); + } + + private static final class TroughPointer { + private final BlockPos pos; + private final FakePlayer foodHolder; + private final Ingredient temptations; + private int eatCooldown = 0; //Ideally cooldown should be per entity... Assuming troughs don't change much this is fine + private int giveUpCooldown = 20 * 20; //max seconds till we give up + + private TroughPointer(BlockPos pos, FakePlayer player, Ingredient temptations) { + this.pos = pos; + this.foodHolder = player; + this.temptations = temptations; + } + + // This is a bit ugly. 0 = new pointer, 1 = end of life, other = ticking cooldown + // Once a through is found and an animal is fed, its considered valid until cooldown runs out. + // Then its invalidated so animals can find possibly closer ones + boolean valid(Animal animal) { + if (eatCooldown == 1){ + return false; + } + if (giveUpCooldown <= 0){ + return false; + } + if (eatCooldown != 0) return true; + if (foodHolder.isRemoved() || foodHolder.level() != animal.level() || pos.distSqr(animal.blockPosition()) > range * range) { + return false; + } + //check if it has food and tile is valid + if(animal.level().getBlockEntity(pos) instanceof FeedingTroughBlockEntity trough && !foodHolder.getMainHandItem().isEmpty()){ + //this should be called in tick but we save one tile call by doing this... + trough.updateFoodHolder(animal, temptations, foodHolder); + //if it still has food + return !foodHolder.getMainHandItem().isEmpty(); + } + return false; + } + + void tryEatingOrTickCooldown(Animal animal) { + giveUpCooldown--; + if (eatCooldown == 0) { + //I wish this could be made smaller. Vanilla AI will not keep animals too close to player holding food + float feedDistance = 1.25f; + if (pos.distToCenterSqr(animal.position()) < (feedDistance * feedDistance)) { + if (animal.level().getBlockEntity(pos) instanceof FeedingTroughBlockEntity trough) { + switch (trough.tryFeedingAnimal(animal)) { + case FED -> eatCooldown = cooldown; // just fed. set normal cooldown + case SECS -> eatCooldown = 1; // remove immediately, as it will use animal own love cooldown for feeding again + } + } + } + } else eatCooldown--; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (TroughPointer) obj; + return Objects.equals(this.pos, that.pos) && + Objects.equals(this.foodHolder, that.foodHolder); + } + + @Override + public int hashCode() { + return Objects.hash(pos, foodHolder); + } + + // If animal cant eat. + // Pointer won't be erased + // until cooldown is 0. Maybe would have been better with entity nbt data like it was so it persisted + public boolean isOnCooldown() { + return eatCooldown != 0; + } + + + @Nullable + static TroughPointer find(ServerLevel level, Animal animal, Ingredient temptations) { + // this is an expensive part + BlockPos position = animal.getOnPos(); + var opt = level.getPoiManager().findClosest( + holder -> holder.is(FEEDING_TROUGH_POI_KEY), p -> p.distSqr(position) <= range * range, + position, (int) range, PoiManager.Occupancy.ANY); + if (opt.isPresent()) { + BlockPos pos = opt.get(); + + if (level.getBlockEntity(pos) instanceof FeedingTroughBlockEntity trough) { + //only returns if it has the right food + FakePlayer foodHolder = FakePlayerFactory.get(level, DUMMY_PROFILE); + if (foodHolder != null) { + trough.updateFoodHolder(animal, temptations, foodHolder); + // if it has a food item + if (!foodHolder.getMainHandItem().isEmpty()) { + return new TroughPointer(pos, foodHolder, temptations); + } + } + return null; + } + } + return null; + } + } }