Skip to content

Commit

Permalink
Port fabric-events-interaction-v0
Browse files Browse the repository at this point in the history
  • Loading branch information
Su5eD committed Jun 18, 2024
1 parent 2929fe8 commit 7be4f54
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 422 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,134 +16,28 @@

package net.fabricmc.fabric.mixin.event.interaction.client;

import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.client.multiplayer.prediction.PredictiveAction;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket;
import net.minecraft.network.protocol.game.ServerboundUseItemPacket;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;

@Mixin(MultiPlayerGameMode.class)
public abstract class ClientPlayerInteractionManagerMixin {
@Shadow
@Final
private Minecraft minecraft;
@Shadow
@Final
private ClientPacketListener connection;
@Shadow
private GameType localPlayerMode;

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/GameType;isCreative()Z", ordinal = 0), method = "startDestroyBlock", cancellable = true)
public void attackBlock(BlockPos pos, Direction direction, CallbackInfoReturnable<Boolean> info) {
fabric_fireAttackBlockCallback(pos, direction, info);
}

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/GameType;isCreative()Z", ordinal = 0), method = "continueDestroyBlock", cancellable = true)
public void method_2902(BlockPos pos, Direction direction, CallbackInfoReturnable<Boolean> info) {
if (localPlayerMode.isCreative()) {
fabric_fireAttackBlockCallback(pos, direction, info);
}
}

@Unique
private void fabric_fireAttackBlockCallback(BlockPos pos, Direction direction, CallbackInfoReturnable<Boolean> info) {
InteractionResult result = AttackBlockCallback.EVENT.invoker().interact(minecraft.player, minecraft.level, InteractionHand.MAIN_HAND, pos, direction);

if (result != InteractionResult.PASS) {
// Returning true will spawn particles and trigger the animation of the hand -> only for SUCCESS.
info.setReturnValue(result == InteractionResult.SUCCESS);

// We also need to let the server process the action if it's accepted.
if (result.consumesAction()) {
startPrediction(minecraft.level, id -> new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, pos, direction, id));
}
}
}

