[1.20] Add events for checking spawn conditions (SpawnPlacementCheck and PositionCheck) (#9469)

This commit is contained in:
Brennan Ward authored and GitHub committed 2023-06-27 13:53:00 +02:00
1 parent 7a25e83d34
commit 2d8d6b2a0a
6 files changed
+296 -16

No files matched your search

@@ -40,6 +40,7 @@ import net.minecraft.commands.Commands;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.SpawnGroupData;
import net.minecraft.world.entity.SpawnPlacements;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
@@ -75,6 +76,7 @@ import net.minecraft.world.level.Explosion;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.SpawnData;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.storage.ServerLevelData;
@@ -103,6 +105,8 @@ import net.minecraftforge.event.entity.living.LivingHealEvent;
import net.minecraftforge.event.entity.living.LivingPackSizeEvent;
import net.minecraftforge.event.entity.living.MobSpawnEvent;
import net.minecraftforge.event.entity.living.MobSpawnEvent.AllowDespawn;
import net.minecraftforge.event.entity.living.MobSpawnEvent.PositionCheck;
import net.minecraftforge.event.entity.living.MobSpawnEvent.SpawnPlacementCheck;
import net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent;
import net.minecraftforge.event.entity.player.AdvancementEvent.AdvancementEarnEvent;
import net.minecraftforge.event.entity.player.AdvancementEvent.AdvancementProgressEvent;
@@ -187,6 +191,55 @@ public class ForgeEventFactory
MinecraftForge.EVENT_BUS.post(new PlayerDestroyItemEvent(player, stack, hand));
}
/**
* Internal, should only be called via {@link SpawnPlacements#checkSpawnRules}.
* @see SpawnPlacementCheck
*/
@ApiStatus.Internal
public static boolean checkSpawnPlacements(EntityType<?> entityType, ServerLevelAccessor level, MobSpawnType spawnType, BlockPos pos, RandomSource random, boolean defaultResult)
{
var event = new SpawnPlacementCheck(entityType, level, spawnType, pos, random, defaultResult);
MinecraftForge.EVENT_BUS.post(event);
return event.getResult() == Result.DEFAULT ? defaultResult : event.getResult() == Result.ALLOW;
}
/**
* Checks if the current position of the passed mob is valid for spawning, by firing {@link PositionCheck}.<br>
* The default check is to perform the logical and of {@link Mob#checkSpawnRules} and {@link Mob#checkSpawnObstruction}.<br>
* @param mob The mob being spawned.
* @param level The level the mob will be added to, if successful.
* @param spawnType The spawn type of the spawn.
* @return True, if the position is valid, as determined by the contract of {@link PositionCheck}.
* @see PositionCheck
*/
public static boolean checkSpawnPosition(Mob mob, ServerLevelAccessor level, MobSpawnType spawnType)
{
var event = new PositionCheck(mob, level, spawnType, null);
MinecraftForge.EVENT_BUS.post(event);
if (event.getResult() == Result.DEFAULT)
{
return mob.checkSpawnRules(level, spawnType) && mob.checkSpawnObstruction(level);
}
return event.getResult() == Result.ALLOW;
}
/**
* Specialized variant of {@link #checkSpawnPosition} for spawners, as they have slightly different checks.
* @see #CheckSpawnPosition
* @implNote See in-line comments about custom spawn rules.
*/
public static boolean checkSpawnPositionSpawner(Mob mob, ServerLevelAccessor level, MobSpawnType spawnType, SpawnData spawnData, BaseSpawner spawner)
{
var event = new PositionCheck(mob, level, spawnType, null);
MinecraftForge.EVENT_BUS.post(event);
if (event.getResult() == Result.DEFAULT)
{
// Spawners do not evaluate Mob#checkSpawnRules if any custom rules are present. This is despite the fact that these two methods do not check the same things.
return (spawnData.getCustomSpawnRules().isPresent() || mob.checkSpawnRules(level, spawnType)) && mob.checkSpawnObstruction(level);
}
return event.getResult() == Result.ALLOW;
}
/**
* Vanilla calls to {@link Mob#finalizeSpawn} are replaced with calls to this method via coremod.<br>
* Mods should call this method in place of calling {@link Mob#finalizeSpawn}. Super calls (from within overrides) should not be wrapped.
@@ -236,7 +289,7 @@ public class ForgeEventFactory
* Returns the FinalizeSpawn event instance, or null if it was canceled.<br>
* This is separate since mob spawners perform special finalizeSpawn handling when NBT data is present, but we still want to fire the event.<br>
* This overload is also the only way to pass through a {@link BaseSpawner} instance.
* @see MobSpawnEvent.FinalizeSpawn
* @see #onFinalizeSpawn
*/
@Nullable
public static MobSpawnEvent.FinalizeSpawn onFinalizeSpawnSpawner(Mob mob, ServerLevelAccessor level, DifficultyInstance difficulty, @Nullable SpawnGroupData spawnData, @Nullable CompoundTag spawnTag, BaseSpawner spawner)
@@ -8,24 +8,38 @@ package net.minecraftforge.event.entity.living;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.SpawnGroupData;
import net.minecraft.world.entity.SpawnPlacements;
import net.minecraft.world.level.BaseSpawner;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraftforge.common.ForgeInternalHandler;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.event.entity.EntityEvent;
import net.minecraftforge.event.entity.SpawnPlacementRegisterEvent;
import net.minecraftforge.eventbus.api.Cancelable;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
/**
* The subclasses of this event are fired whenever a mob performs a spawning-related action.
* This class holds all events relating to the entire flow of mob spawns.<br>
* Currently, the events have the following flow for any given mob spawn:
* <p>
* Before the spawn is attempted {@link SpawnPlacementCheck} is fired, to determine if the spawn may occur based on mob-specific rules.<br>
* After the entity is created {@link PositionCheck} is fired, to determine if the selected position is legal for the entity.<br>
* If both checks succeeded, {@link FinalizeSpawn} is fired, which performs initialization on the newly-spawned entity.<br>
* Finally, if the spawn was not cancelled via {@link FinalizeSpawn#setSpawnCancelled}, then {@link EntityJoinLevelEvent} is fired as the entity enters the world.
* <p>
* {@link AllowDespawn} is not related to the mob spawn event flow, as it fires when a despawn is attempted.
*/
public abstract class MobSpawnEvent extends EntityEvent
{
@@ -34,11 +48,8 @@ public abstract class MobSpawnEvent extends EntityEvent
private final double y;
private final double z;
/**
* @apiNote Do not construct directly. Access via {@link ForgeEventFactory#onFinalizeSpawn}.
*/
@ApiStatus.Internal
public MobSpawnEvent(Mob mob, ServerLevelAccessor level, double x, double y, double z)
protected MobSpawnEvent(Mob mob, ServerLevelAccessor level, double x, double y, double z)
{
super(mob);
this.level = level;
@@ -85,6 +96,166 @@ public abstract class MobSpawnEvent extends EntityEvent
return this.z;
}
/**
* This event is fired {@linkplain SpawnPlacements#checkSpawnRules when Spawn Placements (aka Spawn Rules) are checked}, before a mob attempts to spawn.<br>
* Spawn Placement checks include light levels, slime chunks, grass blocks for animals, and others in the same vein.<br>
* The purpose of this event is to permit runtime changes to any or all spawn placement logic without having to wrap the placement for each entity.
* <p>
* This event {@linkplain HasResult has a result}.<br>
* To change the result of this event, use {@link #setResult}. Results are interpreted in the following manner:
* <ul>
* <li>Allow - The check will succeed, and the spawn process will continue.</li>
* <li>Default - The value of the vanilla check will be used to determine success.</li>
* <li>Deny - The check will fail, and the spawn process will abort.</li>
* </ul>
* This event is fired on the {@linkplain MinecraftForge#EVENT_BUS main Forge event bus},
* only on the {@linkplain LogicalSide#SERVER logical server}.
* <p>
* This event is not fired for mob spawners which utilize {@link CustomSpawnRules}, as they do not check spawn placements.
* @apiNote If your modifications are for a single entity, and do not vary at runtime, use {@link SpawnPlacementRegisterEvent}.
* @see SpawnPlacementRegisterEvent
*/
@HasResult
public static class SpawnPlacementCheck extends Event
{
private final EntityType<?> entityType;
private final ServerLevelAccessor level;
private final MobSpawnType spawnType;
private final BlockPos pos;
private final RandomSource random;
private final boolean defaultResult;
/**
* Internal.
* @see {@link SpawnPlacements#checkSpawnRules} for the single call site of this event.
*/
@ApiStatus.Internal
public SpawnPlacementCheck(EntityType<?> entityType, ServerLevelAccessor level, MobSpawnType spawnType, BlockPos pos, RandomSource random, boolean defaultResult)
{
this.entityType = entityType;
this.level = level;
this.spawnType = spawnType;
this.pos = pos;
this.random = random;
this.defaultResult = defaultResult;
}
/**
* @return The type of entity that checks are being performed for.
*/
public EntityType<?> getEntityType()
{
return this.entityType;
}
/**
* @return The level relating to the mob spawn action
*/
public ServerLevelAccessor getLevel()
{
return this.level;
}
/**
* Retrieves the type of mob spawn that is happening.
* @return The mob spawn type.
* @see MobSpawnType
*/
public MobSpawnType getSpawnType()
{
return this.spawnType;
}
/**
* @return The position where checks are being evaluated.
*/
public BlockPos getPos()
{
return this.pos;
}
/**
* In all vanilla cases, this is equal to {@link ServerLevelAccessor#getRandom()}.
* @return The random source being used.
*/
public RandomSource getRandom()
{
return this.random;
}
/**
* The default vanilla result is useful if an additional check wants to force {@link Result#ALLOW} only if the vanilla check would succeed.
* @return The result of the vanilla spawn placement check.
*/
public boolean getDefaultResult()
{
return this.defaultResult;
}
}
/**
* This event is fired when a mob checks for a valid spawn position, after {@link SpawnPlacements#checkSpawnRules} has been evaluated.<br>
* Conditions validated here include the following:
* <ul>
* <li>Obstruction - mobs inside blocks or fluids.</li>
* <li>Pathfinding - if the spawn block is valid for pathfinding.</li>
* <li>Sea Level - Ocelots check if the position is above sea level.</li>
* <li>Spawn Block - Ocelots check if the below block is grass or leaves.</li>
* </ul>
* <p>
* These checks are performed by the vanilla methods {@link Mob#checkSpawnRules} and {@link Mob#checkSpawnObstruction}.<br>
* The logical-and of both methods forms the default result of this event.
* <p>
* This event {@linkplain HasResult has a result}.<br>
* To change the result of this event, use {@link #setResult}. Results are interpreted in the following manner:
* <ul>
* <li>Allow - The position will be accepted, and the spawn process will continue.</li>
* <li>Default - The position will be accepted if {@link Mob#checkSpawnRules} and {@link Mob#checkSpawnObstruction} are both true.</li>
* <li>Deny - The position will not be accepted. The spawn process will abort, and further events will not be called.</li>
* </ul>
* This event is fired on the {@linkplain MinecraftForge#EVENT_BUS main Forge event bus},
* only on the {@linkplain LogicalSide#SERVER logical server}.
*
* @apiNote This event fires after Spawn Placement checks, which are the primary set of spawn checks.
* @see {@link SpawnPlacementRegisterEvent} To modify spawn placements statically at startup.
* @see {@link SpawnPlacementCheck} To modify the result of spawn placements at runtime.
*/
@HasResult
public static class PositionCheck extends MobSpawnEvent
{
@Nullable
private final BaseSpawner spawner;
private final MobSpawnType spawnType;
public PositionCheck(Mob mob, ServerLevelAccessor level, MobSpawnType spawnType, @Nullable BaseSpawner spawner)
{
super(mob, level, mob.getX(), mob.getY(), mob.getZ());
this.spawnType = spawnType;
this.spawner = spawner;
}
/**
* Retrieves the underlying {@link BaseSpawner} instance if this mob was created by a Mob Spawner of some form.
* This is always null unless {@link #getSpawnType()} is {@link MobSpawnType#SPAWNER}, and may still be null even then.
* @return The BaseSpawner responsible for triggering the spawn, or null if none is available.
*/
@Nullable
public BaseSpawner getSpawner()
{
return spawner;
}
/**
* Retrieves the type of mob spawn that is happening.
* @return The mob spawn type.
* @see MobSpawnType
*/
public MobSpawnType getSpawnType()
{
return this.spawnType;
}
}
/**
* This event is fired before {@link Mob#finalizeSpawn} is called.<br>
* This allows mods to control mob initialization.<br>
@@ -101,12 +272,19 @@ public abstract class MobSpawnEvent extends EntityEvent
public static class FinalizeSpawn extends MobSpawnEvent
{
private final MobSpawnType spawnType;
@Nullable private final BaseSpawner spawner;
private DifficultyInstance difficulty;
@Nullable private SpawnGroupData spawnData;
@Nullable private CompoundTag spawnTag;
@Nullable
private final BaseSpawner spawner;
private DifficultyInstance difficulty;
@Nullable
private SpawnGroupData spawnData;
@Nullable
private CompoundTag spawnTag;
/**
* @apiNote Do not construct directly. Access via {@link ForgeEventFactory#onFinalizeSpawn} / {@link ForgeEventFactory#onFinalizeSpawnSpawner}.
*/
@ApiStatus.Internal
public FinalizeSpawn(Mob entity, ServerLevelAccessor level, double x, double y, double z, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnData, @Nullable CompoundTag spawnTag, @Nullable BaseSpawner spawner)
{
super(entity, level, x, y, z);
@@ -135,7 +313,7 @@ public abstract class MobSpawnEvent extends EntityEvent
{
this.difficulty = inst;
}
/**
* Retrieves the type of mob spawn that happened (the event that caused the spawn). The enum names are self-explanatory.
* @return The mob spawn type.
@@ -206,7 +384,8 @@ public abstract class MobSpawnEvent extends EntityEvent
* Usually that has no side effects, but callers should be aware.
* @param cancel If the spawn should be cancelled (or not).
*/
public void setSpawnCancelled(boolean cancel) {
public void setSpawnCancelled(boolean cancel)
{
this.getEntity().setSpawnCancelled(cancel);
}
@@ -215,7 +394,8 @@ public abstract class MobSpawnEvent extends EntityEvent
* @return If this mob's spawn is cancelled or not.
* @implNote This is enforced in {@link ForgeInternalHandler#builtinMobSpawnBlocker} and a patch in {@link WorldGenRegion#addEntity}
*/
public boolean isSpawnCancelled() {
public boolean isSpawnCancelled()
{
return this.getEntity().isSpawnCancelled();
}
}
@@ -236,6 +416,8 @@ public abstract class MobSpawnEvent extends EntityEvent
* @see LivingEntity#checkDespawn()
* @author cpw
*/
// TODO: 1.20 Move to standalone class, as it is unrelated to the complex mob spawning flow.
// Such a refactor will allow the BaseSpawner and MobSpawnType params to be hoisted to MobSpawnEvent.
@HasResult
public static class AllowDespawn extends MobSpawnEvent
{
@@ -244,4 +426,5 @@ public abstract class MobSpawnEvent extends EntityEvent
super(mob, level, mob.getX(), mob.getY(), mob.getZ());
}
}
}