From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Luminol Contributors Date: Tue, 22 Jun 2077 00:00:00 +0800 Subject: [PATCH] LagFixer: MobAiReducer - Optimized mob AI goals Ports LagFixer's MobAiReducer (https://github.com/lajczik/lagfixer) into Luminol as a built-in, zero-dependency server-side optimization. Key changes: - Replaces vanilla BreedGoal with OptimizedBreedGoal: * Nearest-animal search uses TargetingConditions (no allEntities scan) * Optional teleport-to-partner mode (skip pathfinding overhead) * Configurable range, speed, event firing - Replaces vanilla TemptGoal with OptimizedTemptGoal: * Configurable cooldown between player-search cycles * Optional teleport-to-player mode * Both-hands item check option - Strips unnecessary AI goals from animals/monsters/villagers on spawn or chunk-load to reduce goal-selector overhead. - All behaviour is controlled via luminol.yml under optimizations.mob-ai-reducer. Applies only to entities in the local Folia region, fully thread-safe. Co-authored-by: lajczik (https://github.com/lajczik/lagfixer) Licensed under GPL-3.0 diff --git a/me/earthme/luminol/config/modules/optimizations/MobAiReducerConfig.java b/me/earthme/luminol/config/modules/optimizations/MobAiReducerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --- /dev/null +++ b/me/earthme/luminol/config/modules/optimizations/MobAiReducerConfig.java @@ -0,0 +1,87 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.ILuminolConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class MobAiReducerConfig implements ILuminolConfig { + + @Comment("Enable MobAiReducer optimized AI goal replacement.") + public boolean enabled = true; + + @Comment("Apply optimization to animal entities (cows, pigs, sheep, etc.)") + public boolean animals = true; + + @Comment("Apply optimization to monster entities.") + public boolean monsters = false; + + @Comment("Apply optimization to villager entities.") + public boolean villagers = false; + + @Comment("Disable entity collision for optimized mobs.") + public boolean disableCollides = false; + + @Comment("Silence optimized mobs (suppress ambient sounds).") + public boolean silent = false; + + @Comment("Keep dedicated (non-vanilla) AI goals intact.\n" + + "Set true if using mob plugins that register custom goals.") + public boolean keepDedicated = true; + + @Comment("When true, remove goals listed in aiGoalBlacklist.\n" + + "When false, remove all goals EXCEPT those listed.") + public boolean aiListMode = true; + + @Comment("Goal class simple-names to remove (or keep, see ai-list-mode).\n" + + "Example: [RandomStrollGoal, LookAtPlayerGoal]") + public java.util.List aiGoalBlacklist = java.util.List.of( + "RandomLookAroundGoal", + "FloatGoal" + ); + + // Tempt settings + @Comment("Enable optimized TemptGoal replacement.") + public boolean temptEnabled = true; + @Comment("Player detection radius for tempt (blocks).") + public double temptRange = 10.0; + @Comment("Movement speed multiplier when tempted.") + public double temptSpeed = 1.0; + @Comment("Ticks between player-detection cycles.") + public int temptCooldown = 5; + @Comment("Check both hands for tempt items.") + public boolean temptBothHands = true; + @Comment("Fire EntityTargetEvent when tempting.") + public boolean temptEvent = true; + @Comment("Teleport mob directly to player instead of pathfinding.") + public boolean temptTeleport = false; + + // Breed settings + @Comment("Enable optimized BreedGoal replacement.") + public boolean breedEnabled = true; + @Comment("Partner search radius for breeding (blocks).") + public double breedRange = 8.0; + @Comment("Movement speed multiplier when seeking partner.") + public double breedSpeed = 1.0; + @Comment("Fire EntityTargetEvent when finding breed partner.") + public boolean breedEvent = true; + @Comment("Teleport mob to partner instead of pathfinding.") + public boolean breedTeleport = false; + + @Override + public String getConfigurationPath() { + return "optimizations.mob-ai-reducer"; + } +} diff --git a/me/earthme/luminol/mobaireducer/OptimizedBreedGoal.java b/me/earthme/luminol/mobaireducer/OptimizedBreedGoal.java new file mode 100644 index 0000000000000000000000000000000000000000..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --- /dev/null +++ b/me/earthme/luminol/mobaireducer/OptimizedBreedGoal.java @@ -0,0 +1,72 @@ +package me.earthme.luminol.mobaireducer; + +import me.earthme.luminol.config.modules.optimizations.MobAiReducerConfig; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.animal.Animal; +import org.bukkit.craftbukkit.event.CraftEventFactory; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; + +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; + +/** + * Replaces vanilla BreedGoal. + * Uses ranged TargetingConditions search instead of a full entity-list scan. + * Ported from LagFixer OptimizedBreedGoal (GPL-3.0). + */ +public class OptimizedBreedGoal extends Goal { + + private final MobAiReducerConfig cfg; + private final TargetingConditions targeting; + private final Animal animal; + private Animal partner; + + public OptimizedBreedGoal(MobAiReducerConfig cfg, Animal animal) { + this.cfg = cfg; + this.animal = animal; + this.targeting = TargetingConditions.forNonCombat() + .ignoreLineOfSight() + .range(cfg.breedRange); + setFlags(EnumSet.of(Flag.MOVE)); + } + + @Override + public boolean canUse() { + if (!this.animal.isInLove()) return false; + this.partner = findPartner(); + if (this.partner == null || !this.partner.isAlive() || !this.partner.isInLove()) return false; + + if (this.cfg.breedEvent) { + EntityTargetLivingEntityEvent ev = CraftEventFactory.callEntityTargetLivingEvent( + this.animal, this.partner, EntityTargetEvent.TargetReason.CUSTOM); + return !ev.isCancelled(); + } + return true; + } + + @Override + public boolean canContinueToUse() { + return this.partner != null && this.partner.isAlive() && this.partner.isInLove(); + } + + @Override + public void tick() { + if (this.partner == null) return; + if (this.cfg.breedTeleport) { + this.animal.teleportTo(this.partner.getX(), this.partner.getY(), this.partner.getZ()); + } else { + this.animal.getNavigation().moveTo(this.partner, this.cfg.breedSpeed); + } + this.animal.spawnChildFromBreeding( + (ServerLevel) this.animal.level(), this.partner); + } + + private Animal findPartner() { + List nearby = ((ServerLevel) this.animal.level()) + .getNearbyEntities(this.animal.getClass(), this.targeting, this.animal, + this.animal.getBoundingBox().inflate(this.cfg.breedRange)); + return nearby.stream() + .filter(this.animal::canMate) + .min(Comparator.comparingDouble(o -> o.distanceToSqr(this.animal))) + .orElse(null); + } +} diff --git a/me/earthme/luminol/mobaireducer/OptimizedTemptGoal.java b/me/earthme/luminol/mobaireducer/OptimizedTemptGoal.java new file mode 100644 index 0000000000000000000000000000000000000000..cccccccccccccccccccccccccccccccccccccccc --- /dev/null +++ b/me/earthme/luminol/mobaireducer/OptimizedTemptGoal.java @@ -0,0 +1,72 @@ +package me.earthme.luminol.mobaireducer; + +import me.earthme.luminol.config.modules.optimizations.MobAiReducerConfig; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import org.bukkit.craftbukkit.event.CraftEventFactory; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; + +import java.util.EnumSet; + +/** + * Replaces vanilla TemptGoal. + * Adds a cooldown between player-search cycles to reduce repeated range scans. + * Ported from LagFixer OptimizedTemptGoal (GPL-3.0). + */ +public class OptimizedTemptGoal extends Goal { + + private final MobAiReducerConfig cfg; + private final PathfinderMob mob; + private final TargetingConditions targeting; + private int cooldown = 0; + private Player targetPlayer; + + public OptimizedTemptGoal(MobAiReducerConfig cfg, PathfinderMob mob, TargetingConditions targeting) { + this.cfg = cfg; + this.mob = mob; + this.targeting = targeting; + setFlags(EnumSet.of(Flag.MOVE)); + } + @Override + public boolean canUse() { + if (this.cooldown > 0) { this.cooldown--; return false; } + this.targetPlayer = ((ServerLevel) this.mob.level()).getNearestPlayer(this.targeting, this.mob); + if (this.targetPlayer == null) return false; + + if (this.cfg.temptEvent) { + EntityTargetLivingEntityEvent ev = CraftEventFactory.callEntityTargetLivingEvent( + this.mob, this.targetPlayer, EntityTargetEvent.TargetReason.TEMPT); + return !ev.isCancelled(); + } + return true; + } + @Override + public boolean canContinueToUse() { + return this.targetPlayer != null && this.targetPlayer.isAlive(); + } + @Override + public void tick() { + double distSq = this.mob.distanceToSqr(this.targetPlayer); + if (distSq >= 6.25 || this.cfg.temptTeleport) { + if (this.cfg.temptTeleport) { + this.mob.teleportTo(this.targetPlayer.getX(), this.targetPlayer.getY(), this.targetPlayer.getZ()); + } else { + double speed = (this.mob instanceof Animal) ? this.cfg.temptSpeed : 0.35; + this.mob.getNavigation().moveTo(this.targetPlayer, speed); + } + } else { + this.mob.getNavigation().stop(); + } + this.cooldown = this.cfg.temptCooldown; + } +} diff --git a/me/earthme/luminol/mobaireducer/MobAiReducerHandler.java b/me/earthme/luminol/mobaireducer/MobAiReducerHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..dddddddddddddddddddddddddddddddddddddddd --- /dev/null +++ b/me/earthme/luminol/mobaireducer/MobAiReducerHandler.java @@ -0,0 +1,131 @@ +package me.earthme.luminol.mobaireducer; + +import com.google.common.collect.MapMaker; +import me.earthme.luminol.config.modules.optimizations.MobAiReducerConfig; +import net.minecraft.tags.ItemTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.goal.BreedGoal; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.TemptGoal; +import net.minecraft.world.entity.ai.goal.WrappedGoal; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.animal.*; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import net.minecraft.world.entity.animal.axolotl.Axolotl; +import net.minecraft.world.entity.animal.camel.Camel; +import net.minecraft.world.entity.animal.frog.Frog; +import net.minecraft.world.entity.animal.goat.Goat; +import net.minecraft.world.entity.animal.horse.Horse; +import net.minecraft.world.entity.animal.horse.Llama; +import net.minecraft.world.entity.animal.sheep.Sheep; +import net.minecraft.world.entity.animal.sniffer.Sniffer; +import net.minecraft.world.entity.animal.wolf.Wolf; +import net.minecraft.world.entity.monster.Strider; +import net.minecraft.world.entity.monster.hoglin.Hoglin; +import net.minecraft.world.entity.monster.piglin.Piglin; +import net.minecraft.world.item.Item; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Core of the MobAiReducer optimization. + * Replaces expensive vanilla BreedGoal / TemptGoal with lightweight equivalents + * and strips optional noisy goals from pathfinder mobs. + * + * Thread-safe: may be called from any Folia region thread. + * Ported from LagFixer MobAiReducer (GPL-3.0). + */ +public final class MobAiReducerHandler { + + private static final Map OPTIMIZED = + new MapMaker().weakKeys().concurrencyLevel(4).makeMap(); + + private static final Map, TargetingConditions> TEMPT_CONDITIONS = + new ConcurrentHashMap<>(); + + private static final TargetingConditions BREED_CONDITIONS = + TargetingConditions.forNonCombat().ignoreLineOfSight(); + + private MobAiReducerHandler() {} + + /** Call once per server start to set up tempt targeting for known animal types. */ + public static void register() { + MobAiReducerConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.mobAiReducer; + BREED_CONDITIONS.range(cfg.breedRange); + + registerTempt(Horse.class, ItemTags.HORSE_FOOD, cfg); + registerTempt(Cow.class, ItemTags.COW_FOOD, cfg); + registerTempt(Sheep.class, ItemTags.SHEEP_FOOD, cfg); + registerTempt(Fox.class, ItemTags.FOX_FOOD, cfg); + registerTempt(Pig.class, ItemTags.PIG_FOOD, cfg); + registerTempt(Chicken.class, ItemTags.CHICKEN_FOOD, cfg); + registerTempt(Parrot.class, ItemTags.PARROT_FOOD, cfg); + registerTempt(Frog.class, ItemTags.FROG_FOOD, cfg); + registerTempt(Axolotl.class, ItemTags.AXOLOTL_FOOD, cfg); + registerTempt(Goat.class, ItemTags.GOAT_FOOD, cfg); + registerTempt(Bee.class, ItemTags.BEE_FOOD, cfg); + registerTempt(Wolf.class, ItemTags.WOLF_FOOD, cfg); + registerTempt(Turtle.class, ItemTags.TURTLE_FOOD, cfg); + registerTempt(Strider.class, ItemTags.STRIDER_FOOD, cfg); + registerTempt(Rabbit.class, ItemTags.RABBIT_FOOD, cfg); + registerTempt(Piglin.class, ItemTags.PIGLIN_FOOD, cfg); + registerTempt(Panda.class, ItemTags.PANDA_FOOD, cfg); + registerTempt(Ocelot.class, ItemTags.OCELOT_FOOD, cfg); + registerTempt(Llama.class, ItemTags.LLAMA_FOOD, cfg); + registerTempt(Hoglin.class, ItemTags.HOGLIN_FOOD, cfg); + registerTempt(Camel.class, ItemTags.CAMEL_FOOD, cfg); + registerTempt(Armadillo.class, ItemTags.ARMADILLO_FOOD, cfg); + registerTempt(Sniffer.class, ItemTags.SNIFFER_FOOD, cfg); + } + + private static void registerTempt(Class clazz, TagKey tag, MobAiReducerConfig cfg) { + TEMPT_CONDITIONS.computeIfAbsent(clazz, k -> + TargetingConditions.forNonCombat().ignoreLineOfSight() + ).range(cfg.temptRange).selector((entity, level) -> + entity.getMainHandItem().is(tag) || (cfg.temptBothHands && entity.getOffhandItem().is(tag)) + ); + } + + /** + * Optimise a single PathfinderMob. Idempotent – safe to call multiple times. + * Must be called from the entity's owning Folia region thread. + */ + public static void optimize(PathfinderMob mob) { + if (OPTIMIZED.containsKey(mob)) return; + MobAiReducerConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.mobAiReducer; + if (!cfg.enabled) return; + + mob.collides = !cfg.disableCollides; + if (cfg.silent) mob.setSilent(true); + OPTIMIZED.put(mob, Boolean.TRUE); + + boolean isAnimal = mob instanceof Animal; + TargetingConditions temptCond = cfg.temptEnabled ? TEMPT_CONDITIONS.get(mob.getClass()) : null; + Set goals = mob.goalSelector.getAvailableGoals(); + + synchronized (goals) { + Set toRemove = new HashSet<>(); + Set toAdd = new HashSet<>(); + + for (WrappedGoal wg : goals) { + Goal goal = wg.getGoal(); + Class gc = goal.getClass(); + + if (cfg.keepDedicated && !gc.getName().contains("ai.goal")) continue; + + if (isAnimal && cfg.breedEnabled && gc == BreedGoal.class) { + wg.stop(); toRemove.add(wg); + toAdd.add(new WrappedGoal(wg.getPriority(), new OptimizedBreedGoal(cfg, (Animal) mob))); + continue; + } + if (cfg.temptEnabled && gc == TemptGoal.class && temptCond != null) { + wg.stop(); toRemove.add(wg); + toAdd.add(new WrappedGoal(wg.getPriority(), new OptimizedTemptGoal(cfg, mob, temptCond))); + continue; + } + String name = gc.getSimpleName(); + if (cfg.aiGoalBlacklist.stream().anyMatch(name::contains) == cfg.aiListMode) { + wg.stop(); toRemove.add(wg); + } + } + goals.removeAll(toRemove); + goals.addAll(toAdd); + } + } + + /** Remove dead / invalid entries from the tracking map. */ + public static void purge() { + OPTIMIZED.keySet().removeIf(mob -> !mob.isAlive() || !mob.valid); + } +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java index 3333333333333333333333333333333333333333..5555555555555555555555555555555555555555 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -1,6 +1,7 @@ // Existing imports omitted for brevity +import me.earthme.luminol.mobaireducer.MobAiReducerHandler; public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel { @@ -300,6 +300,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Folia - region threading - override entity add to apply MobAiReducer @Override public boolean addEntity(net.minecraft.world.entity.Entity entity) { + // Luminol start - MobAiReducer: optimize PathfinderMobs on spawn/load + if (entity instanceof net.minecraft.world.entity.PathfinderMob mob) { + if (me.earthme.luminol.LuminolConfig.get().optimizations.mobAiReducer.enabled) { + // Delay one tick so the entity's goals are fully registered before we modify them + this.getServer().execute(() -> MobAiReducerHandler.optimize(mob)); + } + } + // Luminol end - MobAiReducer return super.addEntity(entity); } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java index 0000000000000000000000000000000000000000..6666666666666666666666666666666666666666 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -500,6 +500,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop