From 569075ccc791be3a6f3dd4b9fd913da3fe48c6b1 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Thu, 15 Apr 2021 19:26:07 +0800 Subject: [PATCH] new event system --- .../event/CompoundEventResult.java | 114 ++++++++++++++++++ .../architectury/event/EventActor.java | 25 ++++ .../architectury/event/EventFactory.java | 98 +++++++++++++++ .../architectury/event/EventResult.java | 85 +++++++++++++ .../event/events/EntityEvent.java | 7 +- .../event/events/InteractionEvent.java | 5 +- .../event/events/PlayerEvent.java | 9 +- .../event/fabric/EventFactoryImpl.java | 12 ++ .../mixin/fabric/MixinBucketItem.java | 8 +- .../mixin/fabric/MixinCatSpawner.java | 4 +- .../mixin/fabric/MixinFarmBlock.java | 28 +++-- .../mixin/fabric/MixinNaturalSpawner.java | 59 +++------ .../mixin/fabric/MixinPatrolSpawner.java | 8 +- .../mixin/fabric/MixinPhantomSpawner.java | 3 +- .../event/forge/EventFactoryImpl.java | 37 +++++- .../event/forge/EventHandlerImplCommon.java | 41 +++---- .../architectury/test/events/DebugEvents.java | 8 +- 17 files changed, 450 insertions(+), 101 deletions(-) create mode 100644 common/src/main/java/me/shedaniel/architectury/event/CompoundEventResult.java create mode 100644 common/src/main/java/me/shedaniel/architectury/event/EventActor.java create mode 100644 common/src/main/java/me/shedaniel/architectury/event/EventResult.java diff --git a/common/src/main/java/me/shedaniel/architectury/event/CompoundEventResult.java b/common/src/main/java/me/shedaniel/architectury/event/CompoundEventResult.java new file mode 100644 index 00000000..368982fe --- /dev/null +++ b/common/src/main/java/me/shedaniel/architectury/event/CompoundEventResult.java @@ -0,0 +1,114 @@ +/* + * This file is part of architectury. + * Copyright (C) 2020, 2021 architectury + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package me.shedaniel.architectury.event; + +import net.minecraft.world.InteractionResultHolder; + +/** + * A result from an event, determines if the event should continue to other listeners, + * determines the outcome of the event, and provides extra result for the outcome. + * + * @param the type of the extra result + * @see #pass() + * @see #interrupt(Boolean, Object) + * @see CompoundEventResult + */ +public class CompoundEventResult { + private static final CompoundEventResult PASS = new CompoundEventResult<>(EventResult.pass(), null); + private final EventResult result; + private final T object; + + /** + * Passes the event to other listeners, and does not set an outcome of the event. + * + * @param the type of the extra result + * @return an event that passes the event to other listeners + */ + public static CompoundEventResult pass() { + return (CompoundEventResult) PASS; + } + + /** + * Interrupts the event and stops it from being passed on to other listeners, + * may or may not set an outcome and extra data of the event. + * + * @param value the outcome of the event, passing {@code null} here means the default outcome, + * which often means falling back to vanilla logic + * @param object the extra data of the result, this usually is the returning value of the event + * @return an event that interrupts the event + */ + public static CompoundEventResult interrupt(Boolean value, T object) { + return new CompoundEventResult<>(EventResult.interrupt(value), object); + } + + private CompoundEventResult(EventResult result, T object) { + this.result = result; + this.object = object; + } + + /** + * Returns whether this result interrupts the evaluation of other listeners. + * + * @return whether this result interrupts the evaluation of other listeners + */ + public boolean interruptsFurtherEvaluation() { + return result.interruptsFurtherEvaluation(); + } + + /** + * Returns the outcome of the result, an passing result will never have an outcome. + * + * @return the outcome of the result, returns {@code null} if fallback + */ + public Boolean value() { + return result.value(); + } + + /** + * Returns the {@link EventResult} view of the result, this returns the same values as + * {@link #interruptsFurtherEvaluation()} and {@link #value()}. + * + * @return the {@link EventResult} view of the result. + */ + public EventResult result() { + return result; + } + + /** + * Returns the extra data of the result, an passing result will never contain any extra data. + * + * @return the extra data of the result, returns {@code null} if passing + */ + public T object() { + return object; + } + + /** + * Returns the Minecraft-facing result, however ignores {@link #interruptsFurtherEvaluation()}. + * + * @return the Minecraft-facing result + */ + public InteractionResultHolder asMinecraft() { + if (value() != null) { + return value() ? InteractionResultHolder.success(object()) : InteractionResultHolder.fail(object()); + } + return InteractionResultHolder.pass(object()); + } +} diff --git a/common/src/main/java/me/shedaniel/architectury/event/EventActor.java b/common/src/main/java/me/shedaniel/architectury/event/EventActor.java new file mode 100644 index 00000000..bb3c7f95 --- /dev/null +++ b/common/src/main/java/me/shedaniel/architectury/event/EventActor.java @@ -0,0 +1,25 @@ +/* + * This file is part of architectury. + * Copyright (C) 2020, 2021 architectury + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package me.shedaniel.architectury.event; + +@FunctionalInterface +public interface EventActor { + EventResult act(T t); +} diff --git a/common/src/main/java/me/shedaniel/architectury/event/EventFactory.java b/common/src/main/java/me/shedaniel/architectury/event/EventFactory.java index f6bce4b3..ba678f4b 100644 --- a/common/src/main/java/me/shedaniel/architectury/event/EventFactory.java +++ b/common/src/main/java/me/shedaniel/architectury/event/EventFactory.java @@ -99,6 +99,28 @@ public final class EventFactory { })); } + @SafeVarargs + public static Event createEventResult(T... typeGetter) { + if (typeGetter.length != 0) throw new IllegalStateException("array must be empty!"); + return createEventResult((Class) typeGetter.getClass().getComponentType()); + } + + @SuppressWarnings("UnstableApiUsage") + public static Event createEventResult(Class clazz) { + return of(listeners -> (T) Proxy.newProxyInstance(EventFactory.class.getClassLoader(), new Class[]{clazz}, new AbstractInvocationHandler() { + @Override + protected Object handleInvocation(@NotNull Object proxy, @NotNull Method method, Object @NotNull [] args) throws Throwable { + for (T listener : listeners) { + EventResult result = (EventResult) Objects.requireNonNull(method.invoke(listener, args)); + if (result.interruptsFurtherEvaluation()) { + return result; + } + } + return EventResult.pass(); + } + })); + } + @SafeVarargs public static Event createInteractionResultHolder(T... typeGetter) { if (typeGetter.length != 0) throw new IllegalStateException("array must be empty!"); @@ -121,6 +143,28 @@ public final class EventFactory { })); } + @SafeVarargs + public static Event createCompoundEventResult(T... typeGetter) { + if (typeGetter.length != 0) throw new IllegalStateException("array must be empty!"); + return createCompoundEventResult((Class) typeGetter.getClass().getComponentType()); + } + + @SuppressWarnings("UnstableApiUsage") + public static Event createCompoundEventResult(Class clazz) { + return of(listeners -> (T) Proxy.newProxyInstance(EventFactory.class.getClassLoader(), new Class[]{clazz}, new AbstractInvocationHandler() { + @Override + protected Object handleInvocation(@NotNull Object proxy, @NotNull Method method, Object @NotNull [] args) throws Throwable { + for (T listener : listeners) { + CompoundEventResult result = (CompoundEventResult) Objects.requireNonNull(method.invoke(listener, args)); + if (result.interruptsFurtherEvaluation()) { + return result; + } + } + return CompoundEventResult.pass(); + } + })); + } + @SafeVarargs public static Event> createConsumerLoop(T... typeGetter) { if (typeGetter.length != 0) throw new IllegalStateException("array must be empty!"); @@ -187,21 +231,75 @@ public final class EventFactory { return event; } + @SafeVarargs + public static Event> createEventActorLoop(T... typeGetter) { + if (typeGetter.length != 0) throw new IllegalStateException("array must be empty!"); + return createEventActorLoop((Class) typeGetter.getClass().getComponentType()); + } + + @SuppressWarnings("UnstableApiUsage") + public static Event> createEventActorLoop(Class clazz) { + Event> event = of(listeners -> (EventActor) Proxy.newProxyInstance(EventFactory.class.getClassLoader(), new Class[]{EventActor.class}, new AbstractInvocationHandler() { + @Override + protected Object handleInvocation(@NotNull Object proxy, @NotNull Method method, Object @NotNull [] args) throws Throwable { + for (EventActor listener : listeners) { + EventResult result = (EventResult) method.invoke(listener, args); + if (result.interruptsFurtherEvaluation()) { + return result; + } + } + return EventResult.pass(); + } + })); + Class superClass = clazz; + do { + + if (superClass.isAnnotationPresent(ForgeEventCancellable.class)) { + return attachToForgeEventActorCancellable(event); + } + superClass = superClass.getSuperclass(); + } while (superClass != null); + superClass = clazz; + do { + + if (superClass.isAnnotationPresent(ForgeEvent.class)) { + return attachToForgeEventActor(event); + } + superClass = superClass.getSuperclass(); + } while (superClass != null); + return event; + } + @ExpectPlatform + @ApiStatus.Internal public static Event> attachToForge(Event> event) { throw new AssertionError(); } @ExpectPlatform + @ApiStatus.Internal public static Event> attachToForgeActor(Event> event) { throw new AssertionError(); } @ExpectPlatform + @ApiStatus.Internal public static Event> attachToForgeActorCancellable(Event> event) { throw new AssertionError(); } + @ExpectPlatform + @ApiStatus.Internal + public static Event> attachToForgeEventActor(Event> event) { + throw new AssertionError(); + } + + @ExpectPlatform + @ApiStatus.Internal + public static Event> attachToForgeEventActorCancellable(Event> event) { + throw new AssertionError(); + } + private static class EventImpl implements Event { private final Function, T> function; private T invoker = null; diff --git a/common/src/main/java/me/shedaniel/architectury/event/EventResult.java b/common/src/main/java/me/shedaniel/architectury/event/EventResult.java new file mode 100644 index 00000000..39ca7bc3 --- /dev/null +++ b/common/src/main/java/me/shedaniel/architectury/event/EventResult.java @@ -0,0 +1,85 @@ +/* + * This file is part of architectury. + * Copyright (C) 2020, 2021 architectury + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package me.shedaniel.architectury.event; + +/** + * A result from an event, determines if the event should continue to other listeners, + * and determines the outcome of the event. + * + * @see #pass() + * @see #interrupt(Boolean) + * @see CompoundEventResult + */ +public final class EventResult { + private static final EventResult TRUE = new EventResult(true, true); + private static final EventResult STOP = new EventResult(true, null); + private static final EventResult PASS = new EventResult(false, null); + private static final EventResult FALSE = new EventResult(true, false); + + /** + * Passes the event to other listeners, and does not set an outcome of the event. + * + * @return an event that passes the event to other listeners + */ + public static EventResult pass() { + return PASS; + } + + /** + * Interrupts the event and stops it from being passed on to other listeners, + * may or may not set an outcome of the event. + * + * @param value the outcome of the event, passing {@code null} here means the default outcome, + * which often means falling back to vanilla logic + * @return an event that interrupts the event + */ + public static EventResult interrupt(Boolean value) { + if (value == null) return STOP; + if (value) return TRUE; + return FALSE; + } + + private final boolean interruptsFurtherEvaluation; + + private final Boolean value; + + EventResult(boolean interruptsFurtherEvaluation, Boolean value) { + this.interruptsFurtherEvaluation = interruptsFurtherEvaluation; + this.value = value; + } + + /** + * Returns whether this result interrupts the evaluation of other listeners. + * + * @return whether this result interrupts the evaluation of other listeners + */ + public boolean interruptsFurtherEvaluation() { + return interruptsFurtherEvaluation; + } + + /** + * Returns the outcome of the result, an passing result will never have an outcome. + * + * @return the outcome of the result, returns {@code null} if fallback + */ + public Boolean value() { + return value; + } +} diff --git a/common/src/main/java/me/shedaniel/architectury/event/events/EntityEvent.java b/common/src/main/java/me/shedaniel/architectury/event/events/EntityEvent.java index 3c55e814..aac223af 100644 --- a/common/src/main/java/me/shedaniel/architectury/event/events/EntityEvent.java +++ b/common/src/main/java/me/shedaniel/architectury/event/events/EntityEvent.java @@ -21,6 +21,7 @@ package me.shedaniel.architectury.event.events; import me.shedaniel.architectury.event.Event; import me.shedaniel.architectury.event.EventFactory; +import me.shedaniel.architectury.event.EventResult; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; @@ -49,13 +50,13 @@ public interface EntityEvent { */ Event ADD = EventFactory.createInteractionResult(); /** - * Invoked when an entity enters a chunk. + * Invoked when an entity enters a chunk, equivalent to forge's {@code EnteringChunk} */ Event ENTER_CHUNK = EventFactory.createLoop(); /** * Invoked when an entity is about to be spawned, equivalent to forge's {@code LivingSpawnEvent.CheckSpawn} */ - Event CHECK_SPAWN = EventFactory.createInteractionResult(); + Event CHECK_SPAWN = EventFactory.createEventResult(); /** * @deprecated use {@link BlockEvent#PLACE} @@ -85,6 +86,6 @@ public interface EntityEvent { } interface CheckSpawn { - InteractionResult canSpawn(Entity entity, LevelAccessor world, double x, double y, double z, MobSpawnType type, @Nullable BaseSpawner spawner); + EventResult canSpawn(Entity entity, LevelAccessor world, double x, double y, double z, MobSpawnType type, @Nullable BaseSpawner spawner); } } diff --git a/common/src/main/java/me/shedaniel/architectury/event/events/InteractionEvent.java b/common/src/main/java/me/shedaniel/architectury/event/events/InteractionEvent.java index 646ce603..8e62f002 100644 --- a/common/src/main/java/me/shedaniel/architectury/event/events/InteractionEvent.java +++ b/common/src/main/java/me/shedaniel/architectury/event/events/InteractionEvent.java @@ -21,6 +21,7 @@ package me.shedaniel.architectury.event.events; import me.shedaniel.architectury.event.Event; import me.shedaniel.architectury.event.EventFactory; +import me.shedaniel.architectury.event.EventResult; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.InteractionHand; @@ -42,7 +43,7 @@ public interface InteractionEvent { /** * Invoked before a farmland block is trampled by an entity, equivalent to forge's {@code BlockEvent.FarmlandTrampleEvent} */ - Event FARMLAND_TRAMPLE = EventFactory.createInteractionResult(); + Event FARMLAND_TRAMPLE = EventFactory.createEventResult(); interface RightClickBlock { InteractionResult click(Player player, InteractionHand hand, BlockPos pos, Direction face); @@ -73,6 +74,6 @@ public interface InteractionEvent { } interface FarmlandTrample { - InteractionResult trample(Level world, BlockPos pos, BlockState state, float distance, Entity entity); + EventResult trample(Level world, BlockPos pos, BlockState state, float distance, Entity entity); } } diff --git a/common/src/main/java/me/shedaniel/architectury/event/events/PlayerEvent.java b/common/src/main/java/me/shedaniel/architectury/event/events/PlayerEvent.java index 503f7357..caa121a6 100644 --- a/common/src/main/java/me/shedaniel/architectury/event/events/PlayerEvent.java +++ b/common/src/main/java/me/shedaniel/architectury/event/events/PlayerEvent.java @@ -19,6 +19,7 @@ package me.shedaniel.architectury.event.events; +import me.shedaniel.architectury.event.CompoundEventResult; import me.shedaniel.architectury.event.Event; import me.shedaniel.architectury.event.EventFactory; import me.shedaniel.architectury.utils.IntValue; @@ -28,14 +29,12 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.InteractionResult; -import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -57,11 +56,11 @@ public interface PlayerEvent { /** * Invoked when a player attempts to fill a bucket using right-click. * You can return a non-PASS interaction result to cancel further processing by other mods. - * + *

* On Forge, FAIL cancels the event, and SUCCESS sets the event as handled. * On Fabric, any non-PASS result is returned directly and immediately. */ - Event FILL_BUCKET = EventFactory.createInteractionResultHolder(); + Event FILL_BUCKET = EventFactory.createCompoundEventResult(); /** * @deprecated use {@link BlockEvent#BREAK} @@ -127,6 +126,6 @@ public interface PlayerEvent { } interface FillBucket { - InteractionResultHolder fill(Player player, Level level, ItemStack stack, @Nullable HitResult target); + CompoundEventResult fill(Player player, Level level, ItemStack stack, @Nullable HitResult target); } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/event/fabric/EventFactoryImpl.java b/fabric/src/main/java/me/shedaniel/architectury/event/fabric/EventFactoryImpl.java index 96261c60..f2f18400 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/event/fabric/EventFactoryImpl.java +++ b/fabric/src/main/java/me/shedaniel/architectury/event/fabric/EventFactoryImpl.java @@ -21,6 +21,8 @@ package me.shedaniel.architectury.event.fabric; import me.shedaniel.architectury.event.Actor; import me.shedaniel.architectury.event.Event; +import me.shedaniel.architectury.event.EventActor; +import org.jetbrains.annotations.ApiStatus; import java.util.function.Consumer; @@ -36,4 +38,14 @@ public class EventFactoryImpl { public static Event> attachToForgeActorCancellable(Event> event) { return event; } + + @ApiStatus.Internal + public static Event> attachToForgeEventActor(Event> event) { + return event; + } + + @ApiStatus.Internal + public static Event> attachToForgeEventActorCancellable(Event> event) { + return event; + } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinBucketItem.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinBucketItem.java index 7cd65381..da207df4 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinBucketItem.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinBucketItem.java @@ -19,9 +19,9 @@ package me.shedaniel.architectury.mixin.fabric; +import me.shedaniel.architectury.event.CompoundEventResult; import me.shedaniel.architectury.event.events.PlayerEvent; import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BucketItem; @@ -48,9 +48,9 @@ public class MixinBucketItem { cancellable = true ) public void fillBucket(Level level, Player player, InteractionHand hand, CallbackInfoReturnable> cir, ItemStack stack, HitResult target) { - InteractionResultHolder event = PlayerEvent.FILL_BUCKET.invoker().fill(player, level, stack, target); - if (event.getResult() != InteractionResult.PASS) { - cir.setReturnValue(event); + CompoundEventResult result = PlayerEvent.FILL_BUCKET.invoker().fill(player, level, stack, target); + if (result.interruptsFurtherEvaluation() && result.value() != null) { + cir.setReturnValue(result.asMinecraft()); cir.cancel(); } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinCatSpawner.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinCatSpawner.java index f0ea55a1..ab8f2ac5 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinCatSpawner.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinCatSpawner.java @@ -16,6 +16,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + package me.shedaniel.architectury.mixin.fabric; import me.shedaniel.architectury.event.events.EntityEvent; @@ -38,7 +39,6 @@ import java.util.Random; @Mixin(CatSpawner.class) public abstract class MixinCatSpawner { - @Inject( method = "spawnCat", at = @At( @@ -50,7 +50,7 @@ public abstract class MixinCatSpawner { locals = LocalCapture.CAPTURE_FAILHARD ) private void checkCatSpawn(BlockPos pos, ServerLevel level, CallbackInfoReturnable cir, Cat entity) { - if (EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.NATURAL, null) == InteractionResult.FAIL) { + if (EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.NATURAL, null).value() == Boolean.FALSE) { cir.setReturnValue(0); cir.cancel(); } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinFarmBlock.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinFarmBlock.java index 11390a49..43a491e9 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinFarmBlock.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinFarmBlock.java @@ -21,33 +21,45 @@ package me.shedaniel.architectury.mixin.fabric; import me.shedaniel.architectury.event.events.InteractionEvent; import net.minecraft.core.BlockPos; +import net.minecraft.util.Tuple; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.FarmBlock; import net.minecraft.world.level.block.state.BlockState; +import org.apache.commons.lang3.tuple.Triple; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(FarmBlock.class) public abstract class MixinFarmBlock { + @Unique + private static ThreadLocal> turnToDirtLocal = new ThreadLocal<>(); - @Shadow - public static void turnToDirt(BlockState blockState, Level level, BlockPos blockPos) { - } - - @Redirect( + @Inject( method = "fallOn", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/level/block/FarmBlock;turnToDirt(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)V" ) ) - private void redirectTurnToDirt(BlockState state, Level level, BlockPos pos, Level l2, BlockPos pos2, Entity entity, float f) { - if (InteractionEvent.FARMLAND_TRAMPLE.invoker().trample(level, pos, state, f, entity) == InteractionResult.PASS) { - turnToDirt(state, level, pos); + private void fallOn(Level level, BlockPos blockPos, Entity entity, float f, CallbackInfo ci) { + turnToDirtLocal.set(Triple.of(blockPos.asLong(), f, entity)); + } + + @Inject(method = "turnToDirt", at = @At("HEAD"), cancellable = true) + private static void turnToDirt(BlockState state, Level level, BlockPos pos, CallbackInfo ci) { + Triple triple = turnToDirtLocal.get(); + turnToDirtLocal.remove(); + if (triple != null && triple.getLeft() == pos.asLong()) { + if (InteractionEvent.FARMLAND_TRAMPLE.invoker().trample(level, pos, state, triple.getMiddle(), triple.getRight()).value() != null) { + ci.cancel(); + } } } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinNaturalSpawner.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinNaturalSpawner.java index 4a276e93..29426307 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinNaturalSpawner.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinNaturalSpawner.java @@ -16,8 +16,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + package me.shedaniel.architectury.mixin.fabric; +import me.shedaniel.architectury.event.EventResult; import me.shedaniel.architectury.event.events.EntityEvent; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; @@ -49,38 +51,11 @@ import java.util.Random; @Mixin(NaturalSpawner.class) public abstract class MixinNaturalSpawner { - - @Unique - private static int arch$naturalSpawnOverride = 0; - @Unique - private static boolean arch$skipChunkGenSpawn = false; - @Shadow private static boolean isValidPositionForMob(ServerLevel serverLevel, Mob mob, double d) { return false; } - @Inject( - method = "spawnCategoryForPosition", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/NaturalSpawner;isValidPositionForMob(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Mob;D)Z", - ordinal = 0 - ), - locals = LocalCapture.CAPTURE_FAILHARD - ) - private static void checkNaturalSpawn(MobCategory cat, ServerLevel level, ChunkAccess ca, BlockPos pos, SpawnPredicate pred, AfterSpawnCallback cb, CallbackInfo ci, - StructureFeatureManager sfm, ChunkGenerator cg, int i, BlockPos.MutableBlockPos mutablePos, int j, int k, int l, int m, int n, - SpawnerData spawnerData, SpawnGroupData sgd, int o, int p, int q, double d, double e, Player player, double f, Mob entity) { - InteractionResult result = EntityEvent.CHECK_SPAWN.invoker() - .canSpawn(entity, level, d, i, e, MobSpawnType.NATURAL, null); - if (result == InteractionResult.FAIL) { - arch$naturalSpawnOverride = -1; - } else if (result == InteractionResult.SUCCESS || result == InteractionResult.CONSUME) { - arch$naturalSpawnOverride = 1; - } - } - @Redirect( method = "spawnCategoryForPosition", at = @At( @@ -90,23 +65,12 @@ public abstract class MixinNaturalSpawner { ) ) private static boolean overrideNaturalSpawnCondition(ServerLevel level, Mob entity, double f) { - return arch$naturalSpawnOverride != -1 && (arch$naturalSpawnOverride == 1 || isValidPositionForMob(level, entity, f)); - } - - @Inject( - method = "spawnMobsForChunkGeneration", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/Mob;checkSpawnRules(Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/world/entity/MobSpawnType;)Z", - ordinal = 0 - ), - locals = LocalCapture.CAPTURE_FAILHARD - ) - private static void checkChunkGenSpawn(ServerLevelAccessor level, Biome biome, int cx, int cz, Random dx, CallbackInfo ci, - MobSpawnSettings settings, List list, int k, int l, SpawnerData spawnerData, int m, SpawnGroupData sgd, - int n, int o, int p, int q, int r, boolean b, int s, BlockPos pos, double d, double e, Entity entity) { - arch$skipChunkGenSpawn = EntityEvent.CHECK_SPAWN.invoker() - .canSpawn(entity, level, d, pos.getY(), e, MobSpawnType.CHUNK_GENERATION, null) == InteractionResult.FAIL; + EventResult result = EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, entity.xOld, entity.yOld, entity.zOld, MobSpawnType.NATURAL, null); + if (result.value() != null) { + return result.value(); + } else { + return isValidPositionForMob(level, entity, f); + } } @Redirect( @@ -118,7 +82,12 @@ public abstract class MixinNaturalSpawner { ) ) private static boolean overrideChunkGenSpawnCondition(Mob mob, LevelAccessor level, MobSpawnType type) { - return !arch$skipChunkGenSpawn && mob.checkSpawnRules(level, type); + EventResult result = EntityEvent.CHECK_SPAWN.invoker().canSpawn(mob, level, mob.xOld, mob.yOld, mob.zOld, MobSpawnType.CHUNK_GENERATION, null); + if (result.value() != null) { + return result.value(); + } else { + return mob.checkSpawnRules(level, type); + } } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPatrolSpawner.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPatrolSpawner.java index c447ac79..eb03bcdd 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPatrolSpawner.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPatrolSpawner.java @@ -16,8 +16,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + package me.shedaniel.architectury.mixin.fabric; +import me.shedaniel.architectury.event.EventResult; import me.shedaniel.architectury.event.events.EntityEvent; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; @@ -50,9 +52,9 @@ public abstract class MixinPatrolSpawner { locals = LocalCapture.CAPTURE_FAILHARD ) private void checkPatrolSpawn(ServerLevel level, BlockPos pos, Random r, boolean b, CallbackInfoReturnable cir, PatrollingMonster entity) { - if (EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.PATROL, null) == InteractionResult.FAIL) { - cir.setReturnValue(false); - cir.cancel(); + EventResult result = EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.PATROL, null); + if (result.value() != null) { + cir.setReturnValue(result.value()); } } } diff --git a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPhantomSpawner.java b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPhantomSpawner.java index bd4f8dff..a24a7950 100644 --- a/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPhantomSpawner.java +++ b/fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/MixinPhantomSpawner.java @@ -16,6 +16,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + package me.shedaniel.architectury.mixin.fabric; import me.shedaniel.architectury.event.events.EntityEvent; @@ -55,7 +56,7 @@ public abstract class MixinPhantomSpawner { private void checkPhantomSpawn(ServerLevel level, boolean bl, boolean bl2, CallbackInfoReturnable cir, Random random, int i, Iterator it, Player player, BlockPos pos, DifficultyInstance diff, BlockPos pos2, SpawnGroupData sgd, int l, int m, Phantom entity) { - if (EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.NATURAL, null) == InteractionResult.FAIL) { + if (EntityEvent.CHECK_SPAWN.invoker().canSpawn(entity, level, pos.getX(), pos.getY(), pos.getZ(), MobSpawnType.NATURAL, null).value() == Boolean.FALSE) { cir.setReturnValue(0); cir.cancel(); } diff --git a/forge/src/main/java/me/shedaniel/architectury/event/forge/EventFactoryImpl.java b/forge/src/main/java/me/shedaniel/architectury/event/forge/EventFactoryImpl.java index 69ab8929..5e397894 100644 --- a/forge/src/main/java/me/shedaniel/architectury/event/forge/EventFactoryImpl.java +++ b/forge/src/main/java/me/shedaniel/architectury/event/forge/EventFactoryImpl.java @@ -21,8 +21,11 @@ package me.shedaniel.architectury.event.forge; import me.shedaniel.architectury.event.Actor; import me.shedaniel.architectury.event.Event; +import me.shedaniel.architectury.event.EventActor; +import me.shedaniel.architectury.event.EventResult; import net.minecraft.world.InteractionResult; import net.minecraftforge.common.MinecraftForge; +import org.jetbrains.annotations.ApiStatus; import java.util.function.Consumer; @@ -63,4 +66,36 @@ public class EventFactoryImpl { }); return event; } -} \ No newline at end of file + + @ApiStatus.Internal + public static Event> attachToForgeEventActor(Event> event) { + event.register(eventObj -> { + if (!(eventObj instanceof net.minecraftforge.eventbus.api.Event)) { + throw new ClassCastException(eventObj.getClass() + " is not an instance of forge Event!"); + } + if (!((net.minecraftforge.eventbus.api.Event) eventObj).isCancelable()) { + throw new ClassCastException(eventObj.getClass() + " is not cancellable Event!"); + } + MinecraftForge.EVENT_BUS.post((net.minecraftforge.eventbus.api.Event) eventObj); + return EventResult.pass(); + }); + return event; + } + + @ApiStatus.Internal + public static Event> attachToForgeEventActorCancellable(Event> event) { + event.register(eventObj -> { + if (!(eventObj instanceof net.minecraftforge.eventbus.api.Event)) { + throw new ClassCastException(eventObj.getClass() + " is not an instance of forge Event!"); + } + if (!((net.minecraftforge.eventbus.api.Event) eventObj).isCancelable()) { + throw new ClassCastException(eventObj.getClass() + " is not cancellable Event!"); + } + if (MinecraftForge.EVENT_BUS.post((net.minecraftforge.eventbus.api.Event) eventObj)) { + return EventResult.interrupt(false); + } + return EventResult.pass(); + }); + return event; + } +} diff --git a/forge/src/main/java/me/shedaniel/architectury/event/forge/EventHandlerImplCommon.java b/forge/src/main/java/me/shedaniel/architectury/event/forge/EventHandlerImplCommon.java index e8cf5407..beb619d6 100644 --- a/forge/src/main/java/me/shedaniel/architectury/event/forge/EventHandlerImplCommon.java +++ b/forge/src/main/java/me/shedaniel/architectury/event/forge/EventHandlerImplCommon.java @@ -19,6 +19,8 @@ package me.shedaniel.architectury.event.forge; +import me.shedaniel.architectury.event.CompoundEventResult; +import me.shedaniel.architectury.event.EventResult; import me.shedaniel.architectury.event.events.PlayerEvent; import me.shedaniel.architectury.event.events.*; import me.shedaniel.architectury.utils.IntValue; @@ -217,7 +219,7 @@ public class EventHandlerImplCommon { @SubscribeEvent(priority = EventPriority.HIGH) public static void event(FarmlandTrampleEvent event) { - if (InteractionEvent.FARMLAND_TRAMPLE.invoker().trample((Level) event.getWorld(), event.getPos(), event.getState(), event.getFallDistance(), event.getEntity()) == InteractionResult.FAIL) { + if (InteractionEvent.FARMLAND_TRAMPLE.invoker().trample((Level) event.getWorld(), event.getPos(), event.getState(), event.getFallDistance(), event.getEntity()).value() != null) { event.setCanceled(true); } } @@ -225,18 +227,14 @@ public class EventHandlerImplCommon { @SubscribeEvent(priority = EventPriority.HIGH) public static void event(FillBucketEvent event) { ItemStack oldItem = event.getEmptyBucket(); - InteractionResultHolder result = PlayerEvent.FILL_BUCKET.invoker().fill(event.getPlayer(), event.getWorld(), oldItem, event.getTarget()); - switch (result.getResult()) { - case FAIL: - event.setCanceled(true); - break; - case SUCCESS: - case CONSUME: - event.setResult(Event.Result.ALLOW); - event.setFilledBucket(result.getObject()); - break; - case PASS: - break; + CompoundEventResult result = PlayerEvent.FILL_BUCKET.invoker().fill(event.getPlayer(), event.getWorld(), oldItem, event.getTarget()); + if (result.interruptsFurtherEvaluation()) { + event.setCanceled(true); + event.setFilledBucket(result.object()); + + if (result.value() != null) { + event.setResult(result.value() ? Event.Result.ALLOW : Event.Result.DENY); + } } } @@ -247,17 +245,12 @@ public class EventHandlerImplCommon { @SubscribeEvent(priority = EventPriority.HIGH) public static void event(LivingSpawnEvent.CheckSpawn event) { - InteractionResult result = EntityEvent.CHECK_SPAWN.invoker().canSpawn(event.getEntity(), event.getWorld(), event.getX(), event.getY(), event.getZ(), event.getSpawnReason(), event.getSpawner()); - switch (result) { - case FAIL: - event.setResult(Event.Result.DENY); - break; - case SUCCESS: - case CONSUME: - event.setResult(Event.Result.ALLOW); - break; - case PASS: - break; + EventResult result = EntityEvent.CHECK_SPAWN.invoker().canSpawn(event.getEntity(), event.getWorld(), event.getX(), event.getY(), event.getZ(), event.getSpawnReason(), event.getSpawner()); + if (result.interruptsFurtherEvaluation()) { + if (result.value() != null) { + event.setResult(result.value() == Boolean.TRUE ? Event.Result.ALLOW : Event.Result.DENY); + } + event.setCanceled(true); } } diff --git a/testmod-common/src/main/java/me/shedaniel/architectury/test/events/DebugEvents.java b/testmod-common/src/main/java/me/shedaniel/architectury/test/events/DebugEvents.java index 50ede5dc..d1895a22 100644 --- a/testmod-common/src/main/java/me/shedaniel/architectury/test/events/DebugEvents.java +++ b/testmod-common/src/main/java/me/shedaniel/architectury/test/events/DebugEvents.java @@ -20,6 +20,8 @@ package me.shedaniel.architectury.test.events; import com.mojang.blaze3d.platform.InputConstants; +import me.shedaniel.architectury.event.CompoundEventResult; +import me.shedaniel.architectury.event.EventResult; import me.shedaniel.architectury.event.events.*; import me.shedaniel.architectury.event.events.client.*; import me.shedaniel.architectury.hooks.ExplosionHooks; @@ -118,10 +120,10 @@ public class DebugEvents { }); InteractionEvent.FARMLAND_TRAMPLE.register((level, pos, state, distance, entity) -> { if (entity instanceof Player && ((Player) entity).getItemBySlot(EquipmentSlot.FEET).getItem() == Items.DIAMOND_BOOTS) { - return InteractionResult.FAIL; + return EventResult.interrupt(false); } SINK.accept("%s trampled farmland (%s) at %s in %s (Fall height: %f blocks)", entity, state, pos, level, distance); - return InteractionResult.PASS; + return EventResult.pass(); }); LifecycleEvent.SERVER_BEFORE_START.register(instance -> { SINK.accept("Server ready to start"); @@ -188,7 +190,7 @@ public class DebugEvents { }); PlayerEvent.FILL_BUCKET.register(((player, level, stack, target) -> { SINK.accept("%s used a bucket (%s) in %s%s while looking at %s", player.getScoreboardName(), stack, level.dimension().location(), logSide(level), target == null ? "nothing" : target.getLocation()); - return InteractionResultHolder.pass(null); + return CompoundEventResult.pass(); })); LightningEvent.STRIKE.register((bolt, level, pos, toStrike) -> { SINK.accept(bolt.getScoreboardName() + " struck at " + toShortString(pos) + logSide(level));