Skip to content

Entity Lifecycle and AI

goshi-hider edited this page Jul 24, 2026 · 1 revision

Entity Lifecycle and AI

Package: dev.goshi.omnimixin.api.event.entity

Ticking, despawning, spawn rules, and mob AI goal registration/targeting.

PlayEntity

Generic per-entity lifecycle hooks — these fire for every entity, so keep your listeners cheap, especially PRE_TICK/POST_TICK, which run every tick for every entity in every loaded chunk.

// A custom "frozen in time" zone that stops entities from ticking at all.
PlayEntity.PRE_TICK.register(ctx -> {
    // Note: PRE_TICK/POST_TICK don't carry a Cancellable - see below.
    if (isInFrozenZone(ctx.entity())) {
        freezeInPlace(ctx.entity());
    }
});

// Custom despawn message for named/tamed entities.
PlayEntity.DESPAWN.register(ctx -> {
    if (ctx.entity() instanceof TameableEntity pet && pet.hasCustomName()) {
        ctx.cancel().cancel(); // keep the player's named pet from despawning
    }
});

TickContext(Entity entity)PRE_TICK / POST_TICK

Note there's no Cancellable on TickContext — you can't skip an entity's tick entirely through this event (that would risk leaving the entity in an inconsistent physics/AI state); use it for observing or applying your own side effects around the tick, not for suppressing it.

DespawnContext(Entity entity, Cancellable cancel)DESPAWN

PlaySpawn

Three events across an entity's decision to spawn into the world.

// Reduce hostile mob spawn rates near a player-built town.
PlaySpawn.CHECK_RULES.register(ctx -> {
    if (isHostile(ctx.entity()) && isNearPlayerTown(ctx.entity())) {
        return false; // veto this spawn
    }
    return true;
});

// Custom spawn-in effect (particles, sound) for any natural spawn.
PlaySpawn.POST_SPAWN.register((ctx, spawned) -> {
    if (spawned && ctx.entity() instanceof HostileEntity) {
        playSpawnFlash(ctx.world(), ctx.entity());
    }
});

SpawnRulesContext(MobEntity entity, WorldView world, SpawnReason spawnReason)CHECK_RULES

CHECK_RULES returns boolean — return false from any listener to block the spawn; if every listener returns true, vanilla's own rules still apply on top.

SpawnContext(Entity entity, ServerWorld world, Cancellable cancel)PRE_SPAWN / POST_SPAWN

PRE_SPAWN's cancel actually stops the entity from being added to the world. POST_SPAWN fires after, with a second spawned boolean telling you whether it actually happened — its own Cancellable has no effect at that point.

PlayGoal

Mob AI hooks — registering new goals onto a mob's GoalSelector, and redirecting what a mob is targeting.

// Prevent a specific goal type from ever being added to skeletons.
PlayGoal.REGISTER_GOALS.register(ctx -> {
    if (ctx.goal() instanceof MeleeAttackGoal) {
        ctx.cancel().cancel();
    }
});

// Make guard mobs prioritize whichever nearby player has a bounty.
PlayGoal.MODIFY_TARGET.register(ctx -> {
    findBountyPlayerNearby(ctx.entity()).ifPresent(ctx.target()::set);
});

GoalRegistrationContext(GoalSelector selector, int priority, Goal goal, Cancellable cancel)

REGISTER_GOALS deviates a little from what you might expect: vanilla mobs each wire up their AI goals inline in their own constructors rather than through one shared "init goals" method, so this hooks the shared GoalSelector.add(...) call instead. That means the context can't tell you which entity, or which selector (the mob's normal goal selector vs. its target selector), a given addition belongs to — you're filtering purely on the Goal instance and priority being added. If you need per-entity control, you'll generally want to check the goal's own type/fields rather than trying to infer the owning entity.

TargetContext(MobEntity entity, LivingEntity oldTarget, MutableReference<LivingEntity> target, Cancellable cancel)

target is a MutableReference<LivingEntity> — call .set(...) to redirect who the mob is targeting, or .set(null) to clear its target entirely. Cancel to keep oldTarget in place instead.

Clone this wiki locally