Skip to content

Latest commit

 

History

History
1270 lines (1058 loc) · 49.7 KB

File metadata and controls

1270 lines (1058 loc) · 49.7 KB

Zen 抄袭 Naven 的源代码证据

本文档基于公开可获取的两个仓库进行代码段并排对比:

  • 参考实现(被抄袭方)Margele/Naven-Modern
  • 待审项目(疑似抄袭方):本仓 OpenZen(Zen 客户端反混淆产物)

字段 / 类 / 方法名是反编译时根据 Naven 重命名出来的,不能算抄袭指纹。所以下面所有证据全是真实的字符串字面量、魔法常量、控制流、lambda 表达式——这些东西在反混淆过程中不会被自动还原,只能是抄过来的。


目录


AntiBots

三条 ChatUtil 字符串字面量与拼接模板

// Naven AntiBots.java
ChatUtils.addChatMessage("Fake Staff Detected! (" + uuidDisplayNames.get(entry.getKey()) + ")");
...
ChatUtils.addChatMessage("Bot Detected! (" + displayName + ")");
...
ChatUtils.addChatMessage("Bot Removed! (" + displayName + ")");
// Zen AntiBots.java
ChatUtil.print("Fake Staff Detected! (" + suspectNames.get(entry.getKey()) + ")");
...
ChatUtil.print("Bot Detected! (" + botName + ")");
...
ChatUtil.print("Bot Removed! (" + name + ")");

三条字面量一字不差,拼接模板 "X! (" + name + ")" 形状完全一致。这是模块作者主观选的英文短句,撞车概率为零。

isBedWarsBot 的核心三步

// Naven AntiBots.java
public static boolean isBedWarsBot(Entity entity) {
    AntiBots module = (AntiBots) Naven.getInstance().getModuleManager().getModule(AntiBots.class);
    if (module.respawnTimeValue.getCurrentValue() < 1) {
        return false;
    }
    if (!respawnTime.containsKey(entity.getUUID())) {
        return false;
    }
    return System.currentTimeMillis() - respawnTime.get(entity.getUUID()) < module.respawnTimeValue.getCurrentValue();
}

public static boolean isBot(Entity entity) {
    return ids.contains(entity.getId());
}
// Zen AntiBots.java
public static boolean isBedWarsBot(Entity entity) {
    AntiBots antiBots = INSTANCE;
    if (entity.getId() >= 1000000000 || entity.getId() <= -1) { return true; }
    if (entity.getName() == null) { return true; }
    if (entity.getScoreboardName().isEmpty()) { return true; }
    if (antiBots.newPlayerTimeout.getValue().floatValue() < 1.0f) { return false; }
    if (!playerAddTimes.containsKey(entity.getUUID())) { return false; }
    return (float)(System.currentTimeMillis() - playerAddTimes.get(entity.getUUID())) < antiBots.newPlayerTimeout.getValue().floatValue();
}

public static boolean isBot(Entity entity) {
    return confirmedBotIds.contains(entity.getId());
}

Zen 在前面加了几条短路判断遮掩,但核心三步 < 1 哨兵 → containsKey 检查 → currentTimeMillis() - map.get(uuid) < timeout 比对完全照抄,isBot 一行实现一模一样。

Setting 字面量 "Respawn Time" + 2500 + 0~10000 + 100

// Naven AntiBots.java
private final FloatValue respawnTimeValue = ValueBuilder.create(this, "Respawn Time")
        .setDefaultFloatValue(2500).setFloatStep(100)
        .setMinFloatValue(0).setMaxFloatValue(10000).build().getFloatValue();
// Zen AntiBots.java
private final NumberSetting newPlayerTimeout =
        new NumberSetting("Respawn Time", 2500.0, 0.0, 10000.0, 100.0);

抛开字段名,"Respawn Time" 字符串、默认 2500、范围 0–10000、步长 100 四个字面量一一对应。

onPacket 三个 instanceof 分支的过滤条件

// Naven AntiBots.java
if (packet.getAction() == ClientboundPlayerInfoPacket.Action.ADD_PLAYER) {
    for (ClientboundPlayerInfoPacket.PlayerUpdate entry : packet.getEntries()) {
        if (entry.getDisplayName() != null && (entry.getDisplayName().getSiblings().isEmpty()
                && entry.getGameMode() == GameType.SURVIVAL)) {
            UUID uuid = entry.getProfile().getId();
            uuids.put(uuid, System.currentTimeMillis());
            uuidDisplayNames.put(uuid, entry.getDisplayName().getString());
        }
    }
} else if (e.getPacket() instanceof ClientboundAddPlayerPacket) {
    if (uuids.containsKey(packet.getPlayerId())) {
        String displayName = uuidDisplayNames.get(packet.getPlayerId());
        ChatUtils.addChatMessage("Bot Detected! (" + displayName + ")");
        entityIdDisplayNames.put(packet.getEntityId(), displayName);
        uuids.remove(packet.getPlayerId());
        ids.add(packet.getEntityId());
    }
}
// Zen AntiBots.java
if (!infoUpdate.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) { return; }
for (ClientboundPlayerInfoUpdatePacket.Entry entry : infoUpdate.entries()) {
    if (entry.displayName() == null
            || !entry.displayName().getSiblings().isEmpty()
            || entry.gameMode() != GameType.SURVIVAL) { continue; }
    UUID uuid = entry.profile().getId();
    suspectJoinTimes.put(uuid, System.currentTimeMillis());
    suspectNames.put(uuid, entry.displayName().getString());
}
...
if (packet instanceof ClientboundAddPlayerPacket addPlayer) {
    if (!suspectJoinTimes.containsKey(addPlayer.getPlayerId())) { return; }
    String botName = suspectNames.get(addPlayer.getPlayerId());
    ChatUtil.print("Bot Detected! (" + botName + ")");
    confirmedBotNames.put(addPlayer.getEntityId(), botName);
    suspectJoinTimes.remove(addPlayer.getPlayerId());
    confirmedBotIds.add(addPlayer.getEntityId());
    return;
}

过滤条件三件套 displayName != null && siblings.isEmpty() && gameMode == SURVIVAL 一字不差(Zen 只用 De Morgan 取反 continue),随后五步 put / get / remove / add 顺序完全相同。

500ms 假员工阈值

// Naven AntiBots.java
for (Map.Entry<UUID, Long> entry : uuids.entrySet()) {
    if (System.currentTimeMillis() - entry.getValue() > 500) {
        ChatUtils.addChatMessage("Fake Staff Detected! (" + uuidDisplayNames.get(entry.getKey()) + ")");
        uuids.remove(entry.getKey());
    }
}
// Zen AntiBots.java
for (Map.Entry<UUID, Long> entry : suspectJoinTimes.entrySet()) {
    if (System.currentTimeMillis() - entry.getValue() <= 500L) continue;
    if (this.debug.getValue()) {
        ChatUtil.print("Fake Staff Detected! (" + suspectNames.get(entry.getKey()) + ")");
    }
    suspectJoinTimes.remove(entry.getKey());
}

