diff --git a/library/entity/interaction/build.gradle b/library/entity/interaction/build.gradle
new file mode 100644
index 0000000000..f1a8984282
--- /dev/null
+++ b/library/entity/interaction/build.gradle
@@ -0,0 +1,16 @@
+plugins {
+ id("qsl.module")
+}
+
+qslModule {
+ name = "Quilt Entity Interaction API"
+ moduleName = "interaction"
+ id = "quilt_entity_interaction"
+ description = "An API to support entity interaction events."
+ library = "entity"
+ moduleDependencies {
+ core {
+ api("qsl_base")
+ }
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/DamageContext.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/DamageContext.java
new file mode 100644
index 0000000000..1d966c8ee4
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/DamageContext.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An encapsulation of several arguments relevant to a damage context with a
+ * mutable damage value and cancellation that is shared across all listeners.
+ */
+public class DamageContext {
+
+ private final @NotNull LivingEntity attacker;
+ private final @NotNull ItemStack stack;
+ private final @NotNull Entity target;
+ private final @NotNull DamageSource source;
+ private float damage;
+ private boolean canceled = false;
+
+ public DamageContext(@NotNull LivingEntity attacker, @NotNull ItemStack stack, @NotNull Entity target, @NotNull DamageSource source, float damage) {
+ this.attacker = attacker;
+ this.stack = stack;
+ this.target = target;
+ this.source = source;
+ this.damage = damage;
+ }
+
+ /**
+ * Gets the attacking {@link LivingEntity} in the damage context.
+ * @return the attacking {@link LivingEntity}
+ */
+ public @NotNull LivingEntity getAttacker() {
+ return this.attacker;
+ }
+
+ /**
+ * Gets the {@link ItemStack} in the attacker's main-hand.
+ * @return the {@link ItemStack} used by the attacker
+ */
+ public @NotNull ItemStack getWeapon() {
+ return this.stack;
+ }
+
+ /**
+ * Gets the targeted {@link Entity} in the damage context.
+ * @return the targeted {@link Entity}
+ */
+ public @NotNull Entity getTarget() {
+ return this.target;
+ }
+
+ /**
+ * Gets the {@link DamageSource} used in the damage context.
+ * @return the damage's {@link DamageSource}
+ */
+ public @NotNull DamageSource getDamageSource() {
+ return source;
+ }
+
+ /**
+ * Gets the damage amount in the damage context.
+ * @return the damage amount
+ */
+ public float getDamage() {
+ return damage;
+ }
+
+ /**
+ * Sets the damage amount in the damage context.
+ * @param damage the desired damage value
+ */
+ public void setDamage(float damage) {
+ this.damage = damage;
+ }
+
+ /**
+ * Cancels the damage event.
+ */
+ public void cancel() {
+ this.canceled = true;
+ }
+
+ /**
+ * Returns whether the event has been canceled or not.
+ * @return {@code true} if the event has been canceled, otherwise {@code false}
+ */
+ public boolean isCanceled() {
+ return this.canceled;
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/LivingEntityAttackEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/LivingEntityAttackEvents.java
new file mode 100644
index 0000000000..1c2d292aed
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/LivingEntityAttackEvents.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api;
+
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.item.ItemStack;
+import org.quiltmc.qsl.base.api.event.Event;
+
+/**
+ * Contains events that invoke when a {@link LivingEntity} attacks another.
+ */
+public class LivingEntityAttackEvents {
+
+ /**
+ * A callback that is invoked when a {@link LivingEntity} attacks another
+ * before the damage is dealt.
+ *
+ * This event is cancellable by calling {@link DamageContext#cancel()}.
+ *
+ * Implementations should not assume the hit will go through. Ideally, this
+ * event is used for conditionally cancelling or altering the damage value.
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> context -> {
+ for (var callback : callbacks) {
+ callback.beforeDamage(context);
+
+ if (context.isCanceled()) return;
+ }
+ });
+
+ /**
+ * A callback that is invoked when a {@link LivingEntity} attacks another
+ * after the damage is dealt.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (attacker, stack, target, source, damage) -> {
+ for (var callback : callbacks) {
+ callback.afterDamage(attacker, stack, target, source, damage);
+ }
+ });
+
+
+
+ @FunctionalInterface
+ public interface Before {
+ /**
+ * Invoked before a {@link LivingEntity} damages another.
+ *
+ * @param context the {@link DamageContext} containing all the relevant arguments
+ */
+ void beforeDamage(DamageContext context);
+ }
+
+ @FunctionalInterface
+ public interface After {
+ /**
+ * Invoked after a {@link LivingEntity} damages another.
+ *
+ * @param attacker the attacking entity
+ * @param stack the {@link ItemStack} in the attacker's main-hand
+ * @param target the target {@link LivingEntity}
+ * @param source the {@link DamageSource} of the attack
+ * @param damage the damage that was dealt
+ */
+ void afterDamage(LivingEntity attacker, ItemStack stack, LivingEntity target, DamageSource source, float damage);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackBlockCallback.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackBlockCallback.java
new file mode 100644
index 0000000000..7cad5faf9c
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackBlockCallback.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.base.api.event.Event;
+
+/**
+ * A callback that is invoked if a player attacks (left-clicks) a block.
+ *
+ * Is hooked before the spectator check, so make sure to check the player's game mode!
+ *
+ * - {@link ActionResult#SUCCESS} cancels further processing and, on the client, sends a packet to the server.
+ * - {@link ActionResult#PASS} falls back to further processing.
+ * - {@link ActionResult#FAIL} cancels further processing and does not send a packet to the server.
+ *
+ */
+public interface AttackBlockCallback {
+
+ Event EVENT = Event.create(AttackBlockCallback.class,
+ callbacks -> (player, world, stack, pos, direction) -> {
+ for (AttackBlockCallback callback : callbacks) {
+ ActionResult result = callback.onAttackBlock(player, world,stack, pos, direction);
+
+ if (result != ActionResult.PASS) return result;
+ }
+ return ActionResult.PASS;
+ });
+
+ /**
+ * Invoked if a player attacks (left-clicks) a block.
+ *
+ * @param player the player attacking the block
+ * @param world the world the event is occurring in
+ * @param pos the block's position
+ * @param direction the side of the block hit
+ * @return {@link ActionResult#SUCCESS} to cancel processing and send a packet to the server,
+ * {@link ActionResult#PASS} to fall back to further processing,
+ * {@link ActionResult#FAIL} to cancel further processing
+ */
+ ActionResult onAttackBlock(PlayerEntity player, World world, ItemStack stack, BlockPos pos, Direction direction);
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackEntityEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackEntityEvents.java
new file mode 100644
index 0000000000..99ad1811d2
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/AttackEntityEvents.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ActionResult;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.base.api.event.Event;
+
+public class AttackEntityEvents {
+
+ /**
+ * A callback that is invoked before a {@link PlayerEntity} attacks (left-clicks) an {@link Entity}.
+ *
+ * This is invoked prior to the Spectator check, so make sure you check the game mode!
+ * Implementations should not assume the action has been completed.
+ *
+ * Upon return:
+ *
+ * - {@link ActionResult#SUCCESS} cancels further processing and, on the client, sends a packet to the server.
+ * - {@link ActionResult#PASS} falls back to further processing.
+ * - {@link ActionResult#FAIL} cancels further processing and does not send a packet to the server.
+ *
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> (player, world, stack, entity) -> {
+ for (var callback : callbacks) {
+ ActionResult result = callback.beforeAttackEntity(player, world, stack, entity);
+
+ if (result != ActionResult.PASS) return result;
+ }
+ return ActionResult.PASS;
+ });
+
+ /**
+ * A callback that is invoked after a {@link PlayerEntity} attacks (left-clicks) an {@link Entity}.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (player, world, stack, entity) -> {
+ for (var callback : callbacks) {
+ callback.afterAttackEntity(player, world, stack, entity);
+ }
+ });
+
+ public interface Before {
+ /**
+ * Invoked before a {@link PlayerEntity} attacks (left-clicks) an {@link Entity}.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param entity the hit {@link Entity}
+ * @return {@link ActionResult#SUCCESS} to cancel processing and send packet to the server,
+ * {@link ActionResult#PASS} to fall back to further processing,
+ * {@link ActionResult#FAIL} to cancel further processing
+ */
+ ActionResult beforeAttackEntity(PlayerEntity player, World world, ItemStack stack, Entity entity);
+ }
+
+ public interface After {
+ /**
+ * Invoked after a {@link PlayerEntity} attacks (left-clicks) an {@link Entity}.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param entity the hit {@link Entity}
+ */
+ void afterAttackEntity(PlayerEntity player, World world, ItemStack stack, Entity entity);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/BreakBlockEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/BreakBlockEvents.java
new file mode 100644
index 0000000000..0e7fbb9b87
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/BreakBlockEvents.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
+import org.quiltmc.qsl.base.api.event.Event;
+
+/**
+ * Contains events that invoke when a player breaks a block.
+ */
+public class BreakBlockEvents {
+
+ /**
+ * A callback that is invoked before the {@link PlayerEntity} breaks a block.
+ * It is invoked on both the client and server, but the client end result will
+ * be synced with the server.
+ *
+ * This event can be canceled by returning {@code false}. Returning {@code true}
+ * passes to the next listener. Implementations should not assume the action
+ * has been completed.
+ *
+ * If any listener cancels the event, the block
+ * breaking action will be canceled and {@link BreakBlockEvents#CANCELED}
+ * will be invoked. If the event is completed,
+ * {@link BreakBlockEvents#AFTER} will be invoked.
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> (player, world, stack, pos, state, blockEntity) -> {
+ for (var callback : callbacks) {
+ boolean result = callback.beforePlayerBreaksBlock(player, world, stack, pos, state, blockEntity);
+
+ if (!result) return false;
+ }
+ return true;
+ });
+
+ /**
+ * A callback that is invoked after a block is broken by a {@link PlayerEntity}.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (player, world, stack, pos, state, blockEntity) -> {
+ for (var callback : callbacks) {
+ callback.afterPlayerBreaksBlock(player, world, stack, pos, state, blockEntity);
+ }
+ });
+
+ /**
+ * A callback that is invoked if the block breaking event is canceled.
+ */
+ public static final Event CANCELED = Event.create(Canceled.class,
+ callbacks -> (player, world, stack, pos, state, blockEntity) -> {
+ for (var callback : callbacks) {
+ callback.onCancelPlayerBreaksBlock(player, world, stack, pos, state, blockEntity);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Before {
+ /**
+ * Invoked before a block is broken by a {@link PlayerEntity} and allows cancellation of the action.
+ *
+ * Implementations should not assume the event has completed.
+ *
+ * @param player the {@link PlayerEntity} breaking the block
+ * @param world the {@link World} the block in broken in
+ * @param pos the {@link BlockPos} of the block
+ * @param state the {@link BlockState} before the block is broken
+ * @param blockEntity the {@link BlockEntity} before the block is broken, can be {@code null}
+ * @return {@code false} to cancel the event and the block breaking action,
+ * otherwise {@code true} to pass to the next listener
+ */
+ boolean beforePlayerBreaksBlock(PlayerEntity player, World world, ItemStack stack, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity);
+ }
+
+ @FunctionalInterface
+ public interface After {
+ /**
+ * Invoked after a block has been broken by a {@link PlayerEntity}.
+ *
+ * @param player the {@link PlayerEntity} breaking the block
+ * @param world the {@link World} the block in broken in
+ * @param pos the {@link BlockPos} of the block
+ * @param state the {@link BlockState} before the block is broken
+ * @param blockEntity the {@link BlockEntity} before the block is broken, can be {@code null}
+ */
+ void afterPlayerBreaksBlock(PlayerEntity player, World world, ItemStack stack, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity);
+ }
+
+ @FunctionalInterface
+ public interface Canceled {
+ /**
+ * Invoked if the block breaking event has been canceled.
+ *
+ * @param player the {@link PlayerEntity} breaking the block
+ * @param world the {@link World} the block in broken in
+ * @param pos the {@link BlockPos} of the block
+ * @param state the {@link BlockState} before the block is broken
+ * @param blockEntity the {@link BlockEntity} before the block is broken, can be {@code null}
+ */
+ void onCancelPlayerBreaksBlock(PlayerEntity player, World world, ItemStack stack, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseBlockEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseBlockEvents.java
new file mode 100644
index 0000000000..32f072ed7d
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseBlockEvents.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.base.api.event.Event;
+
+/**
+ * Contains events related to a Player right-clicking a block.
+ */
+public class UseBlockEvents {
+
+ /**
+ * A callback that is invoked before a {@link PlayerEntity} right-clicks a block.
+ *
+ * Implementations should not assume the action has been completed.
+ *
+ * Upon return:
+ *
+ * - {@link ActionResult#SUCCESS}, {@link ActionResult#CONSUME}, or {@link ActionResult#CONSUME_PARTIAL}
+ * cancels further processing and, on the client, sends a packet to the server.
+ * - {@link ActionResult#PASS} falls back to further processing.
+ * - {@link ActionResult#FAIL} cancels further processing and does not send a packet to the server.
+ *
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> (player, world, hand, stack, pos, hitResult) -> {
+ for (var callback : callbacks) {
+ ActionResult result = callback.beforeUseBlock(player, world, hand, stack, pos, hitResult);
+
+ if (result != ActionResult.PASS) return result;
+ }
+ return ActionResult.PASS;
+ });
+
+ /**
+ * Invoked after a {@link PlayerEntity} right-clicks a block.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (player, world, hand, stack, pos, hitResult) -> {
+ for (var callback : callbacks) {
+ callback.afterUseBlock(player, world, hand, stack, pos, hitResult);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Before {
+ /**
+ * Invoked before a {@link PlayerEntity} right-clicks a block.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ * @param hitResult the {@link BlockHitResult} of the interaction
+ * @return {@link ActionResult#SUCCESS}/{@link ActionResult#CONSUME}/{@link ActionResult#CONSUME_PARTIAL}
+ * to cancel processing and send a packet to the server,
+ * {@link ActionResult#PASS} to fall back to further processing,
+ * {@link ActionResult#FAIL} to cancel further processing.
+ */
+ ActionResult beforeUseBlock(PlayerEntity player, World world, Hand hand, ItemStack stack, BlockPos pos, BlockHitResult hitResult);
+ }
+
+ @FunctionalInterface
+ public interface After {
+ /**
+ * Invoked after a {@link PlayerEntity} right-clicks a block.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ * @param hitResult the {@link BlockHitResult} of the interaction
+ */
+ void afterUseBlock(PlayerEntity player, World world, Hand hand, ItemStack stack, BlockPos pos, BlockHitResult hitResult);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseEntityEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseEntityEvents.java
new file mode 100644
index 0000000000..aa30a7d722
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseEntityEvents.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
+import org.quiltmc.qsl.base.api.event.Event;
+
+public class UseEntityEvents {
+
+ /**
+ * A callback that is invoked before a {@link PlayerEntity} right-clicks an {@link Entity}.
+ *
+ * Implementations should not assume the action was completed.
+ *
+ * Upon return:
+ *
+ * - {@link ActionResult#SUCCESS}, {@link ActionResult#CONSUME}, or {@link ActionResult#CONSUME_PARTIAL}
+ * cancels further processing and, on the client, sends a packet to the server.
+ * - {@link ActionResult#PASS} falls back to further processing.
+ * - {@link ActionResult#FAIL} cancels further processing and does not send a packet to the server.
+ *
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> (player, world, hand, stack, entity, hitResult) -> {
+ for (var callback : callbacks) {
+ ActionResult result = callback.beforeUseEntity(player, world, hand, stack, entity, hitResult);
+
+ if (result != ActionResult.PASS) return result;
+ }
+ return ActionResult.PASS;
+ });
+
+ /**
+ * A callback that is invoked after a {@link PlayerEntity} right-clicks an {@link Entity}.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (player, world, hand, stack, entity, hitResult) -> {
+ for (var callback : callbacks) {
+ callback.afterUseEntity(player, world, hand, stack, entity, hitResult);
+ }
+ });
+
+ public interface Before {
+ /**
+ * Invoked before a player uses (right-clicks) an entity.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ * @param stack the {@link ItemStack} used by the player
+ * @param entity the right-clicked {@link Entity}
+ * @param hitResult the {@link EntityHitResult} of the interaction
+ * @return {@link ActionResult#SUCCESS}/{@link ActionResult#CONSUME}/{@link ActionResult#CONSUME_PARTIAL}
+ * to cancel processing and send a packet to the server,
+ * {@link ActionResult#PASS} to fall back to further processing,
+ * {@link ActionResult#FAIL} to cancel further processing.
+ */
+ ActionResult beforeUseEntity(PlayerEntity player, World world, Hand hand, ItemStack stack, Entity entity, @Nullable EntityHitResult hitResult);
+ }
+
+ public interface After {
+ /**
+ * Invoked before a player uses (right-clicks) an entity.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ * @param stack the {@link ItemStack} used by the player
+ * @param entity the right-clicked {@link Entity}
+ * @param hitResult the {@link EntityHitResult} of the interaction
+ */
+ void afterUseEntity(PlayerEntity player, World world, Hand hand, ItemStack stack, Entity entity, @Nullable EntityHitResult hitResult);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseItemEvents.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseItemEvents.java
new file mode 100644
index 0000000000..ac57b32956
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/api/player/UseItemEvents.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.api.player;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.base.api.event.Event;
+
+public class UseItemEvents {
+
+ /**
+ * A callback that is invoked before a {@link PlayerEntity} right-clicks with an item.
+ *
+ * Implementations should not assume the action has been completed.
+ *
+ * Upon return:
+ *
+ * - {@link ActionResult#SUCCESS}, {@link ActionResult#CONSUME}, or {@link ActionResult#CONSUME_PARTIAL}
+ * cancels further processing and, on the client, sends a packet to the server.
+ * - {@link ActionResult#PASS} falls back to further processing.
+ * - {@link ActionResult#FAIL} cancels further processing and does not send a packet to the server.
+ *
+ */
+ public static final Event BEFORE = Event.create(Before.class,
+ callbacks -> (player, world, hand, stack) -> {
+ for (var callback : callbacks) {
+ ActionResult result = callback.beforeUseItem(player, world, hand, stack);
+
+ if (result != ActionResult.PASS) return result;
+ }
+ return ActionResult.PASS;
+ });
+
+ /**
+ * A callback that is invoked after a {@link PlayerEntity} right-clicks with an item.
+ */
+ public static final Event AFTER = Event.create(After.class,
+ callbacks -> (player, world, hand, stack) -> {
+ for (var callback : callbacks) {
+ callback.afterUseItem(player, world, hand, stack);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Before {
+ /**
+ * Invoked before a {@link PlayerEntity} right-clicks with an item.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ * @return {@link ActionResult#SUCCESS}/{@link ActionResult#CONSUME}/{@link ActionResult#CONSUME_PARTIAL}
+ * to cancel processing and send a packet to the server,
+ * {@link ActionResult#PASS} to fall back to further processing,
+ * {@link ActionResult#FAIL} to cancel further processing.
+ */
+ ActionResult beforeUseItem(PlayerEntity player, World world, Hand hand, ItemStack stack);
+ }
+
+ @FunctionalInterface
+ public interface After {
+ /**
+ * Invoked after a {@link PlayerEntity} right-clicks with an item.
+ *
+ * @param player the interacting {@link PlayerEntity}
+ * @param world the {@link World} the event occurs in
+ * @param hand the {@link Hand} used
+ */
+ void afterUseItem(PlayerEntity player, World world, Hand hand, ItemStack stack);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/LivingEntityMixin.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/LivingEntityMixin.java
new file mode 100644
index 0000000000..b3b5c2b841
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/LivingEntityMixin.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.mixin;
+
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.damage.DamageSource;
+import org.quiltmc.qsl.entity.interaction.api.LivingEntityAttackEvents;
+import org.quiltmc.qsl.entity.interaction.api.DamageContext;
+import org.spongepowered.asm.mixin.Mixin;
+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.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(LivingEntity.class)
+public abstract class LivingEntityMixin {
+
+ @Unique
+ private boolean quilt$damageCanceled = false;
+
+ @ModifyVariable(method = "damage", at = @At("HEAD"), ordinal = 0, argsOnly = true)
+ private float onTakeDamageModify(float damage, DamageSource source) {
+ if (source.getAttacker() instanceof LivingEntity attacker) {
+ DamageContext context = new DamageContext(attacker, attacker.getMainHandStack(), (LivingEntity)(Object)this, source, damage);
+ LivingEntityAttackEvents.BEFORE.invoker().beforeDamage(context);
+ quilt$damageCanceled = context.isCanceled();
+ return context.getDamage();
+ }
+ return damage;
+ }
+
+ @Inject(method = "damage", at = @At(value = "HEAD", shift = At.Shift.AFTER), cancellable = true)
+ private void onTakeDamageCancel(DamageSource source, float amount, CallbackInfoReturnable cir) {
+ if (source.getAttacker() instanceof LivingEntity && quilt$damageCanceled) {
+ quilt$damageCanceled = false;
+ cir.setReturnValue(false);
+ }
+ }
+
+ @Inject(method = "damage", at = @At("TAIL"))
+ private void afterTakeDamage(DamageSource source, float amount, CallbackInfoReturnable cir) {
+ if (source.getAttacker() instanceof LivingEntity attacker) {
+ LivingEntityAttackEvents.AFTER.invoker().afterDamage(attacker, attacker.getMainHandStack(), (LivingEntity)(Object)this, source, amount);
+ }
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayNetworkHandlerMixin.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayNetworkHandlerMixin.java
new file mode 100644
index 0000000000..0a7a9cda91
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayNetworkHandlerMixin.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.mixin;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.util.math.Vec3d;
+import org.quiltmc.qsl.entity.interaction.api.player.UseEntityEvents;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(targets = "net/minecraft/server/network/ServerPlayNetworkHandler$C_wsexhymd")
+public abstract class ServerPlayNetworkHandlerMixin implements PlayerInteractEntityC2SPacket.Handler {
+
+ @Shadow
+ public ServerPlayNetworkHandler field_28963;
+
+ @Shadow
+ public Entity field_28962;
+
+ @Inject(method = "interactAt(Lnet/minecraft/util/Hand;Lnet/minecraft/util/math/Vec3d;)V", at = @At("HEAD"), cancellable = true)
+ private void beforePlayerInteractEntity(Hand hand, Vec3d hitPosition, CallbackInfo ci) {
+ EntityHitResult hitResult = new EntityHitResult(field_28962, hitPosition.add(field_28962.getPos()));
+
+ ActionResult result = UseEntityEvents.BEFORE.invoker().beforeUseEntity(field_28963.player, field_28963.player.world, hand, field_28963.player.getStackInHand(hand), field_28962, hitResult);
+
+ if (result != ActionResult.PASS) ci.cancel();
+ }
+
+ @Inject(method = "interactAt(Lnet/minecraft/util/Hand;Lnet/minecraft/util/math/Vec3d;)V", at = @At("TAIL"))
+ private void afterPlayerInteractEntity(Hand hand, Vec3d hitPosition, CallbackInfo ci) {
+ EntityHitResult hitResult = new EntityHitResult(field_28962, hitPosition.add(field_28962.getPos()));
+
+ UseEntityEvents.AFTER.invoker().afterUseEntity(field_28963.player, field_28963.player.world, hand, field_28963.player.getStackInHand(hand), field_28962, hitResult);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerEntityMixin.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerEntityMixin.java
new file mode 100644
index 0000000000..8835dc8766
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerEntityMixin.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.mixin;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.network.encryption.PlayerPublicKey;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
+import org.quiltmc.qsl.entity.interaction.api.player.AttackEntityEvents;
+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.CallbackInfo;
+
+@Mixin(ServerPlayerEntity.class)
+public abstract class ServerPlayerEntityMixin extends PlayerEntity {
+
+ @Inject(method = "attack", at = @At("HEAD"), cancellable = true)
+ private void beforePlayerAttackEntity(Entity target, CallbackInfo ci) {
+ ActionResult result = AttackEntityEvents.BEFORE.invoker().beforeAttackEntity(this, this.world, this.getMainHandStack(), target);
+
+ if (result != ActionResult.PASS) ci.cancel();
+ }
+
+ @Inject(method = "attack", at = @At("TAIL"))
+ private void afterPlayerAttackEntity(Entity target, CallbackInfo ci) {
+ AttackEntityEvents.AFTER.invoker().afterAttackEntity(this, this.world, this.getMainHandStack(), target);
+ }
+
+ // Ignore
+ public ServerPlayerEntityMixin(World world, BlockPos blockPos, float f, GameProfile gameProfile, @Nullable PlayerPublicKey playerPublicKey) {
+ super(world, blockPos, f, gameProfile, playerPublicKey);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerInteractionManagerMixin.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerInteractionManagerMixin.java
new file mode 100644
index 0000000000..6aef66fa9f
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/ServerPlayerInteractionManagerMixin.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.mixin;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.network.Packet;
+import net.minecraft.network.listener.ClientPlayPacketListener;
+import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket;
+import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.network.ServerPlayerInteractionManager;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.entity.interaction.api.player.AttackBlockCallback;
+import org.quiltmc.qsl.entity.interaction.api.player.BreakBlockEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.UseBlockEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.UseItemEvents;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Slice;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+@Mixin(ServerPlayerInteractionManager.class)
+public class ServerPlayerInteractionManagerMixin {
+
+ @Shadow
+ @Final
+ protected ServerPlayerEntity player;
+
+ @Shadow
+ protected ServerWorld world;
+
+ @Inject(method = "interactItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getCount()I", ordinal = 0), cancellable = true)
+ private void beforePlayerInteractItem(ServerPlayerEntity player, World world, ItemStack stack, Hand hand, CallbackInfoReturnable cir) {
+ ActionResult result = UseItemEvents.BEFORE.invoker().beforeUseItem(player, world, hand, player.getStackInHand(hand));
+
+ if (result != ActionResult.PASS) cir.setReturnValue(result);
+ }
+
+ @Inject(method = "interactItem", at = @At(value = "RETURN", target = "Lnet/minecraft/util/TypedActionResult;getValue()Ljava/lang/Object;"), slice = @Slice(from = @At(value = "INVOKE", target = "Lnet/minecraft/util/TypedActionResult;getValue()Ljava/lang/Object;")))
+ private void afterPlayerInteractItem(ServerPlayerEntity player, World world, ItemStack stack, Hand hand, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != ActionResult.FAIL) {
+ UseItemEvents.AFTER.invoker().afterUseItem(player, world, hand, stack);
+ }
+ }
+
+ @Inject(method = "interactBlock", at = @At("HEAD"), cancellable = true)
+ private void beforePlayerInteractBlock(ServerPlayerEntity player, World world, ItemStack stack, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) {
+ ActionResult result = UseBlockEvents.BEFORE.invoker().beforeUseBlock(player, world, hand, stack, hitResult.getBlockPos(), hitResult);
+
+ if (result != ActionResult.PASS) cir.setReturnValue(result);
+ }
+
+ @Inject(method = "interactBlock", at = @At("RETURN"))
+ private void afterPlayerInteractBlock(ServerPlayerEntity player, World world, ItemStack stack, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != ActionResult.FAIL) {
+ UseBlockEvents.AFTER.invoker().afterUseBlock(player, world, hand, stack, hitResult.getBlockPos(), hitResult);
+ }
+ }
+
+ @Inject(method = "processBlockBreakingAction", at = @At("HEAD"), cancellable = true)
+ private void onPlayerAttackBlock(BlockPos pos, PlayerActionC2SPacket.Action action, Direction direction, int worldHeight, int i, CallbackInfo ci) {
+ if (action != PlayerActionC2SPacket.Action.START_DESTROY_BLOCK) return;
+ ActionResult result = AttackBlockCallback.EVENT.invoker().onAttackBlock(this.player, this.world, this.player.getMainHandStack(), pos, direction);
+
+ if (result != ActionResult.PASS) {
+ this.player.networkHandler.sendPacket(new BlockUpdateS2CPacket(this.world, pos));
+
+ BlockEntity blockEntity = this.world.getBlockEntity(pos);
+ if (blockEntity != null) {
+ Packet packet = blockEntity.toUpdatePacket();
+
+ if (packet != null) {
+ this.player.networkHandler.sendPacket(packet);
+ }
+ }
+
+ ci.cancel();
+ }
+ }
+
+ @Inject(method = "tryBreakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBreak(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/entity/player/PlayerEntity;)V"), locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true)
+ private void onPlayerBreakBlock(BlockPos pos, CallbackInfoReturnable cir, BlockState state, BlockEntity blockEntity) {
+ boolean result = BreakBlockEvents.BEFORE.invoker().beforePlayerBreaksBlock(this.player, this.world, this.player.getMainHandStack(), pos, state, blockEntity);
+
+ if (!result) {
+ BreakBlockEvents.CANCELED.invoker().onCancelPlayerBreaksBlock(this.player, this.world, this.player.getMainHandStack(), pos, state, blockEntity);
+ cir.setReturnValue(false);
+ }
+ }
+
+ @Inject(method = "tryBreakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBroken(Lnet/minecraft/world/WorldAccess;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;)V"), locals = LocalCapture.CAPTURE_FAILHARD)
+ private void afterPlayerBreakBlock(BlockPos pos, CallbackInfoReturnable cir, BlockState state, BlockEntity blockEntity) {
+ BreakBlockEvents.AFTER.invoker().afterPlayerBreaksBlock(this.player, this.world, this.player.getMainHandStack(), pos, state, blockEntity);
+ }
+}
diff --git a/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/client/ClientPlayerInteractionManagerMixin.java b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/client/ClientPlayerInteractionManagerMixin.java
new file mode 100644
index 0000000000..e1af0f1509
--- /dev/null
+++ b/library/entity/interaction/src/main/java/org/quiltmc/qsl/entity/interaction/mixin/client/ClientPlayerInteractionManagerMixin.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2016, 2017, 2018, 2019 FabricMC
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.mixin.client;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.class_7204;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.network.ClientPlayerInteractionManager;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.network.packet.c2s.play.*;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.GameMode;
+import net.minecraft.world.World;
+import org.quiltmc.qsl.entity.interaction.api.player.*;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+@Mixin(ClientPlayerInteractionManager.class)
+public abstract class ClientPlayerInteractionManagerMixin {
+
+ @Shadow @Final
+ private ClientPlayNetworkHandler networkHandler;
+
+ // Method sends a packet with a sequentially assigned id to the server. class_7204 builds the packet from what I can tell.
+ @Shadow
+ protected abstract void m_vvsqjptk(ClientWorld world, class_7204 arg);
+
+ @Shadow
+ @Final
+ private MinecraftClient client;
+
+ @Shadow
+ private GameMode gameMode;
+
+ @Inject(method = "attackEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendPacket(Lnet/minecraft/network/Packet;)V", ordinal = 0), cancellable = true)
+ private void beforePlayerAttackEntity(PlayerEntity player, Entity target, CallbackInfo ci) {
+ ActionResult result = AttackEntityEvents.BEFORE.invoker().beforeAttackEntity(player, player.world, player.getMainHandStack(), target);
+
+ if (result != ActionResult.PASS) {
+ if (result == ActionResult.SUCCESS) {
+ this.networkHandler.sendPacket(PlayerInteractEntityC2SPacket.attack(target, player.isSneaking()));
+ }
+
+ ci.cancel();
+ }
+ }
+
+ @Inject(method = "attackEntity", at = @At("TAIL"))
+ private void afterPlayerAttackEntity(PlayerEntity player, Entity target, CallbackInfo ci) {
+ AttackEntityEvents.AFTER.invoker().afterAttackEntity(player, player.world, player.getMainHandStack(), target);
+ }
+
+ @Inject(method = "interactItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendPacket(Lnet/minecraft/network/Packet;)V"), cancellable = true)
+ private void beforePlayerInteractItem(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) {
+ ActionResult result = UseItemEvents.BEFORE.invoker().beforeUseItem(player, player.world, hand, player.getStackInHand(hand));
+
+ if (result != ActionResult.PASS) {
+ if (result.isAccepted()) {
+ this.networkHandler.sendPacket(new PlayerMoveC2SPacket.Full(player.getX(), player.getY(), player.getZ(), player.getYaw(), player.getPitch(), player.isOnGround()));
+ // method sends a packet with a sequentially assigned id to the server
+ m_vvsqjptk((ClientWorld) player.world, id -> new PlayerInteractItemC2SPacket(hand, id));
+ }
+
+ cir.setReturnValue(result);
+ }
+ }
+
+ @Inject(method = "interactItem", at = @At("TAIL"))
+ private void afterPlayerInteractItem(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != ActionResult.FAIL) {
+ UseItemEvents.AFTER.invoker().afterUseItem(player, player.world, hand, player.getStackInHand(hand));
+ }
+ }
+
+ @Inject(method = "interactBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;m_vvsqjptk(Lnet/minecraft/client/world/ClientWorld;Lnet/minecraft/class_7204;)V"), cancellable = true)
+ private void beforePlayerInteractBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) {
+ ActionResult result = UseBlockEvents.BEFORE.invoker().beforeUseBlock(player, player.world, hand, player.getStackInHand(hand), hitResult.getBlockPos(), hitResult);
+
+ if (result != ActionResult.PASS) {
+ if (result.isAccepted()) {
+ // method sends a packet with a sequentially assigned id to the server
+ this.m_vvsqjptk((ClientWorld) player.world, id -> new PlayerInteractBlockC2SPacket(hand, hitResult, id));
+ }
+
+ cir.setReturnValue(result);
+ }
+ }
+
+ @Inject(method = "interactBlock", at = @At("TAIL"))
+ private void afterPlayerInteractBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != ActionResult.FAIL) {
+ UseBlockEvents.AFTER.invoker().afterUseBlock(player, player.world, hand, player.getStackInHand(hand), hitResult.getBlockPos(), hitResult);
+ }
+ }
+
+ @Inject(method = "interactEntityAtLocation", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/hit/EntityHitResult;getPos()Lnet/minecraft/util/math/Vec3d;"), cancellable = true)
+ private void beforePlayerInteractEntity(PlayerEntity player, Entity entity, EntityHitResult hitResult, Hand hand, CallbackInfoReturnable cir) {
+ ActionResult result = UseEntityEvents.BEFORE.invoker().beforeUseEntity(player, player.world, hand, player.getStackInHand(hand), entity, hitResult);
+
+ if (result != ActionResult.PASS) {
+ if (result.isAccepted()) {
+ Vec3d vec3d = hitResult.getPos().subtract(entity.getPos());
+ this.networkHandler.sendPacket(PlayerInteractEntityC2SPacket.interactAt(entity, player.isSneaking(), hand, vec3d));
+ }
+
+ cir.setReturnValue(result);
+ }
+ }
+
+ @Inject(method = "interactEntityAtLocation", at = @At("TAIL"))
+ private void afterPlayerInteractEntity(PlayerEntity player, Entity entity, EntityHitResult hitResult, Hand hand, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != ActionResult.FAIL) {
+ UseEntityEvents.AFTER.invoker().afterUseEntity(player, player.world, hand, player.getStackInHand(hand), entity, hitResult);
+ }
+ }
+
+ @Inject(method = "attackBlock", at = @At("HEAD"), cancellable = true)
+ private void onPlayerAttackBlock(BlockPos pos, Direction direction, CallbackInfoReturnable cir) {
+ ActionResult result = AttackBlockCallback.EVENT.invoker().onAttackBlock(this.client.player, this.client.world, this.client.player.getMainHandStack(), pos, direction);
+
+ if (result != ActionResult.PASS) {
+ if (result.isAccepted()) {
+ // method sends a packet with a sequentially assigned id to the server
+ this.m_vvsqjptk(this.client.world, id -> new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, pos, direction, id));
+ }
+
+ cir.setReturnValue(result.isAccepted());
+ }
+ }
+
+ @Inject(method = "updateBlockBreakingProgress", at = @At("HEAD"), cancellable = true)
+ private void onPlayerAttackBlockProgress(BlockPos pos, Direction direction, CallbackInfoReturnable cir) {
+ if (!this.gameMode.isCreative()) return;
+ ActionResult result = AttackBlockCallback.EVENT.invoker().onAttackBlock(this.client.player, this.client.world, this.client.player.getMainHandStack(), pos, direction);
+
+ if (result != ActionResult.PASS) {
+ cir.setReturnValue(result.isAccepted());
+ }
+ }
+
+ @Inject(method = "breakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBreak(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/entity/player/PlayerEntity;)V"), locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true)
+ private void onPlayerBreakBlock(BlockPos pos, CallbackInfoReturnable cir, World world, BlockState state) {
+ BlockEntity blockEntity = world.getBlockEntity(pos);
+ boolean result = BreakBlockEvents.BEFORE.invoker().beforePlayerBreaksBlock(this.client.player, world, this.client.player.getMainHandStack(), pos, state, blockEntity);
+
+ if (!result) {
+ BreakBlockEvents.CANCELED.invoker().onCancelPlayerBreaksBlock(this.client.player, world, this.client.player.getMainHandStack(), pos, state, blockEntity);
+ cir.setReturnValue(false);
+ }
+ }
+
+ @Inject(method = "breakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBroken(Lnet/minecraft/world/WorldAccess;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;)V"), locals = LocalCapture.CAPTURE_FAILHARD)
+ private void afterPlayerBreakBlock(BlockPos pos, CallbackInfoReturnable cir, World world, BlockState state) {
+ BreakBlockEvents.AFTER.invoker().afterPlayerBreaksBlock(this.client.player, world, this.client.player.getMainHandStack(), pos, state, world.getBlockEntity(pos));
+ }
+}
diff --git a/library/entity/interaction/src/main/resources/assets/quilt_entity_interaction/icon.png b/library/entity/interaction/src/main/resources/assets/quilt_entity_interaction/icon.png
new file mode 100644
index 0000000000..8d54ad0021
Binary files /dev/null and b/library/entity/interaction/src/main/resources/assets/quilt_entity_interaction/icon.png differ
diff --git a/library/entity/interaction/src/main/resources/quilt_entity_interaction.mixins.json b/library/entity/interaction/src/main/resources/quilt_entity_interaction.mixins.json
new file mode 100644
index 0000000000..e73093356c
--- /dev/null
+++ b/library/entity/interaction/src/main/resources/quilt_entity_interaction.mixins.json
@@ -0,0 +1,17 @@
+{
+ "required": true,
+ "package": "org.quiltmc.qsl.entity.interaction.mixin",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ "LivingEntityMixin",
+ "ServerPlayerEntityMixin",
+ "ServerPlayerInteractionManagerMixin",
+ "ServerPlayNetworkHandlerMixin"
+ ],
+ "client": [
+ "client.ClientPlayerInteractionManagerMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/library/entity/interaction/src/testmod/java/org/quiltmc/qsl/entity/interaction/test/InteractionTest.java b/library/entity/interaction/src/testmod/java/org/quiltmc/qsl/entity/interaction/test/InteractionTest.java
new file mode 100644
index 0000000000..b428987852
--- /dev/null
+++ b/library/entity/interaction/src/testmod/java/org/quiltmc/qsl/entity/interaction/test/InteractionTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2022 QuiltMC
+ *
+ * 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 org.quiltmc.qsl.entity.interaction.test;
+
+import net.minecraft.block.Blocks;
+import net.minecraft.entity.EntityType;
+import net.minecraft.entity.LightningEntity;
+import net.minecraft.entity.mob.CreeperEntity;
+import net.minecraft.entity.mob.ZombieEntity;
+import net.minecraft.entity.passive.IronGolemEntity;
+import net.minecraft.item.Items;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.ActionResult;
+import org.quiltmc.loader.api.ModContainer;
+import org.quiltmc.qsl.base.api.entrypoint.ModInitializer;
+import org.quiltmc.qsl.entity.interaction.api.LivingEntityAttackEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.AttackEntityEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.BreakBlockEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.UseEntityEvents;
+import org.quiltmc.qsl.entity.interaction.api.player.UseItemEvents;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InteractionTest implements ModInitializer {
+
+ public static final Logger LOGGER = LoggerFactory.getLogger("qsl_entity_interaction_testmod");
+
+ @Override
+ public void onInitialize(ModContainer mod) {
+ AttackEntityEvents.BEFORE.register((player, world, stack, entity) -> {
+ if (stack.isOf(Items.DIAMOND_SWORD)) {
+ return ActionResult.FAIL;
+ }
+ if (stack.isOf(Items.DIAMOND_SHOVEL)) {
+ return ActionResult.SUCCESS;
+ }
+ return ActionResult.PASS;
+ });
+
+ LivingEntityAttackEvents.BEFORE.register(context -> {
+ if (context.getAttacker() instanceof IronGolemEntity) context.cancel();
+ });
+
+ LivingEntityAttackEvents.BEFORE.register(context -> {
+ if (context.getAttacker() instanceof ZombieEntity) {
+ context.setDamage(context.getDamage() + 3);
+ System.out.println(context.getDamage());
+ }
+ });
+
+ LivingEntityAttackEvents.BEFORE.register(context -> {
+ if (context.getAttacker() instanceof ZombieEntity) {
+ context.setDamage(context.getDamage() + 2);
+ System.out.println(context.getDamage());
+ }
+ });
+
+ LivingEntityAttackEvents.BEFORE.register(context -> {
+ if (context.getAttacker() instanceof ZombieEntity) {
+ context.setDamage(context.getDamage() + 1);
+ System.out.println(context.getDamage());
+ }
+ });
+
+ UseEntityEvents.BEFORE.register((player, world, hand, stack, entity, hitResult) -> {
+ if (entity instanceof CreeperEntity) {
+ if (world instanceof ServerWorld) {
+ System.out.println("creeper " + world);
+ return ActionResult.FAIL;
+ }
+ }
+ return ActionResult.PASS;
+ });
+
+ UseItemEvents.AFTER.register((player, world, hand, stack) -> {
+ if (player.getStackInHand(hand).isOf(Items.DIAMOND_SWORD)) {
+ LightningEntity lightning = new LightningEntity(EntityType.LIGHTNING_BOLT, world);
+ lightning.setPos(player.getX(), player.getY(), player.getZ());
+ world.spawnEntity(lightning);
+ }
+ });
+
+ BreakBlockEvents.BEFORE.register((player, world, stack, pos, state, blockEntity) -> {
+ if (state.getBlock() == Blocks.GRASS_BLOCK) {
+ //if (world.isClient) return false;
+ }
+ return true;
+ });
+
+ BreakBlockEvents.AFTER.register((player, world, stack, pos, state, blockEntity) -> {
+ if (state.getBlock() == Blocks.GRASS_BLOCK) {
+ world.setBlockState(pos, Blocks.LAVA.getDefaultState());
+ }
+ });
+
+ LOGGER.info("Finished Interaction Module test init");
+ }
+}
diff --git a/library/entity/interaction/src/testmod/resources/quilt.mod.json b/library/entity/interaction/src/testmod/resources/quilt.mod.json
new file mode 100644
index 0000000000..fcca5828e3
--- /dev/null
+++ b/library/entity/interaction/src/testmod/resources/quilt.mod.json
@@ -0,0 +1,24 @@
+{
+ "schema_version": 1,
+ "quilt_loader": {
+ "group": "org.quiltmc.qsl.entity.interaction",
+ "id": "qsl_entity_interaction_testmod",
+ "version": "1.0.0",
+ "metadata": {
+ "name": "Quilt Entity Interaction API Test Mod",
+ "license": "Apache-2.0"
+ },
+ "intermediate_mappings": "net.fabricmc:intermediary",
+ "load_type": "always",
+ "entrypoints": {
+ "init": [
+ "org.quiltmc.qsl.entity.interaction.test.InteractionTest"
+ ]
+ },
+ "depends": [
+ "quilt_loader",
+ "quilt_entity_interaction"
+ ]
+ },
+ "mixin": "quilt_entity_interaction_testmod.mixins.json"
+}
diff --git a/library/entity/interaction/src/testmod/resources/quilt_entity_interaction_testmod.mixins.json b/library/entity/interaction/src/testmod/resources/quilt_entity_interaction_testmod.mixins.json
new file mode 100644
index 0000000000..fd02522534
--- /dev/null
+++ b/library/entity/interaction/src/testmod/resources/quilt_entity_interaction_testmod.mixins.json
@@ -0,0 +1,10 @@
+{
+ "required": true,
+ "package": "org.quiltmc.qsl.entity.interaction.test.mixin",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}