diff --git a/leaf-server/minecraft-patches/features/0156-Async-Target-Finding.patch b/leaf-server/minecraft-patches/features/0156-Async-Target-Finding.patch deleted file mode 100644 index 9aaa7f88..00000000 --- a/leaf-server/minecraft-patches/features/0156-Async-Target-Finding.patch +++ /dev/null @@ -1,319 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Taiyou06 -Date: Sat, 29 Mar 2025 13:40:46 +0100 -Subject: [PATCH] Async Target Finding - - -diff --git a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java -index 41ee3cdc45ecc8376a2203ed588bb544ed377294..ed731177585051abbf129a48dfe4766265cf5617 100644 ---- a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java -+++ b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java -@@ -16,9 +16,37 @@ public class NearestAttackableTargetGoal extends TargetG - protected final Class targetType; - protected final int randomInterval; - @Nullable -- protected LivingEntity target; -+ protected volatile LivingEntity target; // Leaf - Async Target Finding - protected TargetingConditions targetConditions; - -+ // Leaf start - Async Target Finding -+ // Single thread executor to prevent overwhelming the server -+ private static final java.util.concurrent.ExecutorService TARGET_FINDER_EXECUTOR = java.util.concurrent.Executors.newSingleThreadExecutor(r -> { -+ Thread thread = new Thread(r, "Leaf - Target-Finder-Thread"); -+ thread.setDaemon(true); -+ thread.setPriority(Thread.MIN_PRIORITY); // Lower priority to avoid competing with main thread -+ return thread; -+ }); -+ -+ // Flag to track if a search is in progress -+ private final java.util.concurrent.atomic.AtomicBoolean isSearching = new java.util.concurrent.atomic.AtomicBoolean(false); -+ private final java.util.concurrent.atomic.AtomicReference pendingTarget = new java.util.concurrent.atomic.AtomicReference<>(null); -+ static { -+ Runtime.getRuntime().addShutdownHook(new Thread(() -> { -+ try { -+ TARGET_FINDER_EXECUTOR.shutdown(); -+ TARGET_FINDER_EXECUTOR.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS); -+ } catch (InterruptedException e) { -+ Thread.currentThread().interrupt(); -+ } finally { -+ if (!TARGET_FINDER_EXECUTOR.isTerminated()) { -+ TARGET_FINDER_EXECUTOR.shutdownNow(); -+ } -+ } -+ })); -+ } -+ // Leaf end - Async Target Finding -+ - public NearestAttackableTargetGoal(Mob mob, Class targetType, boolean mustSee) { - this(mob, targetType, 10, mustSee, false, null); - } -@@ -46,8 +74,14 @@ public class NearestAttackableTargetGoal extends TargetG - if (this.randomInterval > 0 && this.mob.getRandom().nextInt(this.randomInterval) != 0) { - return false; - } else { -- this.findTarget(); -- return this.target != null; -+ // Leaf start - Async Target Finding -+ findTarget(); -+ LivingEntity pending = pendingTarget.getAndSet(null); -+ if (pending != null && !pending.isRemoved() && pending.isAlive()) { -+ this.target = pending; -+ } -+ return this.target != null && this.target.isAlive() && !this.target.isRemoved(); -+ // Leaf end - Async Target Finding - } - } - -@@ -55,25 +89,239 @@ public class NearestAttackableTargetGoal extends TargetG - return this.mob.getBoundingBox().inflate(targetDistance, targetDistance, targetDistance); - } - -+ // Leaf start - Async Target Finding -+ // Async find target implementation with safer entity handling - protected void findTarget() { -- ServerLevel serverLevel = getServerLevel(this.mob); -- if (this.targetType != Player.class && this.targetType != ServerPlayer.class) { -- this.target = serverLevel.getNearestEntity( -- this.mob.level().getEntitiesOfClass(this.targetType, this.getTargetSearchArea(this.getFollowDistance()), entity -> true), -- this.getTargetConditions(), -- this.mob, -- this.mob.getX(), -- this.mob.getEyeY(), -- this.mob.getZ() -- ); -- } else { -- this.target = serverLevel.getNearestPlayer(this.getTargetConditions(), this.mob, this.mob.getX(), this.mob.getEyeY(), this.mob.getZ()); -+ // If async is disabled or we're already searching, use sync method -+ if (!org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled || !isSearching.compareAndSet(false, true)) { -+ findTargetSync(); -+ return; - } -+ -+ // Capture mutable state to avoid race conditions -+ final Mob mob = this.mob; -+ -+ // Safety check -+ if (mob == null || mob.isRemoved() || !mob.isAlive()) { -+ isSearching.set(false); -+ return; -+ } -+ -+ final double x = mob.getX(); -+ final double y = mob.getEyeY(); -+ final double z = mob.getZ(); -+ final double followDistance = this.getFollowDistance(); -+ final TargetingConditions targetConditions = this.getTargetConditions(); -+ final Class targetType = this.targetType; -+ -+ // Start async search with immutable captured state - using submit instead of runAsync -+ java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ try { -+ ServerLevel serverLevel = getServerLevel(mob); -+ if (serverLevel == null) { -+ return null; -+ } -+ if (mob.isRemoved() || !mob.isAlive()) { -+ return null; -+ } -+ -+ try { -+ if (targetType != Player.class && targetType != ServerPlayer.class) { -+ AABB searchArea = new AABB( -+ x - followDistance, y - followDistance, z - followDistance, -+ x + followDistance, y + followDistance, z + followDistance -+ ); -+ -+ java.util.List entities = null; -+ try { -+ entities = mob.level().getEntitiesOfClass(targetType, searchArea, entity -> true); -+ } catch (Exception e) { -+ System.err.println("Error getting entities: " + e.getMessage()); -+ return null; -+ } -+ -+ if (entities != null && !entities.isEmpty()) { -+ return findNearestEntitySafely(entities, targetConditions, mob, x, y, z, serverLevel); -+ } -+ } else { -+ return findNearestPlayerSafely(targetConditions, mob, x, y, z, serverLevel); -+ } -+ } catch (Exception e) { -+ System.err.println("Error finding entities in async target finder: " + e.getMessage()); -+ } -+ -+ return null; -+ } catch (Exception e) { -+ System.err.println("Error during async target finding: " + e.getMessage()); -+ return null; -+ } finally { -+ isSearching.set(false); -+ } -+ }, TARGET_FINDER_EXECUTOR).thenAccept(result -> { -+ if (result != null && result.isAlive() && !result.isRemoved()) { -+ pendingTarget.set(result); -+ } -+ }); - } - -+ @Nullable -+ private LivingEntity findNearestEntitySafely( -+ java.util.List entities, -+ TargetingConditions conditions, -+ Mob source, -+ double x, -+ double y, -+ double z, -+ ServerLevel level) { -+ -+ if (entities == null || entities.isEmpty() || level == null) { -+ return null; -+ } -+ -+ try { -+ double closestDistSq = -1.0; -+ LivingEntity closest = null; -+ -+ for (int i = 0; i < entities.size(); i++) { -+ try { -+ LivingEntity entity = entities.get(i); -+ if (entity == null || entity.isRemoved() || !entity.isAlive()) { -+ continue; -+ } -+ -+ if (conditions.test(level, source, entity)) { -+ double dx = entity.getX() - x; -+ double dy = entity.getY() - y; -+ double dz = entity.getZ() - z; -+ double distSq = dx * dx + dy * dy + dz * dz; -+ -+ if (closestDistSq == -1.0 || distSq < closestDistSq) { -+ closestDistSq = distSq; -+ closest = entity; -+ } -+ } -+ } catch (IndexOutOfBoundsException e) { -+ break; -+ } catch (Exception e) { -+ System.err.println("Error processing entity in findNearestEntitySafely: " + e.getMessage()); -+ continue; -+ } -+ } -+ -+ return closest; -+ } catch (Exception e) { -+ System.err.println("Error in findNearestEntitySafely: " + e.getMessage()); -+ return null; -+ } -+ } -+ -+ @Nullable -+ private Player findNearestPlayerSafely( -+ TargetingConditions conditions, -+ Mob source, -+ double x, -+ double y, -+ double z, -+ ServerLevel level) { -+ -+ if (level == null) { -+ return null; -+ } -+ -+ try { -+ java.util.List players = level.players(); -+ if (players == null || players.isEmpty()) { -+ return null; -+ } -+ -+ double closestDistSq = -1.0; -+ Player closest = null; -+ -+ for (int i = 0; i < players.size(); i++) { -+ try { -+ Player player = players.get(i); -+ if (player == null || player.isRemoved() || !player.isAlive()) { -+ continue; -+ } -+ -+ if (conditions.test(level, source, player)) { -+ double dx = player.getX() - x; -+ double dy = player.getY() - y; -+ double dz = player.getZ() - z; -+ double distSq = dx * dx + dy * dy + dz * dz; -+ -+ if (closestDistSq == -1.0 || distSq < closestDistSq) { -+ closestDistSq = distSq; -+ closest = player; -+ } -+ } -+ } catch (IndexOutOfBoundsException e) { -+ break; -+ } catch (Exception e) { -+ System.err.println("Error processing player in findNearestPlayerSafely: " + e.getMessage()); -+ continue; -+ } -+ } -+ -+ return closest; -+ } catch (Exception e) { -+ System.err.println("Error in findNearestPlayerSafely: " + e.getMessage()); -+ return null; -+ } -+ } -+ -+ // Synchronous fallback method -+ private void findTargetSync() { -+ try { -+ ServerLevel serverLevel = getServerLevel(this.mob); -+ if (serverLevel == null) { -+ return; -+ } -+ -+ if (this.targetType != Player.class && this.targetType != ServerPlayer.class) { -+ try { -+ this.target = serverLevel.getNearestEntity( -+ this.mob.level().getEntitiesOfClass(this.targetType, this.getTargetSearchArea(this.getFollowDistance()), entity -> true), -+ this.getTargetConditions(), -+ this.mob, -+ this.mob.getX(), -+ this.mob.getEyeY(), -+ this.mob.getZ() -+ ); -+ } catch (Exception e) { -+ System.err.println("Error in sync entity finding: " + e.getMessage()); -+ this.target = null; -+ } -+ } else { -+ try { -+ this.target = serverLevel.getNearestPlayer(this.getTargetConditions(), this.mob, this.mob.getX(), this.mob.getEyeY(), this.mob.getZ()); -+ } catch (Exception e) { -+ System.err.println("Error in sync player finding: " + e.getMessage()); -+ this.target = null; -+ } -+ } -+ } catch (Exception e) { -+ System.err.println("Error in findTargetSync: " + e.getMessage()); -+ this.target = null; -+ } -+ } -+ // Leaf end - Async Target Finding -+ - @Override - public void start() { -- this.mob.setTarget(this.target, this.target instanceof ServerPlayer ? org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); // CraftBukkit - reason -+ // Leaf start - Async Target Finding -+ LivingEntity targetEntity = this.target; -+ if (targetEntity != null && !targetEntity.isRemoved() && targetEntity.isAlive()) { -+ try { -+ this.mob.setTarget(targetEntity, targetEntity instanceof ServerPlayer ? -+ org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : -+ org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); -+ } catch (Exception e) { -+ System.err.println("Error in setTarget: " + e.getMessage()); -+ this.target = null; -+ } -+ } -+ // Leaf end - Async Target Finding - super.start(); - } - diff --git a/leaf-server/minecraft-patches/features/0156-Async-target-finding.patch b/leaf-server/minecraft-patches/features/0156-Async-target-finding.patch new file mode 100644 index 00000000..e03e8c9f --- /dev/null +++ b/leaf-server/minecraft-patches/features/0156-Async-target-finding.patch @@ -0,0 +1,437 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sat, 29 Mar 2025 13:40:46 +0100 +Subject: [PATCH] Async target finding + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 24926aa7ed5c78b235659daf18b224b14beb744c..64d5cebd488892c93f07b938ce8dc3e99fddcdad 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1088,6 +1088,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop extends TargetG + protected final int randomInterval; + @Nullable + protected LivingEntity target; +- protected TargetingConditions targetConditions; ++ @Nullable protected TargetingConditions.Selector selector; // Leaf - create TargetingConditions instead of reusing it ++ // Leaf start - Async Target Finding ++ private static final org.apache.logging.log4j.Logger LOGGER = org.apache.logging.log4j.LogManager.getLogger("Leaf Async Target Lookup"); ++ // Single thread executor to prevent overwhelming the server ++ public static final java.util.concurrent.ExecutorService TARGET_EXECUTOR = java.util.concurrent.Executors.newSingleThreadExecutor( ++ new com.google.common.util.concurrent.ThreadFactoryBuilder() ++ .setNameFormat("Leaf Async Target Finding Thread") ++ .setDaemon(true) ++ .setPriority(Thread.NORM_PRIORITY - 2) ++ .build()); ++ ++ // Flag to track if a search is in progress ++ private final java.util.concurrent.atomic.AtomicBoolean isSearching = new java.util.concurrent.atomic.AtomicBoolean(false); ++ private final java.util.concurrent.atomic.AtomicReference pendingTarget = new java.util.concurrent.atomic.AtomicReference<>(null); ++ // Leaf end - Async Target Finding + + public NearestAttackableTargetGoal(Mob mob, Class targetType, boolean mustSee) { + this(mob, targetType, 10, mustSee, false, null); +@@ -36,19 +50,45 @@ public class NearestAttackableTargetGoal extends TargetG + ) { + super(mob, mustSee, mustReach); + this.targetType = targetType; +- this.randomInterval = reducedTickDelay(interval); ++ // Leaf start - update every tick ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { ++ this.randomInterval = interval; ++ } else { ++ this.randomInterval = reducedTickDelay(interval); ++ } ++ // Leaf end - update every tick + this.setFlags(EnumSet.of(Goal.Flag.TARGET)); +- this.targetConditions = TargetingConditions.forCombat().range(this.getFollowDistance()).selector(selector); ++ this.selector = selector; // Leaf + } + + @Override + public boolean canUse() { ++ // Leaf start - Async target finding ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { ++ LivingEntity t = pendingTarget.getAcquire(); ++ if (t != null) { ++ pendingTarget.setRelease(null); ++ ServerLevel serverLevel = getServerLevel(this.mob); ++ if (serverLevel != null && t.isAlive() && this.getTargetConditions().test(serverLevel, this.mob, t)) { ++ this.target = t; ++ } ++ return true; ++ } ++ } + if (this.randomInterval > 0 && this.mob.getRandom().nextInt(this.randomInterval) != 0) { + return false; ++ } else if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { ++ this.findTargetAsync(); ++ ServerLevel serverLevel = getServerLevel(this.mob); ++ if (this.target != null && !(serverLevel != null && this.target.isAlive() && this.getTargetConditions().test(serverLevel, this.mob, this.target))) { ++ this.target = null; ++ } ++ return false; + } else { + this.findTarget(); + return this.target != null; + } ++ // Leaf end - Async target finding + } + + protected AABB getTargetSearchArea(double targetDistance) { +@@ -71,6 +111,176 @@ public class NearestAttackableTargetGoal extends TargetG + } + } + ++ // Leaf start - find target async ++ protected void findTargetAsync() { ++ if (isSearching.getAcquire()) { ++ return; ++ } ++ isSearching.setRelease(true); ++ ++ // Capture mutable state to avoid race conditions ++ final Mob mob = this.mob; ++ ++ // Safety check ++ if (mob == null || mob.isRemoved() || !mob.isAlive()) { ++ isSearching.setRelease(false); ++ return; ++ } ++ ++ final double x = mob.getX(); ++ final double y = mob.getEyeY(); ++ final double z = mob.getZ(); ++ final double followDistance = this.getFollowDistance(); ++ final TargetingConditions targetConditions = this.getTargetConditions(); ++ final Class targetType = this.targetType; ++ final double maxDistSqr = followDistance * followDistance; ++ final AABB targetSearch = getTargetSearchArea(this.getFollowDistance()); ++ ++ TARGET_EXECUTOR.execute(() -> { ++ try { ++ ServerLevel serverLevel = getServerLevel(mob); ++ if (serverLevel == null) { ++ return; ++ } ++ if (mob.isRemoved() || !mob.isAlive()) { ++ return; ++ } ++ ++ try { ++ if (targetType != Player.class && targetType != ServerPlayer.class) { ++ java.util.List entities; ++ try { ++ entities = mob.level().getEntitiesOfClass(targetType, targetSearch, entity -> entity != null && entity != mob && !entity.isRemoved() && entity.isAlive()); ++ } catch (Exception e) { ++ LOGGER.warn("Error getting entities", e); ++ return; ++ } ++ ++ if (entities != null && !entities.isEmpty()) { ++ var result = findNearestEntitySafely(entities, targetConditions, mob, x, y, z, serverLevel, maxDistSqr); ++ pendingTarget.setRelease(result); ++ } ++ } else { ++ var result = findNearestPlayerSafely(targetConditions, mob, x, y, z, serverLevel); ++ pendingTarget.setRelease(result); ++ } ++ } catch (Exception e) { ++ LOGGER.warn("Error finding entities in async target finder", e); ++ } ++ } catch (Exception e) { ++ LOGGER.warn("Error during async target finding", e); ++ } finally { ++ isSearching.setRelease(false); ++ } ++ }); ++ } ++ ++ @Nullable ++ private LivingEntity findNearestEntitySafely( ++ java.util.List entities, ++ TargetingConditions conditions, ++ Mob source, ++ double x, ++ double y, ++ double z, ++ ServerLevel level, ++ double maxDistSqr) { ++ ++ if (entities == null || entities.isEmpty() || level == null) { ++ return null; ++ } ++ ++ try { ++ double closestDistSq = maxDistSqr; ++ LivingEntity closest = null; ++ ++ for (int i = 0; i < entities.size(); i++) { ++ try { ++ LivingEntity entity = entities.get(i); ++ if (entity == null || entity == source || entity.isRemoved() || !entity.isAlive()) { ++ continue; ++ } ++ ++ if (conditions.test(level, source, entity)) { ++ double dx = entity.getX() - x; ++ double dy = entity.getY() - y; ++ double dz = entity.getZ() - z; ++ double distSq = dx * dx + dy * dy + dz * dz; ++ ++ if (distSq < closestDistSq) { ++ closestDistSq = distSq; ++ closest = entity; ++ } ++ } ++ } catch (IndexOutOfBoundsException e) { ++ break; ++ } catch (Exception e) { ++ LOGGER.warn("Error processing entity in findNearestEntitySafely", e); ++ } ++ } ++ ++ return closest; ++ } catch (Exception e) { ++ LOGGER.warn("Error in findNearestEntitySafely", e); ++ return null; ++ } ++ } ++ ++ @Nullable ++ private Player findNearestPlayerSafely( ++ TargetingConditions conditions, ++ Mob source, ++ double x, ++ double y, ++ double z, ++ ServerLevel level) { ++ ++ if (level == null) { ++ return null; ++ } ++ ++ try { ++ java.util.List players = level.players(); ++ if (players == null || players.isEmpty()) { ++ return null; ++ } ++ ++ double closestDistSq = -1.0; ++ Player closest = null; ++ ++ for (int i = 0; i < players.size(); i++) { ++ try { ++ Player player = players.get(i); ++ if (player == null || player.isRemoved() || !player.isAlive()) { ++ continue; ++ } ++ ++ if (conditions.test(level, source, player)) { ++ double dx = player.getX() - x; ++ double dy = player.getY() - y; ++ double dz = player.getZ() - z; ++ double distSq = dx * dx + dy * dy + dz * dz; ++ ++ if (closestDistSq == -1.0 || distSq < closestDistSq) { ++ closestDistSq = distSq; ++ closest = player; ++ } ++ } ++ } catch (IndexOutOfBoundsException e) { ++ break; ++ } catch (Exception e) { ++ System.err.println("Error processing player in findNearestPlayerSafely: " + e.getMessage()); ++ } ++ } ++ ++ return closest; ++ } catch (Exception e) { ++ System.err.println("Error in findNearestPlayerSafely: " + e.getMessage()); ++ return null; ++ } ++ } ++ // Leaf end - find target async ++ + @Override + public void start() { + this.mob.setTarget(this.target, this.target instanceof ServerPlayer ? org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); // CraftBukkit - reason +@@ -81,7 +291,16 @@ public class NearestAttackableTargetGoal extends TargetG + this.target = target; + } + +- private TargetingConditions getTargetConditions() { +- return this.targetConditions.range(this.getFollowDistance()); ++ // Leaf start ++ protected TargetingConditions getTargetConditions() { ++ return TargetingConditions.forCombat().range(this.getFollowDistance()).selector(this.selector); ++ } ++ // Leaf end ++ ++ // Leaf start - update every tick ++ @Override ++ public boolean requiresUpdateEveryTick() { ++ return org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled; + } ++ // Leaf end - update every tick + } +diff --git a/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java +index abf57494950f55bbd75f335f26736cb9e703c197..683722fbd07fcae9cdd72928ed25fd084202b57e 100644 +--- a/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java ++++ b/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java +@@ -20,6 +20,15 @@ public class NonTameRandomTargetGoal extends NearestAtta + + @Override + public boolean canContinueToUse() { +- return this.targetConditions != null ? this.targetConditions.test(getServerLevel(this.mob), this.mob, this.target) : super.canContinueToUse(); ++ // Leaf start ++ if (this.target == null || !this.target.isAlive() || this.target.isRemoved()) { ++ return false; ++ } ++ var serverLevel = getServerLevel(this.mob); ++ if (serverLevel == null) { ++ return false; ++ } ++ return this.getTargetConditions().test(serverLevel, this.mob, this.target) && super.canContinueToUse(); ++ // Leaf end + } + } +diff --git a/net/minecraft/world/entity/ai/sensing/Sensing.java b/net/minecraft/world/entity/ai/sensing/Sensing.java +index 002d3c0d8b1107a275020d5c582c37e9a5c536ee..3f8c18f4040f4929df79ba85906330b150720cb0 100644 +--- a/net/minecraft/world/entity/ai/sensing/Sensing.java ++++ b/net/minecraft/world/entity/ai/sensing/Sensing.java +@@ -32,9 +32,21 @@ public class Sensing { + // Gale end - Petal - reduce line of sight updates - expiring entity id lists + } + ++ // Leaf start - async target finding + public void tick() { ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { ++ synchronized (this) { ++ tick1(); ++ } ++ } else { ++ tick1(); ++ } ++ } ++ ++ private void tick1() { ++ // Leaf end - async target finding + if (this.expiring == null) { // Gale - Petal - reduce line of sight updates +- this.seen.clear(); ++ this.seen.clear(); + // Gale start - Petal - reduce line of sight updates + } else { + var expiringNow = this.expiring[this.nextToExpireIndex]; +@@ -62,7 +74,19 @@ public class Sensing { + // Gale end - Petal - reduce line of sight updates + } + ++ // Leaf start - async target finding + public boolean hasLineOfSight(Entity entity) { ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { ++ synchronized (this) { ++ return hasLineOfSight1(entity); ++ } ++ } else { ++ return hasLineOfSight1(entity); ++ } ++ } ++ ++ private boolean hasLineOfSight1(Entity entity) { ++ // Leaf end - async target finding + int id = entity.getId(); + // Gale start - Petal - reduce line of sight cache lookups - merge sets + int cached = this.seen.get(id); +diff --git a/net/minecraft/world/entity/animal/Fox.java b/net/minecraft/world/entity/animal/Fox.java +index 90452f0945e761077608692877677f522d38bccd..736eea50b4460041242bdd4b7191d67d7dd4524d 100644 +--- a/net/minecraft/world/entity/animal/Fox.java ++++ b/net/minecraft/world/entity/animal/Fox.java +@@ -849,13 +849,18 @@ public class Fox extends Animal implements VariantHolder { + return false; + } else { + ServerLevel serverLevel = getServerLevel(Fox.this.level()); ++ // Leaf start ++ if (serverLevel == null) { ++ return false; ++ } ++ // Leaf end + + for (UUID uuid : Fox.this.getTrustedUUIDs()) { + if (serverLevel.getEntity(uuid) instanceof LivingEntity livingEntity) { + this.trustedLastHurt = livingEntity; + this.trustedLastHurtBy = livingEntity.getLastHurtByMob(); + int lastHurtByMobTimestamp = livingEntity.getLastHurtByMobTimestamp(); +- return lastHurtByMobTimestamp != this.timestamp && this.canAttack(this.trustedLastHurtBy, this.targetConditions); ++ return lastHurtByMobTimestamp != this.timestamp && this.canAttack(this.trustedLastHurtBy, this.getTargetConditions()); // Leaf + } + } +