combat folia anti-cheat
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user