@Inject(method = "destroyBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;destroy(Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), locals = LocalCapture.CAPTURE_FAILHARD)
private void fabric$onBlockBroken(BlockPos pos, CallbackInfoReturnable<Boolean> cir, Level world, BlockState blockState) {
ClientPlayerBlockBreakEvents.AFTER.invoker().afterBlockBreak(minecraft.level, minecraft.player, pos, blockState);
}

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;startPrediction(Lnet/minecraft/client/multiplayer/ClientLevel;Lnet/minecraft/client/multiplayer/prediction/PredictiveAction;)V"), method = "useItemOn", cancellable = true)
public void interactBlock(LocalPlayer player, InteractionHand hand, BlockHitResult blockHitResult, CallbackInfoReturnable<InteractionResult> info) {
// hook interactBlock between the world border check and the actual block interaction to invoke the use block event first
// this needs to be in interactBlock to avoid sending a packet in line with the event javadoc

if (player.isSpectator()) return; // vanilla spectator check happens later, repeat it before the event to avoid false invocations

InteractionResult result = UseBlockCallback.EVENT.invoker().interact(player, player.level(), hand, blockHitResult);

if (result != InteractionResult.PASS) {
if (result == InteractionResult.SUCCESS) {
// send interaction packet to the server with a new sequentially assigned id
startPrediction(player.clientLevel, id -> new ServerboundUseItemOnPacket(hand, blockHitResult, id));
}

info.setReturnValue(result);
}
}

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;ensureHasSentCarriedItem()V", ordinal = 0), method = "useItem", cancellable = true)
public void interactItem(Player player, InteractionHand hand, CallbackInfoReturnable<InteractionResult> info) {
// hook interactBlock between the spectator check and sending the first packet to invoke the use item event first
// this needs to be in interactBlock to avoid sending a packet in line with the event javadoc
InteractionResultHolder<ItemStack> result = UseItemCallback.EVENT.invoker().interact(player, player.level(), hand);

if (result.getResult() != InteractionResult.PASS) {
if (result.getResult() == InteractionResult.SUCCESS) {
// send interaction packet to the server with a new sequentially assigned id
startPrediction((ClientLevel) player.level(), id -> new ServerboundUseItemPacket(hand, id, player.getYRot(), player.getXRot()));
}

info.setReturnValue(result.getResult());
}
}

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;sendPacket(Lnet/minecraft/network/protocol/Packet;)V", ordinal = 0), method = "attack", cancellable = true)
public void attackEntity(Player player, Entity entity, CallbackInfo info) {
InteractionResult result = AttackEntityCallback.EVENT.invoker().interact(player, player.getCommandSenderWorld(), InteractionHand.MAIN_HAND /* TODO */, entity, null);

if (result != InteractionResult.PASS) {
if (result == InteractionResult.SUCCESS) {
this.connection.send(ServerboundInteractPacket.createAttackPacket(entity, player.isShiftKeyDown()));
}

info.cancel();
}
}

@Shadow
protected abstract void startPrediction(ClientLevel clientWorld, PredictiveAction supplier);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,32 @@

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.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;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Options;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
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;

@Mixin(Minecraft.class)
public abstract class MinecraftClientMixin {
Expand Down Expand Up @@ -133,9 +125,6 @@ public void cancelItemPick(CallbackInfo info) {
@Shadow
private LocalPlayer player;

@Shadow
public abstract ClientPacketListener getConnection();

@Shadow
@Final
public Options options;
Expand All @@ -151,32 +140,6 @@ public void cancelItemPick(CallbackInfo info) {
@Nullable
public ClientLevel level;

@Inject(
at = @At(
value = "INVOKE",
target = "net/minecraft/client/network/ClientPlayerInteractionManager.interactEntityAtLocation(Lnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/entity/Entity;Lnet/minecraft/util/hit/EntityHitResult;Lnet/minecraft/util/Hand;)Lnet/minecraft/util/ActionResult;"
),
method = "startUseItem",
cancellable = true,
locals = LocalCapture.CAPTURE_FAILHARD
)
private void injectUseEntityCallback(CallbackInfo ci, InteractionHand[] hands, int i1, int i2, InteractionHand hand, ItemStack stack, EntityHitResult hitResult, Entity entity) {
InteractionResult result = UseEntityCallback.EVENT.invoker().interact(player, player.getCommandSenderWorld(), hand, entity, hitResult);

if (result != InteractionResult.PASS) {
if (result.consumesAction()) {
Vec3 hitVec = hitResult.getLocation().subtract(entity.getX(), entity.getY(), entity.getZ());
getConnection().send(ServerboundInteractPacket.createInteractionPacket(entity, player.isShiftKeyDown(), hand, hitVec));
}

if (result.shouldSwing()) {
player.swing(hand);
}

ci.cancel();
}
}

@Inject(
method = "handleKeybinds",
at = @At(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,13 @@

import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.UUID;

import com.google.common.collect.MapMaker;
import com.mojang.authlib.GameProfile;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.fabric.impl.event.interaction.FakePlayerNetworkHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.Stat;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.animal.horse.AbstractHorse;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.scores.PlayerTeam;

/**
* A "fake player" is a {@link ServerPlayer} that is not a human player.
Expand All @@ -59,7 +47,7 @@
* In some edge cases, or for gameplay considerations, it might be necessary to check whether a {@link ServerPlayer} is a fake player.
* This can be done with an {@code instanceof} check: {@code player instanceof FakePlayer}.
*/
public class FakePlayer extends ServerPlayer {
public class FakePlayer extends net.neoforged.neoforge.common.util.FakePlayer {
/**
* Default UUID, for fake players not associated with a specific (human) player.
*/
Expand Down Expand Up @@ -100,53 +88,8 @@ private record FakePlayerKey(ServerLevel world, GameProfile profile) { }
private static final Map<FakePlayerKey, FakePlayer> FAKE_PLAYER_MAP = new MapMaker().weakValues().makeMap();

protected FakePlayer(ServerLevel world, GameProfile profile) {
super(world.getServer(), world, profile, ClientInformation.createDefault());
super(world, profile);

this.connection = new FakePlayerNetworkHandler(this);
}

@Override
public void tick() { }

@Override
public void updateOptions(ClientInformation settings) { }

@Override
public void awardStat(Stat<?> stat, int amount) { }

@Override
public void resetStat(Stat<?> stat) { }

@Override
public boolean isInvulnerableTo(DamageSource damageSource) {
return true;
}

@Nullable
@Override
public PlayerTeam getTeam() {
// Scoreboard team is checked using the gameprofile name by default, which we don't want.
return null;
}

@Override
public void startSleeping(BlockPos pos) {
// Don't lock bed forever.
}

@Override
public boolean startRiding(Entity entity, boolean force) {
return false;
}

@Override
public void openTextEdit(SignBlockEntity sign, boolean front) { }

@Override
public OptionalInt openMenu(@Nullable MenuProvider factory) {
return OptionalInt.empty();
}

@Override
public void openHorseInventory(AbstractHorse horse, Container inventory) { }
}
Loading

0 comments on commit 7be4f54

Please sign in to comment.