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! + *

+ */ +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: + *

+ */ + 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 + } +}