combat folia anti-cheat

This commit is contained in:
seb
2026-05-14 15:28:19 +02:00
parent 31b1c6b6bb
commit 786d78e6fe
2 changed files with 160 additions and 31 deletions

View File

@@ -37,10 +37,12 @@ import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.item.trading.MerchantOffers;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
final class AutoTradeClientTick {
@@ -68,6 +70,16 @@ final class AutoTradeClientTick {
private int outputContainerHighlightTicks = 0;
private int postMerchantInventorySyncTicks = 0;
/**
* Entity we have already snapped the camera onto and are waiting for line of
* sight to clear on. Prevents re-rotating to the same villager every tick
* while a block briefly obstructs the view; reset once we interact (or after
* a timeout) so the next villager can be acquired.
*/
private int rotatingTargetId = -1;
private int rotatingTargetTicks = 0;
private static final int LOS_TIMEOUT_TICKS = 60;
/**
* Entity to draw in-world highlight for; {@code null} when inactive or unknown
* id.
@@ -157,20 +169,38 @@ final class AutoTradeClientTick {
if (!found) {
if (!newVillagersInRange.contains(entity)) {
found = true;
newVillagersInRange.add(entity);
// Paper/Folia rejects entity-interact packets unless the player is
// actually facing the entity, so align the look vector first and
// mirror vanilla's full INTERACT_AT + INTERACT + swing sequence.
Vec3 eyePos = mc.player.getEyePosition();
Vec3 targetEye = entity.getEyePosition();
Vec3 delta = targetEye.subtract(eyePos);
double horiz = Math.sqrt(delta.x * delta.x + delta.z * delta.z);
float yaw = (float) (Math.toDegrees(Math.atan2(delta.z, delta.x)) - 90.0);
float pitch = (float) -Math.toDegrees(Math.atan2(delta.y, horiz));
mc.player.setYRot(yaw);
mc.player.setXRot(pitch);
mc.player.yHeadRot = yaw;
EntityHitResult ehr = new EntityHitResult(entity, targetEye);
// actually facing a visible part of the entity hitbox. Aim at any
// point we have a clean line of sight to (eyes, head, chest, feet);
// if none of them are visible yet, wait without flooding the camera
// with re-rotations every tick.
Vec3 aimPoint = firstVisiblePoint(mc, entity);
if (rotatingTargetId != entity.getId()) {
rotatingTargetId = entity.getId();
rotatingTargetTicks = 0;
Vec3 lookAt = aimPoint != null ? aimPoint : entity.getEyePosition();
Vec3 eyePos = mc.player.getEyePosition();
Vec3 delta = lookAt.subtract(eyePos);
double horiz = Math.sqrt(delta.x * delta.x + delta.z * delta.z);
float yaw = (float) (Math.toDegrees(Math.atan2(delta.z, delta.x)) - 90.0);
float pitch = (float) -Math.toDegrees(Math.atan2(delta.y, horiz));
mc.player.setYRot(yaw);
mc.player.setXRot(pitch);
mc.player.yHeadRot = yaw;
} else {
rotatingTargetTicks++;
}
if (aimPoint == null) {
if (rotatingTargetTicks > LOS_TIMEOUT_TICKS) {
// Give up so the player can move past this villager.
newVillagersInRange.add(entity);
rotatingTargetId = -1;
}
break;
}
newVillagersInRange.add(entity);
rotatingTargetId = -1;
EntityHitResult ehr = new EntityHitResult(entity, aimPoint);
AutoTrade.autoInteracting = true;
try {
//? if mc26 {
@@ -459,6 +489,38 @@ final class AutoTradeClientTick {
traderGlowTicksRemaining = TRADER_HIGHLIGHT_TICKS;
}
/**
* Returns the first point on {@code target}'s bounding box (eyes, head,
* chest, feet, or one of the four upper corners) that has an unobstructed
* block ray-cast from the player's eyes; {@code null} when the entire body
* is occluded.
*/
private static Vec3 firstVisiblePoint(Minecraft mc, Entity target) {
Vec3 eye = mc.player.getEyePosition();
AABB box = target.getBoundingBox();
double cx = (box.minX + box.maxX) * 0.5;
double cz = (box.minZ + box.maxZ) * 0.5;
Vec3[] candidates = {
target.getEyePosition(),
new Vec3(cx, (box.minY + box.maxY) * 0.5, cz),
new Vec3(cx, box.maxY - 0.05, cz),
new Vec3(cx, box.minY + 0.1, cz),
new Vec3(box.minX + 0.05, box.maxY - 0.2, box.minZ + 0.05),
new Vec3(box.maxX - 0.05, box.maxY - 0.2, box.minZ + 0.05),
new Vec3(box.minX + 0.05, box.maxY - 0.2, box.maxZ - 0.05),
new Vec3(box.maxX - 0.05, box.maxY - 0.2, box.maxZ - 0.05),
};
for (Vec3 p : candidates) {
BlockHitResult hit = mc.level.clip(new ClipContext(eye, p, ClipContext.Block.COLLIDER,
ClipContext.Fluid.NONE, mc.player));
if (hit.getType() == HitResult.Type.MISS
|| eye.distanceToSqr(hit.getLocation()) >= eye.distanceToSqr(p) - 1.0E-4) {
return p;
}
}
return null;
}
private static Entity findEntityById(Minecraft mc, int entityId) {
for (Entity e : mc.level.entitiesForRendering()) {
if (e.getId() == entityId) {

View File

@@ -12,6 +12,7 @@ import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.client.screen.v1.Screens;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.StringWidget;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import net.minecraft.network.chat.Component;
@@ -48,36 +49,55 @@ public final class MerchantScreenButtonInjector {
// Position buttons safely to the right of the merchant GUI (276 px wide, centered).
int x = scaledWidth / 2 + 140;
int y = scaledHeight / 2 - 83;
int w = 160;
int bw = 160;
int lw = 110;
int h = 20;
int gap = 2;
int labelX = x + bw + 4;
Button openSettings = Button
.builder(Component.literal("Open Settings"), btn -> GuiBase.openGui(new GuiConfigs()))
.bounds(x, y, w, h).build();
.builder(Component.literal("Open Settings"), btn -> {
// vanilla Screen.onClose -> setScreen(null) sends the container-close
// packet; switching the screen out from under the merchant GUI via
// GuiBase.openGui skips that path, so the server still thinks we are
// trading with this villager and rejects the next interact packet.
if (client.player != null) {
client.player.closeContainer();
}
GuiBase.openGui(new GuiConfigs());
})
.bounds(x, y, bw, h).build();
Button selectSell = Button
.builder(sellButtonLabel(), btn -> applySelectedTradeAsSell(client, merchantScreen))
.bounds(x, y + (h + gap), w, h).build();
.builder(sellButtonLabel(null), btn -> onSellButton(client, merchantScreen))
.bounds(x, y + (h + gap), bw, h).build();
StringWidget sellCurrent = new StringWidget(labelX, y + (h + gap), lw, h,
currentSellLabel(), client.font);
Button selectBuy = Button
.builder(buyButtonLabel(), btn -> applySelectedTradeAsBuy(client, merchantScreen))
.bounds(x, y + 2 * (h + gap), w, h).build();
.builder(buyButtonLabel(null), btn -> onBuyButton(client, merchantScreen))
.bounds(x, y + 2 * (h + gap), bw, h).build();
StringWidget buyCurrent = new StringWidget(labelX, y + 2 * (h + gap), lw, h,
currentBuyLabel(), client.font);
Button enableAutotrade = Button
.builder(autotradeButtonLabel(), btn -> toggleAutotrade(btn))
.bounds(x, y + 3 * (h + gap), w, h).build();
.bounds(x, y + 3 * (h + gap), bw, h).build();
Screen asScreen = merchantScreen;
//? if mc26 {
Screens.getWidgets(asScreen).add(openSettings);
Screens.getWidgets(asScreen).add(selectSell);
Screens.getWidgets(asScreen).add(sellCurrent);
Screens.getWidgets(asScreen).add(selectBuy);
Screens.getWidgets(asScreen).add(buyCurrent);
Screens.getWidgets(asScreen).add(enableAutotrade);
//?} else {
Screens.getButtons(asScreen).add(openSettings);
Screens.getButtons(asScreen).add(selectSell);
Screens.getButtons(asScreen).add(sellCurrent);
Screens.getButtons(asScreen).add(selectBuy);
Screens.getButtons(asScreen).add(buyCurrent);
Screens.getButtons(asScreen).add(enableAutotrade);
//?}
@@ -93,27 +113,74 @@ public final class MerchantScreenButtonInjector {
}
MerchantOffer current = currentSelectedOffer(merchantScreen);
selectSell.active = current != null && isSellOffer(current);
selectBuy.active = current != null && isBuyOffer(current);
selectSell.setMessage(sellButtonLabel());
selectBuy.setMessage(buyButtonLabel());
MerchantOffer sellOffer = (current != null && isSellOffer(current)) ? current : null;
MerchantOffer buyOffer = (current != null && isBuyOffer(current)) ? current : null;
boolean sellOn = Configs.Generic.ENABLE_SELL.getBooleanValue();
boolean buyOn = Configs.Generic.ENABLE_BUY.getBooleanValue();
// Button doubles as a "Disable" toggle when no applicable trade is selected
// but the corresponding ENABLE flag is currently on — so the user can turn
// it off without leaving the merchant screen.
selectSell.active = sellOffer != null || sellOn;
selectBuy.active = buyOffer != null || buyOn;
selectSell.setMessage(sellOffer != null ? sellButtonLabel(sellOffer)
: (sellOn ? Component.literal("Disable sell") : sellButtonLabel(null)));
selectBuy.setMessage(buyOffer != null ? buyButtonLabel(buyOffer)
: (buyOn ? Component.literal("Disable buy") : buyButtonLabel(null)));
sellCurrent.setMessage(currentSellLabel());
buyCurrent.setMessage(currentBuyLabel());
enableAutotrade.setMessage(autotradeButtonLabel());
});
}
private static void onSellButton(Minecraft client, MerchantScreen screen) {
MerchantOffer offer = currentSelectedOffer(screen);
if (offer != null && isSellOffer(offer)) {
applySelectedTradeAsSell(client, screen);
return;
}
if (Configs.Generic.ENABLE_SELL.getBooleanValue()) {
Configs.Generic.ENABLE_SELL.setBooleanValue(false);
Configs.saveToFile();
}
}
private static void onBuyButton(Minecraft client, MerchantScreen screen) {
MerchantOffer offer = currentSelectedOffer(screen);
if (offer != null && isBuyOffer(offer)) {
applySelectedTradeAsBuy(client, screen);
return;
}
if (Configs.Generic.ENABLE_BUY.getBooleanValue()) {
Configs.Generic.ENABLE_BUY.setBooleanValue(false);
Configs.saveToFile();
}
}
private static Component autotradeButtonLabel() {
boolean on = Configs.Generic.ENABLED.getBooleanValue();
return Component.literal(on ? "Disable Autotrade" : "Enable Autotrade");
}
private static Component sellButtonLabel() {
String enabled = Configs.Generic.ENABLE_SELL.getBooleanValue() ? "" : " (off)";
return Component.literal("Sell: " + describeSpec(Configs.Generic.SELL_ITEM.getStringValue()) + enabled);
/** Button label previews the item that would be written if clicked. */
private static Component sellButtonLabel(MerchantOffer offer) {
String item = offer == null ? "-" : describeSpec(TradeItemSpec.encodeFromStack(offer.getCostA()));
return Component.literal("Set " + item + " as sell item");
}
private static Component buyButtonLabel() {
String enabled = Configs.Generic.ENABLE_BUY.getBooleanValue() ? "" : " (off)";
return Component.literal("Buy: " + describeSpec(Configs.Generic.BUY_ITEM.getStringValue()) + enabled);
private static Component buyButtonLabel(MerchantOffer offer) {
String item = offer == null ? "-" : describeSpec(TradeItemSpec.encodeFromStack(offer.getResult()));
return Component.literal("Set " + item + " as buy item");
}
/** Side-label shows the currently-configured sell/buy item from config. */
private static Component currentSellLabel() {
String off = Configs.Generic.ENABLE_SELL.getBooleanValue() ? "" : " (off)";
return Component.literal(describeSpec(Configs.Generic.SELL_ITEM.getStringValue()) + off);
}
private static Component currentBuyLabel() {
String off = Configs.Generic.ENABLE_BUY.getBooleanValue() ? "" : " (off)";
return Component.literal(describeSpec(Configs.Generic.BUY_ITEM.getStringValue()) + off);
}
/**