diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerEntityEvents.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerEntityEvents.java index c88507f825..d5d0f8e554 100644 --- a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerEntityEvents.java +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerEntityEvents.java @@ -18,6 +18,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; @@ -33,6 +34,8 @@ private ServerEntityEvents() { * Called when an Entity is loaded into a ServerLevel. * *

When this event is called, the entity is already in the level. + * + *

If you want to get load context use ENTITY_LOAD_CONTEXT scoped value

*/ public static final Event ENTITY_LOAD = EventFactory.createArrayBacked(ServerEntityEvents.Load.class, callbacks -> (entity, level) -> { for (Load callback : callbacks) { @@ -40,6 +43,34 @@ private ServerEntityEvents() { } }); + /** + * Called before an Entity is added to a ServerLevel. + * + *

If return value is {@code false} entity will not be added to a server.

+ * + *

Should be used when you want to add another entity instead of added or to disallow add specific entities.

+ * + * {@snippet : + * ServerEntityEvents.ALLOW_ADD.register((entity, level) -> { + * // Spawn with 25% chance zombie instead of creeper + * if (entity instanceof Creeper && level.getRandom().nextFloat() <= 0.25f) { + * var zombie = EntityType.ZOMBIE.create(level, null, entity.blockPosition(), EntitySpawnReason.EVENT, true, false); + * if (zombie != null) return !level.addFreshEntity(zombie); + * } + * return true; + * }); + *} + */ + public static final Event ALLOW_ADD = EventFactory.createArrayBacked(AllowAdd.class, callbacks -> (entity, level) -> { + boolean bl = true; + + for (AllowAdd callback : callbacks) { + bl = bl && callback.allowAdd(entity, level); + } + + return bl; + }); + /** * Called when an Entity is unloaded from a ServerLevel. * @@ -68,6 +99,11 @@ public interface Load { void onLoad(Entity entity, ServerLevel level); } + @FunctionalInterface + public interface AllowAdd { + boolean allowAdd(Entity entity, ServerLevel level); + } + @FunctionalInterface public interface Unload { void onUnload(Entity entity, ServerLevel level); @@ -77,4 +113,6 @@ public interface Unload { public interface EquipmentChange { void onChange(LivingEntity livingEntity, EquipmentSlot equipmentSlot, ItemStack previousStack, ItemStack currentStack); } + + public static ScopedValue ENTITY_LOAD_REASON = ScopedValue.newInstance(); } diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/EntityTypeMixin.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/EntityTypeMixin.java new file mode 100644 index 0000000000..f31b6009bc --- /dev/null +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/EntityTypeMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.event.lifecycle; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; + +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.function.Consumer; + +@Mixin(EntityType.class) +public class EntityTypeMixin { + @Inject(method = "spawn(Lnet/minecraft/server/level/ServerLevel;Ljava/util/function/Consumer;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/EntitySpawnReason;ZZ)Lnet/minecraft/world/entity/Entity;", at = @At(value = "RETURN")) + private void entityLoadEvent(ServerLevel level, @Nullable Consumer postSpawnConfig, BlockPos spawnPos, EntitySpawnReason spawnReason, boolean tryMoveDown, boolean movedUp, CallbackInfoReturnable cir) { + ScopedValue.where(ServerEntityEvents.ENTITY_LOAD_REASON, spawnReason).run(() -> { + ServerEntityEvents.ENTITY_LOAD.invoker().onLoad(cir.getReturnValue(), level); + }); + } +} diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelEntityCallbacksMixin.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelEntityCallbacksMixin.java index 2bb09a3347..829ff3c91a 100644 --- a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelEntityCallbacksMixin.java +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelEntityCallbacksMixin.java @@ -16,6 +16,8 @@ package net.fabricmc.fabric.mixin.event.lifecycle; +import net.minecraft.world.entity.EntitySpawnReason; + import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -37,7 +39,9 @@ abstract class ServerLevelEntityCallbacksMixin { @Inject(method = "onTrackingStart(Lnet/minecraft/world/entity/Entity;)V", at = @At("TAIL")) private void invokeEntityLoadEvent(Entity entity, CallbackInfo ci) { - ServerEntityEvents.ENTITY_LOAD.invoker().onLoad(entity, this.this$0); + ScopedValue.where(ServerEntityEvents.ENTITY_LOAD_REASON, EntitySpawnReason.LOAD).run(() -> { + ServerEntityEvents.ENTITY_LOAD.invoker().onLoad(entity, this.this$0); + }); } @Inject(method = "onTrackingEnd(Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD")) diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelMixin.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelMixin.java index 42eb4e413c..3137fa1cc2 100644 --- a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelMixin.java +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/ServerLevelMixin.java @@ -18,6 +18,8 @@ import java.util.function.BooleanSupplier; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -25,7 +27,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; @Mixin(ServerLevel.class) @@ -40,4 +44,15 @@ private void startLevelTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) private void endLevelTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) { ServerTickEvents.END_LEVEL_TICK.invoker().onEndTick((ServerLevel) (Object) this); } + + @WrapOperation(method = "addFreshEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerLevel;addEntity(Lnet/minecraft/world/entity/Entity;)Z")) + private boolean afterEntityFreshLoad(ServerLevel instance, Entity entity, Operation original) { + ServerLevel serverLevel = (ServerLevel) (Object) this; + + if (!ServerEntityEvents.ALLOW_ADD.invoker().allowAdd(entity, serverLevel)) { + return false; + } + + return original.call(instance, entity); + } } diff --git a/fabric-lifecycle-events-v1/src/main/resources/fabric-lifecycle-events-v1.mixins.json b/fabric-lifecycle-events-v1/src/main/resources/fabric-lifecycle-events-v1.mixins.json index dbbe06d803..8a30f5bcaf 100644 --- a/fabric-lifecycle-events-v1/src/main/resources/fabric-lifecycle-events-v1.mixins.json +++ b/fabric-lifecycle-events-v1/src/main/resources/fabric-lifecycle-events-v1.mixins.json @@ -6,6 +6,7 @@ "ChunkHolderMixin", "ChunkMapMixin", "ChunkStatusTasksMixin", + "EntityTypeMixin", "LevelMixin", "LivingEntityMixin", "MinecraftServerMixin", diff --git a/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerEntityLifecycleTests.java b/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerEntityLifecycleTests.java index 4a9ccaddd0..7048879db5 100644 --- a/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerEntityLifecycleTests.java +++ b/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerEntityLifecycleTests.java @@ -50,6 +50,20 @@ public void onInitialize() { } }); + ServerEntityEvents.ALLOW_ADD.register((entity, level) -> { + if (PRINT_SERVER_ENTITY_MESSAGES) { + logger.info("[SERVER] ALLOW FRESH LOAD {} IN {}", entity, level); + } + + return true; + }); + + ServerEntityEvents.AFTER_FRESH_LOAD.register((entity, level) -> { + if (PRINT_SERVER_ENTITY_MESSAGES) { + logger.info("[SERVER] FRESH LOADED {} IN {}", entity, level); + } + }); + ServerEntityEvents.ENTITY_UNLOAD.register((entity, level) -> { this.serverEntities.remove(entity);