500 ms 这个魔法常量、currentTimeMillis() - entry.getValue() 减法表达式、循环里就地 remove 的反模式写法三处都对得上。


CrystalAura

onPacket 末影水晶生成 9 步序列

// Naven AttackCrystal.java
if (packet.getType() == EntityType.END_CRYSTAL) {
    EndCrystal pTarget = new EndCrystal(mc.level, packet.getX(), packet.getY(), packet.getZ());
    pTarget.setId(packet.getId());
    if (mc.player.distanceTo(pTarget) <= 4) {
        Vector2f rotations = RotationUtils.getRotations(pTarget);
        mc.getConnection().send(new ServerboundMovePlayerPacket.PosRot(mc.player.getX(), mc.player.getY(), mc.player.getZ(), rotations.getX(), rotations.getY(), mc.player.isOnGround()));
        mc.getConnection().send(new ServerboundUseItemPacket(InteractionHand.MAIN_HAND));
        float currentYaw = mc.player.getYRot();
        float currentPitch = mc.player.getXRot();
        mc.player.setYRot(RotationManager.rotations.x);
        mc.player.setXRot(RotationManager.rotations.y);
        mc.getConnection().send(ServerboundInteractPacket.createAttackPacket(pTarget, false));
        mc.player.swing(InteractionHand.MAIN_HAND);
// Zen CrystalAura.java
if (addEntityPacket.getType() == EntityType.END_CRYSTAL) {
    EndCrystal endCrystal = new EndCrystal(mc.level, addEntityPacket.getX(), addEntityPacket.getY(), addEntityPacket.getZ());
    endCrystal.setId(addEntityPacket.getId());
    if (mc.player.distanceTo(endCrystal) <= 4.0f) {
        Rotation rotation = RotationUtil.entityRotation(endCrystal);
        mc.getConnection().send(new ServerboundMovePlayerPacket.PosRot(mc.player.getX(), mc.player.getY(), mc.player.getZ(), rotation.getYaw(), rotation.getPitch(), mc.player.onGround()));
        PacketUtil.sendPredictive(seq -> new ServerboundUseItemPacket(InteractionHand.MAIN_HAND, seq));
        float prevYaw = mc.player.getYRot();
        float prevPitch = mc.player.getXRot();
        mc.player.setYRot(RotationHandler.targetRotation.getYaw());
        mc.player.setXRot(RotationHandler.targetRotation.getPitch());
        mc.getConnection().send(ServerboundInteractPacket.createAttackPacket(endCrystal, false));
        mc.player.swing(InteractionHand.MAIN_HAND);

相同的距离阈值 <= 4、相同的 9 步操作序列(new EndCrystal → setId → distance → getRotations → PosRot → UseItem → save yaw/pitch → setYRot/XRot → InteractPacket → swing)。

onTick 的 StreamSupport 查找

// Naven AttackCrystal.java
Optional<Entity> any = StreamSupport.stream(mc.level.entitiesForRendering().spliterator(), true).filter(entity -> entity instanceof EndCrystal).findAny();
rotations = null;
if (any.isPresent()) {
    Entity entity = any.get();
    Vector2f rots = RotationUtils.getRotations(entity);
    double minDistance = RotationUtils.getMinDistance(entity, rots);
    if (minDistance <= 3) {
        rotations = rots;
        this.entity = entity;
// Zen CrystalAura.java
Optional<Entity> crystalOpt = StreamSupport.stream(mc.level.entitiesForRendering().spliterator(), true).filter(entity -> entity instanceof EndCrystal).findAny();
aimRotation = null;
if (crystalOpt.isPresent() && (hitDistance = RotationUtil.getMinHitDistance(crystalEntity = crystalOpt.get(), rotation = RotationUtil.entityRotation(crystalEntity))) <= 3.0) {
    aimRotation = rotation;
    this.crystalTarget = crystalEntity;

StreamSupport.stream(... .spliterator(), true) 并行流、EndCrystal instanceof 过滤、<= 3 命中距离阈值、aimRotation = null 再赋值的控制流完全一致。


AntiFireball

// Naven AntiFireball.java
Stream<Entity> stream = StreamSupport.stream(mc.level.entitiesForRendering().spliterator(), true);
Optional<Fireball> fireball = stream.filter(entity -> entity instanceof Fireball && mc.player.distanceTo(entity) < 6).map(entity -> (Fireball) entity).findFirst();
if (!fireball.isPresent()) {
    return;
}
Fireball entity = fireball.get();
mc.gameMode.attack(mc.player, entity);
mc.player.swing(InteractionHand.MAIN_HAND);
// Zen AntiFireball.java
Stream<Entity> stream = StreamSupport.stream(mc.level.entitiesForRendering().spliterator(), false);
Optional<Fireball> optional = stream.filter(entity -> entity instanceof Fireball && (double)mc.player.distanceTo(entity) < 6.0).map(entity -> (Fireball)entity).findFirst();
if (!optional.isPresent()) {
    return;
}
Fireball fireball = optional.get();
ChatUtil.print("§c[AntiFireball] Attacking fireball...");
mc.gameMode.attack(mc.player, fireball);
mc.player.swing(InteractionHand.MAIN_HAND);

lambda 链 instanceof Fireball && distanceTo < 6 一字不差,魔法数 6 完全一致,mc.gameMode.attack(mc.player, ...) + swing(MAIN_HAND) 收尾相同。Zen 仅多一行 debug 输出。


Scaffold

onEnablegetYRot() - 180 的初始化

// Naven Scaffold.java
public void onEnable() {
    if (mc.player != null) {
        oldSlot = mc.player.getInventory().selected;
        this.rots.set(mc.player.getYRot() - 180, mc.player.getXRot());
        this.lastRots.set(mc.player.yRotO - 180, mc.player.yRotO);
        this.pos = null;
    }
}
// Zen Scaffold.java
public void onEnable() {
    if (mc.player != null) {
        this.oldSlot = mc.player.getInventory().selected;
        this.rots.setYawPitch(mc.player.getYRot() - 180.0f, mc.player.getXRot());
        this.lastRots.setYawPitch(mc.player.yRotO - 180.0f, mc.player.xRotO);
        ...
    }
    super.onEnable();
}

把 yaw 减 180 度作为脚手架初始 yaw(让玩家朝身后放方块)是 Naven 自定义做法;4 个字段、顺序、yRotO/xRotO 完全对应。

模式名 "Normal" / "Telly Bridge" / "Keep Y" + Eagle/Snap 的可见性 lambda

// Naven Scaffold.java
public ModeValue mode = ValueBuilder.create(this, "Mode").setDefaultModeIndex(0).setModes("Normal", "Telly Bridge", "Keep Y")...
public BooleanValue eagle = ...("Eagle").setDefaultBooleanValue(true).setVisibility(() -> mode.isCurrentMode("Normal"))...
public BooleanValue snap  = ...("Snap").setDefaultBooleanValue(true).setVisibility(() -> mode.isCurrentMode("Normal"))...
public BooleanValue renderItemSpoof = ...("Render Item Spoof").setDefaultBooleanValue(true)...
// Zen Scaffold.java
public final ModeSetting mode = new ModeSetting("Mode", "Normal", "Telly Bridge", "Old Telly", "Keep Y").withDefault("Normal");
public final BooleanSetting eagle = new BooleanSetting("Eagle", true, () -> this.mode.is("Normal"));
public final BooleanSetting snap  = new BooleanSetting("Snap", true, () -> this.mode.is("Normal"));
public final BooleanSetting renderItemSpoof = new BooleanSetting("Render Item Spoof", true);

模式列表 Normal / Telly Bridge / Keep Y 三个字符串字面量和顺序完全一致;Eagle/Snap 都用 visibility = mode == "Normal" 这种 lambda;"Render Item Spoof" 这种特定字符串字面量在两边都用作选项名。

doSnap+ nextFloat(0, 0.5F) - 0.25F 抖动指纹

// Naven Scaffold.java
if (!shouldPlaceBlock) {
    rots.setX(mc.player.getYRot() + RandomUtils.nextFloat(0, 0.5F) - 0.25F);
}
// Zen Scaffold.java
if (!lookingAtBlock && mc.player.tickCount % 4 == 0) {
    this.rots.setYaw(mc.player.getYRot() + RandomUtils.nextFloat(0.0f, 0.5f) - 0.25f);
}

nextFloat(0, 0.5) - 0.25 来得到 [-0.25, +0.25] 抖动量的写法是 Naven 风格(直接 nextFloat(-0.25, 0.25) 更自然)。这种"减半法"指纹两边完全一致。

isOnBlockEdge —— 再来一次

// Naven Scaffold.java
public static boolean isOnBlockEdge(float sensitivity) {
    return !mc.level.getCollisions(mc.player, mc.player.getBoundingBox().move(0.0, -0.5, 0.0).inflate(-sensitivity, 0.0, -sensitivity)).iterator().hasNext();
}
...
mc.options.keyShift.setDown(mc.player.isOnGround() && isOnBlockEdge(0.3F));
// Zen Scaffold.java
public static boolean isOnBlockEdge(float inflate) {
    return !mc.level.getCollisions(mc.player,
            mc.player.getBoundingBox().move(0.0, -0.5, 0.0).inflate(-inflate, 0.0, -inflate))
            .iterator().hasNext();
}
...
mc.options.keyShift.setDown(mc.player.onGround() && isOnBlockEdge(0.3f));

Zen Scaffold 把 SafeWalk 的同一份 isOnBlockEdge 在自己类里又粘贴了一遍,连 0.3f 阈值与 keyShift.setDown(onGround && isOnBlockEdge(0.3f)) 这一整行表达式都一样。


FastWeb

cobweb 阈值 + magic vector (0.88, 1.88, 0.88)

// Naven FastWeb.java
public void onStuck(EventStuckInBlock e) {
    if (e.getState().getBlock() == Blocks.COBWEB) {
        playerInWebTick = mc.player.tickCount;
        ticksInWeb ++;
        if (ticksInWeb > 5) {
            Vec3 newSpeed = new Vec3(0.88, 1.88, 0.88);
            e.setStuckSpeedMultiplier(newSpeed);
        }
    }
}
// Zen FastWeb.java
public void onStuckInBlock(StuckInBlockEvent stuckInBlockEvent) {
    if (stuckInBlockEvent.getBlockState().getBlock() == Blocks.COBWEB && mc.player != null) {
        this.lastWebTick = mc.player.tickCount;
        ++this.webCount;
        if (this.webCount > 5) {
            Vec3 vec3 = new Vec3(0.88, 1.88, 0.88);
            stuckInBlockEvent.setMotion(vec3);
        }
    }
}

> 5 阈值、(0.88, 1.88, 0.88) 三元魔法常量、记录 tickCount 然后递增计数器的顺序——三层细节全部撞车。0.88 / 1.88 不是 Minecraft 物理常量,是 Naven 独有的口味。

onMotion 中的重置逻辑

// Naven FastWeb.java
public void onMotion(EventMotion e) {
    if (e.getType() == EventType.POST) {
        if (playerInWebTick < mc.player.tickCount) {
            ticksInWeb = 0;
        }
    }
}
// Zen FastWeb.java
public void onMotion(MotionEvent motionEvent) {
    if (motionEvent.isPre() && mc.player != null && this.lastWebTick < mc.player.tickCount) {
        this.webCount = 0;
    }
}

lastTick < player.tickCount 条件 + reset 计数到 0 的逻辑完全相同(仅 PRE/POST 差异)。


Stuck

+ 1337 魔法位移

// Naven Stuck.java
if (tryDisable) {
    NetworkUtils.sendPacketNoEvent(new ServerboundMovePlayerPacket.Pos(mc.player.getX() + 1337, mc.player.getY(), mc.player.getZ() + 1337, mc.player.isOnGround()));
    while (!packets.isEmpty()) {
        NetworkUtils.sendPacketNoEvent(packets.poll());
    }
    tryDisable = false;
}
// Zen Stuck.java
if (this.pendingDisable) {
    if (this.modeSetting.is("Delay")) {
        PacketUtil.sendQueued(new ServerboundMovePlayerPacket.Pos(mc.player.getX() + 1337.0, mc.player.getY(), mc.player.getZ() + 1337.0, mc.player.onGround()));
    } ...
    while (!this.pongQueue.isEmpty()) {
        PacketUtil.sendQueued((Packet<ServerGamePacketListener>) this.pongQueue.poll());
    }
    ...
    this.pendingDisable = false;
}

把玩家 X/Z 同时加 1337 这种"骇客致敬"魔法常量、外加紧跟其后的 pongQueue.poll() 循环——这就是经典 Naven 指纹。

onPacket 四个 instanceof 分支链顺序完全相同

// Naven Stuck.java
public void onPacket(EventPacket e) {
    if (e.getPacket() instanceof ServerboundMovePlayerPacket) {
        e.setCancelled(true);
    } else if (e.getPacket() instanceof ServerboundPongPacket) {
        packets.offer((ServerboundPongPacket) e.getPacket());
        e.setCancelled(true);
    } else if (e.getPacket() instanceof ServerboundUseItemPacket || e.getPacket() instanceof ServerboundPlayerActionPacket) {
        packet = e.getPacket();
        stage = 1;
        e.setCancelled(true);
    } else if (e.getPacket() instanceof ClientboundPlayerPositionPacket) {
        while (!packets.isEmpty()) { NetworkUtils.sendPacketNoEvent(packets.poll()); }
        stage = 3;
        toggle();
    }
}
// Zen Stuck.java
public void onPacket(PacketEvent packetEvent) {
    ...
    if (rawPacket instanceof ServerboundMovePlayerPacket movePacket) { ...; packetEvent.setCancelled(true); }
    else if (packetEvent.getPacket() instanceof ServerboundPongPacket) {
        this.pongQueue.offer((ServerboundPongPacket)packetEvent.getPacket());
        packetEvent.setCancelled(true);
    } else if (packetEvent.getPacket() instanceof ServerboundUseItemPacket || packetEvent.getPacket() instanceof ServerboundPlayerActionPacket) {
        this.capturedPacket = packetEvent.getPacket();
        this.stuckState = 1;
        packetEvent.setCancelled(true);
    } else if (packetEvent.getPacket() instanceof ClientboundPlayerPositionPacket && this.modeSetting.is("Delay")) {
        while (!this.pongQueue.isEmpty()) { PacketUtil.sendQueued(...); }
        this.stuckState = 3;
        this.setEnabled(false);
    }
}

四个 instanceof 分支顺序完全相同:Move → Pong → (UseItem/PlayerAction) → ClientboundPos。每个分支内部动作(offer/cancel、记 stage=1、最后 stage=3+toggle)也完全平行。

shouldRotateBowlFoodItem || BowItem 双过滤

// Naven Stuck.java
if (packet instanceof ServerboundUseItemPacket) {
    ServerboundUseItemPacket blockPlacement = (ServerboundUseItemPacket) packet;
    ItemStack item = mc.player.getItemInHand(blockPlacement.getHand());
    if (item.getItem() instanceof BowlFoodItem || item.getItem() instanceof BowItem) {
        return false;
    }
    return true;
} else if (packet instanceof ServerboundPlayerActionPacket) {
    ...
    if (playerDigging.getAction() == ServerboundPlayerActionPacket.Action.RELEASE_USE_ITEM) {
        if (mc.player.getUseItem().getItem() instanceof BowItem) { return true; }
    }
    return false;
}
// Zen Stuck.java
if (this.capturedPacket instanceof ServerboundUseItemPacket useItemPacket) {
    ItemStack heldStack = mc.player.getItemInHand(useItemPacket.getHand());
    return !(heldStack.getItem() instanceof BowlFoodItem) && !(heldStack.getItem() instanceof BowItem);
}
if (this.capturedPacket instanceof ServerboundPlayerActionPacket actionPacket) {
    return actionPacket.getAction() == ServerboundPlayerActionPacket.Action.RELEASE_USE_ITEM
            && mc.player.getUseItem().getItem() instanceof BowItem;
}

BowlFoodItem + BowItem 的组合过滤是 Naven 自创口味(碗食物本就极少有人在这种场景去黑名单)。配上 RELEASE_USE_ITEM + BowItem 的分支,几乎是 1:1 翻译。


AutoMLG

触发结构(落地预测 + 热栏扫描 + 切槽放水)

// Naven AutoMLG.java
public static boolean isOnGround(double height) {
    Iterable<VoxelShape> collisions = mc.level.getBlockCollisions(mc.player, mc.player.getBoundingBox().move(0.0D, height, 0.0D));
    return collisions.iterator().hasNext();
}
...
for (int i = 0; i < 9; i++) {
    ItemStack item = mc.player.getInventory().getItem(i);
    if (!item.isEmpty() && item.getItem() == Items.WATER_BUCKET) {
        originalSlot = mc.player.getInventory().selected;
        mc.player.getInventory().selected = i;
        ...
    }
}
// Zen AutoMLG.java
private boolean hasSolidBelow(BlockPos blockPos) {
    return this.isSolidNonMenu(blockPos.below()) || this.isSolidNonMenu(blockPos.below(2));
}
private boolean isSolidNonMenu(BlockPos blockPos) {
    BlockState blockState = mc.level.getBlockState(blockPos);
    boolean hasCollision = !blockState.getCollisionShape(mc.level, blockPos).isEmpty();
    ...
}
...
slot = ItemUtil.findItemInRange(0, 9, Items.WATER_BUCKET);
if (slot < 0) return;
...
this.selectSlot(slot);   // 内部就是 selected=oldSlot; selected=slot

固定 0..9 热栏扫描 + WATER_BUCKET + 保存 originalSlot 再切换 —— 结构相同,Zen 仅把 for 循环抽成 ItemUtil.findItemInRange 工具函数。Fall Distance 默认 3.0 / 步长 0.1f 两个 NumberSetting 字面量参数也完全一致。


SafeWalk

isOnBlockEdge 静态方法 —— 字符级雷同

// Naven SafeWalk.java
public static boolean isOnBlockEdge(float sensitivity) {
    return !mc.level.getCollisions(mc.player, mc.player.getBoundingBox().move(0.0, -0.5, 0.0).inflate(-sensitivity, 0.0, -sensitivity)).iterator().hasNext();
}
// Zen SafeWalk.java
public static boolean isOnBlockEdge(float inset) {
    return !mc.level.getCollisions(mc.player, mc.player.getBoundingBox().move(0.0, -0.5, 0.0).inflate(-inset, 0.0, -inset)).iterator().hasNext();
}

除参数名(sensitivityinset)外完全一字不差。move(0.0, -0.5, 0.0).inflate(-x, 0.0, -x).iterator().hasNext() 这一连串调用是经典 Naven 指纹。

onDisable 恢复 keyShift

// Naven SafeWalk.java
public void onDisable() {
    boolean isHoldingShift = InputConstants.isKeyDown(mc.getWindow().getWindow(), mc.options.keyShift.getKey().getValue());
    mc.options.keyShift.setDown(isHoldingShift);
}
// Zen SafeWalk.java
public void onDisable() {
    boolean keyDown = InputConstants.isKeyDown(mc.getWindow().getWindow(), mc.options.keyShift.getKey().getValue());
    mc.options.keyShift.setDown(keyDown);
}

仅变量名不同,控制流、API 调用、参数顺序完全一致。


Disabler

duplicateRotPlace 算法 —— > 2 + < 0.0001 双阈值 + rotated 状态机

// Naven Disabler.java
if (packet.hasRotation()) {
    float lastPlayerYaw = playerYaw;
    playerYaw = packet.getYRot(0);
    deltaYaw = Math.abs(playerYaw - lastPlayerYaw);
    rotated = true;
    // Guess what will happen if you placed the block in current tick
    if (deltaYaw > 2) {
        float xDiff = Math.abs(deltaYaw - lastPlacedDeltaYaw);
        if (xDiff < 0.0001) {
            log("Disabling DuplicateRotPlace!");
            ...
        }
    }
} else if (e.getPacket() instanceof ServerboundUseItemOnPacket) {
    if (rotated) {
        lastPlacedDeltaYaw = deltaYaw;
        rotated = false;
    }
}
// Zen Disabler.java
if (movePacket.hasRotation()) {
    float prevYaw = this.currentYaw;
    float prevPitch = this.currentPitch;
    this.currentYaw = ReflectionUtil.getYRot(movePacket);
    this.currentPitch = ReflectionUtil.getXRot(movePacket);
    this.yawDiff = Math.abs(this.currentYaw - prevYaw);
    this.pitchDiff = Math.abs(this.currentPitch - prevPitch);
    this.rotated = true;
    float yawDelta;
    if (this.yawDiff > 2.0f && (double)(yawDelta = Math.abs(this.yawDiff - this.lastPlacedYawDiff)) < 1.0E-4) {
        ...
    }
} else if (packet instanceof ServerboundUseItemOnPacket && this.rotated) {
    this.lastPlacedYawDiff = this.yawDiff;
    this.lastPlacedPitchDiff = this.pitchDiff;
    this.rotated = false;
}

完全一致的算法:取上一 yaw → 算 deltaYaw → > 2 阈值 → 算与上次放置 deltaYaw 的差 → < 0.0001 (1.0E-4) 判定 → USE_ITEM_ON 时把当前 delta 写入 lastPlacedDeltaYaw 并清零 rotated。Zen 仅把单一 yaw 扩展为 yaw + pitch 两份。


AutoTools

getBestTool 算法 —— efficiencyLevel * efficiencyLevel + 1 公式

// Naven AutoTools.java
for (int index = 0; index < 9; index++) {
    ItemStack itemStack = mc.player.getInventory().getItem(index);
    if (InventoryUtils.isGodItem(itemStack)) continue;
    if (!itemStack.isEmpty() && !blockState.isAir() && (!(itemStack.getItem() instanceof SwordItem) || block instanceof WebBlock)) {
        float strVsBlock = itemStack.getItem().getDestroySpeed(itemStack, blockState);
        if (strVsBlock > 1 && !(block instanceof OreBlock || block instanceof RedStoneOreBlock)) {
            int i = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BLOCK_EFFICIENCY, itemStack);
            if (i > 0) {
                strVsBlock += (float) (i * i + 1);
            }
        }
        if (strVsBlock > dmg) {
            slot = index;
            dmg = strVsBlock;
        }
    }
}
if (dmg > 1F) return slot;
return -1;
// Zen AutoTools.java
for (int i = 0; i < 9; ++i) {
    int efficiencyLevel;
    ItemStack itemStack = mc.player.getInventory().getItem(i);
    if (ItemUtil.isWeaponItem(itemStack) || itemStack.isEmpty() || blockState.isAir()
            || itemStack.getItem() instanceof SwordItem && !(block instanceof WebBlock)) continue;
    float destroySpeed = itemStack.getItem().getDestroySpeed(itemStack, blockState);
    if (destroySpeed > 1.0f && !(block instanceof DropExperienceBlock) && !(block instanceof RedStoneOreBlock)
            && (efficiencyLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BLOCK_EFFICIENCY, itemStack)) > 0) {
        destroySpeed += (float)(efficiencyLevel * efficiencyLevel + 1);
    }
    if (!(destroySpeed > bestSpeed)) continue;
    bestSlot = i;
    bestSpeed = destroySpeed;
}
if (bestSpeed > 1.0f) return bestSlot;
return -1;

循环上限 9WebBlock 例外、附魔加成公式 i * i + 1、对矿石方块跳过加成、> 1.0f 的最终阈值、不命中返 -1,逐行对应。OreBlock → DropExperienceBlock 只是 1.17 → 1.19 Mojang 类名重命名后的同义替换。


ChestStealer

三连 Component.translatable("container.chest / chestDouble / enderchest") + 兜底 "Chest"

// Naven ChestStealer.java (1.17 API)
String chestTitle = container.getTitle().getString();
String chest = new TranslatableComponent("container.chest").getString();
String largeChest = new TranslatableComponent("container.chestDouble").getString();
String enderChest = new TranslatableComponent("container.enderchest").getString();
if (chestTitle.equals(chest) || chestTitle.equals(largeChest) || chestTitle.equals("Chest")
        || (pickEnderChest.getCurrentValue() && chestTitle.equals(enderChest))) {
// Zen ChestStealer.java
String title = containerScreen.getTitle().getString();
String chestTitle = Component.translatable("container.chest").getString();
String doubleChestTitle = Component.translatable("container.chestDouble").getString();
String enderChestTitle = Component.translatable("container.enderchest").getString();
ChestMenu chestMenu = containerScreen.getMenu();
if (this.chestSetting.getValue() && (title.equals(chestTitle) || title.equals(doubleChestTitle) || title.equals("Chest"))) {
    ...
} else if (this.enderChestSetting.getValue() && title.equals(enderChestTitle) && ...) {

完全相同的三个翻译键 + 多出来的硬编码英文兜底 "Chest" + 单独把 ender chest 拎出来做开关。这种组合不是写 hack 的标准写法 —— Zen 把 1.17 new TranslatableComponent(...) 机械替换成 1.19+ 的 Component.translatable(...)


InventoryManager

中文字面量 "点击使用" —— 终极铁证

// Naven InventoryCleaner.java
public boolean isItemUseful(ItemStack stack) {
    if (stack.isEmpty()) return false;
    if (InventoryUtils.isGodItem(stack)) return true;
    if (stack.getDisplayName().getString().contains("点击使用")) {
        return true;
    }
    if (stack.getItem() instanceof ArmorItem) { ... }
    else if (stack.getItem() instanceof SwordItem)   { return InventoryUtils.getBestSword()   == stack; }
    else if (stack.getItem() instanceof PickaxeItem) { return InventoryUtils.getBestPickaxe() == stack; }
    else if (stack.getItem() instanceof AxeItem && !InventoryUtils.isSharpnessAxe(stack)) {
        return InventoryUtils.getBestAxe() == stack;
    }
    ...
}
// Zen InventoryManager.java
public boolean isUsefulItem(ItemStack itemStack) {
    if (itemStack.isEmpty()) return false;
    if (ItemUtil.isWeaponItem(itemStack)) return true;
    if (itemStack.getDisplayName().getString().contains("点击使用")) {
        return true;
    }
    if (itemStack.getItem() == Items.COBWEB) return true;
    Item item = itemStack.getItem();
    if (item instanceof ArmorItem armorItem) { ... }
    if (itemStack.getItem() instanceof SwordItem)   { return ItemUtil.getBestSword()   == itemStack; }
    if (itemStack.getItem() instanceof PickaxeItem) { return ItemUtil.getBestPickaxe() == itemStack; }
    if (itemStack.getItem() instanceof AxeItem && !ItemUtil.isLegitAxe(itemStack)) {
        return ItemUtil.getBestAxe() == itemStack;
    }
    ...
}

contains("点击使用") 是 Naven 作者针对国服服务端(hypixel.cn / 网易我的世界类)放进物品名的中文标记,与英文环境的 Minecraft 毫无关联。Zen 一字不差搬了过来,连后续的 getBestSword() == stack 这种"引用比较"风格的奇怪判定也照抄。单凭这一行就足以认定整段 isUsefulItem 是直接复制粘贴的。

四种 offhand 模式与"水/岩浆桶上限 = 1"

// Zen InventoryManager.java
private final ModeSetting offhandItemSetting = new ModeSetting("Offhand Items",
        "Golden Apple", "Fishing Rod", "None").withDefault("Projectile");
...
if ("Golden Apple".equals(offhandPreference))      { if (this.handleGoldenAppleOffhand()) return true; }
else if ("Projectile".equals(offhandPreference))   { if (this.handleProjectileOffhand()) return true; }
else if ("Fishing Rod".equals(offhandPreference))  { if (this.handleFishingRodOffhand()) return true; }
else if ("Block".equals(offhandPreference))        { if (this.handleBlockOffhand())      return true; }

public static int getMaxWaterBuckets() { return 1; }
public static int getMaxLavaBuckets()  { return 1; }

Naven 的 isItemUseful 里也分别处理 GoldenApple / Projectile (Snowball + Egg) / FishingRod / Block 四类,Zen 直接把它们提升为四个 handleXxxOffhand() 函数,并把 Naven 的 getWaterBucketCount() / getLavaBucketCount() 设置项简化成硬编码 return 1;


ChestESP

箱子打开判定 —— 四重条件

// Naven ChestESP.java
if ((packet.getBlock() == Blocks.CHEST || packet.getBlock() == Blocks.TRAPPED_CHEST) && packet.getB0() == 1 && packet.getB1() == 1) {
    openedChests.add(packet.getPos());
}
// Zen ChestESP.java
if ((clientboundBlockEventPacket.getBlock() == Blocks.CHEST || clientboundBlockEventPacket.getBlock() == Blocks.TRAPPED_CHEST) && clientboundBlockEventPacket.getB0() == 1 && clientboundBlockEventPacket.getB1() == 1) {
    this.openedChestPositions.add(clientboundBlockEventPacket.getPos());

四重条件(CHEST、TRAPPED_CHEST、b0 == 1、b1 == 1)完全一样,魔法常数 1 都来自 ChestBlockEntity.lidEvent。

getChestBox 双箱合并算法

// Naven ChestESP.java
ChestType chestType = state.getValue(ChestBlock.TYPE);
if (chestType == ChestType.LEFT) {
    return null;
}
BlockPos pos = chestBE.getBlockPos();
AABB box = BlockUtils.getBoundingBox(pos);
if (chestType != ChestType.SINGLE) {
    BlockPos pos2 = pos.relative(ChestBlock.getConnectedDirection(state));
    if (BlockUtils.canBeClicked(pos2)) {
        AABB box2 = BlockUtils.getBoundingBox(pos2);
        box = box.minmax(box2);
// Zen ChestESP.java
ChestType chestType = blockState.getValue(ChestBlock.TYPE);
if (chestType == ChestType.LEFT) {
    return null;
}
BlockPos blockPos2 = chestBlockEntity.getBlockPos();
AABB aABB = BlockUtil.getBoundingBox(blockPos2);
if (chestType != ChestType.SINGLE && BlockUtil.canBeClicked(blockPos = blockPos2.relative(ChestBlock.getConnectedDirection(blockState)))) {
    AABB aABB2 = BlockUtil.getBoundingBox(blockPos);
    aABB = aABB.minmax(aABB2);

LEFT 跳过、SINGLE 单箱、getConnectedDirection 双箱合并、minmax 合并 AABB —— 整段算法逐行复刻。

箱子颜色常量

// Naven ChestESP.java
private static final float[] chestColor = {0, 1, 0};
private static final float[] openedChestColor = {1, 0, 0};
// Zen ChestESP.java (static init)
chestColor = new float[]{0.0f, 1.0f, 0.0f};
openedChestColor = new float[]{1.0f, 0.0f, 0.0f};

两个 float[] 颜色常数(绿色未开、红色已开)原值搬过来,连透明度 0.25f 也一致。


Compass

yaw 角度计算公式

// Naven Compass.java
float yaw = (float) (Math.toDegrees(Math.atan2(spawnPosition.getZ() - renderZ, spawnPosition.getX() - renderX)) - 90 - renderYaw);
float x = mc.getWindow().getGuiScaledWidth() / 2f;
float y = mc.getWindow().getGuiScaledHeight() / 2f;
// Zen Compass.java
float angle = (float)(Math.toDegrees(Math.atan2((double)this.spawnPosition.getZ() - this.renderZ, (double)this.spawnPosition.getX() - this.renderX)) - 90.0 - (double)this.renderYaw);
float centerX = (float)mc.getWindow().getGuiScaledWidth() / 2.0f;
float centerY = (float)mc.getWindow().getGuiScaledHeight() / 2.0f;

atan2(Z, X) - 90 - yaw 这个公式连项数顺序都没变,魔法常数 90 直接搬。

两个 BooleanSetting 字面量

// Naven Compass.java
public BooleanValue compassOnly = ValueBuilder.create(this, "Compass Only").setDefaultBooleanValue(true).build()...;
public BooleanValue noPlayerOnly = ValueBuilder.create(this, "No Player Only").setDefaultBooleanValue(true).build()...;
// Zen Compass.java
private final BooleanSetting compassOnly = new BooleanSetting("Compass Only", true);
private final BooleanSetting noPlayerOnly = new BooleanSetting("No Player Only", true);

字面量名(含空格大小写)"Compass Only" / "No Player Only" 与默认值 true / true 完全一致,注册顺序也相同。

renderX / renderZ / renderYawMth.lerp 三连

// Naven Compass.java
renderX = Mth.lerp(e.getRenderPartialTicks(), mc.player.xOld, mc.player.getX());
renderZ = Mth.lerp(e.getRenderPartialTicks(), mc.player.zOld, mc.player.getZ());
renderYaw = Mth.lerp(e.getRenderPartialTicks(), mc.player.yRotO, mc.player.getYRot());
// Zen Compass.java
this.renderX = Mth.lerp(renderEvent.partialTick(), mc.player.xOld, mc.player.getX());
this.renderZ = Mth.lerp(renderEvent.partialTick(), mc.player.zOld, mc.player.getZ());
this.renderYaw = Mth.lerp(renderEvent.partialTick(), mc.player.yRotO, mc.player.getYRot());

xOld / zOld / yRotO 的顺序都没换。


Projectiles

弓蓄力公式(含魔法数 72000 / 3 / 0.1)

// Naven Projectile.java
float bowPower = (72000 - player.getUseItemRemainingTicks()) / 20.0f;
bowPower = (bowPower * bowPower + bowPower * 2.0f) / 3.0f;
if (bowPower > 1 || bowPower <= 0.1F) bowPower = 1;
bowPower *= 3F;
// Zen Projectiles.java
float pull = (72000 - localPlayer.getUseItemRemainingTicks()) / 20.0f;
pull = (pull * pull + pull * 2.0f) / 3.0f;
if (pull > 1.0f || pull <= 0.1f) pull = 1.0f;
pull *= 3.0f;

完整公式 (72000 - x) / 20 → (p² + 2p) / 3 → clamp → × 3 一模一样,仅变量名 bowPower → pull

抛物物品白名单 —— 9 项 instanceof 链顺序相同

// Naven Projectile.java
return item instanceof BowItem || item instanceof CrossbowItem || item instanceof SnowballItem || item instanceof EggItem || item instanceof EnderpearlItem || item instanceof SplashPotionItem || item instanceof LingeringPotionItem || item instanceof FishingRodItem || item instanceof TridentItem;
// Zen Projectiles.java
return item instanceof BowItem || item instanceof CrossbowItem
        || item instanceof SnowballItem || item instanceof EggItem
        || item instanceof EnderpearlItem || item instanceof SplashPotionItem
        || item instanceof LingeringPotionItem || item instanceof FishingRodItem
        || item instanceof TridentItem;

9 个 Item 类型按完全相同的顺序排列。

重力 switch + 三个颜色 RGB 常数

// Naven Projectile.java
if (item instanceof BowItem || item instanceof CrossbowItem) return 0.05;
if (item instanceof PotionItem) return 0.4;
if (item instanceof FishingRodItem) return 0.15;
if (item instanceof TridentItem) return 0.015;
return 0.03;
...
new BasicProjectileData(Collections.singleton(ThrownEnderpearl.class), new Color(173, 12, 255));
new BasicProjectileData(Collections.singleton(ThrownEgg.class),       new Color(255, 238, 154));
new BasicProjectileData(Collections.singleton(Snowball.class),         new Color(255, 255, 255));
// Zen Projectiles.java
if (item instanceof BowItem || item instanceof CrossbowItem) return 0.05;
if (item instanceof PotionItem) return 0.4;
if (item instanceof FishingRodItem) return 0.15;
if (item instanceof TridentItem) return 0.015;
return 0.03;
...
new ClassEspColor(Collections.singleton(ThrownEnderpearl.class), new Color(173, 12, 255));
new ClassEspColor(Collections.singleton(ThrownEgg.class),       new Color(255, 238, 154));
new ClassEspColor(Collections.singleton(Snowball.class),         new Color(255, 255, 255));

5 条 gravity 分支顺序与数值(0.05 / 0.4 / 0.15 / 0.015 / 0.03)完全照搬。3 个 RGB 颜色魔法数 (173, 12, 255) (255, 238, 154) (255, 255, 255) 完全一致,且 Collections.singleton(X.class) 包装方式一致。

5 个 BooleanSetting 名 + 默认值

// Naven Projectile.java
ValueBuilder.create(this, "Show Arrows").setDefaultBooleanValue(true)...
ValueBuilder.create(this, "Show Pearls").setDefaultBooleanValue(true)...
ValueBuilder.create(this, "Show Potions").setDefaultBooleanValue(false)...
ValueBuilder.create(this, "Show Eggs").setDefaultBooleanValue(false)...
ValueBuilder.create(this, "Show Snowballs").setDefaultBooleanValue(false)...
// Zen Projectiles.java
new BooleanSetting("Show Arrows", true);
new BooleanSetting("Show Pearls", true);
new BooleanSetting("Show Potions", false);
new BooleanSetting("Show Eggs", false);
new BooleanSetting("Show Snowballs", false);

5 个字符串(大小写、空格、复数 s)、5 个默认值(true / true / false / false / false)、注册顺序全部一致。


工具类

ChunkUtil.getLoadedChunks —— "Stream limit didn't work." 异常文本逐字符一致

// Naven ChunkUtils.java
public static Stream<LevelChunk> getLoadedChunks() {
    int radius = Math.max(2, mc.options.getEffectiveRenderDistance()) + 3;
    int diameter = radius * 2 + 1;
    ChunkPos center = mc.player.chunkPosition();
    ChunkPos min = new ChunkPos(center.x - radius, center.z - radius);
    ChunkPos max = new ChunkPos(center.x + radius, center.z + radius);
    Stream<LevelChunk> stream = Stream.iterate(min, pos -> {
        int x = pos.x;
        int z = pos.z;
        x++;
        if (x > max.x) { x = min.x; z++; }
        if (z > max.z) throw new IllegalStateException("Stream limit didn't work.");
        return new ChunkPos(x, z);
    }).limit((long) diameter * diameter).filter(c -> mc.level.hasChunk(c.x, c.z))
      .map(c -> mc.level.getChunk(c.x, c.z)).filter(Objects::nonNull);
    return stream;
}
// Zen ChunkUtil.java
public static Stream<LevelChunk> getLoadedChunks() {
    if (mc.player == null || mc.level == null) return Stream.empty();
    final int radius = Math.max(2, mc.options.getEffectiveRenderDistance()) + 3;
    final int side = radius * 2 + 1;
    final ChunkPos center = mc.player.chunkPosition();
    final ChunkPos min = new ChunkPos(center.x - radius, center.z - radius);
    final ChunkPos max = new ChunkPos(center.x + radius, center.z + radius);
    return Stream.iterate(min, current -> {
        int x = current.x; int z = current.z;
        if (++x > max.x) { x = min.x; ++z; }
        if (z > max.z) throw new IllegalStateException("Stream limit didn't work.");
        return new ChunkPos(x, z);
    }).limit((long) side * side).filter(pos -> mc.level.hasChunk(pos.x, pos.z))
      .map(pos -> mc.level.getChunk(pos.x, pos.z)).filter(Objects::nonNull);
}

异常字符串 "Stream limit didn't work." 含句点、含撇号、连大小写都一致;Math.max(2, ...) + 3radius * 2 + 1Stream.iterate 的 lambda 行内 if (x > max.x) { x = min.x; z++; } 写法骨架完全一样。这种带异常 throw 的 Stream.iterate 谁都不会自己写两遍。

RotationUtil 把 antlr-runtime 的 OrderedHashSet 当 Set 用

// Naven RotationUtils.java
import org.antlr.v4.runtime.misc.OrderedHashSet;
...
Set<Vec3> points = new OrderedHashSet<>();
points.add(new Vec3(minX + maxX / 2, minY + maxY / 2, minZ + maxZ / 2));
points.add(getClosestPoint(eyePos, targetBox));
// Zen RotationUtil.java
import org.antlr.v4.runtime.misc.OrderedHashSet;
...
OrderedHashSet<Vec3> samplePoints = new OrderedHashSet<>();
samplePoints.add(new Vec3(minX + maxX / 2.0, minY + maxY / 2.0, minZ + maxZ / 2.0));
samplePoints.add(RotationUtil.closestPoint(eyePos, aABB));

org.antlr.v4.runtime.misc.OrderedHashSet 是 ANTLR 解析器运行时里的类,任何一个正常人写的旋转工具类都不会去 import 它。两边都用它存 Vec3 采样点;连"先 add 中心点、再 add 最近点"的顺序和起手式都一模一样。

Sensitivity GCD 公式 f * f * f * 1.2F

// Naven RotationUtils.getFixedRotation
final float f = (float) (mc.options.sensitivity * 0.6F + 0.2F);
final float gcd = f * f * f * 1.2F;
final float deltaYaw = yaw - lastYaw;
final float deltaPitch = pitch - lastPitch;
final float fixedDeltaYaw = deltaYaw - (deltaYaw % gcd);
final float fixedDeltaPitch = deltaPitch - (deltaPitch % gcd);
return new Vector2f(lastYaw + fixedDeltaYaw, lastPitch + fixedDeltaPitch);
// Zen RotationUtil.getSensitivitySnappedRotation
float sensitivityFactor = (float)(mc.options.sensitivity().get() * 0.6f + 0.2f);
float gcd = sensitivityFactor * sensitivityFactor * sensitivityFactor * 1.2f;
float yawDelta = yaw - prevYaw;
float pitchDelta = pitch - prevPitch;
float snappedYawDelta = yawDelta - yawDelta % gcd;
float snappedPitchDelta = pitchDelta - pitchDelta % gcd;
return new Rotation(prevYaw + snappedYawDelta, prevPitch + snappedPitchDelta);

魔法常量 0.6 / 0.2 / 1.2 三件套 + f * f * f 立方 + delta - delta % gcd 求余截断,整套公式逐行同构。

rotationTo 的 yaw / pitch 公式

// Naven RotationUtils.getRotations
double x = target.x - eye.x;
double y = target.y - eye.y;
double z = target.z - eye.z;
double diffXZ = Math.sqrt(x * x + z * z);
float yaw = (float) Math.toDegrees(Math.atan2(z, x)) - 90.0f;
float pitch = (float) (-Math.toDegrees(Math.atan2(y, diffXZ)));
return new Vector2f(MathHelper.wrapDegrees(yaw), MathHelper.wrapDegrees(pitch));
// Zen RotationUtil.exactRotation
double dx = to.x - from.x;
double dy = to.y - from.y;
double dz = to.z - from.z;
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
float yaw = (float)Math.toDegrees(Math.atan2(dz, dx)) - 90.0f;
float pitch = (float)(-Math.toDegrees(Math.atan2(dy, horizontalDist)));
return new Rotation(Mth.wrapDegrees(yaw), Mth.wrapDegrees(pitch));

yaw 用 atan2(z, x) - 90、pitch 用 -atan2(y, sqrt(x² + z²)),外层 Math.toDegrees 强转 float 再 wrapDegrees,连 - 90.0f 的字面量写法都一致。这段代码在 Zen 里出现了 4 次(rotationTo / exactRotation / rotationFromDeltas / createRotation)。

BlockUtil.canBeClicked / getBoundingBox

// Naven BlockUtils.java
public static AABB getBoundingBox(BlockPos pos) {
    return getOutlineShape(pos).bounds().move(pos);
}
private static VoxelShape getOutlineShape(BlockPos pos) {
    return getState(pos).getShape(mc.level, pos);
}
public static boolean canBeClicked(BlockPos pos) {
    return getOutlineShape(pos) != Shapes.empty();
}
// Zen BlockUtil.java
public static boolean canBeClicked(BlockPos blockPos) {
    return BlockUtil.getVoxelShape(blockPos) != Shapes.empty();
}
public static AABB getBoundingBox(BlockPos blockPos) {
    return BlockUtil.getVoxelShape(blockPos).bounds().move(blockPos);
}
private static VoxelShape getVoxelShape(BlockPos blockPos) {
    return BlockUtil.getBlockState(blockPos).getShape(mc.level, blockPos);
}

!= Shapes.empty()(而不是更地道的 .isEmpty())这种引用比较的写法两边一致;三段方法的实现一行一行对得上。

MovementUtil direction 函数 —— 7 个 if 分支顺序与角度全一致

// Naven MoveUtils.direction
if (forward != 0.0F || strafe != 0.0F) {
    if (isMovingBack && !isMovingSideways)        return direction + 180.0F;
    else if (isMovingForward && isMovingLeft)     return direction + 45.0F;
    else if (isMovingForward && isMovingRight)    return direction - 45.0F;
    else if (!isMovingStraight && isMovingLeft)   return direction + 90.0F;
    else if (!isMovingStraight && isMovingRight)  return direction - 90.0F;
    else if (isMovingBack && isMovingLeft)        return direction + 135.0F;
    else if (isMovingBack)                        return direction - 135.0F;
}
return direction;
// Zen MovementUtil.getDirectionAngle
if (forward != 0.0f || strafe != 0.0f) {
    if (forwardNegative && !hasStrafe)        return yaw + 180.0f;
    if (forwardPositive && strafeNegative)    return yaw + 45.0f;
    if (forwardPositive && strafePositive)    return yaw - 45.0f;
    if (!hasForward && strafeNegative)        return yaw + 90.0f;
    if (!hasForward && strafePositive)        return yaw - 90.0f;
    if (forwardNegative && strafeNegative)    return yaw + 135.0f;
    if (forwardNegative)                      return yaw - 135.0f;
}
return yaw;

7 个分支的顺序、判断组合、+180 / +45 / -45 / +90 / -90 / +135 / -135 七个常量一字不差。Zen 仅改了变量名(isMovingBackforwardNegative),把 else if 改成提前 return,这是典型的"反编译后稍微整形"痕迹。注意 Zen 这个 getDirectionAngleprivate根本没被调用(死代码),更说明是整团抄过来的。

RayTraceUtil.pick+ 1.62 硬编码 eye height

// Naven RayTraceUtils.pick
Vec3 vec3 = new Vec3(mc.player.getX(), mc.player.getY() + 1.62, mc.player.getZ());
Vec3 vec31 = calculateViewVector(pXRot, pYRot);
Vec3 vec32 = vec3.add(vec31.x * pHitDistance, vec31.y * pHitDistance, vec31.z * pHitDistance);
return mc.player.level.clip(new ClipContext(vec3, vec32,
    ClipContext.Block.OUTLINE,
    pHitFluids ? ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, mc.player));
// Zen RayTraceUtil.rayTrace
Vec3 eyePos = new Vec3(mc.player.getX(), mc.player.getY() + 1.62, mc.player.getZ());
Vec3 viewVec = RayTraceUtil.getViewVector(pitch, yaw);
Vec3 endPos = eyePos.add(viewVec.x * range, viewVec.y * range, viewVec.z * range);
return mc.player.level().clip(new ClipContext(eyePos, endPos,
    ClipContext.Block.OUTLINE,
    clipFluids ? ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, mc.player));

在 1.17+ 应该用 mc.player.getEyeHeight()getEyePosition(partialTicks),硬编码 + 1.62 是历史遗留写法。两边都犯同一个 hardcode + 同样的 Block.OUTLINE + 同样的三元 Fluid.ANY : Fluid.NONE,ClipContext 的 5 个参数顺序也一致。


结语

上面这些代码段,凡是真正难辩解的硬证据全都集中在三类:

  1. 字符串字面量"Fake Staff Detected! (""Bot Detected! (""Bot Removed! (""Stream limit didn't work.""点击使用""Compass Only""No Player Only""Show Arrows / Pearls / Potions / Eggs / Snowballs""Normal" / "Telly Bridge" / "Keep Y""Render Item Spoof""Respawn Time")。
  2. 魔法常量+1337(0.88, 1.88, 0.88)> 2 && < 0.0001i * i + 1 附魔加成、72000 / 0.1 / 3 弓蓄力、0.6 / 0.2 / 1.2 灵敏度 GCD、+ 1.62 硬编码 eye height、RGB (173, 12, 255) / (255, 238, 154) 等)。
  3. 特征 import 与"死代码"(工具类里出现 org.antlr.v4.runtime.misc.OrderedHashSetMovementUtil.getDirectionAngleprivate 且没人调用却整团抄了过来)。

这些东西在反混淆环节是不会被自动还原的——反编译只能把 var1 / var2 还原成可读变量,但字符串、常数、字节码里的 instanceof 链顺序、import 表都跟字节码绑死。它们出现在两个项目里完全一致,唯一合理解释就是源代码直接复制。