import beame.components.baritone.api.BaritoneAPI;
import beame.components.baritone.api.IBaritone;
import beame.components.baritone.api.process.IMineProcess;
import beame.module.Category;
import beame.module.Module;
import beame.setting.SettingList.BooleanSetting;
import beame.setting.SettingList.EnumSetting;
import events.Event;
import events.impl.player.EventUpdate;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraft.item.PickaxeItem;
import net.minecraft.util.math.BlockPos;
import beame.util.math.TimerUtil;
import beame.components.baritone.api.pathing.goals.GoalXZ;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraft.inventory.container.ClickType;
import beame.components.baritone.api.pathing.path.IPathExecutor;
import beame.components.baritone.api.pathing.movement.IMovement;
import beame.components.baritone.pathing.movement.movements.MovementPillar;
import beame.util.math.RaytraceUtility;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.Direction;
import net.minecraft.util.Hand;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.lang.reflect.Field;
public class AutoMiner extends Module {
// Выбор руд для добычи
private final EnumSetting ores = new EnumSetting("Добывать",
new BooleanSetting("Алмазная руда", true),
new BooleanSetting("Железная руда", false),
new BooleanSetting("Золотая руда", false),
new BooleanSetting("Угольная руда", false)
);
private IMineProcess mineProcess;
private static final int DEFAULT_RADIUS = 30;
private static final int EAT_THRESHOLD = 18;
private boolean isEating = false;
private int previousSlot = -1;
// Sequence after finishing mining
private final String[] AN_COMMANDS = new String[]{
"/an217", "/an218", "/an216", "/an313", "/an312", "/an308", "/an309",
"/an211", "/an115", "/an602", "/an505", "/an311", "/an312", "/an104",
"/an212", "/an213", "/an503", "/an504", "/an507", "/an506"
};
private int anIndex = 0;
private boolean chainInProgress = false;
private int chainStage = 0; // 0 idle, 1 wait 5s after /an, 2 wait 12s after /warp, 3 moving forward
private final TimerUtil chainTimer = new TimerUtil();
private boolean prevMineActive = false;
private static final double PICKAXE_MIN_PCT = 0.20; // 20%
private boolean haltedFromHub = false;
private boolean isUnstucking = false;
private BlockPos unstuckBreakingPos = null;
private final TimerUtil unstuckTimer = new TimerUtil();
// Save and override Baritone render settings to hide path/boxes while AutoMiner works
private Boolean prevRenderPath = null;
// ===== ЗАСТРЕВАНИЕ ПО КООРДИНАТАМ (5 сек на одном месте) =====
private BlockPos lastStuckCheckPos = null;
private final TimerUtil stuckTimer = new TimerUtil();
public AutoMiner() {
super("FTNuker", Category.Misc, true, "Автоматически ходит и добывает выбранные руды");
addSettings(ores);
}
@Override
protected boolean onEnable() {
IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone();
if (baritone == null) return false;
mineProcess = baritone.getMineProcess();
haltedFromHub = false;
prevMineActive = false;
// Hide Baritone path/goal/selection boxes while AutoMiner is active
try {
var settings = BaritoneAPI.getSettings();
if (prevRenderPath == null) prevRenderPath = settings.renderPath.value;
settings.renderPath.value = false;
} catch (Throwable ignored) {}
List<Block> targets = new ArrayList<>();
if (ores.get("Алмазная руда").get()) targets.add(Blocks.DIAMOND_ORE);
if (ores.get("Железная руда").get()) targets.add(Blocks.IRON_ORE);
if (ores.get("Золотая руда").get()) targets.add(Blocks.GOLD_ORE);
if (ores.get("Угольная руда").get()) targets.add(Blocks.COAL_ORE);
if (targets.isEmpty()) {
// Нет выбранных блоков — не запускаем майнинг, модуль остаётся включён
return true;
}
// Запуск майнинга выбранных блоков, без ограничения по количеству
mineProcess.mine(targets.toArray(new Block[0]));
// Сразу ограничим цели радиусом, чтобы старт происходил в рамках DEFAULT_RADIUS
filterTargetsWithinRadius(baritone, DEFAULT_RADIUS);
return true;
}
@Override
protected void onDisable() {
if (mineProcess != null) {
mineProcess.cancel();
}
// Restore Baritone rendering settings
try {
var settings = BaritoneAPI.getSettings();
if (prevRenderPath != null) {
settings.renderPath.value = prevRenderPath;
prevRenderPath = null;
}
} catch (Throwable ignored) {}
// Сброс клика атаки на случай, если держим для разлома блока
try {
mc.gameSettings.keyBindAttack.setPressed(false);
if (isUnstucking) {
mc.playerController.resetBlockRemoving();
isUnstucking = false;
unstuckBreakingPos = null;
}
} catch (Throwable ignored) {}
}
// Возвращает true, если выполнили действие (заменили кирку или ушли на /hub)
private boolean ensurePickaxeDurabilityOrHub() {
ItemStack main = mc.player.getHeldItemMainhand();
boolean hasPickaxeInHand = main != null && main.getItem() instanceof PickaxeItem;
if (hasPickaxeInHand) {
double pct = remainingPct(main);
if (pct <= PICKAXE_MIN_PCT) {
// Найти лучшую кирку с >20%
int best = findBestPickaxeSlotAboveThreshold(PICKAXE_MIN_PCT);
if (best != -1) {
switchToSlot(best);
return true; // заменили
} else {
// Все кирки <=20% (или нет кирок) — уходим в /hub и стоп
mc.player.sendChatMessage("/hub");
haltedFromHub = true;
return true;
}
}
} else {
// В руке не кирка: попробуем взять подходящую кирку >20%, если есть
int best = findBestPickaxeSlotAboveThreshold(PICKAXE_MIN_PCT);
if (best != -1) {
switchToSlot(best);
return true;
}
// Нет подходящей кирки >20% — уходим в /hub и стоп
mc.player.sendChatMessage("/hub");
haltedFromHub = true;
return true;
}
return false;
}
private int findBestPickaxeSlotAboveThreshold(double minPct) {
int bestSlot = -1;
double bestPct = -1.0;
// Проверяем хотбар 0..8 и инвентарь 9..35
for (int i = 0; i < 36; i++) {
ItemStack s = mc.player.inventory.getStackInSlot(i);
if (s == null || s.isEmpty() || !(s.getItem() instanceof PickaxeItem)) continue;
double pct = remainingPct(s);
if (pct > minPct && pct > bestPct) {
bestPct = pct;
bestSlot = i;
}
}
return bestSlot;
}
private double remainingPct(ItemStack stack) {
int max = stack.getMaxDamage();
if (max <= 0) return 1.0; // небьющийся предмет
int used = stack.getDamage();
int remaining = Math.max(0, max - used);
return (double) remaining / (double) max;
}
private void switchToSlot(int invIndex) {
if (invIndex < 0 || invIndex >= 36) return;
int currentHotbar = mc.player.inventory.currentItem; // 0..8
if (invIndex <= 8) {
// Уже в хотбаре
if (currentHotbar != invIndex) {
mc.player.inventory.currentItem = invIndex;
}
} else {
// В инвентаре: свапаем с текущим хотбар слотом
int containerSlot = invIndex; // 9..35
int windowId = mc.player.openContainer.windowId;
mc.playerController.windowClick(windowId, containerSlot, currentHotbar, ClickType.SWAP, mc.player);
}
}
@Override
public void event(Event event) {
if (event instanceof EventUpdate) {
IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone();
if (baritone == null || mc.player == null) return;
if (haltedFromHub) return;
// 1) Если голоден — остановиться, поесть, затем продолжить
boolean hungry = mc.player.getFoodStats().getFoodLevel() < EAT_THRESHOLD;
if (hungry) {
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
// Важно: не запускать /an после отмены майнинга из-за еды
// Сбросим prevMineActive, чтобы не сработало условие prevMineActive && !currentMineActive
prevMineActive = false;
if (!isEating) {
int foodSlot = findFoodHotbarSlot();
if (foodSlot != -1 || hasFoodInHands()) {
startEating(foodSlot);
}
} else {
continueEating();
}
return;
} else if (isEating) {
stopEating();
}
// 1b) Проверка прочности кирки и замена / выход в /hub
if (ensurePickaxeDurabilityOrHub()) {
// либо заменили кирку, либо ушли на /hub и остановились
if (haltedFromHub) {
if (mineProcess != null && mineProcess.isActive()) mineProcess.cancel();
chainInProgress = false;
return;
}
// если была замена инструмента, продолжаем дальше
}
// 1c) Если застряли/душимся в блоках — ломаем блок перед собой и ничего больше не делаем в этот тик
if (handleSuffocationUnstuck()) {
prevMineActive = mineProcess != null && mineProcess.isActive();
return;
}
// 2) Если есть выбранные руды — майним, иначе стоим
List<Block> targets = new ArrayList<>();
if (ores.get("Алмазная руда").get()) targets.add(Blocks.DIAMOND_ORE);
if (ores.get("Железная руда").get()) targets.add(Blocks.IRON_ORE);
if (ores.get("Золотая руда").get()) targets.add(Blocks.GOLD_ORE);
if (ores.get("Угольная руда").get()) targets.add(Blocks.COAL_ORE);
boolean currentMineActive = mineProcess != null && mineProcess.isActive();
// 2a-Застревание: если 5 секунд на одних координатах во время майнинга — делаем /warp mine и идём вперёд
{
BlockPos cur = mc.player.getPosition();
if (lastStuckCheckPos == null || !lastStuckCheckPos.equals(cur)) {
lastStuckCheckPos = cur;
stuckTimer.reset();
} else if (currentMineActive && !chainInProgress && !isEating) {
if (stuckTimer.finished(5000)) {
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
mc.player.sendChatMessage("/warp mine");
chainInProgress = true;
chainStage = 21; // ждать ~10с, затем идти вперёд на 9 блоков
chainTimer.reset();
prevMineActive = currentMineActive;
return;
}
}
}
// 2a0) Если баритон пытается строиться вверх (пиллар) к руде — вместо этого выполняем warp-сценарий
if (!chainInProgress && !isEating) {
try {
IPathExecutor exec = baritone.getPathingBehavior().getCurrent();
if (exec != null && exec.getPath() != null && exec.getPath().movements() != null && !exec.getPath().movements().isEmpty()) {
int idx = Math.min(Math.max(exec.getPosition(), 0), exec.getPath().movements().size() - 1);
IMovement mv = exec.getPath().movements().get(idx);
if (mv instanceof MovementPillar) {
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
mc.player.sendChatMessage("/warp mine");
chainInProgress = true;
chainStage = 21; // ждать 10 секунд, затем идти вперёд на 9 блоков
chainTimer.reset();
prevMineActive = currentMineActive;
return;
}
}
} catch (Throwable ignored) {}
}
// 2a) Обработка последовательности после завершения майнинга И/ИЛИ warp-сценариев
if (chainInProgress) {
// Выполняем этапы: 1) ждать 5с => /warp mine; 2) ждать 12с => пройти вперёд 5 блоков; 3) дождаться завершения движения
switch (chainStage) {
case 1 -> {
if (chainTimer.finished(5000)) {
mc.player.sendChatMessage("/warp mine");
chainStage = 2;
chainTimer.reset();
}
}
case 2 -> {
if (chainTimer.finished(10000)) {
Vector3d origin = mc.player.getPositionVec();
float yaw = mc.player.rotationYaw;
baritone.getCustomGoalProcess().setGoalAndPath(GoalXZ.fromDirection(origin, yaw, 9.0));
chainStage = 3;
}
}
case 3 -> {
if (!baritone.getCustomGoalProcess().isActive()) {
// Движение завершено, завершаем цепочку, далее общий код перезапустит майнинг
chainInProgress = false;
chainStage = 0;
}
}
case 21 -> {
if (chainTimer.finished(10000)) {
Vector3d origin = mc.player.getPositionVec();
float yaw = mc.player.rotationYaw;
baritone.getCustomGoalProcess().setGoalAndPath(GoalXZ.fromDirection(origin, yaw, 9.0));
chainStage = 22;
}
}
case 22 -> {
if (!baritone.getCustomGoalProcess().isActive()) {
chainInProgress = false;
chainStage = 0;
}
}
}
// Поддерживаем ограничение радиуса, даже пока идёт цепочка
filterTargetsWithinRadius(baritone, DEFAULT_RADIUS);
prevMineActive = currentMineActive;
return; // Не перезапускаем майнинг, пока идёт цепочка
}
if (targets.isEmpty()) {
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
prevMineActive = currentMineActive;
return;
}
// 2b) Детектируем окончание майнинга и запускаем цепочку команд
if (prevMineActive && !currentMineActive && !isEating) {
// Завершён майнинг: отправляем следующий /an и запускаем таймер на 5 секунд
String cmd = AN_COMMANDS[anIndex];
anIndex = (anIndex + 1) % AN_COMMANDS.length;
mc.player.sendChatMessage(cmd);
chainInProgress = true;
chainStage = 1;
chainTimer.reset();
prevMineActive = currentMineActive;
return;
}
// 2c) Если процесс не активен и нет цепочки — перезапускаем майнинг
if (mineProcess != null && !currentMineActive && !chainInProgress) {
mineProcess.mine(targets.toArray(new Block[0]));
}
// 3) Ограничение целей радиусом
filterTargetsWithinRadius(baritone, DEFAULT_RADIUS);
// 3a) Если в радиусе нет выбранной руды — запустить цепочку /an -> /warp mine -> идти вперёд
if (!chainInProgress && !isEating && currentMineActive && noTargetsInRadius(baritone)) {
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
String cmd = AN_COMMANDS[anIndex];
anIndex = (anIndex + 1) % AN_COMMANDS.length;
mc.player.sendChatMessage(cmd);
chainInProgress = true;
chainStage = 1; // ждать 5 секунд, затем /warp mine
chainTimer.reset();
prevMineActive = currentMineActive;
return;
}
// Обновляем предыдущее состояние активности майнинга
prevMineActive = currentMineActive;
}
}
private void filterTargetsWithinRadius(IBaritone baritone, int radius) {
if (baritone == null || mc.player == null) return;
if (mineProcess == null) return;
try {
Object impl = baritone.getMineProcess();
Field fKnown = impl.getClass().getDeclaredField("knownOreLocations");
fKnown.setAccessible(true);
Object val = fKnown.get(impl);
if (!(val instanceof List)) return;
@SuppressWarnings("unchecked")
List<BlockPos> locs = (List<BlockPos>) val;
if (locs == null || locs.isEmpty()) return;
BlockPos playerPos = mc.player.getPosition();
double maxSq = (double) radius * (double) radius;
List<BlockPos> filtered = locs.stream()
.filter(p -> p != null && playerPos.distanceSq(p) <= maxSq)
.collect(Collectors.toCollection(ArrayList::new));
// Replace with a new list to avoid concurrent modification
fKnown.set(impl, filtered);
} catch (Throwable ignored) {
// fail silently to avoid crashing if internals change
}
}
private boolean noTargetsInRadius(IBaritone baritone) {
try {
Object impl = baritone.getMineProcess();
Field fKnown = impl.getClass().getDeclaredField("knownOreLocations");
fKnown.setAccessible(true);
Object val = fKnown.get(impl);
if (!(val instanceof List)) return true;
@SuppressWarnings("unchecked")
List<BlockPos> locs = (List<BlockPos>) val;
return locs == null || locs.isEmpty();
} catch (Throwable ignored) {
return false;
}
}
// ===== ЕДА =====
private boolean hasFoodInHands() {
if (mc.player == null) return false;
ItemStack main = mc.player.getHeldItemMainhand();
ItemStack off = mc.player.getHeldItemOffhand();
return (!main.isEmpty() && main.getItem().isFood()) || (!off.isEmpty() && off.getItem().isFood());
}
private int findFoodHotbarSlot() {
if (mc.player == null) return -1;
for (int i = 0; i < 9; i++) {
ItemStack stack = mc.player.inventory.getStackInSlot(i);
if (!stack.isEmpty() && stack.getItem().isFood()) {
return i;
}
}
return -1;
}
private void startEating(int foodSlot) {
isEating = true;
if (!hasFoodInHands()) {
if (foodSlot == -1) {
isEating = false;
return;
}
previousSlot = mc.player.inventory.currentItem;
mc.player.inventory.currentItem = foodSlot;
}
continueEating();
}
private void continueEating() {
if (mc.currentScreen == null) {
mc.gameSettings.keyBindUseItem.setPressed(true);
}
if (mc.player.getFoodStats().getFoodLevel() >= EAT_THRESHOLD || !hasFoodInHands()) {
stopEating();
}
}
private void stopEating() {
mc.gameSettings.keyBindUseItem.setPressed(false);
isEating = false;
if (previousSlot != -1 && mc.player != null) {
mc.player.inventory.currentItem = previousSlot;
previousSlot = -1;
}
}
// ===== АНТИ-ЗАСТРЕВАНИЕ (ломаем блок перед собой при удушье) =====
private boolean handleSuffocationUnstuck() {
if (mc.player == null || mc.world == null) return false;
boolean inside = mc.player.isEntityInsideOpaqueBlock();
if (!inside) {
// Не душимся — сброс состояния
if (isUnstucking) {
try {
mc.gameSettings.keyBindAttack.setPressed(false);
mc.playerController.resetBlockRemoving();
} catch (Throwable ignored) {}
isUnstucking = false;
unstuckBreakingPos = null;
}
return false;
}
// Отменяем майнинг, чтобы не мешал разлому
if (mineProcess != null && mineProcess.isActive()) {
mineProcess.cancel();
}
// Находим блок перед взглядом игрока (до 2 блока)
RayTraceResult rt = RaytraceUtility.rayTrace(2.5, mc.player.rotationYaw, mc.player.rotationPitch, mc.player);
net.minecraft.util.math.BlockPos targetPos = null;
Direction face = Direction.UP;
if (rt != null && rt.getType() == RayTraceResult.Type.BLOCK && rt instanceof BlockRayTraceResult brt) {
targetPos = brt.getPos();
face = brt.getFace();
}
if (targetPos == null) {
// fallback: блок на уровне глаз
targetPos = new net.minecraft.util.math.BlockPos(mc.player.getEyePosition(1.0f));
face = Direction.UP;
}
if (mc.world.isAirBlock(targetPos)) {
// если перед нами воздух, пробуем блок в ногах
targetPos = mc.player.getPosition();
}
if (!mc.world.isAirBlock(targetPos)) {
try {
// Начинаем ломать и продолжаем до разрушения
if (unstuckBreakingPos == null || !unstuckBreakingPos.equals(targetPos)) {
mc.playerController.syncCurrentPlayItem();
mc.playerController.clickBlock(targetPos, face);
unstuckBreakingPos = targetPos;
}
mc.playerController.onPlayerDamageBlock(targetPos, face);
mc.player.swingArm(Hand.MAIN_HAND);
mc.gameSettings.keyBindAttack.setPressed(true);
isUnstucking = true;
} catch (Throwable ignored) {}
}
return true;
}
}