Add an event that fires when client player left-clicks (#3043)

The ClientPreAttackCallback will fire every tick when the attack key is pressed, before vanilla attack handling. If the callback returns true, then the vanilla handling (breaking block, attacking entity, swining hand) will be cancelled. For multiple callbacks, if the former callback returns true, the later callback won't execute.

This event does not consider attack cooldown.
This commit is contained in:
qouteall 2023-05-30 19:51:38 +08:00 committed by modmuss50
parent e808a8f296
commit 4014940769
8 changed files with 193 additions and 0 deletions

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.event.client.player;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
/**
* This event fires every tick when the attack key (left mouse button by default) is pressed
* (including clicking and holding the attack key).
* If the callback returns true,
* the vanilla handling (block breaking, entity attacking, hand swing) will be cancelled,
* and the later callbacks of this event are also cancelled.
*
* <p>This event is client-only, which means handling it may require sending custom packets.
*
* <p>The event fires both when clicking and holding attack key.
* To check whether the attack key is just clicked, use {@code clickCount != 0}
*
* <p>The vanilla attack cooldown and player game mode does not affect this event.
* The mod probably needs to check {@link net.minecraft.client.MinecraftClient#attackCooldown} and the game mode.
* {@link net.minecraft.entity.player.ItemCooldownManager} can be used for custom item cooldown handling.
*/
public interface ClientPreAttackCallback {
Event<ClientPreAttackCallback> EVENT = EventFactory.createArrayBacked(
ClientPreAttackCallback.class,
(listeners) -> (client, player, clickCount) -> {
for (ClientPreAttackCallback event : listeners) {
if (event.onClientPlayerPreAttack(client, player, clickCount)) {
return true;
}
}
return false;
}
);
/**
* @param player the client player
* @param clickCount the click count of the attack key in this tick.
* @return whether to intercept attack handling
*/
boolean onClientPlayerPreAttack(MinecraftClient client, ClientPlayerEntity player, int clickCount);
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.event.interaction.client;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import net.minecraft.client.option.KeyBinding;
@Mixin(KeyBinding.class)
public interface KeyBindingAccessor {
@Accessor("timesPressed")
int fabric_getTimesPressed();
}

View file

@ -16,12 +16,15 @@
package net.fabricmc.fabric.mixin.event.interaction.client;
import org.jetbrains.annotations.Nullable;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import net.minecraft.block.entity.BlockEntity;
@ -29,6 +32,8 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.ClientPlayerInteractionManager;
import net.minecraft.client.option.GameOptions;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
@ -43,11 +48,13 @@ import net.minecraft.util.math.Vec3d;
import net.fabricmc.fabric.api.event.client.player.ClientPickBlockApplyCallback;
import net.fabricmc.fabric.api.event.client.player.ClientPickBlockCallback;
import net.fabricmc.fabric.api.event.client.player.ClientPickBlockGatherCallback;
import net.fabricmc.fabric.api.event.client.player.ClientPreAttackCallback;
import net.fabricmc.fabric.api.event.player.UseEntityCallback;
@Mixin(MinecraftClient.class)
public abstract class MinecraftClientMixin {
private boolean fabric_itemPickCancelled;
private boolean fabric_attackCancelled;
@SuppressWarnings("deprecation")
private ItemStack fabric_emulateOldPick() {
@ -132,6 +139,14 @@ public abstract class MinecraftClientMixin {
@Shadow
public abstract ClientPlayNetworkHandler getNetworkHandler();
@Shadow
@Final
public GameOptions options;
@Shadow
@Nullable
public ClientPlayerInteractionManager interactionManager;
@Inject(
at = @At(
value = "INVOKE",
@ -157,4 +172,42 @@ public abstract class MinecraftClientMixin {
ci.cancel();
}
}
@Inject(
method = "handleInputEvents",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerEntity;isUsingItem()Z",
ordinal = 0
)
)
private void injectHandleInputEventsForPreAttackCallback(CallbackInfo ci) {
int attackKeyPressCount = ((KeyBindingAccessor) options.attackKey).fabric_getTimesPressed();
if (options.attackKey.isPressed() || attackKeyPressCount != 0) {
fabric_attackCancelled = ClientPreAttackCallback.EVENT.invoker().onClientPlayerPreAttack(
(MinecraftClient) (Object) this, player, attackKeyPressCount
);
} else {
fabric_attackCancelled = false;
}
}
@Inject(method = "doAttack", at = @At("HEAD"), cancellable = true)
private void injectDoAttackForCancelling(CallbackInfoReturnable<Boolean> cir) {
if (fabric_attackCancelled) {
cir.setReturnValue(false);
}
}
@Inject(method = "handleBlockBreaking", at = @At("HEAD"), cancellable = true)
private void injectHandleBlockBreakingForCancelling(boolean breaking, CallbackInfo ci) {
if (fabric_attackCancelled) {
if (interactionManager != null) {
interactionManager.cancelBlockBreaking();
}
ci.cancel();
}
}
}

View file

@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_16",
"client": [
"ClientPlayerInteractionManagerMixin",
"KeyBindingAccessor",
"MinecraftClientMixin"
],
"injectors": {

View file

@ -17,6 +17,9 @@
],
"fabric-gametest": [
"net.fabricmc.fabric.test.event.interaction.FakePlayerTests"
],
"client": [
"net.fabricmc.fabric.test.client.event.interaction.ClientPreAttackTests"
]
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.client.event.interaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.item.Items;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.event.client.player.ClientPreAttackCallback;
public class ClientPreAttackTests implements ClientModInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientPreAttackTests.class);
@Override
public void onInitializeClient() {
ClientPreAttackCallback.EVENT.register((client, player, clickCount) -> {
if (!player.isSpectator() && player.getMainHandStack().getItem() == Items.TORCH) {
LOGGER.info("Attacking using torch intercepted. Attack key clicks: {}", clickCount != 0);
return true;
}
return false;
});
}
}

View file

@ -127,6 +127,9 @@ transitive-accessible method net/minecraft/entity/damage/DamageSources create (L
transitive-accessible method net/minecraft/entity/damage/DamageSources create (Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/entity/Entity;)Lnet/minecraft/entity/damage/DamageSource;
transitive-accessible method net/minecraft/entity/damage/DamageSources create (Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/entity/Entity;Lnet/minecraft/entity/Entity;)Lnet/minecraft/entity/damage/DamageSource;
# The attack cooldown
transitive-accessible field net/minecraft/client/MinecraftClient attackCooldown I
### Generated access wideners below
# Constructors of non-abstract block classes
transitive-accessible method net/minecraft/block/AirBlock <init> (Lnet/minecraft/block/AbstractBlock$Settings;)V

View file

@ -122,4 +122,7 @@ transitive-accessible method net/minecraft/entity/damage/DamageSources create (L
transitive-accessible method net/minecraft/entity/damage/DamageSources create (Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/entity/Entity;)Lnet/minecraft/entity/damage/DamageSource;
transitive-accessible method net/minecraft/entity/damage/DamageSources create (Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/entity/Entity;Lnet/minecraft/entity/Entity;)Lnet/minecraft/entity/damage/DamageSource;
# The attack cooldown
transitive-accessible field net/minecraft/client/MinecraftClient attackCooldown I
### Generated access wideners below