You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Minecraft Forge 1.19.4取消方块破坏后BlockEntity同步异常求助

问题

在Minecraft Forge 1.19.4中,通过BlockEvent.BreakEvent事件处理方块破坏逻辑,特定条件下取消破坏操作。但该事件仅在服务端触发,导致客户端出现方块先消失再重现的视觉bug,同时客户端的BlockEntity数据与服务端不一致。

尝试过sendBlockUpdate和自定义数据包同步,但因同步时机(方块销毁重建前)问题未生效,需要可行的解决方案。

EventHandlers类代码

@Mod.EventBusSubscriber(modid = DoorLockPadMod.MODID)
public class EventHandlers {
@SubscribeEvent
    public static void onBlockDestroyed(@NotNull BlockEvent.BreakEvent event) {
        Player playerEntity = event.getPlayer();
        Level level = playerEntity.getLevel();
        BlockPos blockPos = event.getPos();
        BlockState blockState = level.getBlockState(blockPos);
        FunctionUtils.setLevel(level);
        if (blockState.getBlock() instanceof DoorBlock) {
            BlockPos padBlockPos = FunctionUtils.checkIfAssignedDoor(blockPos, null);
            if (padBlockPos != null) {
                PadBlockEntity padBlockEntity = (PadBlockEntity) level.getBlockEntity(padBlockPos);
                BlockState padBlockState = level.getBlockState(padBlockPos);
                if (padBlockEntity != null && (!padBlockEntity.isDoorLocked() || playerEntity.isCreative())) {
                    padBlockEntity.setRelatedDoor(null);
                    padBlockEntity.setPassword(null);
                    level.sendBlockUpdated(padBlockPos, padBlockState, padBlockState, 2);
                    FunctionUtils.removeDoorAssignedPadBlockByPadBlock(padBlockPos);
                } else event.setCanceled(true);
            }
        } else if (blockState.getBlock() instanceof PadBlock) {
            PadBlockEntity padBlockEntity = (PadBlockEntity) level.getBlockEntity(blockPos);
            if (padBlockEntity != null && (!padBlockEntity.isDoorLocked() || playerEntity.isCreative())) {
                FunctionUtils.removeDoorAssignedPadBlockByPadBlock(padBlockPos);
            } else if (padBlockEntity != null) event.setCanceled(true);
        }
    }
}

PadBlockEntity类代码

public class PadBlockEntity extends BlockEntity {
    private BlockPos pos;
    private BlockPos supportBlockPos;
    private String password;
    private boolean isDoorLocked;
    private String relatedDoor;
    private int ticks;

    public PadBlockEntity(BlockPos pos, BlockState state) {
        super(DoorLockPadMod.PAD_BLOCK_ENTITY.get(), pos, state);
        this.pos = pos;
        this.isDoorLocked = true;
        this.ticks = 0;
    }

    public BlockPos getPos() {
        return pos;
    }

    public void setPos(BlockPos pos) {
        this.pos = pos;
    }

    public BlockPos getSupportBlockPos() {
        return supportBlockPos;
    }

    public void setSupportBlockPos(BlockPos supportBlockpos) {
        this.supportBlockPos = supportBlockpos;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public boolean isDoorLocked() {
        return isDoorLocked;
    }

    public void setDoorLocked(boolean doorLocked) {
        isDoorLocked = doorLocked;
    }

    public String getRelatedDoor() {
        return relatedDoor;
    }

    public void setRelatedDoor(String relatedDoor) {
        this.relatedDoor = relatedDoor;
    }

    @Override
    protected void saveAdditional(CompoundTag nbt) {
        super.saveAdditional(nbt);
        if (this.getPassword() != null)
            nbt.putString("password", this.getPassword());
        nbt.putLong("pos", this.getPos().asLong());
        nbt.putLong("supportBlockPos", this.getSupportBlockPos().asLong());
        if (this.getRelatedDoor() != null)
            nbt.putString("relatedDoor", this.getRelatedDoor());
    }

    @Override
    public void load(@NotNull CompoundTag nbt) {
        super.load(nbt);
        this.password = nbt.getString("password");
        this.pos = BlockPos.of(nbt.getLong("pos"));
        this.supportBlockPos = BlockPos.of(nbt.getLong("supportBlockPos"));
        this.relatedDoor = nbt.getString("relatedDoor");
        if (StringUtils.isEmpty(this.relatedDoor)) {
            this.isDoorLocked = true;
            this.ticks = 0;
        }
    }

    @NotNull
    @Override
    public CompoundTag getUpdateTag() {
        super.getUpdateTag();
        return saveWithoutMetadata();
    }

    @Override
    public void handleUpdateTag(CompoundTag nbt) {
        super.handleUpdateTag(nbt);
        this.load(nbt);
    }

    @Override
    public Packet<ClientGamePacketListener> getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }

