diff --git a/src/main/java/xyz/nucleoid/stimuli/event/entity/EntityLeashEvent.java b/src/main/java/xyz/nucleoid/stimuli/event/entity/EntityLeashEvent.java new file mode 100644 index 0000000..d1cb56e --- /dev/null +++ b/src/main/java/xyz/nucleoid/stimuli/event/entity/EntityLeashEvent.java @@ -0,0 +1,41 @@ +package xyz.nucleoid.stimuli.event.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.Nullable; +import xyz.nucleoid.stimuli.event.StimulusEvent; + +public class EntityLeashEvent { + /** + * Called when a leash is attached to either a holding {@link Entity} or a {@link BlockPos}. + * + *

A leash holder can be either a player or an existing leash knot. + * A leash position may or may not contain a leash knot yet. + * + *

Upon return: + *

+ */ + public static final StimulusEvent ATTACH = StimulusEvent.create(Attach.class, ctx -> (entity, leashHolder, leashPos, player, hand) -> { + try { + for (var listener : ctx.getListeners()) { + var result = listener.onAttachLeash(entity, leashHolder, leashPos, player, hand); + if (result != ActionResult.PASS) { + return result; + } + } + } catch (Throwable t) { + ctx.handleException(t); + } + return ActionResult.PASS; + }); + + public interface Attach { + ActionResult onAttachLeash(Entity entity, @Nullable Entity leashHolder, @Nullable BlockPos leashPos, ServerPlayerEntity player, @Nullable Hand hand); + } +} diff --git a/src/main/java/xyz/nucleoid/stimuli/mixin/entity/EntityMixin.java b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/EntityMixin.java new file mode 100644 index 0000000..e11a8ef --- /dev/null +++ b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/EntityMixin.java @@ -0,0 +1,56 @@ +package xyz.nucleoid.stimuli.mixin.entity; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.entity.Entity; +import net.minecraft.entity.Leashable; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import xyz.nucleoid.stimuli.Stimuli; +import xyz.nucleoid.stimuli.event.entity.EntityLeashEvent; + +@Mixin(Entity.class) +public class EntityMixin { + @WrapOperation( + method = "interact", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/entity/Leashable;canLeashAttachTo()Z" + ) + ) + private static boolean onLeashAttach(Leashable leashable, Operation original, @Local(argsOnly = true) PlayerEntity player, @Local(argsOnly = true) Hand hand) { + if (!original.call(leashable)) { + return false; + } + + // Leashable instance is the same as this entity + var entity = (Entity) leashable; + + if (!entity.getWorld().isClient()) { + var serverPlayer = (ServerPlayerEntity) player; + + var events = Stimuli.select(); + + try (var invokers = events.forEntity(serverPlayer)) { + var result = invokers.get(EntityLeashEvent.ATTACH).onAttachLeash(entity, serverPlayer, null, serverPlayer, hand); + if (result == ActionResult.FAIL) { + var stack = player.getStackInHand(hand); + + // notify the client that this action did not go through + int slot = hand == Hand.MAIN_HAND ? serverPlayer.getInventory().selectedSlot : 40; + serverPlayer.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(ScreenHandlerSlotUpdateS2CPacket.UPDATE_PLAYER_INVENTORY_SYNC_ID, 0, slot, stack)); + + return false; + } + } + } + + return true; + } +} diff --git a/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeadItemMixin.java b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeadItemMixin.java new file mode 100644 index 0000000..d6f8d35 --- /dev/null +++ b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeadItemMixin.java @@ -0,0 +1,51 @@ +package xyz.nucleoid.stimuli.mixin.entity; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.entity.Entity; +import net.minecraft.entity.Leashable; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.LeadItem; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import xyz.nucleoid.stimuli.Stimuli; +import xyz.nucleoid.stimuli.event.entity.EntityLeashEvent; + +import java.util.List; +import java.util.function.Predicate; + +@Mixin(LeadItem.class) +public class LeadItemMixin { + @WrapOperation( + method = "attachHeldMobsToBlock", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/LeadItem;collectLeashablesAround(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Ljava/util/function/Predicate;)Ljava/util/List;" + ) + ) + private static List onLeashAttachToBlock(World world, BlockPos pos, Predicate predicate, Operation> original, @Local(argsOnly = true) PlayerEntity player) { + return original.call(world, pos, predicate).stream() + .filter(leashable -> { + // Leashable is a filtered Entity instance + var entity = (Entity) leashable; + var serverPlayer = (ServerPlayerEntity) player; + + var events = Stimuli.select(); + + try (var invokers = events.forEntity(serverPlayer)) { + var result = invokers.get(EntityLeashEvent.ATTACH).onAttachLeash(entity, null, pos, serverPlayer, null); + if (result == ActionResult.FAIL) { + return false; + } + } + + return true; + }) + .toList(); + } +} diff --git a/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeashKnotEntityMixin.java b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeashKnotEntityMixin.java new file mode 100644 index 0000000..09ba126 --- /dev/null +++ b/src/main/java/xyz/nucleoid/stimuli/mixin/entity/LeashKnotEntityMixin.java @@ -0,0 +1,53 @@ +package xyz.nucleoid.stimuli.mixin.entity; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import net.minecraft.entity.Entity; +import net.minecraft.entity.Leashable; +import net.minecraft.entity.decoration.LeashKnotEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import xyz.nucleoid.stimuli.Stimuli; +import xyz.nucleoid.stimuli.event.entity.EntityLeashEvent; + +@Mixin(LeashKnotEntity.class) +public class LeashKnotEntityMixin { + @WrapOperation( + method = "interact", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/entity/Leashable;getLeashHolder()Lnet/minecraft/entity/Entity;", + ordinal = 0 + ) + ) + private Entity onLeashAttachToKnot(Leashable leashable, Operation original, @Local(argsOnly = true) PlayerEntity player, @Local(argsOnly = true) Hand hand, @Share("attached") LocalBooleanRef attached) { + // Leashable is a filtered Entity instance + var entity = (Entity) leashable; + + var leashHolder = (Entity) (Object) this; + var currentHolder = original.call(leashable); + + // Vanilla reattaches leashes already attached to this knot, so these cases must be filtered out + if (leashHolder != currentHolder) { + var serverPlayer = (ServerPlayerEntity) player; + + var events = Stimuli.select(); + + try (var invokers = events.forEntity(serverPlayer)) { + var result = invokers.get(EntityLeashEvent.ATTACH).onAttachLeash(entity, leashHolder, null, serverPlayer, hand); + if (result == ActionResult.FAIL) { + return null; + } + } + } + + return currentHolder; + } +} diff --git a/src/main/resources/stimuli.mixins.json b/src/main/resources/stimuli.mixins.json index d824846..6478e5c 100644 --- a/src/main/resources/stimuli.mixins.json +++ b/src/main/resources/stimuli.mixins.json @@ -16,6 +16,9 @@ "block.FarmlandBlockMixin", "block.FlowerPotBlockMixin", "block.TurtleEggBlockMixin", + "entity.EntityMixin", + "entity.LeadItemMixin", + "entity.LeashKnotEntityMixin", "entity.LivingEntityMixin", "entity.ShearableEntityMixin", "entity.ShearsDispenserBehaviorMixin", diff --git a/src/testmod/java/xyz/nucleoid/stimuli/test/StimuliInitializer.java b/src/testmod/java/xyz/nucleoid/stimuli/test/StimuliInitializer.java index 0e7464d..6b1c804 100644 --- a/src/testmod/java/xyz/nucleoid/stimuli/test/StimuliInitializer.java +++ b/src/testmod/java/xyz/nucleoid/stimuli/test/StimuliInitializer.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import xyz.nucleoid.stimuli.Stimuli; import xyz.nucleoid.stimuli.event.block.FlowerPotModifyEvent; +import xyz.nucleoid.stimuli.event.entity.EntityLeashEvent; import xyz.nucleoid.stimuli.event.entity.EntityShearEvent; import xyz.nucleoid.stimuli.event.projectile.ArrowFireEvent; import xyz.nucleoid.stimuli.event.world.ExplosionDetonatedEvent; @@ -58,5 +59,31 @@ public void onInitialize() { Stimuli.global().listen(ExplosionDetonatedEvent.EVENT, (explosion, particles) -> { server.sendMessage(Text.literal("ExplosionDetonatedEvent: " + explosion.getDestructionType().name())); }); + Stimuli.global().listen(EntityLeashEvent.ATTACH, (entity, leashHolder, leashPos, player, hand) -> { + var message = Text.literal("EntityLeashEvent.Attach: ") + .append(entity.getName()); + + if (leashHolder != null) { + message + .append(" to ") + .append(leashHolder.getName()); + } + + if (hand != null) { + message.append(" with " + hand); + } + + if (leashPos != null) { + message.append(" at " + leashPos); + } + + if (player == null) { + server.sendMessage(message); + } else { + player.sendMessage(message); + } + + return result; + }); } }