    public static <T extends BlockEntity> void tick(Level level, BlockPos blockPos, BlockState state,
                                                    T blockEntity) {
        if (level.isClientSide && blockEntity instanceof PadBlockEntity padBlockEntity) {
            if (!padBlockEntity.isDoorLocked()) {
                padBlockEntity.ticks++;
            }

            if (padBlockEntity.ticks == 200) {
                padBlockEntity.setDoorLocked(true);
                FunctionUtils.sendToServer(padBlockEntity);
                padBlockEntity.ticks = 0;
            }
        }
    }
}

解决方案

1. 客户端提前拦截破坏请求(优先推荐)

客户端在发送破坏请求前就判断是否允许破坏,从根源避免服务端取消后出现视觉闪烁:

  • 监听客户端的InputEvent.InteractionKeyMappingTriggered事件,玩家触发破坏动作时,先向服务端发送自定义查询数据包,询问目标方块是否可破坏。
  • 服务端收到查询后返回权限结果,客户端根据结果决定是否执行本地破坏动画。
  • 核心代码示例:
    @Mod.EventBusSubscriber(modid = DoorLockPadMod.MODID, value = Dist.CLIENT)
    public class ClientEventHandlers {
        @SubscribeEvent
        public static void onInteractionTriggered(InputEvent.InteractionKeyMappingTriggered event) {
            if (event.getKeyMapping() == KeyMapping.MOUSE_BUTTON_LEFT) {
                HitResult hit = Minecraft.getInstance().hitResult;
                if (hit.getType() == HitResult.Type.BLOCK) {
                    BlockPos pos = ((BlockHitResult) hit).getBlockPos();
                    NetworkHooks.sendToServer(new QueryBlockBreakablePacket(pos));
                    event.setCanceled(true); // 暂时阻止本地动作,等待服务端响应
                }
            }
        }
    }
    

2. 服务端取消事件后强制同步区块数据

当服务端取消破坏事件后,直接向客户端发送目标方块所在区块的完整同步包,覆盖客户端错误状态:

// 在event.setCanceled(true)后添加
if (!level.isClientSide) {
    ServerPlayer serverPlayer = (ServerPlayer) playerEntity;
    Chunk chunk = level.getChunkAt(blockPos);
    serverPlayer.connection.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, false));
}

此方式数据量较大,适合小范围场景使用。

3. 优化BlockEntity同步时机

修改PadBlockEntity数据后,手动标记变更并触发同步:

padBlockEntity.setRelatedDoor(null);
padBlockEntity.setPassword(null);
padBlockEntity.setChanged(); // 标记BlockEntity已修改
level.sendBlockUpdated(padBlockPos, padBlockState, padBlockState, BlockFlags.BLOCK_UPDATE | BlockFlags.SEND_TO_CLIENTS);

确保服务端数据变更后立即通知客户端更新。

4. 修复原代码逻辑错误

原EventHandlers类中处理PadBlock的分支存在变量未定义问题,将padBlockPos改为blockPos:

} else if (blockState.getBlock() instanceof PadBlock) {
    PadBlockEntity padBlockEntity = (PadBlockEntity) level.getBlockEntity(blockPos);
    if (padBlockEntity != null && (!padBlockEntity.isDoorLocked() || playerEntity.isCreative())) {
        FunctionUtils.removeDoorAssignedPadBlockByPadBlock(blockPos); // 修正变量名
    } else if (padBlockEntity != null) event.setCanceled(true);
}

内容的提问来源于stack exchange,提问作者Huntwer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 10:44:54